1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

Vertical Vault Navigation (#6957)

* WIP admin console layout

* Update icons

* Migrate more things

* Migrate the last pages

* Move header to web

* Fix story not working

* Convert header component to standalone

* Migrate org layout to standalone

* Enable org switcher

* Add AC to product switcher

* Migrate provider portal to vertical nav

* Migrate PM

* Prettier fixes

* Change AC and PP to use secondary variant layout & update logos

* Remove full width setting

* Remove commented code

* Add header to report pages

* Add provider portal banner

* Fix banner for billing pages

* Move vault title to header

* Prevent scrollbar jumping

* Move send button to header

* Replace search input with bit-search

* Remove unused files and css

* Add banner

* Tweak storage option

* Fix duplicate nav item after merge

* Migrate banner state to state provider framework

* [AC-2078] Fix device approvals header

* [PM-5861] Hide AC from product switcher for users that do not have access

* [PM-5860] Fix Vault and Send page headers

* [AC-2075] Fix missing link on reporting nav group

* [AC-2079] Hide Payment Method and Billing History pages for self-hosted instances

* [AC-2090] Hide reports/event log nav items for users that do not have permission

* [AC-2092] Fix missing provider portal option in product switcher on page load

* Add null check for organization in org layout component

* [AC-2094] Fix missing page header for new client orgs page

* [AC-2093] Update New client button styling

* Fix failing test after merge

* [PM-2087] Use disk-local for web layout banner

* [PM-6041] Update banner copy to read "web app"

* [PM-6094] Update banner link to marketing URL

* [PM-6114] add CL container component to VVR pages (#7802)

* create bit-container component

* add container to all page components

* Fix linting errors after merge with main

* Fix product switcher stories

* Fix web-header stories

* mock org state properly in product switcher stories (#7956)

* refactor: move web layout migration banner logic into a service (#7958)

* make CL codeowner of web header files

* move migration banner logic to service; update stories

* [PM-5862] Ensure a sync has run before hiding navigation links

* Remove leftover banner global state

* Re-add dropped selfHosted ngIf

* Add rel noreferrer

* Remove comment

---------

Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: Will Martin <contact@willmartian.com>
This commit is contained in:
Oscar Hinton
2024-02-23 18:22:45 +01:00
committed by GitHub
parent a31e3bf842
commit 38d8fbdb5a
128 changed files with 4228 additions and 4647 deletions

1
.github/CODEOWNERS vendored
View File

@@ -88,6 +88,7 @@ libs/common/src/autofill @bitwarden/team-autofill-dev
## Component Library ##
.storybook @bitwarden/team-component-library
libs/components @bitwarden/team-component-library
apps/web/src/app/layouts/header
## Desktop native module ##
apps/desktop/desktop_native @bitwarden/team-platform-dev

View File

@@ -1,67 +0,0 @@
<div *ngIf="loaded && activeOrganization != null" class="tw-flex">
<button
class="tw-flex tw-items-center tw-border-none tw-bg-background-alt"
type="button"
id="pickerButton"
[appA11yTitle]="'organizationPicker' | i18n"
[bitMenuTriggerFor]="orgPickerMenu"
>
<bit-avatar [text]="activeOrganization.name"></bit-avatar>
<div class="tw-flex">
<div class="org-name tw-ml-3">
<span>{{ activeOrganization.name }}</span>
<small class="tw-text-muted">{{ "organization" | i18n }}</small>
</div>
<div class="tw-ml-3">
<i class="bwi bwi-angle-down tw-text-main" aria-hidden="true"></i>
</div>
</div>
</button>
<div>
<div
class="tw-ml-3 tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-text-danger"
*ngIf="!activeOrganization.enabled"
>
<div class="tw-px-5 tw-py-2">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "organizationIsDisabled" | i18n }}
</div>
</div>
<div
class="tw-ml-3 tw-rounded tw-border tw-border-solid tw-border-info-500 tw-text-info"
*ngIf="activeOrganization.isProviderUser"
>
<div class="tw-px-5 tw-py-2">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "accessingUsingProvider" | i18n: activeOrganization.providerName }}
</div>
</div>
</div>
<bit-menu #orgPickerMenu>
<ul aria-labelledby="pickerButton" class="tw-m-0 tw-p-0">
<li
*ngFor="let org of organizations$ | async"
class="tw-flex tw-list-none tw-flex-col"
role="none"
>
<a bitMenuItem [routerLink]="['/organizations', org.id]">
<i
class="bwi bwi-check mr-2"
[ngClass]="org.id === activeOrganization.id ? 'visible' : 'invisible'"
>
<span class="tw-sr-only">{{ "currentOrganization" | i18n }}</span>
</i>
{{ org.name }}
</a>
</li>
<bit-menu-divider></bit-menu-divider>
<li class="tw-list-none" role="none">
<a bitMenuItem routerLink="/create-organization">
<i class="bwi bwi-plus mr-2"></i>
{{ "newOrganization" | i18n }}</a
>
</li>
</ul>
</bit-menu>
</div>

View File

@@ -1,35 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { map, Observable } from "rxjs";
import {
canAccessAdmin,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@Component({
selector: "app-organization-switcher",
templateUrl: "organization-switcher.component.html",
})
export class OrganizationSwitcherComponent implements OnInit {
constructor(
private organizationService: OrganizationService,
private i18nService: I18nService,
) {}
@Input() activeOrganization: Organization = null;
organizations$: Observable<Organization[]>;
loaded = false;
async ngOnInit() {
this.organizations$ = this.organizationService.memberOrganizations$.pipe(
canAccessAdmin(this.i18nService),
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))),
);
this.loaded = true;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,41 +1,125 @@
<app-navbar></app-navbar>
<app-payment-method-banners *ngIf="false"></app-payment-method-banners>
<div class="org-nav !tw-h-32" *ngIf="organization$ | async as organization">
<div class="container d-flex">
<div class="d-flex flex-column">
<app-organization-switcher
class="my-auto pl-1"
[activeOrganization]="organization"
></app-organization-switcher>
<bit-tab-nav-bar class="-tw-mb-px">
<bit-tab-link
*ngIf="canShowVaultTab(organization) && organization.flexibleCollections; else vaultTab"
<bit-layout variant="secondary">
<nav slot="sidebar" *ngIf="organization$ | async as organization">
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block">
<bit-icon [icon]="logo"></bit-icon>
</a>
<org-switcher [filter]="orgFilter"></org-switcher>
<bit-nav-item
icon="bwi-collection"
[text]="organization.flexibleCollections ? 'collections' : ('vault' | i18n)"
route="vault"
>{{ "collections" | i18n }}</bit-tab-link
*ngIf="canShowVaultTab(organization)"
>
<ng-template #vaultTab>
<bit-tab-link *ngIf="canShowVaultTab(organization)" route="vault">{{
"vault" | i18n
}}</bit-tab-link>
</ng-template>
<bit-tab-link *ngIf="canShowMembersTab(organization)" route="members">{{
"members" | i18n
}}</bit-tab-link>
<bit-tab-link *ngIf="canShowGroupsTab(organization)" route="groups">{{
"groups" | i18n
}}</bit-tab-link>
<bit-tab-link *ngIf="canShowReportsTab(organization)" route="reporting">
{{ getReportTabLabel(organization) | i18n }}
</bit-tab-link>
<bit-tab-link *ngIf="canShowBillingTab(organization)" route="billing">{{
"billing" | i18n
}}</bit-tab-link>
<bit-tab-link *ngIf="canShowSettingsTab(organization)" route="settings">{{
"settings" | i18n
}}</bit-tab-link>
</bit-tab-nav-bar>
</div>
</div>
</div>
<router-outlet></router-outlet>
<app-footer></app-footer>
</bit-nav-item>
<bit-nav-item
icon="bwi-user"
[text]="'members' | i18n"
route="members"
*ngIf="canShowMembersTab(organization)"
></bit-nav-item>
<bit-nav-item
icon="bwi-users"
[text]="'groups' | i18n"
route="groups"
*ngIf="canShowGroupsTab(organization)"
></bit-nav-item>
<bit-nav-group
icon="bwi-sliders"
[text]="getReportTabLabel(organization) | i18n"
route="reporting"
*ngIf="canShowReportsTab(organization)"
>
<bit-nav-item
[text]="'eventLogs' | i18n"
route="reporting/events"
*ngIf="organization.canAccessEventLogs"
></bit-nav-item>
<bit-nav-item
[text]="'reports' | i18n"
route="reporting/reports"
*ngIf="organization.canAccessReports"
></bit-nav-item>
</bit-nav-group>
<bit-nav-group
icon="bwi-billing"
[text]="'billing' | i18n"
route="billing"
*ngIf="canShowBillingTab(organization)"
>
<bit-nav-item [text]="'subscription' | i18n" route="billing/subscription"></bit-nav-item>
<ng-container *ngIf="showPaymentAndHistory$ | async">
<bit-nav-item [text]="'paymentMethod' | i18n" route="billing/payment-method"></bit-nav-item>
<bit-nav-item [text]="'billingHistory' | i18n" route="billing/history"></bit-nav-item>
</ng-container>
</bit-nav-group>
<bit-nav-group
icon="bwi-cog"
[text]="'settings' | i18n"
route="settings"
*ngIf="canShowSettingsTab(organization)"
>
<bit-nav-item
[text]="'organizationInfo' | i18n"
route="settings/account"
*ngIf="organization.isOwner"
></bit-nav-item>
<bit-nav-item
[text]="'policies' | i18n"
route="settings/policies"
*ngIf="organization.canManagePolicies"
></bit-nav-item>
<bit-nav-item
[text]="'twoStepLogin' | i18n"
route="settings/two-factor"
*ngIf="organization.use2fa && organization.isOwner"
></bit-nav-item>
<bit-nav-item
[text]="'importData' | i18n"
route="settings/tools/import"
*ngIf="organization.canAccessImportExport"
></bit-nav-item>
<bit-nav-item
[text]="'exportVault' | i18n"
route="settings/tools/export"
*ngIf="organization.canAccessImportExport"
></bit-nav-item>
<bit-nav-item
[text]="'domainVerification' | i18n"
route="settings/domain-verification"
*ngIf="organization?.canManageDomainVerification"
></bit-nav-item>
<bit-nav-item
[text]="'singleSignOn' | i18n"
route="settings/sso"
*ngIf="organization.canManageSso"
></bit-nav-item>
<bit-nav-item
[text]="'deviceApprovals' | i18n"
route="settings/device-approvals"
*ngIf="organization.canManageDeviceApprovals"
></bit-nav-item>
<bit-nav-item
[text]="'scim' | i18n"
route="settings/scim"
*ngIf="organization.canManageScim"
></bit-nav-item>
</bit-nav-group>
</nav>
<ng-container *ngIf="organization$ | async as organization">
<bit-banner
*ngIf="organization.isProviderUser"
[showClose]="false"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
>
{{ "accessingUsingProvider" | i18n: organization.providerName }}
</bit-banner>
<app-payment-method-banners
*ngIf="false"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
></app-payment-method-banners>
</ng-container>
<router-outlet></router-outlet>
</bit-layout>

View File

@@ -1,7 +1,9 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
canAccessBillingTab,
canAccessGroupsTab,
@@ -13,19 +15,43 @@ import {
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
import { PaymentMethodBannersComponent } from "../../../components/payment-method-banners/payment-method-banners.component";
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
@Component({
selector: "app-organization-layout",
templateUrl: "organization-layout.component.html",
standalone: true,
imports: [
CommonModule,
RouterModule,
JslibModule,
LayoutComponent,
IconModule,
NavigationModule,
OrgSwitcherComponent,
BannerModule,
PaymentMethodBannersComponent,
],
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
protected readonly logo = AdminConsoleLogo;
protected orgFilter = (org: Organization) => org.isAdmin;
organization$: Observable<Organization>;
showPaymentAndHistory$: Observable<boolean>;
private _destroy = new Subject<void>();
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
) {}
async ngOnInit() {
@@ -41,6 +67,15 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
.pipe(getOrganizationById(id));
}),
);
this.showPaymentAndHistory$ = this.organization$.pipe(
map(
(org) =>
!this.platformUtilsService.isSelfHost() &&
org?.canViewBillingHistory &&
org?.canEditPaymentMethods,
),
);
}
ngOnDestroy() {

View File

@@ -1,5 +1,6 @@
<app-header></app-header>
<div class="tw-mb-4">
<h1>{{ "eventLogs" | i18n }}</h1>
<div class="tw-mt-4 tw-flex tw-items-center">
<bit-form-field>
<bit-label>{{ "from" | i18n }}</bit-label>

View File

@@ -1,35 +1,24 @@
<div class="container page-content">
<div class="tw-mb-4 tw-flex">
<h1>{{ "groups" | i18n }}</h1>
<div class="tw-ml-auto tw-flex tw-items-center">
<div class="tw-mr-2">
<label class="sr-only">{{ "search" | i18n }}</label>
<div class="tw-flex tw-items-center">
<i class="bwi bwi-search bwi-fw tw-z-20 -tw-mr-7 tw-text-muted" aria-hidden="true"></i>
<input
bitInput
type="search"
placeholder="{{ 'searchGroups' | i18n }}"
class="tw-rounded-l tw-pl-9"
<app-header>
<bit-search
[placeholder]="'searchGroups' | i18n"
[(ngModel)]="searchText"
/>
</div>
</div>
class="tw-w-80"
></bit-search>
<button bitButton type="button" buttonType="primary" (click)="add()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newGroup" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
</app-header>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="!loading && visibleGroups">
</ng-container>
<ng-container *ngIf="!loading && visibleGroups">
<p *ngIf="!visibleGroups.length">{{ "noGroupsInList" | i18n }}</p>
<bit-table
*ngIf="visibleGroups.length"
@@ -121,6 +110,5 @@
</tr>
</ng-template>
</bit-table>
</ng-container>
<ng-template #addEdit></ng-template>
</div>
</ng-container>
<ng-template #addEdit></ng-template>

View File

@@ -1,7 +1,17 @@
<div class="container page-content">
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
<h1>{{ "members" | i18n }}</h1>
<div class="tw-flex tw-items-center tw-justify-end tw-space-x-3">
<app-header>
<bit-search
class="tw-grow"
[(ngModel)]="searchText"
[placeholder]="'searchMembers' | i18n"
></bit-search>
<button type="button" bitButton buttonType="primary" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
</app-header>
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
<bit-toggle-group
[selected]="status"
(selectedChange)="filter($event)"
@@ -26,35 +36,23 @@
<span bitBadge variant="info" *ngIf="revokedCount">{{ revokedCount }}</span>
</bit-toggle>
</bit-toggle-group>
<bit-search
class="tw-grow"
[(ngModel)]="searchText"
[placeholder]="'searchMembers' | i18n"
></bit-search>
<button type="button" bitButton buttonType="primary" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
</ng-container>
<ng-container
*ngIf="
!loading &&
(isPaging()
? pagedUsers
: (users | search: searchText : 'name' : 'email' : 'id')) as searchedUsers
"
>
>
<p *ngIf="!searchedUsers.length">{{ "noMembersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
@@ -256,9 +254,7 @@
</span>
</button>
<bit-menu-divider
*ngIf="
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
"
*ngIf="u.status === userStatusType.Accepted || u.status === userStatusType.Invited"
></bit-menu-divider>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
@@ -320,12 +316,11 @@
</ng-template>
</bit-table>
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #resetPasswordTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>
<ng-template #bulkRemoveTemplate></ng-template>
</div>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #resetPasswordTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>
<ng-template #bulkRemoveTemplate></ng-template>

View File

@@ -1,5 +1,7 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "../../shared";
import { CoreOrganizationModule } from "./core";
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
import { GroupsComponent } from "./manage/groups.component";
@@ -13,6 +15,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
AccessSelectorModule,
CoreOrganizationModule,
OrganizationsRoutingModule,
LooseComponentsModule,
],
declarations: [GroupsComponent, GroupAddEditComponent],
})

View File

@@ -1,15 +1,15 @@
<div class="page-header d-flex">
<h1>{{ "policies" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<app-header></app-header>
<bit-container>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<table class="table table-hover table-list" *ngIf="!loading">
</ng-container>
<table class="table table-hover table-list" *ngIf="!loading">
<tbody>
<tr *ngFor="let p of policies">
<td *ngIf="p.display(organization)">
@@ -21,5 +21,6 @@
</td>
</tr>
</tbody>
</table>
<ng-template #editTemplate></ng-template>
</table>
<ng-template #editTemplate></ng-template>
</bit-container>

View File

@@ -14,13 +14,11 @@ import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
import { OrganizationRedirectGuard } from "../guards/org-redirect.guard";
import { EventsComponent } from "../manage/events.component";
import { ReportingComponent } from "./reporting.component";
import { ReportsHomeComponent } from "./reports-home.component";
const routes: Routes = [
{
path: "",
component: ReportingComponent,
canActivate: [OrganizationPermissionsGuard],
data: { organizationPermissions: canAccessReportingTab },
children: [

View File

@@ -1,14 +1,19 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "../../../shared";
import { SharedModule } from "../../../shared/shared.module";
import { ReportsSharedModule } from "../../../tools/reports";
import { OrganizationReportingRoutingModule } from "./organization-reporting-routing.module";
import { ReportingComponent } from "./reporting.component";
import { ReportsHomeComponent } from "./reports-home.component";
@NgModule({
imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule],
declarations: [ReportsHomeComponent, ReportingComponent],
imports: [
SharedModule,
ReportsSharedModule,
OrganizationReportingRoutingModule,
LooseComponentsModule,
],
declarations: [ReportsHomeComponent],
})
export class OrganizationReportingModule {}

View File

@@ -1,30 +0,0 @@
<div class="container page-content">
<div class="row">
<div class="col-3" *ngIf="showLeftNav$ | async">
<div class="card" *ngIf="organization$ | async as org">
<div class="card-header">{{ "reporting" | i18n }}</div>
<div class="list-group list-group-flush">
<a
routerLink="events"
class="list-group-item"
routerLinkActive="active"
*ngIf="org.canAccessEventLogs"
>
{{ "eventLogs" | i18n }}
</a>
<a
routerLink="reports"
class="list-group-item"
routerLinkActive="active"
*ngIf="org.canAccessReports"
>
{{ "reports" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9" [ngClass]="(showLeftNav$ | async) ? 'col-9' : 'col-12'">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,7 +1,5 @@
<ng-container *ngIf="homepage$ | async">
<div class="page-header">
<h1>{{ "reports" | i18n }}</h1>
</div>
<app-header></app-header>
<p>{{ "orgsReportsDesc" | i18n }}</p>

View File

@@ -1,13 +1,15 @@
<h1 bitTypography="h1" class="tw-pb-2.5">{{ "organizationInfo" | i18n }}</h1>
<div *ngIf="loading">
<app-header></app-header>
<bit-container>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<form *ngIf="org && !loading" [bitSubmit]="submit" [formGroup]="formGroup">
</div>
<form *ngIf="org && !loading" [bitSubmit]="submit" [formGroup]="formGroup">
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
<div>
<bit-form-field>
@@ -36,8 +38,8 @@
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "save" | i18n }}
</button>
</form>
<ng-container *ngIf="canUseApi">
</form>
<ng-container *ngIf="canUseApi">
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
<p>
{{ "apiKeyDesc" | i18n }}
@@ -51,12 +53,12 @@
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
{{ "rotateApiKey" | i18n }}
</button>
</ng-container>
<form
</ng-container>
<form
*ngIf="
org && !loading && !org.flexibleCollections && (flexibleCollectionsMigrationEnabled$ | async)
"
>
>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">
{{ "collectionManagement" | i18n }}
</h1>
@@ -74,12 +76,12 @@
>
{{ "enable" | i18n }}
</button>
</form>
<form
</form>
<form
*ngIf="org && !loading && org.flexibleCollections"
[bitSubmit]="submitCollectionManagement"
[formGroup]="collectionManagementFormGroup"
>
>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "collectionManagement" | i18n }}</h1>
<p>{{ "collectionManagementDesc" | i18n }}</p>
<bit-form-control *ngIf="flexibleCollectionsV1Enabled$ | async">
@@ -100,17 +102,18 @@
>
{{ "save" | i18n }}
</button>
</form>
</form>
<app-danger-zone>
<app-danger-zone>
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
{{ "deleteOrganization" | i18n }}
</button>
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
{{ "purgeVault" | i18n }}
</button>
</app-danger-zone>
</app-danger-zone>
<ng-template #purgeOrganizationTemplate></ng-template>
<ng-template #apiKeyTemplate></ng-template>
<ng-template #rotateApiKeyTemplate></ng-template>
<ng-template #purgeOrganizationTemplate></ng-template>
<ng-template #apiKeyTemplate></ng-template>
<ng-template #rotateApiKeyTemplate></ng-template>
</bit-container>

View File

@@ -0,0 +1,21 @@
<app-header></app-header>
<bit-container>
<tools-import
(formDisabled)="this.disabled = $event"
(formLoading)="this.loading = $event"
(onSuccessfulImport)="this.onSuccessfulImport($event)"
organizationId="{{ routeOrgId }}"
></tools-import>
<button
[disabled]="disabled"
[loading]="loading"
form="import_form_importForm"
bitButton
type="submit"
bitFormButton
buttonType="primary"
>
{{ "importData" | i18n }}
</button>
</bit-container>

View File

@@ -0,0 +1,56 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import {
canAccessVaultTab,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ImportCollectionServiceAbstraction } from "@bitwarden/importer/core";
import { ImportComponent } from "@bitwarden/importer/ui";
import { LooseComponentsModule, SharedModule } from "../../../shared";
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
@Component({
templateUrl: "org-import.component.html",
standalone: true,
imports: [SharedModule, ImportComponent, LooseComponentsModule],
providers: [
{
provide: ImportCollectionServiceAbstraction,
useClass: ImportCollectionAdminService,
deps: [CollectionAdminService],
},
],
})
export class OrgImportComponent implements OnInit {
protected routeOrgId: string = null;
protected loading = false;
protected disabled = false;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private router: Router,
) {}
ngOnInit(): void {
this.routeOrgId = this.route.snapshot.paramMap.get("organizationId");
}
/**
* Callback that is called after a successful import.
*/
protected async onSuccessfulImport(organizationId: string): Promise<void> {
const organization = await firstValueFrom(this.organizationService.get$(organizationId));
if (organization == null) {
return;
}
if (canAccessVaultTab(organization)) {
await this.router.navigate(["organizations", organizationId, "vault"]);
}
}
}

View File

@@ -9,13 +9,11 @@ import { OrganizationRedirectGuard } from "../../organizations/guards/org-redire
import { PoliciesComponent } from "../../organizations/policies";
import { AccountComponent } from "./account.component";
import { SettingsComponent } from "./settings.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
const routes: Routes = [
{
path: "",
component: SettingsComponent,
canActivate: [OrganizationPermissionsGuard],
data: { organizationPermissions: canAccessSettingsTab },
children: [
@@ -49,9 +47,7 @@ const routes: Routes = [
{
path: "import",
loadComponent: () =>
import("../../../tools/import/admin-import.component").then(
(mod) => mod.AdminImportComponent,
),
import("./org-import.component").then((mod) => mod.OrgImportComponent),
canActivate: [OrganizationPermissionsGuard],
data: {
titleId: "importData",
@@ -64,6 +60,9 @@ const routes: Routes = [
import("../tools/vault-export/org-vault-export.module").then(
(m) => m.OrganizationVaultExportModule,
),
data: {
titleId: "exportVault",
},
},
],
},

View File

@@ -6,7 +6,6 @@ import { PoliciesModule } from "../../organizations/policies";
import { AccountComponent } from "./account.component";
import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module";
import { SettingsComponent } from "./settings.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
@NgModule({
@@ -17,6 +16,6 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component";
OrganizationSettingsRoutingModule,
AccountFingerprintComponent,
],
declarations: [SettingsComponent, AccountComponent, TwoFactorSetupComponent],
declarations: [AccountComponent, TwoFactorSetupComponent],
})
export class OrganizationSettingsModule {}

View File

@@ -1,79 +0,0 @@
<div class="container page-content">
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="!loading">
<div class="row">
<div class="col-3">
<div class="card mb-4" *ngIf="organization.canAccessImportExport">
<div class="card-header">{{ "tools" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="import" class="list-group-item" routerLinkActive="active">
{{ "importData" | i18n }}
</a>
<a routerLink="export" class="list-group-item" routerLinkActive="active">
{{ "exportVault" | i18n }}
</a>
</div>
</div>
<div class="card" *ngIf="organization.canAccessReports">
<div class="card-header d-flex">
{{ "reports" | i18n }}
<div class="ml-auto">
<a
href="#"
appStopClick
bitBadge
*ngIf="!accessReports"
(click)="upgradeOrganization()"
>
{{ "upgrade" | i18n }}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a
routerLink="exposed-passwords-report"
class="list-group-item"
routerLinkActive="active"
>
{{ "exposedPasswordsReport" | i18n }}
</a>
<a
routerLink="reused-passwords-report"
class="list-group-item"
routerLinkActive="active"
>
{{ "reusedPasswordsReport" | i18n }}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{ "weakPasswordsReport" | i18n }}
</a>
<a
routerLink="unsecured-websites-report"
class="list-group-item"
routerLinkActive="active"
>
{{ "unsecuredWebsitesReport" | i18n }}
</a>
<a
routerLink="inactive-two-factor-report"
class="list-group-item"
routerLinkActive="active"
>
{{ "inactive2faReport" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</ng-container>
</div>

View File

@@ -1,8 +1,6 @@
<app-navbar></app-navbar>
<div class="container page-content">
<div class="page-header d-flex">
<h1>{{ "providers" | i18n }}</h1>
</div>
<app-header></app-header>
<bit-container>
<p *ngIf="!loaded" class="text-muted">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
@@ -29,5 +27,4 @@
</tbody>
</table>
</ng-container>
</div>
<app-footer></app-footer>
</bit-container>

View File

@@ -1,11 +1,6 @@
<div class="container page-content">
<div class="row">
<div class="col-12">
<div class="page-header">
<h1>{{ "newOrganization" | i18n }}</h1>
</div>
<app-header></app-header>
<bit-container>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
</div>
</div>
</div>
</bit-container>

View File

@@ -6,12 +6,13 @@ import { PlanType } from "@bitwarden/common/billing/enums";
import { ProductType } from "@bitwarden/common/enums";
import { OrganizationPlansComponent } from "../../billing";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
@Component({
templateUrl: "create-organization.component.html",
standalone: true,
imports: [SharedModule, OrganizationPlansComponent],
imports: [SharedModule, OrganizationPlansComponent, HeaderModule],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CreateOrganizationComponent implements OnInit {

View File

@@ -1,11 +1,11 @@
<div class="page-header">
<h1>{{ "sponsoredFamilies" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<app-header></app-header>
<bit-container>
<ng-container *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="!loading">
</ng-container>
<ng-container *ngIf="!loading">
<p>
{{ "sponsoredFamiliesEligible" | i18n }}
</p>
@@ -101,4 +101,5 @@
</div>
<small>{{ "sponsoredFamiliesLeaveCopy" | i18n }}</small>
</ng-container>
</ng-container>
</ng-container>
</bit-container>

View File

@@ -207,11 +207,6 @@ export class AppComponent implements OnDestroy, OnInit {
case "showToast":
this.showToast(message);
break;
case "setFullWidth":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setFullWidth();
break;
case "convertAccountToKeyConnector":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -243,10 +238,6 @@ export class AppComponent implements OnDestroy, OnInit {
new DisableSendPolicy(),
new SendOptionsPolicy(),
]);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setFullWidth();
}
ngOnDestroy() {
@@ -356,13 +347,4 @@ export class AppComponent implements OnDestroy, OnInit {
this.notificationsService.reconnectFromActivity();
}
}
private async setFullWidth() {
const enableFullWidth = await this.stateService.getEnableFullWidth();
if (enableFullWidth) {
document.body.classList.add("full-width");
} else {
document.body.classList.remove("full-width");
}
}
}

View File

@@ -88,9 +88,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
ssoLoginService,
webAuthnLoginService,
);
this.onSuccessfulLogin = async () => {
this.messagingService.send("setFullWidth");
};
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
this.showPasswordless = flagEnabled("showPasswordless");
}

View File

@@ -1,14 +1,14 @@
<div class="page-header">
<h1>{{ "myAccount" | i18n }}</h1>
</div>
<app-profile></app-profile>
<app-header></app-header>
<div *ngIf="showChangeEmail" class="tw-mt-16">
<bit-container>
<app-profile></app-profile>
<div *ngIf="showChangeEmail" class="tw-mt-16">
<h1 bitTypography="h1">{{ "changeEmail" | i18n }}</h1>
<app-change-email></app-change-email>
</div>
</div>
<app-danger-zone>
<app-danger-zone>
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
{{ "deauthorizeSessions" | i18n }}
</button>
@@ -18,10 +18,11 @@
<button type="button" bitButton buttonType="danger" (click)="deleteAccount()">
{{ "deleteAccount" | i18n }}
</button>
</app-danger-zone>
</app-danger-zone>
<ng-template #deauthorizeSessionsTemplate></ng-template>
<ng-template #purgeVaultTemplate></ng-template>
<ng-template #deleteAccountTemplate></ng-template>
<ng-template #viewUserApiKeyTemplate></ng-template>
<ng-template #rotateUserApiKeyTemplate></ng-template>
<ng-template #deauthorizeSessionsTemplate></ng-template>
<ng-template #purgeVaultTemplate></ng-template>
<ng-template #deleteAccountTemplate></ng-template>
<ng-template #viewUserApiKeyTemplate></ng-template>
<ng-template #rotateUserApiKeyTemplate></ng-template>
</bit-container>

View File

@@ -1,18 +1,18 @@
<div class="page-header">
<h1>{{ "emergencyAccess" | i18n }}</h1>
</div>
<p>
<app-header></app-header>
<bit-container>
<p>
{{ "emergencyAccessDesc" | i18n }}
<a href="https://bitwarden.com/help/emergency-access/" target="_blank" rel="noreferrer">
{{ "learnMore" | i18n }}.
</a>
</p>
</p>
<p *ngIf="isOrganizationOwner">
<p *ngIf="isOrganizationOwner">
<b>{{ "warning" | i18n }}:</b> {{ "emergencyAccessOwnerWarning" | i18n }}
</p>
</p>
<div class="page-header d-flex">
<div class="page-header d-flex">
<h2>
{{ "trustedEmergencyContacts" | i18n }}
<app-premium-badge></app-premium-badge>
@@ -28,9 +28,12 @@
{{ "addEmergencyContact" | i18n }}
</button>
</div>
</div>
</div>
<table class="table table-hover table-list mb-0" *ngIf="trustedContacts && trustedContacts.length">
<table
class="table table-hover table-list mb-0"
*ngIf="trustedContacts && trustedContacts.length"
>
<tbody>
<tr *ngFor="let c of trustedContacts; let i = index">
<td width="30">
@@ -43,12 +46,18 @@
</td>
<td>
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span bitBadge variant="secondary" *ngIf="c.status === emergencyAccessStatusType.Invited">{{
"invited" | i18n
}}</span>
<span bitBadge variant="warning" *ngIf="c.status === emergencyAccessStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span
bitBadge
variant="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
variant="warning"
@@ -123,9 +132,9 @@
</td>
</tr>
</tbody>
</table>
</table>
<ng-container *ngIf="!trustedContacts || !trustedContacts.length">
<ng-container *ngIf="!trustedContacts || !trustedContacts.length">
<p *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
<ng-container *ngIf="!loaded">
<i
@@ -135,13 +144,16 @@
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container>
</ng-container>
<div class="page-header spaced-header">
<div class="page-header spaced-header">
<h2>{{ "designatedEmergencyContacts" | i18n }}</h2>
</div>
</div>
<table class="table table-hover table-list mb-0" *ngIf="grantedContacts && grantedContacts.length">
<table
class="table table-hover table-list mb-0"
*ngIf="grantedContacts && grantedContacts.length"
>
<tbody>
<tr *ngFor="let c of grantedContacts; let i = index">
<td width="30">
@@ -157,9 +169,12 @@
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
"invited" | i18n
}}</span>
<span bitBadge variant="warning" *ngIf="c.status === emergencyAccessStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
variant="warning"
@@ -231,9 +246,9 @@
</td>
</tr>
</tbody>
</table>
</table>
<ng-container *ngIf="!grantedContacts || !grantedContacts.length">
<ng-container *ngIf="!grantedContacts || !grantedContacts.length">
<p *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
<ng-container *ngIf="!loaded">
<i
@@ -243,7 +258,8 @@
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container>
</ng-container>
</bit-container>
<ng-template #addEdit></ng-template>
<ng-template #takeoverTemplate></ng-template>

View File

@@ -1,22 +1,11 @@
<div class="tabbed-nav d-flex flex-column">
<ul class="nav nav-tabs">
<ng-container *ngIf="showChangePassword">
<li class="nav-item">
<a class="nav-link" routerLink="change-password" routerLinkActive="active">
{{ "masterPassword" | i18n }}
</a>
</li>
</ng-container>
<li class="nav-item">
<a class="nav-link" routerLink="two-factor" routerLinkActive="active">
{{ "twoStepLogin" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="security-keys" routerLinkActive="active">
{{ "keys" | i18n }}
</a>
</li>
</ul>
</div>
<router-outlet></router-outlet>
<app-header>
<bit-tab-nav-bar slot="tabs">
<bit-tab-link route="change-password">{{ "masterPassword" | i18n }}</bit-tab-link>
<bit-tab-link route="two-factor">{{ "twoStepLogin" | i18n }}</bit-tab-link>
<bit-tab-link route="security-keys">{{ "keys" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>
</app-header>
<bit-container>
<router-outlet></router-outlet>
</bit-container>

View File

@@ -1,9 +1,13 @@
<div [ngClass]="tabbedHeader ? 'tabbed-header' : 'page-header'">
<app-header *ngIf="organizationId != null"></app-header>
<bit-container>
<div class="tabbed-header" *ngIf="organizationId == null">
<h1 *ngIf="!organizationId || !isEnterpriseOrg">{{ "twoStepLogin" | i18n }}</h1>
<h1 *ngIf="organizationId && isEnterpriseOrg">{{ "twoStepLoginEnforcement" | i18n }}</h1>
</div>
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
<ng-container *ngIf="organizationId">
</div>
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
<ng-container *ngIf="organizationId">
<p>
<ng-container *ngIf="isEnterpriseOrg; else teamsDescription">
{{ "twoStepLoginEnterpriseDescStart" | i18n }}
@@ -20,14 +24,14 @@
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
</ng-template>
</p>
</ng-container>
<bit-callout type="warning" *ngIf="!organizationId">
</ng-container>
<bit-callout type="warning" *ngIf="!organizationId">
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
<button type="button" bitButton buttonType="secondary" (click)="recoveryCode()">
{{ "viewRecoveryCode" | i18n }}
</button>
</bit-callout>
<h2 [ngClass]="{ 'mt-5': !organizationId }">
</bit-callout>
<h2 [ngClass]="{ 'mt-5': !organizationId }">
{{ "providers" | i18n }}
<small *ngIf="loading">
<i
@@ -37,11 +41,11 @@
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h2>
<bit-callout type="warning" *ngIf="showPolicyWarning">
</h2>
<bit-callout type="warning" *ngIf="showPolicyWarning">
{{ "twoStepLoginPolicyUserWarning" | i18n }}
</bit-callout>
<ul class="list-group list-group-2fa">
</bit-callout>
<ul class="list-group list-group-2fa">
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
<div class="logo-2fa d-flex justify-content-center">
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
@@ -73,7 +77,8 @@
</button>
</div>
</li>
</ul>
</ul>
</bit-container>
<ng-template #authenticatorTemplate></ng-template>
<ng-template #recoveryTemplate></ng-template>

View File

@@ -20,6 +20,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { RouterService } from "../../core";
import { SharedModule } from "../../shared";
import { TrialInitiationComponent } from "./trial-initiation.component";
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
@@ -46,6 +47,7 @@ describe("TrialInitiationComponent", () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({
imports: [
SharedModule,
RouterTestingModule.withRoutes([
{ path: "trial", component: TrialInitiationComponent },
{

View File

@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
import { HeaderModule } from "../../layouts/header/header.module";
import { BillingSharedModule } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
@@ -9,7 +10,7 @@ import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@NgModule({
imports: [IndividualBillingRoutingModule, BillingSharedModule],
imports: [IndividualBillingRoutingModule, BillingSharedModule, HeaderModule],
declarations: [
SubscriptionComponent,
BillingHistoryViewComponent,

View File

@@ -1,20 +1,11 @@
<div class="tabbed-nav d-flex flex-column" *ngIf="!selfHosted">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" [routerLink]="subscriptionRoute" routerLinkActive="active">
{{ "subscription" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="payment-method" routerLinkActive="active">
{{ "paymentMethod" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="billing-history" routerLinkActive="active">
{{ "billingHistory" | i18n }}
</a>
</li>
</ul>
</div>
<router-outlet></router-outlet>
<app-header>
<bit-tab-nav-bar slot="tabs" *ngIf="!selfHosted">
<bit-tab-link [route]="subscriptionRoute">{{ "subscription" | i18n }}</bit-tab-link>
<bit-tab-link route="payment-method">{{ "paymentMethod" | i18n }}</bit-tab-link>
<bit-tab-link route="billing-history">{{ "billingHistory" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>
</app-header>
<bit-container>
<router-outlet></router-outlet>
</bit-container>

View File

@@ -1,7 +1,4 @@
<div class="d-flex page-header">
<h1>
{{ "billingHistory" | i18n }}
</h1>
<app-header>
<button
type="button"
bitButton
@@ -14,15 +11,18 @@
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
{{ "refresh" | i18n }}
</button>
</div>
<ng-container *ngIf="!firstLoaded && loading">
</app-header>
<bit-container>
<ng-container *ngIf="!firstLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="billing">
</ng-container>
<ng-container *ngIf="billing">
<app-billing-history [billing]="billing"></app-billing-history>
</ng-container>
</ng-container>
</bit-container>

View File

@@ -9,14 +9,12 @@ import { WebPlatformUtilsService } from "../../core/web-platform-utils.service";
import { PaymentMethodComponent } from "../shared";
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component";
const routes: Routes = [
{
path: "",
component: OrganizationBillingTabComponent,
canActivate: [OrganizationPermissionsGuard],
data: { organizationPermissions: canAccessBillingTab },
children: [

View File

@@ -1,33 +0,0 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{ "billing" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
{{ "subscription" | i18n }}
</a>
<a
*ngIf="showPaymentAndHistory$ | async"
routerLink="payment-method"
class="list-group-item"
routerLinkActive="active"
>
{{ "paymentMethod" | i18n }}
</a>
<a
*ngIf="showPaymentAndHistory$ | async"
routerLink="history"
class="list-group-item"
routerLinkActive="active"
>
{{ "billingHistory" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core";
import { UserVerificationModule } from "../../auth/shared/components/user-verification";
import { LooseComponentsModule } from "../../shared";
import { BillingSharedModule } from "../shared";
import { AdjustSubscription } from "./adjust-subscription.component";
@@ -10,7 +11,6 @@ import { ChangePlanComponent } from "./change-plan.component";
import { DownloadLicenseComponent } from "./download-license.component";
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
import { OrganizationBillingRoutingModule } from "./organization-billing-routing.module";
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
import { OrganizationPlansComponent } from "./organization-plans.component";
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component";
@@ -24,6 +24,7 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
UserVerificationModule,
BillingSharedModule,
OrganizationPlansComponent,
LooseComponentsModule,
],
declarations: [
AdjustSubscription,
@@ -31,7 +32,6 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
BillingSyncKeyComponent,
ChangePlanComponent,
DownloadLicenseComponent,
OrganizationBillingTabComponent,
OrganizationSubscriptionCloudComponent,
OrganizationSubscriptionSelfhostComponent,
OrgBillingHistoryViewComponent,

View File

@@ -1,27 +1,17 @@
<div class="tw-mb-2">
<h1 bitTypography="h1">
{{ "subscription" | i18n }}
<small *ngIf="firstLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<ng-container *ngIf="!firstLoaded && loading">
<app-header></app-header>
<bit-container>
<ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container>
<app-org-subscription-hidden
<app-org-subscription-hidden
*ngIf="firstLoaded && !userOrg.canViewSubscription"
[providerName]="userOrg.providerName"
></app-org-subscription-hidden>
></app-org-subscription-hidden>
<ng-container *ngIf="sub && firstLoaded">
<ng-container *ngIf="sub && firstLoaded">
<bit-callout
type="warning"
title="{{ 'canceled' | i18n }}"
@@ -269,4 +259,5 @@
</button>
</div>
</ng-container>
</ng-container>
</ng-container>
</bit-container>

View File

@@ -1,28 +1,17 @@
<div class="page-header">
<h1>
{{ "subscription" | i18n }}
<small *ngIf="firstLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<app-header></app-header>
<ng-container *ngIf="!firstLoaded && loading">
<bit-container>
<ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container>
<app-org-subscription-hidden
<app-org-subscription-hidden
*ngIf="firstLoaded && !userOrg.canViewSubscription"
[providerName]="userOrg.providerName"
></app-org-subscription-hidden>
></app-org-subscription-hidden>
<ng-container *ngIf="subscription && firstLoaded">
<ng-container *ngIf="subscription && firstLoaded">
<dl>
<dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ subscription.planName }}</dd>
@@ -118,7 +107,11 @@
</button>
</ng-container>
<bit-radio-button id="manual-upload" [value]="licenseOptions.UPLOAD" class="tw-mt-6 tw-block">
<bit-radio-button
id="manual-upload"
[value]="licenseOptions.UPLOAD"
class="tw-mt-6 tw-block"
>
<bit-label>{{ "manualUpload" | i18n }}</bit-label>
<bit-hint>
{{ "manualUploadDesc" | i18n }}
@@ -134,4 +127,5 @@
</ng-container>
</bit-radio-group>
</form>
</ng-container>
</ng-container>
</bit-container>

View File

@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { AddCreditComponent } from "./add-credit.component";
@@ -14,7 +15,7 @@ import { TaxInfoComponent } from "./tax-info.component";
import { UpdateLicenseComponent } from "./update-license.component";
@NgModule({
imports: [SharedModule, PaymentComponent, TaxInfoComponent],
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
declarations: [
AddCreditComponent,
AdjustPaymentComponent,

View File

@@ -1,7 +1,4 @@
<div class="d-flex" [ngClass]="headerClass">
<h1>
{{ "paymentMethod" | i18n }}
</h1>
<app-header *ngIf="organizationId">
<button
type="button"
bitButton
@@ -14,16 +11,23 @@
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
{{ "refresh" | i18n }}
</button>
</div>
<ng-container *ngIf="!firstLoaded && loading">
</app-header>
<bit-container>
<div class="tabbed-header" *ngIf="!organizationId">
<!-- TODO: Organization and individual should use different "page" components -->
<h1>{{ "paymentMethod" | i18n }}</h1>
</div>
<ng-container *ngIf="!firstLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="billing">
</ng-container>
<ng-container *ngIf="billing">
<h2>{{ (isCreditBalance ? "accountCredit" : "accountBalance") | i18n }}</h2>
<p class="text-lg">
<strong>{{ creditOrBalance | currency: "$" }}</strong>
@@ -84,7 +88,11 @@
class="btn-submit"
[disabled]="verifyForm.loading"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "verifyBankAccount" | i18n }}</span>
</button>
</form>
@@ -143,4 +151,5 @@
</button>
</form>
</ng-container>
</ng-container>
</ng-container>
</bit-container>

View File

@@ -3,8 +3,8 @@ import { combineLatest, Observable, switchMap } from "rxjs";
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
OrganizationService,
canAccessAdmin,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,9 +0,0 @@
<div class="container footer text-muted">
<div class="row">
<div class="col">&copy; {{ year }} Bitwarden Inc.</div>
<div class="col text-center"></div>
<div class="col text-right">
{{ "versionNumber" | i18n: version }}
</div>
</div>
</div>

View File

@@ -1,19 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Component({
selector: "app-footer",
templateUrl: "footer.component.html",
})
export class FooterComponent implements OnInit {
version: string;
year = "2015";
constructor(private platformUtilsService: PlatformUtilsService) {}
async ngOnInit() {
this.year = new Date().getFullYear().toString();
this.version = await this.platformUtilsService.getApplicationVersion();
}
}

View File

@@ -1,5 +1,7 @@
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";
@@ -7,7 +9,7 @@ import { ProductSwitcherModule } from "../product-switcher/product-switcher.modu
import { WebHeaderComponent } from "./web-header.component";
@NgModule({
imports: [SharedModule, DynamicAvatarComponent, ProductSwitcherModule],
imports: [SharedModule, DynamicAvatarComponent, ProductSwitcherModule, BannerModule],
declarations: [WebHeaderComponent],
exports: [WebHeaderComponent],
})

View File

@@ -1,3 +1,18 @@
<bit-banner
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
(onClose)="webLayoutMigrationBannerService.hideBanner()"
*ngIf="webLayoutMigrationBannerService.showBanner$ | async"
>
{{ "newWebApp" | i18n }}
<a
href="https://bitwarden.com/blog/bitwarden-design-updating-the-navigation-in-the-web-app"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>{{ "releaseBlog" | i18n }}</a
>
</bit-banner>
<header
*ngIf="routeData$ | async as routeData"
class="-tw-m-6 tw-mb-3 tw-flex tw-flex-col tw-p-6"

View File

@@ -9,6 +9,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountProfile } from "@bitwarden/common/platform/models/domain/account";
import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
@Component({
selector: "app-header",
templateUrl: "./web-header.component.html",
@@ -36,6 +38,7 @@ export class WebHeaderComponent {
private platformUtilsService: PlatformUtilsService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private messagingService: MessagingService,
protected webLayoutMigrationBannerService: WebLayoutMigrationBannerService,
) {
this.routeData$ = this.route.data.pipe(
map((params) => {

View File

@@ -1,14 +1,14 @@
import { CommonModule } from "@angular/common";
import { Component, Injectable, importProvidersFrom } from "@angular/core";
import { Component, importProvidersFrom, Injectable, Input } from "@angular/core";
import { RouterModule } from "@angular/router";
import {
Meta,
Story,
moduleMetadata,
applicationConfig,
componentWrapperDecorator,
Meta,
moduleMetadata,
Story,
} from "@storybook/angular";
import { BehaviorSubject, combineLatest, map } from "rxjs";
import { BehaviorSubject, combineLatest, map, of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@@ -22,16 +22,19 @@ import {
ButtonModule,
IconButtonModule,
IconModule,
InputModule,
MenuModule,
NavigationModule,
TabsModule,
TypographyModule,
InputModule,
} from "@bitwarden/components";
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
import { PreloadedEnglishI18nModule } from "../../core/tests";
import { WebHeaderComponent } from "../header/web-header.component";
import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
@Injectable({
providedIn: "root",
})
@@ -70,7 +73,7 @@ class MockProductSwitcher {}
standalone: true,
imports: [CommonModule, AvatarModule],
})
class MockDynamicAvatar {
class MockDynamicAvatar implements Partial<DynamicAvatarComponent> {
protected name$ = combineLatest([
this.stateService.accounts$,
this.stateService.activeAccount$,
@@ -79,6 +82,10 @@ class MockDynamicAvatar {
([accounts, activeAccount]) => accounts[activeAccount as keyof typeof accounts].profile.name,
),
);
@Input()
text: string;
constructor(private stateService: MockStateService) {}
}
@@ -107,6 +114,12 @@ export default {
declarations: [WebHeaderComponent, MockProductSwitcher],
providers: [
{ provide: StateService, useClass: MockStateService },
{
provide: WebLayoutMigrationBannerService,
useValue: {
showBanner$: of(false),
} as Partial<WebLayoutMigrationBannerService>,
},
{ provide: PlatformUtilsService, useClass: MockPlatformUtilsService },
{ provide: VaultTimeoutSettingsService, useClass: MockVaultTimeoutService },
{

View File

@@ -0,0 +1,29 @@
import { Injectable } from "@angular/core";
import {
GlobalStateProvider,
KeyDefinition,
NEW_WEB_LAYOUT_BANNER_DISK,
} from "@bitwarden/common/platform/state";
const SHOW_BANNER_KEY = new KeyDefinition<boolean>(NEW_WEB_LAYOUT_BANNER_DISK, "showBanner", {
deserializer: (b) => {
if (b === null) {
return true;
}
return b;
},
});
/** Displays a banner that introduces users to the new web vault layout. */
@Injectable({ providedIn: "root" })
export class WebLayoutMigrationBannerService {
private _showBannerState = this.globalStateProvider.get(SHOW_BANNER_KEY);
showBanner$ = this._showBannerState.state$;
constructor(private globalStateProvider: GlobalStateProvider) {}
async hideBanner() {
await this._showBannerState.update(() => false);
}
}

View File

@@ -1,100 +0,0 @@
<nav class="navbar navbar-expand navbar-dark" [ngClass]="{ 'nav-background-alt': selfHosted }">
<div class="container">
<a class="navbar-brand" routerLink="/" appA11yTitle="{{ 'bitWebVault' | i18n }}">
<i class="bwi bwi-shield" aria-hidden="true"></i>
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/vault">{{ "vaults" | i18n }}</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/sends">{{ "send" | i18n }}</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/tools">{{ "tools" | i18n }}</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/reports">{{ "reports" | i18n }}</a>
</li>
<li
*ngIf="(organizations$ | async)?.length >= 1"
class="nav-item"
routerLinkActive="active"
>
<a class="nav-link" [routerLink]="['/organizations', (organizations$ | async)[0].id]">{{
"organizations" | i18n
}}</a>
</li>
<ng-container *ngIf="providers.length >= 1">
<li class="nav-item" routerLinkActive="active" *ngIf="providers.length == 1">
<a class="nav-link" [routerLink]="['/providers', providers[0].id]">{{
"provider" | i18n
}}</a>
</li>
<li class="nav-item" routerLinkActive="active" *ngIf="providers.length > 1">
<a class="nav-link" routerLink="/providers">{{ "provider" | i18n }}</a>
</li>
</ng-container>
</ul>
</div>
<product-switcher buttonType="light"></product-switcher>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li>
<button
type="button"
[bitMenuTriggerFor]="accountMenu"
class="tw-border-0 tw-bg-transparent tw-text-alt2 tw-opacity-70 hover:tw-opacity-90"
attr.aria-label="{{ 'accountLoggedInAsName' | i18n: name }}"
>
<dynamic-avatar
[text]="name"
[id]="userId"
size="xsmall"
aria-hidden="true"
></dynamic-avatar>
<i class="bwi bwi-caret-down bwi-sm" aria-hidden="true"></i>
</button>
<bit-menu class="dropdown-menu" #accountMenu>
<div class="tw-flex tw-min-w-[200px] tw-max-w-[300px] tw-flex-col">
<div
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-info"
*ngIf="name"
appStopProp
>
<dynamic-avatar [text]="name" [id]="userId" size="small"></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 tw-text-muted">{{
name
}}</small>
</div>
</div>
<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>
<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>
</li>
</ul>
</div>
</nav>

View File

@@ -1,75 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { map, Observable } from "rxjs";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import {
canAccessAdmin,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "app-navbar",
templateUrl: "navbar.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class NavbarComponent implements OnInit {
selfHosted = false;
name: string;
email: string;
providers: Provider[] = [];
userId: string;
organizations$: Observable<Organization[]>;
canLock$: Observable<boolean>;
constructor(
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private tokenService: TokenService,
private providerService: ProviderService,
private syncService: SyncService,
private organizationService: OrganizationService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private i18nService: I18nService,
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
async ngOnInit() {
this.name = await this.tokenService.getName();
this.email = await this.tokenService.getEmail();
this.userId = await this.tokenService.getUserId();
if (this.name == null || this.name.trim() === "") {
this.name = this.email;
}
// Ensure providers and organizations are loaded
if ((await this.syncService.getLastSync()) == null) {
await this.syncService.fullSync(false);
}
this.providers = await this.providerService.getAll();
this.organizations$ = this.organizationService.memberOrganizations$.pipe(
canAccessAdmin(this.i18nService),
);
this.canLock$ = this.vaultTimeoutSettingsService
.availableVaultTimeoutActions$()
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
}
lock() {
this.messagingService.send("lockVault");
}
logOut() {
this.messagingService.send("logout");
}
}

View File

@@ -1,13 +1,18 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { combineLatest, map, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import type { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { NavigationModule } from "@bitwarden/components";
@Component({
selector: "org-switcher",
templateUrl: "org-switcher.component.html",
standalone: true,
imports: [CommonModule, JslibModule, NavigationModule],
})
export class OrgSwitcherComponent {
protected organizations$: Observable<Organization[]> =

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,12 @@
import { Component, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, map } from "rxjs";
import { combineLatest, concatMap } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import {
canAccessOrgAdmin,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { MenuComponent } from "@bitwarden/components";
type ProductSwitcherItem = {
@@ -44,7 +48,7 @@ export class ProductSwitcherContentComponent {
this.organizationService.organizations$,
this.route.paramMap,
]).pipe(
map(([orgs, paramMap]) => {
concatMap(async ([orgs, paramMap]) => {
const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
// If the active route org doesn't have access to SM, find the first org that does.
const smOrg =
@@ -52,17 +56,29 @@ export class ProductSwitcherContentComponent {
? routeOrg
: orgs.find((o) => o.canAccessSecretsManager && o.enabled == true);
// If the active route org doesn't have access to AC, find the first org that does.
const acOrg =
routeOrg != null && canAccessOrgAdmin(routeOrg) && routeOrg.enabled
? routeOrg
: orgs.find((o) => canAccessOrgAdmin(o) && o.enabled);
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
const providers = await this.providerService.getAll();
/**
* We can update this to the "satisfies" type upon upgrading to TypeScript 4.9
* https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#satisfies
*/
const products: Record<"pm" | "sm" | "orgs", ProductSwitcherItem> = {
const products: Record<"pm" | "sm" | "ac" | "provider" | "orgs", ProductSwitcherItem> = {
pm: {
name: "Password Manager",
icon: "bwi-lock",
appRoute: "/vault",
marketingRoute: "https://bitwarden.com/products/personal/",
isActive: !this.router.url.includes("/sm/"),
isActive:
!this.router.url.includes("/sm/") &&
!this.router.url.includes("/organizations/") &&
!this.router.url.includes("/providers/"),
},
sm: {
name: "Secrets Manager",
@@ -71,6 +87,19 @@ export class ProductSwitcherContentComponent {
marketingRoute: "https://bitwarden.com/products/secrets-manager/",
isActive: this.router.url.includes("/sm/"),
},
ac: {
name: "Admin Console",
icon: "bwi-business",
appRoute: ["/organizations", acOrg?.id],
marketingRoute: "https://bitwarden.com/products/business/",
isActive: this.router.url.includes("/organizations/"),
},
provider: {
name: "Provider Portal",
icon: "bwi-provider",
appRoute: ["/providers", providers[0]?.id],
isActive: this.router.url.includes("/providers/"),
},
orgs: {
name: "Organizations",
icon: "bwi-business",
@@ -81,7 +110,9 @@ export class ProductSwitcherContentComponent {
const bento: ProductSwitcherItem[] = [products.pm];
const other: ProductSwitcherItem[] = [];
if (orgs.length === 0) {
if (acOrg) {
bento.push(products.ac);
} else {
other.push(products.orgs);
}
@@ -91,6 +122,10 @@ export class ProductSwitcherContentComponent {
other.push(products.sm);
}
if (providers.length > 0) {
bento.push(products.provider);
}
return {
bento,
other,
@@ -100,6 +135,7 @@ export class ProductSwitcherContentComponent {
constructor(
private organizationService: OrganizationService,
private providerService: ProviderService,
private route: ActivatedRoute,
private router: Router,
) {}

View File

@@ -1,11 +1,13 @@
import { Component, Directive, Input, importProvidersFrom } from "@angular/core";
import { Component, Directive, importProvidersFrom, Input } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Meta, Story, applicationConfig, moduleMetadata } from "@storybook/angular";
import { BehaviorSubject } from "rxjs";
import { applicationConfig, Meta, moduleMetadata, Story } from "@storybook/angular";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components";
import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service";
@@ -26,6 +28,22 @@ class MockOrganizationService implements Partial<OrganizationService> {
}
}
@Directive({
selector: "[mockProviders]",
})
class MockProviderService implements Partial<ProviderService> {
private static _providers = new BehaviorSubject<Provider[]>([]);
async getAll() {
return await firstValueFrom(MockProviderService._providers);
}
@Input()
set mockProviders(providers: Provider[]) {
MockProviderService._providers.next(providers);
}
}
@Component({
selector: "story-layout",
template: `<ng-content></ng-content>`,
@@ -46,6 +64,7 @@ export default {
ProductSwitcherContentComponent,
ProductSwitcherComponent,
MockOrganizationService,
MockProviderService,
StoryLayoutComponent,
StoryContentComponent,
],
@@ -53,6 +72,8 @@ export default {
providers: [
{ provide: OrganizationService, useClass: MockOrganizationService },
MockOrganizationService,
{ provide: ProviderService, useClass: MockProviderService },
MockProviderService,
{
provide: I18nService,
useFactory: () => {
@@ -82,6 +103,10 @@ export default {
path: "sm/:organizationId",
component: StoryContentComponent,
},
{
path: "providers/:providerId",
component: StoryContentComponent,
},
{
path: "vault",
component: StoryContentComponent,
@@ -100,7 +125,7 @@ export default {
const Template: Story = (args) => ({
props: args,
template: `
<router-outlet [mockOrgs]="mockOrgs"></router-outlet>
<router-outlet [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></router-outlet>
<div class="tw-flex tw-gap-[200px]">
<div>
<h1 class="tw-text-main tw-text-base tw-underline">Closed</h1>
@@ -119,17 +144,26 @@ const Template: Story = (args) => ({
`,
});
export const NoOrgs = Template.bind({});
NoOrgs.args = {
export const OnlyPM = Template.bind({});
OnlyPM.args = {
mockOrgs: [],
mockProviders: [],
};
export const OrgWithoutSecretsManager = Template.bind({});
OrgWithoutSecretsManager.args = {
mockOrgs: [{ id: "a" }],
export const WithSM = Template.bind({});
WithSM.args = {
mockOrgs: [{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true }],
mockProviders: [],
};
export const OrgWithSecretsManager = Template.bind({});
OrgWithSecretsManager.args = {
mockOrgs: [{ id: "b", canAccessSecretsManager: true, enabled: true }],
export const WithSMAndAC = Template.bind({});
WithSMAndAC.args = {
mockOrgs: [{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true }],
mockProviders: [],
};
export const WithAllOptions = Template.bind({});
WithAllOptions.args = {
mockOrgs: [{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true }],
mockProviders: [{ id: "provider-a" }],
};

View File

@@ -1,4 +1,41 @@
<app-navbar></app-navbar>
<app-payment-method-banners *ngIf="false"></app-payment-method-banners>
<router-outlet></router-outlet>
<app-footer></app-footer>
<bit-layout>
<nav slot="sidebar">
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block">
<bit-icon [icon]="logo"></bit-icon>
</a>
<bit-nav-item icon="bwi-collection" [text]="'vaults' | i18n" route="vault"></bit-nav-item>
<bit-nav-item icon="bwi-send" [text]="'send' | i18n" route="sends"></bit-nav-item>
<bit-nav-group icon="bwi-wrench" [text]="'tools' | i18n" route="tools">
<bit-nav-item [text]="'generator' | i18n" route="tools/generator"></bit-nav-item>
<bit-nav-item [text]="'importData' | i18n" route="tools/import"></bit-nav-item>
<bit-nav-item [text]="'exportVault' | i18n" route="tools/export"></bit-nav-item>
</bit-nav-group>
<bit-nav-item icon="bwi-sliders" [text]="'reports' | i18n" route="reports"></bit-nav-item>
<bit-nav-group icon="bwi-cog" [text]="'settings' | i18n" route="settings">
<bit-nav-item [text]="'myAccount' | i18n" route="settings/account"></bit-nav-item>
<bit-nav-item [text]="'security' | i18n" route="settings/security"></bit-nav-item>
<bit-nav-item [text]="'preferences' | i18n" route="settings/preferences"></bit-nav-item>
<bit-nav-item
[text]="'subscription' | i18n"
route="settings/subscription"
*ngIf="!hideSubscription"
></bit-nav-item>
<bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item>
<bit-nav-item
[text]="'emergencyAccess' | i18n"
route="settings/emergency-access"
></bit-nav-item>
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="settings/sponsored-families"
*ngIf="hasFamilySponsorshipAvailable"
></bit-nav-item>
</bit-nav-group>
</nav>
<app-payment-method-banners
*ngIf="false"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
></app-payment-method-banners>
<router-outlet></router-outlet>
</bit-layout>

View File

@@ -1,11 +1,86 @@
import { Component, OnInit } from "@angular/core";
import { CommonModule } from "@angular/common";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
import { PaymentMethodBannersComponent } from "../components/payment-method-banners/payment-method-banners.component";
import { PasswordManagerLogo } from "./password-manager-logo";
const BroadcasterSubscriptionId = "UserLayoutComponent";
@Component({
selector: "app-user-layout",
templateUrl: "user-layout.component.html",
standalone: true,
imports: [
CommonModule,
RouterModule,
JslibModule,
LayoutComponent,
IconModule,
NavigationModule,
PaymentMethodBannersComponent,
],
})
export class UserLayoutComponent implements OnInit {
ngOnInit() {
export class UserLayoutComponent implements OnInit, OnDestroy {
protected readonly logo = PasswordManagerLogo;
hasFamilySponsorshipAvailable: boolean;
hideSubscription: boolean;
constructor(
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private stateService: StateService,
private apiService: ApiService,
private syncService: SyncService,
) {}
async ngOnInit() {
document.body.classList.remove("layout_frontend");
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "purchasedPremium":
await this.load();
break;
default:
}
});
});
await this.syncService.fullSync(false);
await this.load();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async load() {
const premium = await this.stateService.getHasPremiumPersonally();
const selfHosted = this.platformUtilsService.isSelfHost();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await this.stateService.getHasPremiumFromOrganization();
let billing = null;
if (!selfHosted) {
// TODO: We should remove the need to call this!
billing = await this.apiService.getUserBillingHistory();
}
this.hideSubscription = !premium && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory);
}
}

View File

@@ -42,12 +42,10 @@ import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
import { UserLayoutComponent } from "./layouts/user-layout.component";
import { DomainRulesComponent } from "./settings/domain-rules.component";
import { PreferencesComponent } from "./settings/preferences.component";
import { SettingsComponent } from "./settings/settings.component";
import { GeneratorComponent } from "./tools/generator.component";
import { ReportsModule } from "./tools/reports";
import { AccessComponent } from "./tools/send/access.component";
import { SendComponent } from "./tools/send/send.component";
import { ToolsComponent } from "./tools/tools.component";
import { VaultModule } from "./vault/individual-vault/vault.module";
const routes: Routes = [
@@ -199,7 +197,7 @@ const routes: Routes = [
path: "vault",
loadChildren: () => VaultModule,
},
{ path: "sends", component: SendComponent, data: { title: "Send" } },
{ path: "sends", component: SendComponent, data: { titleId: "send" } },
{
path: "create-organization",
component: CreateOrganizationComponent,
@@ -207,7 +205,6 @@ const routes: Routes = [
},
{
path: "settings",
component: SettingsComponent,
children: [
{ path: "", pathMatch: "full", redirectTo: "account" },
{ path: "account", component: AccountComponent, data: { titleId: "myAccount" } },
@@ -256,7 +253,6 @@ const routes: Routes = [
},
{
path: "tools",
component: ToolsComponent,
canActivate: [AuthGuard],
children: [
{ path: "", pathMatch: "full", redirectTo: "generator" },

View File

@@ -1,8 +1,8 @@
<div class="page-header">
<h1>{{ "domainRules" | i18n }}</h1>
</div>
<p>{{ "domainRulesDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<app-header></app-header>
<bit-container>
<p>{{ "domainRulesDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<h2>{{ "customEqDomains" | i18n }}</h2>
<p *ngIf="loading">
<i
@@ -105,4 +105,5 @@
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
</form>
</form>
</bit-container>

View File

@@ -1,8 +1,8 @@
<div class="page-header">
<h1>{{ "preferences" | i18n }}</h1>
</div>
<p>{{ "preferencesDesc" | i18n }}</p>
<form [formGroup]="form" (ngSubmit)="submit()" ngNativeValidate>
<app-header></app-header>
<bit-container>
<p>{{ "preferencesDesc" | i18n }}</p>
<form [formGroup]="form" (ngSubmit)="submit()" ngNativeValidate>
<div class="row">
<div class="col-6">
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
@@ -111,21 +111,6 @@
</div>
<small class="form-text text-muted">{{ "faviconDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enableFullWidth"
name="enableFullWidth"
formControlName="enableFullWidth"
/>
<label class="form-check-label" for="enableFullWidth">
{{ "enableFullWidth" | i18n }}
</label>
</div>
<small class="form-text text-muted">{{ "enableFullWidthDesc" | i18n }}</small>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
@@ -140,4 +125,5 @@
<button type="submit" class="btn btn-primary">
{{ "save" | i18n }}
</button>
</form>
</form>
</bit-container>

View File

@@ -42,7 +42,6 @@ export class PreferencesComponent implements OnInit {
vaultTimeout: [null as number | null],
vaultTimeoutAction: [VaultTimeoutAction.Lock],
enableFavicons: true,
enableFullWidth: false,
theme: [ThemeType.Light],
locale: [null as string | null],
});
@@ -142,7 +141,6 @@ export class PreferencesComponent implements OnInit {
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
),
enableFavicons: !(await this.settingsService.getDisableFavicon()),
enableFullWidth: await this.stateService.getEnableFullWidth(),
theme: await this.stateService.getTheme(),
locale: (await this.stateService.getLocale()) ?? null,
};
@@ -167,8 +165,6 @@ export class PreferencesComponent implements OnInit {
values.vaultTimeoutAction,
);
await this.settingsService.setDisableFavicon(!values.enableFavicons);
await this.stateService.setEnableFullWidth(values.enableFullWidth);
this.messagingService.send("setFullWidth");
if (values.theme !== this.startingTheme) {
await this.themingService.updateConfiguredTheme(values.theme);
this.startingTheme = values.theme;

View File

@@ -1,45 +0,0 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{ "accountSettings" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "myAccount" | i18n }}
</a>
<a routerLink="security" class="list-group-item" routerLinkActive="active">
{{ "security" | i18n }}
</a>
<a routerLink="preferences" class="list-group-item" routerLinkActive="active">
{{ "preferences" | i18n }}
</a>
<a
routerLink="subscription"
class="list-group-item"
routerLinkActive="active"
*ngIf="!hideSubscription"
>
{{ "subscription" | i18n }}
</a>
<a routerLink="domain-rules" class="list-group-item" routerLinkActive="active">
{{ "domainRules" | i18n }}
</a>
<a routerLink="emergency-access" class="list-group-item" routerLinkActive="active">
{{ "emergencyAccess" | i18n }}
</a>
<a
routerLink="sponsored-families"
class="list-group-item"
routerLinkActive="active"
*ngIf="hasFamilySponsorshipAvailable"
>
{{ "sponsoredFamilies" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { NgModule } from "@angular/core";
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
import { OrganizationSwitcherComponent } from "../admin-console/components/organization-switcher.component";
import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component";
import { EventsComponent as OrgEventsComponent } from "../admin-console/organizations/manage/events.component";
import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component";
@@ -10,7 +10,6 @@ import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../admin-console/organizations/tools/exposed-passwords-report.component";
import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../admin-console/organizations/tools/inactive-two-factor-report.component";
import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../admin-console/organizations/tools/reused-passwords-report.component";
import { ToolsComponent as OrgToolsComponent } from "../admin-console/organizations/tools/tools.component";
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component";
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component";
import { ProvidersComponent } from "../admin-console/providers/providers.component";
@@ -62,20 +61,17 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
import { PaymentMethodBannersComponent } from "../components/payment-method-banners/payment-method-banners.component";
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
import { FooterComponent } from "../layouts/footer.component";
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
import { NavbarComponent } from "../layouts/navbar.component";
import { HeaderModule } from "../layouts/header/header.module";
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
import { UserLayoutComponent } from "../layouts/user-layout.component";
import { DomainRulesComponent } from "../settings/domain-rules.component";
import { LowKdfComponent } from "../settings/low-kdf.component";
import { PreferencesComponent } from "../settings/preferences.component";
import { SettingsComponent } from "../settings/settings.component";
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
import { GeneratorComponent } from "../tools/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component";
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
import { ToolsComponent } from "../tools/tools.component";
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
import { AddEditCustomFieldsComponent } from "../vault/individual-vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/individual-vault/add-edit.component";
@@ -111,6 +107,11 @@ import { SharedModule } from "./shared.module";
PasswordCalloutComponent,
DangerZoneComponent,
PaymentMethodBannersComponent,
LayoutComponent,
NavigationModule,
HeaderModule,
OrganizationLayoutComponent,
UserLayoutComponent,
],
declarations: [
AcceptFamilySponsorshipComponent,
@@ -134,21 +135,16 @@ import { SharedModule } from "./shared.module";
EmergencyAccessViewComponent,
EmergencyAddEditCipherComponent,
FolderAddEditComponent,
FooterComponent,
FrontendLayoutComponent,
HintComponent,
LockComponent,
NavbarComponent,
OrganizationSwitcherComponent,
OrgAddEditComponent,
OrganizationLayoutComponent,
OrgAttachmentsComponent,
OrgCollectionsComponent,
OrgEventsComponent,
OrgExposedPasswordsReportComponent,
OrgInactiveTwoFactorReportComponent,
OrgReusedPasswordsReportComponent,
OrgToolsComponent,
OrgUnsecuredWebsitesReportComponent,
OrgUserConfirmComponent,
OrgWeakPasswordsReportComponent,
@@ -168,12 +164,10 @@ import { SharedModule } from "./shared.module";
SelectableAvatarComponent,
SendAddEditComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
ToolsComponent,
TwoFactorAuthenticatorComponent,
TwoFactorComponent,
TwoFactorDuoComponent,
@@ -186,7 +180,6 @@ import { SharedModule } from "./shared.module";
TwoFactorYubiKeyComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
UserLayoutComponent,
VaultTimeoutInputComponent,
VerifyEmailComponent,
VerifyEmailTokenComponent,
@@ -217,12 +210,9 @@ import { SharedModule } from "./shared.module";
EmergencyAccessViewComponent,
EmergencyAddEditCipherComponent,
FolderAddEditComponent,
FooterComponent,
FrontendLayoutComponent,
HintComponent,
LockComponent,
NavbarComponent,
OrganizationSwitcherComponent,
OrgAddEditComponent,
OrganizationLayoutComponent,
OrgAttachmentsComponent,
@@ -231,7 +221,6 @@ import { SharedModule } from "./shared.module";
OrgExposedPasswordsReportComponent,
OrgInactiveTwoFactorReportComponent,
OrgReusedPasswordsReportComponent,
OrgToolsComponent,
OrgUnsecuredWebsitesReportComponent,
OrgUserConfirmComponent,
OrgWeakPasswordsReportComponent,
@@ -251,12 +240,10 @@ import { SharedModule } from "./shared.module";
SelectableAvatarComponent,
SendAddEditComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
ToolsComponent,
TwoFactorAuthenticatorComponent,
TwoFactorComponent,
TwoFactorDuoComponent,
@@ -275,6 +262,7 @@ import { SharedModule } from "./shared.module";
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
LowKdfComponent,
HeaderModule,
DangerZoneComponent,
],
})

View File

@@ -16,6 +16,7 @@ import {
CalloutModule,
CheckboxModule,
ColorPasswordModule,
ContainerComponent,
DialogModule,
FormFieldModule,
IconButtonModule,
@@ -63,6 +64,7 @@ import "./locales";
CalloutModule,
CheckboxModule,
ColorPasswordModule,
ContainerComponent,
DialogModule,
FormFieldModule,
IconButtonModule,
@@ -98,6 +100,7 @@ import "./locales";
CalloutModule,
CheckboxModule,
ColorPasswordModule,
ContainerComponent,
DialogModule,
FormFieldModule,
IconButtonModule,

View File

@@ -1,18 +1,18 @@
<div class="page-header">
<h1>{{ "generator" | i18n }}</h1>
</div>
<app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'">
<app-header></app-header>
<bit-container>
<app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'">
{{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout>
<div class="card card-generated bg-light my-4">
</app-callout>
<div class="card card-generated bg-light my-4">
<div class="card-body">
<bit-color-password
[password]="type === 'password' ? password : username"
[appCopyText]="type === 'password' ? password : username"
></bit-color-password>
</div>
</div>
<div class="form-group" role="radiogroup" aria-labelledby="typeHeading">
</div>
<div class="form-group" role="radiogroup" aria-labelledby="typeHeading">
<label id="typeHeading" class="d-block">{{ "whatWouldYouLikeToGenerate" | i18n }}</label>
<div class="form-check form-check-inline" *ngFor="let o of typeOptions">
<input
@@ -29,8 +29,8 @@
{{ o.name }}
</label>
</div>
</div>
<ng-container *ngIf="type === 'password'">
</div>
<ng-container *ngIf="type === 'password'">
<div aria-labelledby="passwordTypeHeading" class="form-group" role="radiogroup">
<label id="passwordTypeHeading" class="d-block">{{ "passwordType" | i18n }}</label>
<div class="form-check form-check-inline" *ngFor="let o of passTypeOptions">
@@ -244,8 +244,8 @@
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="type === 'username'">
</ng-container>
<ng-container *ngIf="type === 'username'">
<div aria-labelledby="usernameTypeHeading" class="form-group" role="radiogroup">
<div class="d-block">
<label id="usernameTypeHeading">{{ "usernameType" | i18n }}</label>
@@ -442,7 +442,9 @@
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordCapitalize"
/>
<label for="capitalizeUsername" class="form-check-label">{{ "capitalize" | i18n }}</label>
<label for="capitalizeUsername" class="form-check-label">{{
"capitalize" | i18n
}}</label>
</div>
<div class="form-check">
<input
@@ -472,5 +474,6 @@
{{ "copyUsername" | i18n }}
</button>
</div>
</ng-container>
<ng-template #historyTemplate></ng-template>
</ng-container>
<ng-template #historyTemplate></ng-template>
</bit-container>

View File

@@ -1,24 +0,0 @@
import { Component } from "@angular/core";
import { ImportCollectionServiceAbstraction } from "@bitwarden/importer/core";
import { ImportComponent } from "@bitwarden/importer/ui";
import { SharedModule } from "../../shared";
import { CollectionAdminService } from "../../vault/core/collection-admin.service";
import { ImportCollectionAdminService } from "./import-collection-admin.service";
import { ImportWebComponent } from "./import-web.component";
@Component({
templateUrl: "import-web.component.html",
standalone: true,
imports: [SharedModule, ImportComponent],
providers: [
{
provide: ImportCollectionServiceAbstraction,
useClass: ImportCollectionAdminService,
deps: [CollectionAdminService],
},
],
})
export class AdminImportComponent extends ImportWebComponent {}

View File

@@ -1,11 +1,12 @@
<h1 bitTypography="h1">{{ "importData" | i18n }}</h1>
<tools-import
<app-header></app-header>
<bit-container>
<tools-import
(formDisabled)="this.disabled = $event"
(formLoading)="this.loading = $event"
(onSuccessfulImport)="this.onSuccessfulImport($event)"
organizationId="{{ routeOrgId }}"
></tools-import>
<button
></tools-import>
<button
[disabled]="disabled"
[loading]="loading"
form="import_form_importForm"
@@ -13,6 +14,7 @@
type="submit"
bitFormButton
buttonType="primary"
>
>
{{ "importData" | i18n }}
</button>
</button>
</bit-container>

View File

@@ -1,51 +1,26 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import {
OrganizationService,
canAccessVaultTab,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ImportComponent } from "@bitwarden/importer/ui";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
@Component({
templateUrl: "import-web.component.html",
standalone: true,
imports: [SharedModule, ImportComponent],
imports: [SharedModule, ImportComponent, HeaderModule],
})
export class ImportWebComponent implements OnInit {
protected routeOrgId: string = null;
export class ImportWebComponent {
protected loading = false;
protected disabled = false;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private router: Router,
) {}
ngOnInit(): void {
this.routeOrgId = this.route.snapshot.paramMap.get("organizationId");
}
constructor(private router: Router) {}
/**
* Callback that is called after a successful import.
*/
protected async onSuccessfulImport(organizationId: string): Promise<void> {
if (!this.routeOrgId) {
await this.router.navigate(["vault"]);
return;
}
const organization = await firstValueFrom(this.organizationService.get$(organizationId));
if (organization == null) {
return;
}
if (canAccessVaultTab(organization)) {
await this.router.navigate(["organizations", organizationId, "vault"]);
}
}
}

View File

@@ -1,8 +1,8 @@
<div class="page-header">
<h1>{{ "dataBreachReport" | i18n }}</h1>
</div>
<p>{{ "breachDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<app-header></app-header>
<bit-container>
<p>{{ "breachDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="row">
<div class="form-group col-6">
<label for="username">{{ "username" | i18n }}</label>
@@ -20,8 +20,8 @@
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
{{ "checkBreaches" | i18n }}
</button>
</form>
<div class="mt-4" *ngIf="!form.loading && checkedUsername">
</form>
<div class="mt-4" *ngIf="!form.loading && checkedUsername">
<p *ngIf="error">{{ "reportError" | i18n }}...</p>
<ng-container *ngIf="!error">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!breachedAccounts.length">
@@ -60,4 +60,5 @@
</li>
</ul>
</ng-container>
</div>
</div>
</bit-container>

View File

@@ -1,11 +1,11 @@
<div class="page-header">
<h1>{{ "exposedPasswordsReport" | i18n }}</h1>
</div>
<p>{{ "exposedPasswordsReportDesc" | i18n }}</p>
<button type="submit" buttonType="primary" bitButton [loading]="loading" (click)="load()">
<app-header></app-header>
<bit-container>
<p>{{ "exposedPasswordsReportDesc" | i18n }}</p>
<button type="submit" buttonType="primary" bitButton [loading]="loading" (click)="load()">
{{ "checkExposedPasswords" | i18n }}
</button>
<div class="mt-4" *ngIf="hasLoaded">
</button>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
{{ "noExposedPasswords" | i18n }}
</app-callout>
@@ -21,9 +21,13 @@
</td>
<td class="reduced-lh wrap">
<ng-container *ngIf="!organization || canManageCipher(c); else cantManage">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
c.name
}}</a>
<a
href="#"
appStopClick
(click)="selectCipher(c)"
title="{{ 'editItem' | i18n }}"
>{{ c.name }}</a
>
</ng-container>
<ng-template #cantManage>
<span>{{ c.name }}</span>
@@ -68,5 +72,6 @@
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
</div>
<ng-template #cipherAddEdit></ng-template>
</bit-container>

View File

@@ -1,26 +1,16 @@
<div class="page-header">
<h1>
{{ "inactive2faReport" | i18n }}
<small *ngIf="hasLoaded && loading">
<app-header></app-header>
<bit-container>
<p>{{ "inactive2faReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<p>{{ "inactive2faReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="mt-4" *ngIf="hasLoaded">
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
{{ "noInactive2fa" | i18n }}
</app-callout>
@@ -84,5 +74,6 @@
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
</div>
<ng-template #cipherAddEdit></ng-template>
</bit-container>

View File

@@ -1,7 +1,7 @@
<div class="page-header">
<h1>{{ "reports" | i18n }}</h1>
</div>
<app-header></app-header>
<p>{{ "reportsDesc" | i18n }}</p>
<bit-container>
<p>{{ "reportsDesc" | i18n }}</p>
<app-report-list [reports]="reports"></app-report-list>
<app-report-list [reports]="reports"></app-report-list>
</bit-container>

View File

@@ -1,26 +1,16 @@
<div class="page-header">
<h1>
{{ "reusedPasswordsReport" | i18n }}
<small *ngIf="hasLoaded && loading">
<app-header></app-header>
<bit-container>
<p>{{ "reusedPasswordsReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<p>{{ "reusedPasswordsReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="mt-4" *ngIf="hasLoaded">
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
{{ "noReusedPasswords" | i18n }}
</app-callout>
@@ -36,9 +26,13 @@
</td>
<td class="reduced-lh wrap">
<ng-container *ngIf="!organization || canManageCipher(c); else cantManage">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
c.name
}}</a>
<a
href="#"
appStopClick
(click)="selectCipher(c)"
title="{{ 'editItem' | i18n }}"
>{{ c.name }}</a
>
</ng-container>
<ng-template #cantManage>
<span>{{ c.name }}</span>
@@ -83,5 +77,6 @@
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
</div>
<ng-template #cipherAddEdit></ng-template>
</bit-container>

View File

@@ -1,26 +1,16 @@
<div class="page-header">
<h1>
{{ "unsecuredWebsitesReport" | i18n }}
<small *ngIf="hasLoaded && loading">
<app-header></app-header>
<bit-container>
<p>{{ "unsecuredWebsitesReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<p>{{ "unsecuredWebsitesReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="mt-4" *ngIf="hasLoaded">
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
{{ "noUnsecuredWebsites" | i18n }}
</app-callout>
@@ -73,5 +63,6 @@
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
</div>
<ng-template #cipherAddEdit></ng-template>
</bit-container>

View File

@@ -1,26 +1,16 @@
<div class="page-header">
<h1>
{{ "weakPasswordsReport" | i18n }}
<small *ngIf="hasLoaded && loading">
<app-header></app-header>
<bit-container>
<p>{{ "weakPasswordsReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<p>{{ "weakPasswordsReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="mt-4" *ngIf="hasLoaded">
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
{{ "noWeakPasswords" | i18n }}
</app-callout>
@@ -36,9 +26,13 @@
</td>
<td class="reduced-lh wrap">
<ng-container *ngIf="!organization || canManageCipher(c); else cantManage">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
c.name
}}</a>
<a
href="#"
appStopClick
(click)="selectCipher(c)"
title="{{ 'editItem' | i18n }}"
>{{ c.name }}</a
>
</ng-container>
<ng-template #cantManage>
<span>{{ c.name }}</span>
@@ -83,5 +77,6 @@
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
</div>
<ng-template #cipherAddEdit></ng-template>
</bit-container>

View File

@@ -1,12 +1,10 @@
<div class="container page-content">
<router-outlet></router-outlet>
<router-outlet></router-outlet>
<div class="row mt-4">
<div class="row mt-4">
<div class="col">
<a bitButton routerLink="./" *ngIf="!homepage">
<i class="bwi bwi-angle-left" aria-hidden="true"></i>
{{ "backToReports" | i18n }}
</a>
</div>
</div>
</div>

View File

@@ -20,7 +20,12 @@ const routes: Routes = [
component: ReportsLayoutComponent,
canActivate: [AuthGuard],
children: [
{ path: "", pathMatch: "full", component: ReportsHomeComponent, data: { homepage: true } },
{
path: "",
pathMatch: "full",
component: ReportsHomeComponent,
data: { titleId: "reports", homepage: true },
},
{
path: "breach-report",
component: BreachReportComponent,

View File

@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
@@ -24,6 +25,7 @@ import { ReportsSharedModule } from "./shared";
ReportsRoutingModule,
OrganizationBadgeModule,
PipesModule,
HeaderModule,
],
declarations: [
BreachReportComponent,

View File

@@ -1,8 +1,27 @@
<div class="container page-content">
<bit-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
<app-header>
<ng-container slot="title-suffix">
<small #actionSpinner [appApiAction]="actionPromise">
<ng-container *ngIf="$any(actionSpinner).loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</ng-container>
<button type="button" bitButton buttonType="primary" (click)="addSend()" [disabled]="disableSend">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "createSend" | i18n }}
</button>
</app-header>
<bit-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
{{ "sendDisabledWarning" | i18n }}
</bit-callout>
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
</bit-callout>
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="groupings tw-col-span-3">
<div class="card vault-filters">
<div class="card-header d-flex">
@@ -63,33 +82,6 @@
</div>
</div>
<div class="tw-col-span-9">
<div class="tw-flex">
<h1 bitTypography="h1">
{{ "send" | i18n }}
<small #actionSpinner [appApiAction]="actionPromise">
<ng-container *ngIf="$any(actionSpinner).loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</h1>
<div class="tw-ml-auto">
<button
type="button"
bitButton
buttonType="primary"
(click)="addSend()"
[disabled]="disableSend"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "createSend" | i18n }}
</button>
</div>
</div>
<!--Listing Table-->
<bit-table [dataSource]="dataSource" *ngIf="filteredSends && filteredSends.length">
<ng-container header>
@@ -203,13 +195,7 @@
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
<button
slot="button"
type="button"
bitButton
buttonType="secondary"
(click)="addSend()"
>
<button slot="button" type="button" bitButton buttonType="secondary" (click)="addSend()">
<i class="bwi bwi-plus" aria-hidden="true"></i>
{{ "createSend" | i18n }}
</button>
@@ -217,6 +203,5 @@
</ng-container>
</div>
</div>
</div>
</div>
<ng-template #sendAddEdit></ng-template>

View File

@@ -14,6 +14,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService, NoItemsModule, SearchModule, TableDataSource } from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { AddEditComponent } from "./add-edit.component";
@@ -24,7 +25,7 @@ const BroadcasterSubscriptionId = "SendComponent";
@Component({
selector: "app-send",
standalone: true,
imports: [SharedModule, SearchModule, NoItemsModule],
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
templateUrl: "send.component.html",
})
export class SendComponent extends BaseSendComponent {

View File

@@ -1,23 +0,0 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card mb-4">
<div class="card-header">{{ "tools" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="generator" class="list-group-item" routerLinkActive="active">
{{ "generator" | i18n }}
</a>
<a routerLink="import" class="list-group-item" routerLinkActive="active">
{{ "importData" | i18n }}
</a>
<a routerLink="export" class="list-group-item" routerLinkActive="active">
{{ "exportVault" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,12 +1,13 @@
<form
<app-header></app-header>
<bit-container>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="exportForm"
*ngIf="exportForm"
>
<h1 bitTypography="h1">{{ "exportVault" | i18n }}</h1>
>
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
{{ "personalVaultExportPolicyInEffect" | i18n }}
</bit-callout>
@@ -84,7 +85,8 @@
></button>
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
</bit-form-field>
<app-password-strength [password]="filePassword" [showText]="true"> </app-password-strength>
<app-password-strength [password]="filePassword" [showText]="true">
</app-password-strength>
</div>
<bit-form-field>
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
@@ -115,4 +117,5 @@
>
{{ "confirmFormat" | i18n }}
</button>
</form>
</form>
</bit-container>

View File

@@ -1,5 +1,7 @@
import { NgModule } from "@angular/core";
import { SearchModule } from "@bitwarden/components";
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
import { LinkSsoDirective } from "./components/link-sso.directive";
@@ -9,7 +11,7 @@ import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/
import { VaultFilterService } from "./services/vault-filter.service";
@NgModule({
imports: [VaultFilterSharedModule],
imports: [VaultFilterSharedModule, SearchModule],
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoDirective],
exports: [VaultFilterComponent],
providers: [

View File

@@ -1,6 +1,5 @@
<div class="tw-mb-4 tw-flex tw-items-start tw-justify-between">
<div>
<bit-breadcrumbs *ngIf="showBreadcrumbs">
<app-header [title]="title" [icon]="icon">
<bit-breadcrumbs *ngIf="showBreadcrumbs" slot="breadcrumbs">
<bit-breadcrumb
*ngIf="activeOrganizationId"
[route]="[]"
@@ -21,20 +20,15 @@
</bit-breadcrumb>
</ng-container>
</bit-breadcrumbs>
<h1 class="tw-mb-0 tw-mt-1 tw-flex tw-items-center tw-space-x-2">
<i
*ngIf="filter.collectionId && filter.collectionId !== All"
class="bwi bwi-collection"
aria-hidden="true"
></i>
<span>{{ title }}</span>
<ng-container slot="title-suffix">
<ng-container *ngIf="collection != null && (canEditCollection || canDeleteCollection)">
<button
bitIconButton="bwi-angle-down"
[bitMenuTriggerFor]="editCollectionMenu"
size="small"
type="button"
aria-haspopup
aria-haspopup="true"
></button>
<bit-menu #editCollectionMenu>
<button
@@ -55,12 +49,7 @@
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
<button
type="button"
*ngIf="canDeleteCollection"
bitMenuItem
(click)="deleteCollection()"
>
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
@@ -76,8 +65,7 @@
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
</ng-container>
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
<div appListDropdown>
@@ -107,4 +95,4 @@
</bit-menu>
</div>
</div>
</div>
</app-header>

View File

@@ -103,6 +103,10 @@ export class VaultHeaderComponent {
return this.i18nService.t("allVaults");
}
protected get icon() {
return this.filter.collectionId && this.filter.collectionId !== All ? "bwi-collection" : "";
}
/**
* A list of collection filters that form a chain from the organization root to currently selected collection.
* Begins from the organization root and excludes the currently selected collection.

View File

@@ -1,7 +1,19 @@
<div class="container page-content">
<app-vault-onboarding [ciphers]="ciphers" (onAddCipher)="addCipher()"> </app-vault-onboarding>
<app-vault-header
[filter]="filter"
[loading]="refreshing && !performingInitialLoad"
[organizations]="allOrganizations"
[canCreateCollections]="canCreateCollections"
[collection]="selectedCollection"
(onAddCipher)="addCipher()"
(onAddCollection)="addCollection()"
(onAddFolder)="addFolder()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-vault-header>
<div class="row">
<app-vault-onboarding [ciphers]="ciphers" (onAddCipher)="addCipher()"> </app-vault-onboarding>
<div class="row">
<div class="col-3">
<div class="groupings">
<div class="content">
@@ -18,18 +30,6 @@
</div>
</div>
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
<app-vault-header
[filter]="filter"
[loading]="refreshing && !performingInitialLoad"
[organizations]="allOrganizations"
[canCreateCollections]="canCreateCollections"
[collection]="selectedCollection"
(onAddCipher)="addCipher()"
(onAddCollection)="addCollection()"
(onAddFolder)="addFolder()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-vault-header>
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
{{ trashCleanupWarning }}
</app-callout>
@@ -113,17 +113,14 @@
</div>
<div class="card-body">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<a
class="btn btn-block btn-outline-secondary"
routerLink="/settings/subscription/premium"
>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/subscription/premium">
{{ "goPremium" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #folderAddEdit></ng-template>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -1,5 +1,7 @@
import { NgModule } from "@angular/core";
import { SearchModule } from "@bitwarden/components";
import { VaultFilterService as VaultFilterServiceAbstraction } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
@@ -7,7 +9,7 @@ import { VaultFilterComponent } from "./vault-filter.component";
import { VaultFilterService } from "./vault-filter.service";
@NgModule({
imports: [VaultFilterSharedModule],
imports: [VaultFilterSharedModule, SearchModule],
declarations: [VaultFilterComponent],
exports: [VaultFilterComponent],
providers: [

View File

@@ -1,6 +1,5 @@
<div class="tw-mb-4 tw-flex tw-items-start tw-justify-between">
<div>
<bit-breadcrumbs *ngIf="showBreadcrumbs">
<app-header [title]="title" [icon]="icon">
<bit-breadcrumbs *ngIf="showBreadcrumbs" slot="breadcrumbs">
<bit-breadcrumb
[route]="[]"
[queryParams]="{ organizationId: organization.id, collectionId: null }"
@@ -14,6 +13,7 @@
{{ "collections" | i18n | lowercase }}
</span>
</bit-breadcrumb>
<ng-container>
<bit-breadcrumb
*ngFor="let collection of collections"
@@ -26,20 +26,15 @@
</bit-breadcrumb>
</ng-container>
</bit-breadcrumbs>
<h1 class="tw-mb-0 tw-mt-1 tw-flex tw-items-center tw-space-x-2">
<i
*ngIf="filter.collectionId !== undefined"
class="bwi bwi-collection"
aria-hidden="true"
></i>
<span>{{ title }}</span>
<ng-container slot="title-suffix">
<ng-container *ngIf="collection != null && (canEditCollection || canDeleteCollection)">
<button
bitIconButton="bwi-angle-down"
[bitMenuTriggerFor]="editCollectionMenu"
size="small"
type="button"
aria-haspopup
aria-haspopup="true"
></button>
<bit-menu #editCollectionMenu>
<button
@@ -60,12 +55,7 @@
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
<button
type="button"
*ngIf="canDeleteCollection"
bitMenuItem
(click)="deleteCollection()"
>
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
@@ -81,8 +71,7 @@
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
</ng-container>
<div *ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned" class="tw-shrink-0">
<div *ngIf="organization?.canCreateNewCollections" appListDropdown>
@@ -118,4 +107,4 @@
{{ "newItem" | i18n }}
</button>
</div>
</div>
</app-header>

View File

@@ -80,6 +80,10 @@ export class VaultHeaderComponent {
return `${this.organization?.name} ${headerType}`;
}
get icon() {
return this.filter.collectionId !== undefined ? "bwi-collection" : "";
}
protected get showBreadcrumbs() {
return this.filter.collectionId !== undefined && this.filter.collectionId !== All;
}

View File

@@ -1,5 +1,15 @@
<div class="container page-content">
<div class="row">
<app-org-vault-header
[filter]="filter"
[loading]="refreshing"
[organization]="organization"
[collection]="selectedCollection"
(onAddCipher)="addCipher()"
(onAddCollection)="addCollection()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-org-vault-header>
<div class="row">
<div class="col-3">
<div class="groupings">
<div class="content">
@@ -16,21 +26,7 @@
</div>
</div>
<div class="col-9">
<app-org-vault-header
[filter]="filter"
[loading]="refreshing"
[organization]="organization"
[collection]="selectedCollection"
(onAddCipher)="addCipher()"
(onAddCollection)="addCollection()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-org-vault-header>
<app-callout
type="warning"
*ngIf="activeFilter.isDeleted"
icon="bwi bwi-exclamation-triangle"
>
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi bwi-exclamation-triangle">
{{ trashCleanupWarning }}
</app-callout>
<app-vault-items
@@ -58,7 +54,6 @@
[showBulkEditCollectionAccess]="
(showBulkEditCollectionAccess$ | async) && organization?.flexibleCollections
"
[viewingOrgVault]="true"
>
</app-vault-items>
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
@@ -85,8 +80,8 @@
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
</div>
</ng-container>
</div></ng-container
>
<ng-container *ngIf="flexibleCollectionsV1Enabled && !performingInitialLoad && isEmpty">
<bit-no-items *ngIf="!showCollectionAccessRestricted">
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
@@ -120,8 +115,7 @@
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collectionsModal></ng-template>
</div>
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collectionsModal></ng-template>

View File

@@ -1472,13 +1472,6 @@
"faviconDesc": {
"message": "Show a recognizable image next to each login."
},
"enableFullWidth": {
"message": "Display full width layout",
"description": "Allows scaling the web vault UI's width"
},
"enableFullWidthDesc": {
"message": "Allow the web vault to expand the full width of the browser window."
},
"default": {
"message": "Default"
},
@@ -4950,6 +4943,9 @@
"newClientOrganizationDesc": {
"message": "Create a new client organization that will be associated with you as the Provider. You will be able to access and manage this organization."
},
"newClient": {
"message": "New client"
},
"addExistingOrganization": {
"message": "Add existing organization"
},
@@ -7587,5 +7583,11 @@
},
"freeForOneYear": {
"message": "Free for 1 year"
},
"newWebApp": {
"message": "Welcome to the new and improved web app. Learn more about whats changed."
},
"releaseBlog": {
"message": "Read release blog"
}
}

View File

@@ -90,14 +90,6 @@ img.logo-themed {
margin-top: 20px;
}
.footer {
margin-top: 40px;
padding: 40px 0 40px 0;
@include themify($themes) {
border-top: 1px solid themed("separator");
}
}
hr,
.dropdown-divider {
@include themify($themes) {

View File

@@ -199,17 +199,3 @@ button.no-btn {
color: inherit;
}
}
button.header-expandable {
background: transparent;
border: none;
padding: 0;
color: inherit;
margin-bottom: 5px;
text-transform: uppercase;
}
/* special case for Send options */
h3 button.header-expandable .bwi {
line-height: 1.5;
}

View File

@@ -1,104 +0,0 @@
.navbar {
padding-left: 0;
padding-right: 0;
@include themify($themes) {
background-color: themed("navBackground");
}
&.nav-background-alt {
@include themify($themes) {
background-color: themed("navBackgroundAlt");
}
}
.nav-item {
> .nav-link {
@include themify($themes) {
font-weight: themed("navWeight");
}
}
&.active > .nav-link {
@include themify($themes) {
font-weight: themed("navActiveWeight");
}
}
}
}
.navbar-brand {
margin-bottom: -20px;
margin-top: -20px;
}
.nav-tabs .nav-link.active {
@include themify($themes) {
background: themed("navActiveBackground");
border-color: themed("borderColor");
}
}
.org-nav {
height: 100px;
min-height: 100px;
@include themify($themes) {
background-color: themed("navOrgBackgroundColor");
border-bottom: 1px solid themed("borderColor");
color: themed("textColor");
}
.container {
height: 100%;
}
.org-name {
line-height: 1;
text-align: left;
font-weight: normal;
span {
display: block;
font-size: $font-size-lg;
@include themify($themes) {
color: themed("textHeadingColor");
}
}
}
}
.tabbed-nav {
@include themify($themes) {
border-bottom: 1px solid themed("borderColor");
color: themed("textColor");
}
}
.org-nav,
.tabbed-nav {
.nav-tabs {
border-bottom: none;
a {
&:not(.active) {
border-color: transparent;
@include themify($themes) {
color: themed("textColor");
}
}
&.active {
font-weight: bold;
padding-top: calc(#{$nav-link-padding-y} - 2px);
@include themify($themes) {
border-top: 3px solid themed("primary");
border-bottom: 1px solid themed("backgroundColor");
color: themed("linkColor");
}
}
&.disabled {
@include themify($themes) {
color: themed("inputDisabledColor");
}
}
}
}
}

View File

@@ -24,7 +24,7 @@
@import "~bootstrap/scss/_input-group";
// @import "~bootstrap/scss/_custom-forms";
@import "~bootstrap/scss/_nav";
@import "~bootstrap/scss/_navbar";
// @import "~bootstrap/scss/_navbar";
@import "~bootstrap/scss/_card";
// @import "~bootstrap/scss/_breadcrumb";
// @import "~bootstrap/scss/_pagination";
@@ -50,7 +50,6 @@
@import "./callouts";
@import "./cards";
@import "./forms";
@import "./navigation";
@import "./modals";
@import "./pages";
@import "./plugins";

View File

@@ -3,3 +3,8 @@
@tailwind utilities;
@import "../../../../libs/components/src/tw-theme.css";
/* Prevent page from jumping when showing scrollbar */
html {
margin-right: calc(-100vw + 100%);
}

View File

@@ -34,7 +34,6 @@ $small-font-size: 90%;
$font-size-lg: 1.15rem;
$code-font-size: 100%;
$navbar-padding-y: 0.75rem;
$grid-gutter-width: 20px;
$card-spacer-y: 0.6rem;
@@ -49,13 +48,6 @@ $dropdown-link-active-color: $dropdown-link-color;
$dropdown-link-active-bg: rgba(#000000, 0.1);
$dropdown-item-padding-x: 1rem;
$navbar-brand-font-size: 35px;
$navbar-brand-height: 35px;
$navbar-brand-padding-y: 0;
$navbar-dark-color: rgba($white, 0.7);
$navbar-dark-hover-color: rgba($white, 0.9);
$navbar-nav-link-padding-x: 0.8rem;
$input-bg: #fbfbfb;
$input-focus-bg: $white;
$input-disabled-bg: #e0e0e0;
@@ -197,12 +189,6 @@ $themes: (
loadingSvg: url("../images/loading.svg"),
logoSuffix: "dark",
mfaLogoSuffix: ".png",
navActiveBackground: $white,
navActiveWeight: 600,
navBackground: $primary,
navBackgroundAlt: $secondary-alt,
navOrgBackgroundColor: #fbfbfb,
navWeight: 600,
pwStrengthBackground: #e9ecef,
separator: $secondary,
separatorHr: rgb(0, 0, 0, 0.1),
@@ -306,12 +292,6 @@ $themes: (
loadingSvg: url("../images/loading-white.svg"),
logoSuffix: "white",
mfaLogoSuffix: "-w.png",
navActiveBackground: $darkDarkBlue2,
navActiveWeight: 600,
navBackground: $darkDarkBlue1,
navBackgroundAlt: $darkDarkBlue1,
navOrgBackgroundColor: #161c26,
navWeight: 400,
pwStrengthBackground: $darkBlue2,
separator: $darkBlue1,
separatorHr: $darkBlue1,

Some files were not shown because too many files have changed in this diff Show More