mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
SM-1146: Display total counts of projects, machine accounts, secrets in Secrets Manager (#9791)
* SM-1146: Secrets Manager total counts * SM-1146: Tab link component simplifications * SM-1146: Total counts update on CRUD * SM-1146: Total counts API paths update * SM-1146: Unit test coverage for services * SM-1146: Fix incorrect types returned * SM-1146: Storybook example for tab-link with child counter * SM-1146: Tailwind states with groups * SM-1146: Moving counts view types in one file * SM-1146: Moving counts methods, responses to one shared service * SM-1146: Removing redundant services imports * SM-1146: Removing redundant observables * SM-1337: Total counts hidden for suspended organizations * SM-1336: Total counts updated on import * SM-1336: Import error handling refactor * SM-1336: Import error handling improvements * SM-1336: Import error not working with project errors, Unit Test coverage * Update bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * SM-1336: UT deprecation removal * SM-1336: Better UT * SM-1336: Lint fix * SM-1146: Linter fix --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
parent
dfb69f8130
commit
a3bf74ae1b
@ -6,19 +6,31 @@
|
||||
[text]="'projects' | i18n"
|
||||
route="projects"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
>
|
||||
<div slot="end" *ngIf="isOrgEnabled$ | async">
|
||||
{{ organizationCounts?.projects }}
|
||||
</div>
|
||||
</bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-key"
|
||||
[text]="'secrets' | i18n"
|
||||
route="secrets"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
>
|
||||
<div slot="end" *ngIf="isOrgEnabled$ | async">
|
||||
{{ organizationCounts?.secrets }}
|
||||
</div>
|
||||
</bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-wrench"
|
||||
[text]="'machineAccounts' | i18n"
|
||||
route="machine-accounts"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
>
|
||||
<div slot="end" *ngIf="isOrgEnabled$ | async">
|
||||
{{ organizationCounts?.serviceAccounts }}
|
||||
</div>
|
||||
</bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-providers"
|
||||
[text]="'integrations' | i18n"
|
||||
|
@ -1,26 +1,91 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap } from "rxjs";
|
||||
import {
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
Observable,
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo";
|
||||
|
||||
import { OrganizationCounts } from "../models/view/counts.view";
|
||||
import { ProjectService } from "../projects/project.service";
|
||||
import { SecretService } from "../secrets/secret.service";
|
||||
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
||||
import { SecretsManagerPortingApiService } from "../settings/services/sm-porting-api.service";
|
||||
import { CountService } from "../shared/counts/count.service";
|
||||
|
||||
@Component({
|
||||
selector: "sm-navigation",
|
||||
templateUrl: "./navigation.component.html",
|
||||
})
|
||||
export class NavigationComponent {
|
||||
export class NavigationComponent implements OnInit, OnDestroy {
|
||||
protected readonly logo = SecretsManagerLogo;
|
||||
protected orgFilter = (org: Organization) => org.canAccessSecretsManager;
|
||||
protected isAdmin$ = this.route.params.pipe(
|
||||
concatMap(
|
||||
async (params) => (await this.organizationService.get(params.organizationId))?.isAdmin,
|
||||
),
|
||||
);
|
||||
protected isAdmin$: Observable<boolean>;
|
||||
protected isOrgEnabled$: Observable<boolean>;
|
||||
protected organizationCounts: OrganizationCounts;
|
||||
private destroy$: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private countService: CountService,
|
||||
private projectService: ProjectService,
|
||||
private secretService: SecretService,
|
||||
private serviceAccountService: ServiceAccountService,
|
||||
private portingApiService: SecretsManagerPortingApiService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const org$ = this.route.params.pipe(
|
||||
concatMap((params) => this.organizationService.get(params.organizationId)),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.isAdmin$ = org$.pipe(
|
||||
map((org) => org?.isAdmin),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.isOrgEnabled$ = org$.pipe(
|
||||
map((org) => org?.enabled),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
org$,
|
||||
this.projectService.project$.pipe(startWith(null)),
|
||||
this.secretService.secret$.pipe(startWith(null)),
|
||||
this.serviceAccountService.serviceAccount$.pipe(startWith(null)),
|
||||
this.portingApiService.imports$.pipe(startWith(null)),
|
||||
])
|
||||
.pipe(
|
||||
filter(([org]) => org?.enabled),
|
||||
switchMap(([org]) => this.countService.getOrganizationCounts(org.id)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((organizationCounts) => {
|
||||
this.organizationCounts = {
|
||||
projects: organizationCounts.projects,
|
||||
secrets: organizationCounts.secrets,
|
||||
serviceAccounts: organizationCounts.serviceAccounts,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
export type OrganizationCounts = {
|
||||
projects: number;
|
||||
secrets: number;
|
||||
serviceAccounts: number;
|
||||
};
|
||||
|
||||
export type ProjectCounts = {
|
||||
secrets: number;
|
||||
people: number;
|
||||
serviceAccounts: number;
|
||||
};
|
||||
|
||||
export type ServiceAccountCounts = {
|
||||
projects: number;
|
||||
people: number;
|
||||
accessTokens: number;
|
||||
};
|
@ -54,7 +54,7 @@
|
||||
[projects]="view.latestProjects"
|
||||
></sm-projects-list>
|
||||
<div *ngIf="view.allProjects.length > 0" class="tw-ml-auto tw-mt-4 tw-max-w-max">
|
||||
{{ "showingPortionOfTotal" | i18n: view.latestProjects.length : view.allProjects.length }}
|
||||
{{ "showingPortionOfTotal" | i18n: view.latestProjects.length : view.counts.projects }}
|
||||
<a bitLink routerLink="projects" class="tw-ml-2">{{ "viewAll" | i18n }}</a>
|
||||
</div>
|
||||
</sm-section>
|
||||
@ -72,7 +72,7 @@
|
||||
[secrets]="view.latestSecrets"
|
||||
></sm-secrets-list>
|
||||
<div *ngIf="view.allSecrets.length > 0" class="tw-ml-auto tw-mt-4 tw-max-w-max">
|
||||
{{ "showingPortionOfTotal" | i18n: view.latestSecrets.length : view.allSecrets.length }}
|
||||
{{ "showingPortionOfTotal" | i18n: view.latestSecrets.length : view.counts.secrets }}
|
||||
<a bitLink routerLink="secrets" class="tw-ml-2">{{ "viewAll" | i18n }}</a>
|
||||
</div>
|
||||
</sm-section>
|
||||
|
@ -21,6 +21,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationCounts } from "../models/view/counts.view";
|
||||
import { ProjectListView } from "../models/view/project-list.view";
|
||||
import { SecretListView } from "../models/view/secret-list.view";
|
||||
import {
|
||||
@ -51,6 +52,7 @@ import {
|
||||
ServiceAccountOperation,
|
||||
} from "../service-accounts/dialog/service-account-dialog.component";
|
||||
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
||||
import { CountService } from "../shared/counts/count.service";
|
||||
import { SecretsListComponent } from "../shared/secrets-list.component";
|
||||
|
||||
import { SMOnboardingTasks, SMOnboardingTasksService } from "./sm-onboarding-tasks.service";
|
||||
@ -87,11 +89,13 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||
latestProjects: ProjectListView[];
|
||||
latestSecrets: SecretListView[];
|
||||
tasks: OrganizationTasks;
|
||||
counts: OrganizationCounts;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private projectService: ProjectService,
|
||||
private countService: CountService,
|
||||
private secretService: SecretService,
|
||||
private serviceAccountService: ServiceAccountService,
|
||||
private dialogService: DialogService,
|
||||
@ -148,10 +152,19 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||
share(),
|
||||
);
|
||||
|
||||
const counts$ = combineLatest([
|
||||
orgId$,
|
||||
this.secretService.secret$.pipe(startWith(null)),
|
||||
this.projectService.project$.pipe(startWith(null)),
|
||||
]).pipe(
|
||||
switchMap(([orgId]) => this.countService.getOrganizationCounts(orgId)),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.view$ = orgId$.pipe(
|
||||
switchMap((orgId) =>
|
||||
combineLatest([projects$, secrets$, serviceAccounts$]).pipe(
|
||||
switchMap(async ([projects, secrets, serviceAccounts]) => ({
|
||||
combineLatest([projects$, secrets$, serviceAccounts$, counts$]).pipe(
|
||||
switchMap(async ([projects, secrets, serviceAccounts, counts]) => ({
|
||||
latestProjects: this.getRecentItems(projects, this.tableSize),
|
||||
latestSecrets: this.getRecentItems(secrets, this.tableSize),
|
||||
allProjects: projects,
|
||||
@ -162,6 +175,11 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||
createProject: projects.length > 0,
|
||||
createServiceAccount: serviceAccounts.length > 0,
|
||||
}),
|
||||
counts: {
|
||||
projects: counts.projects,
|
||||
secrets: counts.secrets,
|
||||
serviceAccounts: counts.serviceAccounts,
|
||||
},
|
||||
})),
|
||||
),
|
||||
),
|
||||
|
@ -3,10 +3,25 @@
|
||||
<bit-breadcrumb [route]="['..']" icon="bwi-angle-left">{{ "projects" | i18n }}</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
<bit-tab-nav-bar label="Main" slot="tabs">
|
||||
<bit-tab-link [route]="['secrets']">{{ "secrets" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['secrets']">
|
||||
{{ "secrets" | i18n }}
|
||||
<div slot="end" class="tw-text-muted">
|
||||
{{ projectCounts?.secrets }}
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
<ng-container *ngIf="project.write">
|
||||
<bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['machine-accounts']">{{ "machineAccounts" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['people']">
|
||||
{{ "people" | i18n }}
|
||||
<div slot="end" class="tw-text-muted">
|
||||
{{ projectCounts?.people }}
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
<bit-tab-link [route]="['machine-accounts']">
|
||||
{{ "machineAccounts" | i18n }}
|
||||
<div slot="end" class="tw-text-muted">
|
||||
{{ projectCounts?.serviceAccounts }}
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
</ng-container>
|
||||
</bit-tab-nav-bar>
|
||||
<sm-new-menu></sm-new-menu>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
@ -13,11 +13,13 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ProjectCounts } from "../../models/view/counts.view";
|
||||
import { ProjectView } from "../../models/view/project.view";
|
||||
import { SecretService } from "../../secrets/secret.service";
|
||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||
import { CountService } from "../../shared/counts/count.service";
|
||||
import {
|
||||
OperationType,
|
||||
ProjectDialogComponent,
|
||||
@ -31,6 +33,7 @@ import { ProjectService } from "../project.service";
|
||||
})
|
||||
export class ProjectComponent implements OnInit, OnDestroy {
|
||||
protected project$: Observable<ProjectView>;
|
||||
protected projectCounts: ProjectCounts;
|
||||
|
||||
private organizationId: string;
|
||||
private projectId: string;
|
||||
@ -40,11 +43,11 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private projectService: ProjectService,
|
||||
private router: Router,
|
||||
private secretService: SecretService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
private dialogService: DialogService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService,
|
||||
private countService: CountService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -62,13 +65,23 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
||||
const organization$ = this.route.params.pipe(
|
||||
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
||||
);
|
||||
const projectCounts$ = combineLatest([
|
||||
this.route.params,
|
||||
this.secretService.secret$.pipe(startWith(null)),
|
||||
this.accessPolicyService.accessPolicy$.pipe(startWith(null)),
|
||||
]).pipe(switchMap(([params]) => this.countService.getProjectCounts(params.projectId)));
|
||||
|
||||
combineLatest([projectId$, organization$])
|
||||
combineLatest([projectId$, organization$, projectCounts$])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([projectId, organization]) => {
|
||||
.subscribe(([projectId, organization, projectCounts]) => {
|
||||
this.organizationId = organization.id;
|
||||
this.projectId = projectId;
|
||||
this.organizationEnabled = organization.enabled;
|
||||
this.projectCounts = {
|
||||
secrets: projectCounts.secrets,
|
||||
people: projectCounts.people,
|
||||
serviceAccounts: projectCounts.serviceAccounts,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view";
|
||||
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
|
||||
import {
|
||||
ApItemValueType,
|
||||
@ -179,7 +180,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy {
|
||||
private async updateServiceAccountPeopleAccessPolicies(
|
||||
serviceAccountId: string,
|
||||
selectedPolicies: ApItemValueType[],
|
||||
) {
|
||||
): Promise<ServiceAccountPeopleAccessPoliciesView> {
|
||||
const serviceAccountPeopleView = convertToPeopleAccessPoliciesView(selectedPolicies);
|
||||
return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies(
|
||||
serviceAccountId,
|
||||
|
@ -10,9 +10,24 @@
|
||||
</bit-breadcrumbs>
|
||||
<sm-new-menu></sm-new-menu>
|
||||
<bit-tab-nav-bar label="Main" slot="tabs">
|
||||
<bit-tab-link [route]="['projects']">{{ "projects" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['access']">{{ "accessTokens" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['projects']">
|
||||
{{ "projects" | i18n }}
|
||||
<div slot="end" class="tw-text-muted">
|
||||
{{ serviceAccountCounts?.projects }}
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
<bit-tab-link [route]="['people']">
|
||||
{{ "people" | i18n }}
|
||||
<div slot="end" class="tw-text-muted">
|
||||
{{ serviceAccountCounts?.people }}
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
<bit-tab-link [route]="['access']">
|
||||
{{ "accessTokens" | i18n }}
|
||||
<div slot="end" class="tw-text-muted">
|
||||
{{ serviceAccountCounts?.accessTokens }}
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
<bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
<button
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Subject, combineLatest, filter, startWith, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ServiceAccountCounts } from "../models/view/counts.view";
|
||||
import { ServiceAccountView } from "../models/view/service-account.view";
|
||||
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
|
||||
import { CountService } from "../shared/counts/count.service";
|
||||
|
||||
import { AccessService } from "./access/access.service";
|
||||
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
|
||||
import { ServiceAccountService } from "./service-account.service";
|
||||
|
||||
@ -17,7 +19,6 @@ import { ServiceAccountService } from "./service-account.service";
|
||||
})
|
||||
export class ServiceAccountComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private organizationId: string;
|
||||
private serviceAccountId: string;
|
||||
|
||||
private onChange$ = this.serviceAccountService.serviceAccount$.pipe(
|
||||
@ -34,20 +35,39 @@ export class ServiceAccountComponent implements OnInit, OnDestroy {
|
||||
),
|
||||
),
|
||||
);
|
||||
protected serviceAccountCounts: ServiceAccountCounts;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private serviceAccountService: ServiceAccountService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
private accessService: AccessService,
|
||||
private dialogService: DialogService,
|
||||
private router: Router,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private countService: CountService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.serviceAccount$.pipe(takeUntil(this.destroy$)).subscribe((serviceAccountView) => {
|
||||
this.serviceAccountView = serviceAccountView;
|
||||
});
|
||||
const serviceAccountCounts$ = combineLatest([
|
||||
this.route.params,
|
||||
this.accessPolicyService.accessPolicy$.pipe(startWith(null)),
|
||||
this.accessService.accessToken$.pipe(startWith(null)),
|
||||
this.onChange$,
|
||||
]).pipe(
|
||||
switchMap(([params, _]) =>
|
||||
this.countService.getServiceAccountCounts(params.serviceAccountId),
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest([this.serviceAccount$, serviceAccountCounts$])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([serviceAccountView, serviceAccountCounts]) => {
|
||||
this.serviceAccountView = serviceAccountView;
|
||||
this.serviceAccountCounts = {
|
||||
projects: serviceAccountCounts.projects,
|
||||
people: serviceAccountCounts.people,
|
||||
accessTokens: serviceAccountCounts.accessTokens,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@ -1,6 +1,6 @@
|
||||
export class SecretsManagerImportErrorLine {
|
||||
id: number;
|
||||
type: "Project" | "Secret";
|
||||
key: "string";
|
||||
key: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
@ -3,13 +3,11 @@ import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
SecretsManagerImportErrorDialogComponent,
|
||||
@ -33,8 +31,7 @@ export class SecretsManagerImportComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
private logService: LogService,
|
||||
private secretsManagerPortingApiService: SecretsManagerPortingApiService,
|
||||
@ -60,45 +57,42 @@ export class SecretsManagerImportComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
if (importContents == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("selectFile"),
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectFile"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const error = await this.secretsManagerPortingApiService.import(this.orgId, importContents);
|
||||
await this.secretsManagerPortingApiService.import(this.orgId, importContents);
|
||||
|
||||
if (error?.lines?.length > 0) {
|
||||
this.openImportErrorDialog(error);
|
||||
return;
|
||||
} else if (!Utils.isNullOrWhitespace(error?.message)) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
error.message,
|
||||
);
|
||||
return;
|
||||
} else if (error != null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("errorReadingImportFile"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess"));
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("importSuccess"),
|
||||
});
|
||||
this.clearForm();
|
||||
} catch (error) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("errorReadingImportFile"),
|
||||
);
|
||||
this.logService.error(error);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SecretsManagerImportError && error?.lines?.length > 0) {
|
||||
this.openImportErrorDialog(error);
|
||||
} else {
|
||||
let message;
|
||||
if (error instanceof Error && !Utils.isNullOrWhitespace(error?.message)) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = this.i18nService.t("errorReadingImportFile");
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message,
|
||||
});
|
||||
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,286 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { SecretsManagerImportError } from "../models/error/sm-import-error";
|
||||
import { SecretsManagerImportRequest } from "../models/requests/sm-import.request";
|
||||
import { SecretsManagerImportedProjectRequest } from "../models/requests/sm-imported-project.request";
|
||||
import { SecretsManagerImportedSecretRequest } from "../models/requests/sm-imported-secret.request";
|
||||
|
||||
import { SecretsManagerPortingApiService } from "./sm-porting-api.service";
|
||||
|
||||
describe("SecretsManagerPortingApiService", () => {
|
||||
let sut: SecretsManagerPortingApiService;
|
||||
|
||||
const apiService = mock<ApiService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
sut = new SecretsManagerPortingApiService(apiService, encryptService, cryptoService);
|
||||
|
||||
encryptService.encrypt.mockResolvedValue(mockEncryptedString);
|
||||
encryptService.decryptToUtf8.mockResolvedValue(mockUnencryptedString);
|
||||
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("import", () => {
|
||||
const organizationId = Utils.newGuid();
|
||||
const project1 = createProject();
|
||||
const project2 = createProject();
|
||||
const secret1 = createSecret(project1.id);
|
||||
const secret2 = createSecret(project1.id);
|
||||
|
||||
const importData = createImportData([project1, project2], [secret1, secret2]);
|
||||
|
||||
it("emits the import successful", async () => {
|
||||
const expectedRequest = toRequest([project1, project2], [secret1, secret2]);
|
||||
|
||||
let subscriptionCount = 0;
|
||||
sut.imports$.subscribe((request) => {
|
||||
expect(request).toBeDefined();
|
||||
expect(request.projects.length).toEqual(2);
|
||||
expect(request.secrets.length).toEqual(2);
|
||||
expect(request.projects[0]).toEqual(expectedRequest.projects[0]);
|
||||
expect(request.projects[1]).toEqual(expectedRequest.projects[1]);
|
||||
expect(request.secrets[0]).toEqual(expectedRequest.secrets[0]);
|
||||
expect(request.secrets[1]).toEqual(expectedRequest.secrets[1]);
|
||||
subscriptionCount++;
|
||||
});
|
||||
|
||||
await sut.import(organizationId, importData);
|
||||
|
||||
expect(subscriptionCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("correct api service send parameters", async () => {
|
||||
const expectedRequest = toRequest([project1, project2], [secret1, secret2]);
|
||||
|
||||
await sut.import(organizationId, importData);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
`/sm/${organizationId}/import`,
|
||||
expectedRequest,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
describe("throws SecretsManagerImportError", () => {
|
||||
it("server error", async () => {
|
||||
apiService.send.mockRejectedValue(new Error("server error"));
|
||||
|
||||
await expect(async () => {
|
||||
await sut.import(organizationId, importData);
|
||||
}).rejects.toThrow(new SecretsManagerImportError("server error"));
|
||||
});
|
||||
|
||||
it("validation error project invalid field in list", async () => {
|
||||
apiService.send.mockRejectedValue({
|
||||
message: "invalid field",
|
||||
validationErrors: {
|
||||
"$.Projects[1].id": ["invalid id"],
|
||||
},
|
||||
} as ValidationError);
|
||||
|
||||
const expectedError = new SecretsManagerImportError();
|
||||
expectedError.lines = [
|
||||
{
|
||||
id: 2,
|
||||
type: "Project",
|
||||
key: project2.name,
|
||||
errorMessage: "invalid id",
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await sut.import(organizationId, importData);
|
||||
// Expected to throw error before this line
|
||||
expect(true).toBe(false);
|
||||
} catch (err: unknown) {
|
||||
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||
const importError = err as SecretsManagerImportError;
|
||||
expect(importError.lines).toEqual(expectedError.lines);
|
||||
}
|
||||
});
|
||||
|
||||
it("validation error project invalid field in field", async () => {
|
||||
apiService.send.mockRejectedValue({
|
||||
message: "invalid field",
|
||||
validationErrors: {
|
||||
"Projects[1].id": ["invalid id"],
|
||||
},
|
||||
} as ValidationError);
|
||||
|
||||
const expectedError = new SecretsManagerImportError();
|
||||
expectedError.lines = [
|
||||
{
|
||||
id: 2,
|
||||
type: "Project",
|
||||
key: project2.name,
|
||||
errorMessage: "invalid id",
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await sut.import(organizationId, importData);
|
||||
// Expected to throw error before this line
|
||||
expect(true).toBe(false);
|
||||
} catch (err: unknown) {
|
||||
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||
const importError = err as SecretsManagerImportError;
|
||||
expect(importError.lines).toEqual(expectedError.lines);
|
||||
}
|
||||
});
|
||||
|
||||
it("validation error secret invalid field in list", async () => {
|
||||
apiService.send.mockRejectedValue({
|
||||
message: "invalid field",
|
||||
validationErrors: {
|
||||
"$.Secrets[1].id": ["invalid id"],
|
||||
},
|
||||
} as ValidationError);
|
||||
|
||||
const expectedError = new SecretsManagerImportError();
|
||||
expectedError.lines = [
|
||||
{
|
||||
id: 2,
|
||||
type: "Secret",
|
||||
key: secret2.key,
|
||||
errorMessage: "invalid id",
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await sut.import(organizationId, importData);
|
||||
// Expected to throw error before this line
|
||||
expect(true).toBe(false);
|
||||
} catch (err: unknown) {
|
||||
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||
const importError = err as SecretsManagerImportError;
|
||||
expect(importError.lines).toEqual(expectedError.lines);
|
||||
}
|
||||
});
|
||||
|
||||
it("validation error secret invalid field in field", async () => {
|
||||
apiService.send.mockRejectedValue({
|
||||
message: "invalid field",
|
||||
validationErrors: {
|
||||
"Secrets[1].id": ["invalid id"],
|
||||
},
|
||||
} as ValidationError);
|
||||
|
||||
const expectedError = new SecretsManagerImportError();
|
||||
expectedError.lines = [
|
||||
{
|
||||
id: 2,
|
||||
type: "Secret",
|
||||
key: secret2.key,
|
||||
errorMessage: "invalid id",
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await sut.import(organizationId, importData);
|
||||
// Expected to throw error before this line
|
||||
expect(true).toBe(false);
|
||||
} catch (err: unknown) {
|
||||
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||
const importError = err as SecretsManagerImportError;
|
||||
expect(importError.lines).toEqual(expectedError.lines);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type ValidationError = {
|
||||
message: string;
|
||||
validationErrors: Record<string, string[]>;
|
||||
};
|
||||
|
||||
type ImportProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ImportSecret = {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
note: string;
|
||||
projectIds: string[];
|
||||
};
|
||||
|
||||
function createProject(): ImportProject {
|
||||
const id = Utils.newGuid();
|
||||
return {
|
||||
id: id,
|
||||
name: "project " + id,
|
||||
};
|
||||
}
|
||||
|
||||
function createSecret(projectId: string): ImportSecret {
|
||||
const id = Utils.newGuid();
|
||||
return {
|
||||
id: id,
|
||||
key: "key " + id,
|
||||
value: "value " + id,
|
||||
note: "note " + id,
|
||||
projectIds: [projectId],
|
||||
};
|
||||
}
|
||||
|
||||
function createImportData(projects: ImportProject[], secrets: ImportSecret[]): string {
|
||||
return JSON.stringify({
|
||||
projects: projects,
|
||||
secrets: secrets,
|
||||
});
|
||||
}
|
||||
|
||||
function toRequest(
|
||||
projects: ImportProject[],
|
||||
secrets: ImportSecret[],
|
||||
): SecretsManagerImportRequest {
|
||||
return {
|
||||
projects: projects.map(
|
||||
(project) =>
|
||||
({
|
||||
id: project.id,
|
||||
name: mockEncryptedString,
|
||||
}) as SecretsManagerImportedProjectRequest,
|
||||
),
|
||||
secrets: secrets.map(
|
||||
(secret) =>
|
||||
({
|
||||
id: secret.id,
|
||||
key: mockEncryptedString,
|
||||
value: mockEncryptedString,
|
||||
note: mockEncryptedString,
|
||||
projectIds: secret.projectIds,
|
||||
}) as SecretsManagerImportedSecretRequest,
|
||||
),
|
||||
} as SecretsManagerImportRequest;
|
||||
}
|
||||
|
||||
const mockEncryptedString = {
|
||||
encryptedString: "mockEncryptedString",
|
||||
} as EncString;
|
||||
const mockUnencryptedString = "mockUnEncryptedString";
|
@ -1,10 +1,10 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
|
||||
import { SecretsManagerImportError } from "../models/error/sm-import-error";
|
||||
@ -22,11 +22,13 @@ import {
|
||||
providedIn: "root",
|
||||
})
|
||||
export class SecretsManagerPortingApiService {
|
||||
protected _imports = new Subject<SecretsManagerImportRequest>();
|
||||
imports$ = this._imports.asObservable();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async export(organizationId: string): Promise<string> {
|
||||
@ -45,7 +47,7 @@ export class SecretsManagerPortingApiService {
|
||||
);
|
||||
}
|
||||
|
||||
async import(organizationId: string, fileContents: string): Promise<SecretsManagerImportError> {
|
||||
async import(organizationId: string, fileContents: string): Promise<void> {
|
||||
let requestObject = {};
|
||||
|
||||
try {
|
||||
@ -59,9 +61,11 @@ export class SecretsManagerPortingApiService {
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
this._imports.next(requestBody);
|
||||
} catch (error) {
|
||||
const errorResponse = new ErrorResponse(error, 400);
|
||||
return this.handleServerError(errorResponse, requestObject);
|
||||
throw this.handleServerError(errorResponse, requestObject);
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,12 +166,12 @@ export class SecretsManagerPortingApiService {
|
||||
let itemType;
|
||||
const id = Number(key.match(/[0-9]+/)[0]);
|
||||
|
||||
switch (key.match(/^\w+/)[0]) {
|
||||
case "Projects":
|
||||
switch (key.match(/^[$\\.]*(\w+)/)[1].toLowerCase()) {
|
||||
case "projects":
|
||||
item = importResult.projects[id];
|
||||
itemType = "Project";
|
||||
break;
|
||||
case "Secrets":
|
||||
case "secrets":
|
||||
item = importResult.secrets[id];
|
||||
itemType = "Secret";
|
||||
break;
|
||||
@ -177,8 +181,8 @@ export class SecretsManagerPortingApiService {
|
||||
|
||||
result.lines.push({
|
||||
id: id + 1,
|
||||
type: itemType == "Project" ? "Project" : "Secret",
|
||||
key: item.key,
|
||||
type: itemType === "Project" ? "Project" : "Secret",
|
||||
key: itemType === "Project" ? item.name : item.key,
|
||||
errorMessage: value.length > 0 ? value[0] : "",
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||
import { SecretsManagerImportErrorDialogComponent } from "./dialog/sm-import-error-dialog.component";
|
||||
import { SecretsManagerExportComponent } from "./porting/sm-export.component";
|
||||
import { SecretsManagerImportComponent } from "./porting/sm-import.component";
|
||||
import { SecretsManagerPortingApiService } from "./services/sm-porting-api.service";
|
||||
import { SecretsManagerPortingService } from "./services/sm-porting.service";
|
||||
import { SettingsRoutingModule } from "./settings-routing.module";
|
||||
|
||||
@ -16,6 +15,6 @@ import { SettingsRoutingModule } from "./settings-routing.module";
|
||||
SecretsManagerExportComponent,
|
||||
SecretsManagerImportErrorDialogComponent,
|
||||
],
|
||||
providers: [SecretsManagerPortingService, SecretsManagerPortingApiService],
|
||||
providers: [SecretsManagerPortingService],
|
||||
})
|
||||
export class SettingsModule {}
|
||||
|
@ -0,0 +1,359 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import {
|
||||
GroupAccessPolicyView,
|
||||
ServiceAccountAccessPolicyView,
|
||||
UserAccessPolicyView,
|
||||
} from "../../models/view/access-policies/access-policy.view";
|
||||
import { ProjectPeopleAccessPoliciesView } from "../../models/view/access-policies/project-people-access-policies.view";
|
||||
import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view";
|
||||
import {
|
||||
GrantedProjectPolicyPermissionDetailsView,
|
||||
ServiceAccountGrantedPoliciesView,
|
||||
} from "../../models/view/access-policies/service-account-granted-policies.view";
|
||||
import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view";
|
||||
|
||||
import { AccessPolicyService } from "./access-policy.service";
|
||||
import { PeopleAccessPoliciesRequest } from "./models/requests/people-access-policies.request";
|
||||
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
|
||||
import { ServiceAccountGrantedPoliciesRequest } from "./models/requests/service-account-granted-policies.request";
|
||||
|
||||
import { trackEmissions } from "@bitwarden/common/../spec";
|
||||
|
||||
describe("AccessPolicyService", () => {
|
||||
let sut: AccessPolicyService;
|
||||
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
sut = new AccessPolicyService(cryptoService, apiService, encryptService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("putProjectPeopleAccessPolicies", () => {
|
||||
it("emits the updated policies", async () => {
|
||||
const userAccessPolicyView1 = createUserAccessPolicyView(false, true);
|
||||
const userAccessPolicyView2 = createUserAccessPolicyView(true, false);
|
||||
const groupAccessPolicyView1 = createGroupAccessPolicyView(false, true);
|
||||
const groupAccessPolicyView2 = createGroupAccessPolicyView(true, false);
|
||||
|
||||
const view = {
|
||||
userAccessPolicies: [userAccessPolicyView1, userAccessPolicyView2],
|
||||
groupAccessPolicies: [groupAccessPolicyView1, groupAccessPolicyView2],
|
||||
} as ProjectPeopleAccessPoliciesView;
|
||||
|
||||
apiService.send.mockResolvedValue(toProjectPeopleAccessPoliciesResponseRaw(view));
|
||||
const emissions = trackEmissions(sut.accessPolicy$);
|
||||
const expectedRequest = toPeopleAccessPoliciesRequest(view);
|
||||
const projectId = Utils.newGuid();
|
||||
|
||||
const result = await sut.putProjectPeopleAccessPolicies(projectId, view);
|
||||
|
||||
expect(result).toEqual(view);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/projects/" + projectId + "/access-policies/people",
|
||||
expectedRequest,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(emissions).toEqual([view]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putServiceAccountPeopleAccessPolicies", () => {
|
||||
it("emits the updated policies", async () => {
|
||||
const userAccessPolicyView1 = createUserAccessPolicyView(false, true);
|
||||
const userAccessPolicyView2 = createUserAccessPolicyView(true, false);
|
||||
const groupAccessPolicyView1 = createGroupAccessPolicyView(false, true);
|
||||
const groupAccessPolicyView2 = createGroupAccessPolicyView(true, false);
|
||||
|
||||
const view = {
|
||||
userAccessPolicies: [userAccessPolicyView1, userAccessPolicyView2],
|
||||
groupAccessPolicies: [groupAccessPolicyView1, groupAccessPolicyView2],
|
||||
} as ServiceAccountPeopleAccessPoliciesView;
|
||||
|
||||
apiService.send.mockResolvedValue(toServiceAccountPeopleAccessPoliciesResponseRaw(view));
|
||||
const emissions = trackEmissions(sut.accessPolicy$);
|
||||
const expectedRequest = toPeopleAccessPoliciesRequest(view);
|
||||
const projectId = Utils.newGuid();
|
||||
|
||||
const result = await sut.putServiceAccountPeopleAccessPolicies(projectId, view);
|
||||
|
||||
expect(result).toEqual(view);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/service-accounts/" + projectId + "/access-policies/people",
|
||||
expectedRequest,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(emissions).toEqual([view]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putServiceAccountGrantedPolicies", () => {
|
||||
it("emits the updated policies", async () => {
|
||||
const policyPermissionDetailsView1 = createGrantedProjectPolicyPermissionDetailsView(
|
||||
false,
|
||||
false,
|
||||
);
|
||||
const policyPermissionDetailsView2 = createGrantedProjectPolicyPermissionDetailsView(
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const policyPermissionDetailsView3 = createGrantedProjectPolicyPermissionDetailsView(
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
const view = {
|
||||
grantedProjectPolicies: [
|
||||
policyPermissionDetailsView1,
|
||||
policyPermissionDetailsView2,
|
||||
policyPermissionDetailsView3,
|
||||
],
|
||||
} as ServiceAccountGrantedPoliciesView;
|
||||
|
||||
apiService.send.mockResolvedValue(toServiceAccountGrantedPoliciesResponseRaw(view));
|
||||
const emissions = trackEmissions(sut.accessPolicy$);
|
||||
const expectedRequest = toServiceAccountGrantedPoliciesRequest(view);
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
|
||||
const organizationId = Utils.newGuid();
|
||||
const serviceAccountId = Utils.newGuid();
|
||||
|
||||
const result = await sut.putServiceAccountGrantedPolicies(
|
||||
organizationId,
|
||||
serviceAccountId,
|
||||
view,
|
||||
);
|
||||
|
||||
expect(result).toEqual(view);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/service-accounts/" + serviceAccountId + "/granted-policies",
|
||||
expectedRequest,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(emissions).toEqual([view]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putProjectServiceAccountsAccessPolicies", () => {
|
||||
it("emits the updated policies", async () => {
|
||||
const accessPolicyView1 = createServiceAccountAccessPolicyView(false);
|
||||
const accessPolicyView2 = createServiceAccountAccessPolicyView(true);
|
||||
|
||||
const view = {
|
||||
serviceAccountAccessPolicies: [accessPolicyView1, accessPolicyView2],
|
||||
} as ProjectServiceAccountsAccessPoliciesView;
|
||||
|
||||
apiService.send.mockResolvedValue(toProjectServiceAccountsAccessPoliciesResponseRaw(view));
|
||||
const emissions = trackEmissions(sut.accessPolicy$);
|
||||
const expectedRequest = toProjectServiceAccountsAccessPoliciesRequest(view);
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
|
||||
const organizationId = Utils.newGuid();
|
||||
const projectId = Utils.newGuid();
|
||||
|
||||
const result = await sut.putProjectServiceAccountsAccessPolicies(
|
||||
organizationId,
|
||||
projectId,
|
||||
view,
|
||||
);
|
||||
|
||||
expect(result).toEqual(view);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/projects/" + projectId + "/access-policies/service-accounts",
|
||||
expectedRequest,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(emissions).toEqual([view]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createUserAccessPolicyView(isWrite: boolean, currentUser: boolean): UserAccessPolicyView {
|
||||
const id = Utils.newGuid();
|
||||
return {
|
||||
organizationUserId: id,
|
||||
organizationUserName: "Example organization user name " + id,
|
||||
read: true,
|
||||
write: isWrite,
|
||||
currentUser: currentUser,
|
||||
};
|
||||
}
|
||||
|
||||
function createGroupAccessPolicyView(
|
||||
isWrite: boolean,
|
||||
currentUserInGroup: boolean,
|
||||
): GroupAccessPolicyView {
|
||||
const id = Utils.newGuid();
|
||||
return {
|
||||
groupId: id,
|
||||
groupName: "Example group name " + id,
|
||||
currentUserInGroup: currentUserInGroup,
|
||||
read: true,
|
||||
write: isWrite,
|
||||
};
|
||||
}
|
||||
|
||||
function createServiceAccountAccessPolicyView(isWrite: boolean): ServiceAccountAccessPolicyView {
|
||||
const id = Utils.newGuid();
|
||||
return {
|
||||
serviceAccountId: id,
|
||||
serviceAccountName: "Example service account name " + id,
|
||||
read: true,
|
||||
write: isWrite,
|
||||
};
|
||||
}
|
||||
|
||||
function createGrantedProjectPolicyPermissionDetailsView(
|
||||
isWrite: boolean,
|
||||
hasPermissions: boolean,
|
||||
): GrantedProjectPolicyPermissionDetailsView {
|
||||
const id = Utils.newGuid();
|
||||
return {
|
||||
accessPolicy: {
|
||||
grantedProjectId: id,
|
||||
grantedProjectName: "Example project name " + id,
|
||||
read: true,
|
||||
write: isWrite,
|
||||
},
|
||||
hasPermission: hasPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
function toPeopleAccessPoliciesRequest(
|
||||
view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView,
|
||||
): PeopleAccessPoliciesRequest {
|
||||
return {
|
||||
userAccessPolicyRequests: view.userAccessPolicies.map((ap) => ({
|
||||
granteeId: ap.organizationUserId,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
groupAccessPolicyRequests: view.groupAccessPolicies.map((ap) => ({
|
||||
granteeId: ap.groupId,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function toServiceAccountGrantedPoliciesRequest(
|
||||
view: ServiceAccountGrantedPoliciesView,
|
||||
): ServiceAccountGrantedPoliciesRequest {
|
||||
return {
|
||||
projectGrantedPolicyRequests: view.grantedProjectPolicies.map((ap) => ({
|
||||
grantedId: ap.accessPolicy.grantedProjectId,
|
||||
read: ap.accessPolicy.read,
|
||||
write: ap.accessPolicy.write,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function toProjectServiceAccountsAccessPoliciesRequest(
|
||||
view: ProjectServiceAccountsAccessPoliciesView,
|
||||
): ProjectServiceAccountsAccessPoliciesRequest {
|
||||
return {
|
||||
serviceAccountAccessPolicyRequests: view.serviceAccountAccessPolicies.map((ap) => {
|
||||
return {
|
||||
granteeId: ap.serviceAccountId,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function toProjectPeopleAccessPoliciesResponseRaw(view: ProjectPeopleAccessPoliciesView) {
|
||||
return {
|
||||
userAccessPolicies: view.userAccessPolicies.map((ap) => ({
|
||||
organizationUserId: ap.organizationUserId,
|
||||
organizationUserName: ap.organizationUserName,
|
||||
currentUser: ap.currentUser,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
groupAccessPolicies: view.groupAccessPolicies.map((ap) => ({
|
||||
groupId: ap.groupId,
|
||||
groupName: ap.groupName,
|
||||
currentUserInGroup: ap.currentUserInGroup,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function toServiceAccountPeopleAccessPoliciesResponseRaw(
|
||||
view: ServiceAccountPeopleAccessPoliciesView,
|
||||
) {
|
||||
return {
|
||||
userAccessPolicies: view.userAccessPolicies.map((ap) => ({
|
||||
organizationUserId: ap.organizationUserId,
|
||||
organizationUserName: ap.organizationUserName,
|
||||
currentUser: ap.currentUser,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
groupAccessPolicies: view.groupAccessPolicies.map((ap) => ({
|
||||
groupId: ap.groupId,
|
||||
groupName: ap.groupName,
|
||||
currentUserInGroup: ap.currentUserInGroup,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function toServiceAccountGrantedPoliciesResponseRaw(view: ServiceAccountGrantedPoliciesView) {
|
||||
return {
|
||||
grantedProjectPolicies: view.grantedProjectPolicies.map((ap) => ({
|
||||
accessPolicy: {
|
||||
grantedProjectId: ap.accessPolicy.grantedProjectId,
|
||||
grantedProjectName: ap.accessPolicy.grantedProjectName,
|
||||
read: ap.accessPolicy.read,
|
||||
write: ap.accessPolicy.write,
|
||||
},
|
||||
hasPermission: ap.hasPermission,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function toProjectServiceAccountsAccessPoliciesResponseRaw(
|
||||
view: ProjectServiceAccountsAccessPoliciesView,
|
||||
) {
|
||||
return {
|
||||
serviceAccountAccessPolicies: view.serviceAccountAccessPolicies.map((ap) => ({
|
||||
serviceAccountId: ap.serviceAccountId,
|
||||
serviceAccountName: ap.serviceAccountName,
|
||||
currentUser: true,
|
||||
read: ap.read,
|
||||
write: ap.write,
|
||||
})),
|
||||
};
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
@ -46,6 +47,15 @@ import { GrantedProjectAccessPolicyPermissionDetailsResponse } from "./models/re
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AccessPolicyService {
|
||||
protected _accessPolicy: Subject<
|
||||
| ProjectPeopleAccessPoliciesView
|
||||
| ProjectServiceAccountsAccessPoliciesView
|
||||
| ServiceAccountPeopleAccessPoliciesView
|
||||
| ServiceAccountGrantedPoliciesView
|
||||
> = new Subject();
|
||||
|
||||
accessPolicy$ = this._accessPolicy.asObservable();
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
@ -70,7 +80,7 @@ export class AccessPolicyService {
|
||||
async putProjectPeopleAccessPolicies(
|
||||
projectId: string,
|
||||
peoplePoliciesView: ProjectPeopleAccessPoliciesView,
|
||||
) {
|
||||
): Promise<ProjectPeopleAccessPoliciesView> {
|
||||
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
||||
const r = await this.apiService.send(
|
||||
"PUT",
|
||||
@ -80,7 +90,9 @@ export class AccessPolicyService {
|
||||
true,
|
||||
);
|
||||
const results = new ProjectPeopleAccessPoliciesResponse(r);
|
||||
return this.createPeopleAccessPoliciesView(results);
|
||||
const view = this.createPeopleAccessPoliciesView(results);
|
||||
this._accessPolicy.next(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
async getServiceAccountPeopleAccessPolicies(
|
||||
@ -101,7 +113,7 @@ export class AccessPolicyService {
|
||||
async putServiceAccountPeopleAccessPolicies(
|
||||
serviceAccountId: string,
|
||||
peoplePoliciesView: ServiceAccountPeopleAccessPoliciesView,
|
||||
) {
|
||||
): Promise<ServiceAccountPeopleAccessPoliciesView> {
|
||||
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
||||
const r = await this.apiService.send(
|
||||
"PUT",
|
||||
@ -111,7 +123,9 @@ export class AccessPolicyService {
|
||||
true,
|
||||
);
|
||||
const results = new ServiceAccountPeopleAccessPoliciesResponse(r);
|
||||
return this.createPeopleAccessPoliciesView(results);
|
||||
const view = this.createPeopleAccessPoliciesView(results);
|
||||
this._accessPolicy.next(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
async getServiceAccountGrantedPolicies(
|
||||
@ -145,7 +159,9 @@ export class AccessPolicyService {
|
||||
);
|
||||
|
||||
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
|
||||
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
||||
const view = await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
||||
this._accessPolicy.next(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
async getProjectServiceAccountsAccessPolicies(
|
||||
@ -179,7 +195,9 @@ export class AccessPolicyService {
|
||||
);
|
||||
|
||||
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
||||
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||
const view = await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||
this._accessPolicy.next(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
async getSecretAccessPolicies(
|
||||
@ -450,7 +468,7 @@ export class AccessPolicyService {
|
||||
|
||||
private createPeopleAccessPoliciesView(
|
||||
response: ProjectPeopleAccessPoliciesResponse | ServiceAccountPeopleAccessPoliciesResponse,
|
||||
) {
|
||||
): ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView {
|
||||
return {
|
||||
userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies),
|
||||
groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies),
|
||||
|
@ -0,0 +1,98 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { CountService } from "./count.service";
|
||||
|
||||
describe("SecretsManagerService", () => {
|
||||
let sut: CountService;
|
||||
|
||||
const apiService = mock<ApiService>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
sut = new CountService(apiService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("getOrganizationCounts", () => {
|
||||
it("returns counts", async () => {
|
||||
apiService.send.mockResolvedValue({
|
||||
projects: 1,
|
||||
secrets: 2,
|
||||
serviceAccounts: 3,
|
||||
});
|
||||
|
||||
const organizationId = Utils.newGuid();
|
||||
|
||||
const result = await sut.getOrganizationCounts(organizationId);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.projects).toEqual(1);
|
||||
expect(result.secrets).toEqual(2);
|
||||
expect(result.serviceAccounts).toEqual(3);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/sm-counts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProjectCounts", () => {
|
||||
it("returns counts", async () => {
|
||||
apiService.send.mockResolvedValue({
|
||||
people: 1,
|
||||
secrets: 2,
|
||||
serviceAccounts: 3,
|
||||
});
|
||||
const projectId = Utils.newGuid();
|
||||
|
||||
const result = await sut.getProjectCounts(projectId);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.people).toEqual(1);
|
||||
expect(result.secrets).toEqual(2);
|
||||
expect(result.serviceAccounts).toEqual(3);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
"/projects/" + projectId + "/sm-counts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getServiceAccountCounts", () => {
|
||||
it("returns counts", async () => {
|
||||
const serviceAccountId = Utils.newGuid();
|
||||
apiService.send.mockResolvedValue({
|
||||
projects: 1,
|
||||
people: 2,
|
||||
accessTokens: 3,
|
||||
});
|
||||
|
||||
const result = await sut.getServiceAccountCounts(serviceAccountId);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.projects).toEqual(1);
|
||||
expect(result.people).toEqual(2);
|
||||
expect(result.accessTokens).toEqual(3);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
"/service-accounts/" + serviceAccountId + "/sm-counts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
import { OrganizationCountsResponse } from "./models/responses/organization-counts.response";
|
||||
import { ProjectCountsResponse } from "./models/responses/project-counts.response";
|
||||
import { ServiceAccountCountsResponse } from "./models/responses/service-account-counts.response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class CountService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getOrganizationCounts(organizationId: string): Promise<OrganizationCountsResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/sm-counts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new OrganizationCountsResponse(r);
|
||||
}
|
||||
|
||||
async getProjectCounts(projectId: string): Promise<ProjectCountsResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/projects/" + projectId + "/sm-counts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ProjectCountsResponse(r);
|
||||
}
|
||||
|
||||
async getServiceAccountCounts(serviceAccountId: string): Promise<ServiceAccountCountsResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/service-accounts/" + serviceAccountId + "/sm-counts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ServiceAccountCountsResponse(r);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class OrganizationCountsResponse extends BaseResponse {
|
||||
projects: number;
|
||||
secrets: number;
|
||||
serviceAccounts: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.projects = this.getResponseProperty("Projects");
|
||||
this.secrets = this.getResponseProperty("Secrets");
|
||||
this.serviceAccounts = this.getResponseProperty("ServiceAccounts");
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class ProjectCountsResponse extends BaseResponse {
|
||||
people: number;
|
||||
secrets: number;
|
||||
serviceAccounts: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.people = this.getResponseProperty("People");
|
||||
this.secrets = this.getResponseProperty("Secrets");
|
||||
this.serviceAccounts = this.getResponseProperty("ServiceAccounts");
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class ServiceAccountCountsResponse extends BaseResponse {
|
||||
projects: number;
|
||||
people: number;
|
||||
accessTokens: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.projects = this.getResponseProperty("Projects");
|
||||
this.people = this.getResponseProperty("People");
|
||||
this.accessTokens = this.getResponseProperty("AccessTokens");
|
||||
}
|
||||
}
|
@ -100,13 +100,9 @@
|
||||
</ng-template>
|
||||
|
||||
<div
|
||||
#endSlot
|
||||
*ngIf="data.open"
|
||||
class="tw-flex -tw-ml-3 tw-pr-4 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
|
||||
[ngClass]="[
|
||||
variant === 'tree' ? 'tw-py-1' : 'tw-py-2',
|
||||
endSlot.childElementCount === 0 ? 'tw-hidden' : '',
|
||||
]"
|
||||
class="tw-flex -tw-ml-3 tw-pr-4 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
|
||||
[ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']"
|
||||
>
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
|
@ -9,6 +9,12 @@
|
||||
[attr.aria-disabled]="disabled"
|
||||
ariaCurrentWhenActive="page"
|
||||
role="link"
|
||||
class="tw-flex tw-group/tab hover:tw-no-underline"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
<div class="group-hover/tab:tw-underline">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="tw-font-normal tw-ml-2 empty:tw-ml-0">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -33,6 +33,12 @@ class ItemThreeDummyComponent {}
|
||||
})
|
||||
class DisabledDummyComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "bit-tab-item-with-child-counter-dummy",
|
||||
template: "Router - Item With Child Counter selected",
|
||||
})
|
||||
class ItemWithChildCounterDummyComponent {}
|
||||
|
||||
export default {
|
||||
title: "Component Library/Tabs",
|
||||
component: TabGroupComponent,
|
||||
@ -42,6 +48,7 @@ export default {
|
||||
ActiveDummyComponent,
|
||||
ItemTwoDummyComponent,
|
||||
ItemThreeDummyComponent,
|
||||
ItemWithChildCounterDummyComponent,
|
||||
DisabledDummyComponent,
|
||||
],
|
||||
imports: [CommonModule, TabsModule, ButtonModule, FormFieldModule, RouterModule],
|
||||
@ -55,6 +62,7 @@ export default {
|
||||
{ path: "active", component: ActiveDummyComponent },
|
||||
{ path: "item-2", component: ItemTwoDummyComponent },
|
||||
{ path: "item-3", component: ItemThreeDummyComponent },
|
||||
{ path: "item-with-child-counter", component: ItemWithChildCounterDummyComponent },
|
||||
{ path: "disabled", component: DisabledDummyComponent },
|
||||
],
|
||||
{ useHash: true },
|
||||
@ -102,6 +110,12 @@ export const NavigationTabs: Story = {
|
||||
<bit-tab-link [route]="['active']">Active</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-2']">Item 2</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-3']">Item 3</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-with-child-counter']">
|
||||
Item With Counter
|
||||
<div slot="end" class="tw-pl-2 tw-text-muted">
|
||||
42
|
||||
</div>
|
||||
</bit-tab-link>
|
||||
<bit-tab-link [route]="['disable']" [disabled]="true">Disabled</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
<div class="tw-bg-transparent tw-text-semibold tw-text-center tw-text-main tw-py-10">
|
||||
|
Loading…
Reference in New Issue
Block a user