1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-04 09:33:27 +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

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"
route="vault"
>{{ "collections" | i18n }}</bit-tab-link
>
<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-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"
*ngIf="canShowVaultTab(organization)"
>
</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,126 +1,114 @@
<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>
<app-header>
<bit-search
[placeholder]="'searchGroups' | i18n"
[(ngModel)]="searchText"
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>
</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">
<p *ngIf="!visibleGroups.length">{{ "noGroupsInList" | i18n }}</p>
<bit-table
*ngIf="visibleGroups.length"
infinite-scroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<input
bitInput
type="search"
placeholder="{{ 'searchGroups' | i18n }}"
class="tw-rounded-l tw-pl-9"
[(ngModel)]="searchText"
type="checkbox"
bitCheckbox
class="tw-mr-2"
(change)="toggleAllVisible($event)"
id="selectAll"
/>
</div>
</div>
<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">
<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">
<p *ngIf="!visibleGroups.length">{{ "noGroupsInList" | i18n }}</p>
<bit-table
*ngIf="visibleGroups.length"
infinite-scroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<input
type="checkbox"
bitCheckbox
class="tw-mr-2"
(change)="toggleAllVisible($event)"
id="selectAll"
/>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-10">
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-10">
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<button type="button" bitMenuItem (click)="deleteAllSelected()">
<span class="tw-text-danger"
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
>
</button>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let g of visibleGroups">
<td bitCell (click)="check(g)" class="tw-cursor-pointer">
<input type="checkbox" bitCheckbox [(ngModel)]="g.checked" />
</td>
<td bitCell class="tw-cursor-pointer tw-font-bold" (click)="edit(g)">
<button type="button" bitLink>
{{ g.details.name }}
<bit-menu #headerMenu>
<button type="button" bitMenuItem (click)="deleteAllSelected()">
<span class="tw-text-danger"
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
>
</button>
</td>
<td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer">
<bit-badge-list
*ngIf="!g.details.accessAll"
[items]="g.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
<span *ngIf="g.details.accessAll">{{ "all" | i18n }}</span>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let g of visibleGroups">
<td bitCell (click)="check(g)" class="tw-cursor-pointer">
<input type="checkbox" bitCheckbox [(ngModel)]="g.checked" />
</td>
<td bitCell class="tw-cursor-pointer tw-font-bold" (click)="edit(g)">
<button type="button" bitLink>
{{ g.details.name }}
</button>
</td>
<td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer">
<bit-badge-list
*ngIf="!g.details.accessAll"
[items]="g.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
<span *ngIf="g.details.accessAll">{{ "all" | i18n }}</span>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="edit(g)">
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editInfo" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Members)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "members" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
</button>
<button type="button" bitMenuItem (click)="delete(g)">
<span class="tw-text-danger"
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
<ng-template #addEdit></ng-template>
</div>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="edit(g)">
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editInfo" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Members)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "members" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
</button>
<button type="button" bitMenuItem (click)="delete(g)">
<span class="tw-text-danger"
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
<ng-template #addEdit></ng-template>

View File

@@ -1,331 +1,326 @@
<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">
<bit-toggle-group
[selected]="status"
(selectedChange)="filter($event)"
[attr.aria-label]="'memberStatusFilter' | i18n"
>
<bit-toggle [value]="null">
{{ "all" | i18n }} <span bitBadge variant="info" *ngIf="allCount">{{ allCount }}</span>
</bit-toggle>
<app-header>
<bit-search
class="tw-grow"
[(ngModel)]="searchText"
[placeholder]="'searchMembers' | i18n"
></bit-search>
<bit-toggle [value]="userStatusType.Invited">
{{ "invited" | i18n }}
<span bitBadge variant="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</bit-toggle>
<button type="button" bitButton buttonType="primary" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
</app-header>
<bit-toggle [value]="userStatusType.Accepted">
{{ "needsConfirmation" | i18n }}
<span bitBadge variant="info" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</bit-toggle>
<bit-toggle [value]="userStatusType.Revoked">
{{ "revoked" | i18n }}
<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">
<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 &&
(isPaging()
? pagedUsers
: (users | search: searchText : 'name' : 'email' : 'id')) as searchedUsers
"
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
<bit-toggle-group
[selected]="status"
(selectedChange)="filter($event)"
[attr.aria-label]="'memberStatusFilter' | i18n"
>
<p *ngIf="!searchedUsers.length">{{ "noMembersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "usersNeedConfirmed" | i18n }}
</app-callout>
<bit-table
infinite-scroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<input
type="checkbox"
bitCheckbox
class="tw-mr-1"
(change)="selectAll($any($event.target).checked)"
id="selectAll"
/>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
<th bitCell>{{ "role" | i18n }}</th>
<th bitCell>{{ "policies" | i18n }}</th>
<th bitCell class="tw-w-10">
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-toggle [value]="null">
{{ "all" | i18n }} <span bitBadge variant="info" *ngIf="allCount">{{ allCount }}</span>
</bit-toggle>
<bit-menu #headerMenu>
<ng-container *ngIf="canUseSecretsManager$ | async">
<button type="button" bitMenuItem (click)="bulkEnableSM()">
{{ "activateSecretsManager" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
</ng-container>
<button type="button" bitMenuItem (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<span class="tw-text-success">
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</span>
</button>
<button type="button" bitMenuItem (click)="bulkRestore()">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRevoke()">
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRemove()">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i>
{{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let u of searchedUsers" alignContent="middle">
<td bitCell (click)="checkUser(u)">
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
</td>
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div>
<button type="button" bitLink>
{{ u.name ?? u.email }}
</button>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
class="tw-text-xs"
variant="warning"
*ngIf="u.status === userStatusType.Accepted"
>{{ "needsConfirmation" | i18n }}</span
>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Revoked"
>{{ "revoked" | i18n }}</span
>
</div>
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
{{ u.email }}
</div>
</div>
</div>
</td>
<bit-toggle [value]="userStatusType.Invited">
{{ "invited" | i18n }}
<span bitBadge variant="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</bit-toggle>
<td
bitCell
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
class="tw-cursor-pointer"
>
<bit-badge-list
*ngIf="organization.useGroups || !u.accessAll"
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
<span *ngIf="!organization.useGroups && u.accessAll">{{ "all" | i18n }}</span>
</td>
<bit-toggle [value]="userStatusType.Accepted">
{{ "needsConfirmation" | i18n }}
<span bitBadge variant="info" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</bit-toggle>
<td
bitCell
(click)="edit(u, memberTab.Role)"
class="tw-cursor-pointer tw-text-sm tw-text-muted"
>
{{ u.type | userType }}
</td>
<bit-toggle [value]="userStatusType.Revoked">
{{ "revoked" | i18n }}
<span bitBadge variant="info" *ngIf="revokedCount">{{ revokedCount }}</span>
</bit-toggle>
</bit-toggle-group>
</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
*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
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "usersNeedConfirmed" | i18n }}
</app-callout>
<bit-table
infinite-scroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<input
type="checkbox"
bitCheckbox
class="tw-mr-1"
(change)="selectAll($any($event.target).checked)"
id="selectAll"
/>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
<th bitCell>{{ "role" | i18n }}</th>
<th bitCell>{{ "policies" | i18n }}</th>
<th bitCell class="tw-w-10">
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<td bitCell class="tw-text-muted">
<ng-container *ngIf="u.twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus($any(u))">
<i
class="bwi bwi-key"
title="{{ 'enrolledAccountRecovery' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
</ng-container>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button
type="button"
bitMenuItem
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i aria-hidden="true" class="bwi bwi-envelope"></i>
{{ "resendInvitation" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<span class="tw-text-success">
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
</span>
</button>
<bit-menu-divider
*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 }}
</button>
<button
type="button"
bitMenuItem
(click)="edit(u, memberTab.Groups)"
*ngIf="organization.useGroups"
>
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
<bit-menu #headerMenu>
<ng-container *ngIf="canUseSecretsManager$ | async">
<button type="button" bitMenuItem (click)="bulkEnableSM()">
{{ "activateSecretsManager" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
<button
type="button"
bitMenuItem
(click)="events(u)"
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
>
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="resetPassword(u)"
*ngIf="allowResetPassword(u)"
>
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="restore(u)"
*ngIf="u.status === userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
{{ "restoreAccess" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="revoke(u)"
*ngIf="u.status !== userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(u)">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
</ng-container>
<button type="button" bitMenuItem (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<span class="tw-text-success">
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</span>
</button>
<button type="button" bitMenuItem (click)="bulkRestore()">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRevoke()">
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRemove()">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i>
{{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let u of searchedUsers" alignContent="middle">
<td bitCell (click)="checkUser(u)">
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
</td>
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div>
<button type="button" bitLink>
{{ u.name ?? u.email }}
</button>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
class="tw-text-xs"
variant="warning"
*ngIf="u.status === userStatusType.Accepted"
>{{ "needsConfirmation" | i18n }}</span
>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Revoked"
>{{ "revoked" | i18n }}</span
>
</div>
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
{{ u.email }}
</div>
</div>
</div>
</td>
<td
bitCell
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
class="tw-cursor-pointer"
>
<bit-badge-list
*ngIf="organization.useGroups || !u.accessAll"
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
<span *ngIf="!organization.useGroups && u.accessAll">{{ "all" | i18n }}</span>
</td>
<td
bitCell
(click)="edit(u, memberTab.Role)"
class="tw-cursor-pointer tw-text-sm tw-text-muted"
>
{{ u.type | userType }}
</td>
<td bitCell class="tw-text-muted">
<ng-container *ngIf="u.twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus($any(u))">
<i
class="bwi bwi-key"
title="{{ 'enrolledAccountRecovery' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
</ng-container>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button
type="button"
bitMenuItem
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i aria-hidden="true" class="bwi bwi-envelope"></i>
{{ "resendInvitation" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<span class="tw-text-success">
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
</span>
</button>
<bit-menu-divider
*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 }}
</button>
<button
type="button"
bitMenuItem
(click)="edit(u, memberTab.Groups)"
*ngIf="organization.useGroups"
>
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
<button
type="button"
bitMenuItem
(click)="events(u)"
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
>
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="resetPassword(u)"
*ngIf="allowResetPassword(u)"
>
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="restore(u)"
*ngIf="u.status === userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
{{ "restoreAccess" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="revoke(u)"
*ngIf="u.status !== userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(u)">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</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,25 +1,26 @@
<div class="page-header d-flex">
<h1>{{ "policies" | i18n }}</h1>
</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>
<table class="table table-hover table-list" *ngIf="!loading">
<tbody>
<tr *ngFor="let p of policies">
<td *ngIf="p.display(organization)">
<a href="#" appStopClick (click)="edit(p)">{{ p.name | i18n }}</a>
<span bitBadge variant="success" *ngIf="policiesEnabledMap.get(p.type)">{{
"on" | i18n
}}</span>
<small class="text-muted d-block">{{ p.description | i18n }}</small>
</td>
</tr>
</tbody>
</table>
<ng-template #editTemplate></ng-template>
<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">
<tbody>
<tr *ngFor="let p of policies">
<td *ngIf="p.display(organization)">
<a href="#" appStopClick (click)="edit(p)">{{ p.name | i18n }}</a>
<span bitBadge variant="success" *ngIf="policiesEnabledMap.get(p.type)">{{
"on" | i18n
}}</span>
<small class="text-muted d-block">{{ p.description | i18n }}</small>
</td>
</tr>
</tbody>
</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,116 +1,119 @@
<h1 bitTypography="h1" class="tw-pb-2.5">{{ "organizationInfo" | i18n }}</h1>
<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 class="tw-grid tw-grid-cols-2 tw-gap-5">
<div>
<bit-form-field>
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput id="orgName" type="text" formControlName="orgName" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input bitInput id="billingEmail" formControlName="billingEmail" type="email" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "businessName" | i18n }}</bit-label>
<input bitInput id="businessName" formControlName="businessName" type="text" />
</bit-form-field>
</div>
<div>
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
<app-account-fingerprint
[fingerprintMaterial]="organizationId"
[publicKeyBuffer]="publicKeyBuffer"
fingerprintLabel="{{ 'yourOrganizationsFingerprint' | i18n }}"
>
</app-account-fingerprint>
</div>
<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>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "save" | i18n }}
</button>
</form>
<ng-container *ngIf="canUseApi">
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
<p>
{{ "apiKeyDesc" | i18n }}
<a href="https://docs.bitwarden.com" target="_blank" rel="noreferrer">
{{ "learnMore" | i18n }}
</a>
</p>
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
{{ "viewApiKey" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
{{ "rotateApiKey" | i18n }}
</button>
</ng-container>
<form
*ngIf="
org && !loading && !org.flexibleCollections && (flexibleCollectionsMigrationEnabled$ | async)
"
>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">
{{ "collectionManagement" | i18n }}
</h1>
<p>
{{ "collectionEnhancementsDesc" | i18n }}
<a href="https://bitwarden.com/help/collection-management" target="_blank" rel="noreferrer">
{{ "collectionEnhancementsLearnMore" | i18n }}
</a>
</p>
<button
type="button"
bitButton
buttonType="primary"
(click)="showConfirmCollectionEnhancementsDialog()"
<form *ngIf="org && !loading" [bitSubmit]="submit" [formGroup]="formGroup">
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
<div>
<bit-form-field>
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput id="orgName" type="text" formControlName="orgName" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input bitInput id="billingEmail" formControlName="billingEmail" type="email" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "businessName" | i18n }}</bit-label>
<input bitInput id="businessName" formControlName="businessName" type="text" />
</bit-form-field>
</div>
<div>
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
<app-account-fingerprint
[fingerprintMaterial]="organizationId"
[publicKeyBuffer]="publicKeyBuffer"
fingerprintLabel="{{ 'yourOrganizationsFingerprint' | i18n }}"
>
</app-account-fingerprint>
</div>
</div>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "save" | i18n }}
</button>
</form>
<ng-container *ngIf="canUseApi">
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
<p>
{{ "apiKeyDesc" | i18n }}
<a href="https://docs.bitwarden.com" target="_blank" rel="noreferrer">
{{ "learnMore" | i18n }}
</a>
</p>
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
{{ "viewApiKey" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
{{ "rotateApiKey" | i18n }}
</button>
</ng-container>
<form
*ngIf="
org && !loading && !org.flexibleCollections && (flexibleCollectionsMigrationEnabled$ | async)
"
>
{{ "enable" | i18n }}
</button>
</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">
<bit-label>{{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="allowAdminAccessToAllCollectionItems" />
</bit-form-control>
<bit-form-control>
<bit-label>{{ "limitCollectionCreationDeletionDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionCreationDeletion" />
</bit-form-control>
<button
*ngIf="!selfHosted"
type="submit"
bitButton
bitFormButton
buttonType="primary"
id="collectionManagementSubmitButton"
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">
{{ "collectionManagement" | i18n }}
</h1>
<p>
{{ "collectionEnhancementsDesc" | i18n }}
<a href="https://bitwarden.com/help/collection-management" target="_blank" rel="noreferrer">
{{ "collectionEnhancementsLearnMore" | i18n }}
</a>
</p>
<button
type="button"
bitButton
buttonType="primary"
(click)="showConfirmCollectionEnhancementsDialog()"
>
{{ "enable" | i18n }}
</button>
</form>
<form
*ngIf="org && !loading && org.flexibleCollections"
[bitSubmit]="submitCollectionManagement"
[formGroup]="collectionManagementFormGroup"
>
{{ "save" | i18n }}
</button>
</form>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "collectionManagement" | i18n }}</h1>
<p>{{ "collectionManagementDesc" | i18n }}</p>
<bit-form-control *ngIf="flexibleCollectionsV1Enabled$ | async">
<bit-label>{{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="allowAdminAccessToAllCollectionItems" />
</bit-form-control>
<bit-form-control>
<bit-label>{{ "limitCollectionCreationDeletionDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionCreationDeletion" />
</bit-form-control>
<button
*ngIf="!selfHosted"
type="submit"
bitButton
bitFormButton
buttonType="primary"
id="collectionManagementSubmitButton"
>
{{ "save" | i18n }}
</button>
</form>
<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>
<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>
<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>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
</div>
</div>
</div>
<app-header></app-header>
<bit-container>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
</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,104 +1,105 @@
<div class="page-header">
<h1>{{ "sponsoredFamilies" | i18n }}</h1>
</div>
<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">
<p>
{{ "sponsoredFamiliesEligible" | i18n }}
</p>
<div>
{{ "sponsoredFamiliesInclude" | i18n }}:
<ul class="inset-list">
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
</ul>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="sponsorshipForm"
ngNativeValidate
*ngIf="anyOrgsAvailable$ | async"
>
<div class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
<select
id="availableSponsorshipOrg"
name="Available Sponsorship Organization"
formControlName="selectedSponsorshipOrgId"
class="form-control"
required
>
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs$ | async" [ngValue]="o.id">
{{ o.name }}
</option>
</select>
</div>
<div class="form-group col-7">
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<input
id="sponsorshipEmail"
class="form-control"
inputmode="email"
formControlName="sponsorshipEmail"
name="sponsorshipEmail"
required
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
/>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "cannotSponsorSelf" | i18n }}
</small>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.email"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "invalidEmail" | i18n }}
</small>
</div>
<div class="form-group col-7">
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "redeem" | i18n }}</span>
</button>
</div>
</form>
<ng-container *ngIf="anyActiveSponsorships$ | async">
<div class="border-bottom">
<table class="table table-hover table-list">
<thead>
<tr>
<th>{{ "recipient" | i18n }}</th>
<th>{{ "sponsoringOrg" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs$ | async">
<tr
sponsoring-org-row
[sponsoringOrg]="o"
[isSelfHosted]="isSelfHosted"
(sponsorshipRemoved)="forceReload()"
></tr>
</ng-container>
</tbody>
</table>
</div>
<small>{{ "sponsoredFamiliesLeaveCopy" | i18n }}</small>
<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>
<ng-container *ngIf="!loading">
<p>
{{ "sponsoredFamiliesEligible" | i18n }}
</p>
<div>
{{ "sponsoredFamiliesInclude" | i18n }}:
<ul class="inset-list">
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
</ul>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="sponsorshipForm"
ngNativeValidate
*ngIf="anyOrgsAvailable$ | async"
>
<div class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
<select
id="availableSponsorshipOrg"
name="Available Sponsorship Organization"
formControlName="selectedSponsorshipOrgId"
class="form-control"
required
>
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs$ | async" [ngValue]="o.id">
{{ o.name }}
</option>
</select>
</div>
<div class="form-group col-7">
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<input
id="sponsorshipEmail"
class="form-control"
inputmode="email"
formControlName="sponsorshipEmail"
name="sponsorshipEmail"
required
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
/>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "cannotSponsorSelf" | i18n }}
</small>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.email"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "invalidEmail" | i18n }}
</small>
</div>
<div class="form-group col-7">
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "redeem" | i18n }}</span>
</button>
</div>
</form>
<ng-container *ngIf="anyActiveSponsorships$ | async">
<div class="border-bottom">
<table class="table table-hover table-list">
<thead>
<tr>
<th>{{ "recipient" | i18n }}</th>
<th>{{ "sponsoringOrg" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs$ | async">
<tr
sponsoring-org-row
[sponsoringOrg]="o"
[isSelfHosted]="isSelfHosted"
(sponsorshipRemoved)="forceReload()"
></tr>
</ng-container>
</tbody>
</table>
</div>
<small>{{ "sponsoredFamiliesLeaveCopy" | i18n }}</small>
</ng-container>
</ng-container>
</bit-container>