mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[SM-896] When org is disabled disable the logic and show warning symbols (#6225)
* When org is disabled disable the logic and show warning symbols * fixing org enabled logic * removing unused code * Adding route gaurd logic and new org suspended page * fixing lint issue * fixing issues * Requested changes * adding back code that was accidentally removed from organization-switcher * Update bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Removing unused code and updating storybook to set enabled:true * removing onDestroy * Will's suggestions * will's suggested change * fix nav-item color in story * Thomas Rittson's suggested changes * adding back removed spaces * Adding back white space * updating guard * Update bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * removing ununsed data * Updating incorrect messages --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: William Martin <contact@willmartian.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
parent
2dc94ede97
commit
c3856ce821
@ -47,9 +47,10 @@ export class ProductSwitcherContentComponent {
|
|||||||
map(([orgs, paramMap]) => {
|
map(([orgs, paramMap]) => {
|
||||||
const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
|
const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
|
||||||
// If the active route org doesn't have access to SM, find the first org that does.
|
// If the active route org doesn't have access to SM, find the first org that does.
|
||||||
const smOrg = routeOrg?.canAccessSecretsManager
|
const smOrg =
|
||||||
|
routeOrg?.canAccessSecretsManager && routeOrg?.enabled == true
|
||||||
? routeOrg
|
? routeOrg
|
||||||
: orgs.find((o) => o.canAccessSecretsManager);
|
: orgs.find((o) => o.canAccessSecretsManager && o.enabled == true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We can update this to the "satisfies" type upon upgrading to TypeScript 4.9
|
* We can update this to the "satisfies" type upon upgrading to TypeScript 4.9
|
||||||
|
@ -131,5 +131,5 @@ OrgWithoutSecretsManager.args = {
|
|||||||
|
|
||||||
export const OrgWithSecretsManager = Template.bind({});
|
export const OrgWithSecretsManager = Template.bind({});
|
||||||
OrgWithSecretsManager.args = {
|
OrgWithSecretsManager.args = {
|
||||||
mockOrgs: [{ id: "b", canAccessSecretsManager: true }],
|
mockOrgs: [{ id: "b", canAccessSecretsManager: true, enabled: true }],
|
||||||
};
|
};
|
||||||
|
@ -3638,6 +3638,18 @@
|
|||||||
"organizationIsDisabled": {
|
"organizationIsDisabled": {
|
||||||
"message": "Organization suspended"
|
"message": "Organization suspended"
|
||||||
},
|
},
|
||||||
|
"secretsAccessSuspended": {
|
||||||
|
"message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance."
|
||||||
|
},
|
||||||
|
"secretsCannotCreate": {
|
||||||
|
"message": "Secrets cannot be created in suspended organizations. Please contact your organization owner for assistance."
|
||||||
|
},
|
||||||
|
"projectsCannotCreate": {
|
||||||
|
"message": "Projects cannot be created in suspended organizations. Please contact your organization owner for assistance."
|
||||||
|
},
|
||||||
|
"serviceAccountsCannotCreate": {
|
||||||
|
"message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance."
|
||||||
|
},
|
||||||
"disabledOrganizationFilterError": {
|
"disabledOrganizationFilterError": {
|
||||||
"message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance."
|
"message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance."
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects from root `/sm` to first organization with access to SM
|
||||||
|
*/
|
||||||
|
export const organizationEnabledGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||||
|
const syncService = inject(SyncService);
|
||||||
|
const orgService = inject(OrganizationService);
|
||||||
|
|
||||||
|
/** Workaround to avoid service initialization race condition. */
|
||||||
|
if ((await syncService.getLastSync()) == null) {
|
||||||
|
await syncService.fullSync(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const org = orgService.get(route.params.organizationId);
|
||||||
|
if (org == null || !org.canAccessSecretsManager) {
|
||||||
|
return createUrlTreeFromSnapshot(route, ["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.enabled) {
|
||||||
|
return createUrlTreeFromSnapshot(route, ["/sm", org.id, "organization-suspended"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -7,6 +7,11 @@
|
|||||||
[(open)]="open"
|
[(open)]="open"
|
||||||
[exactMatch]="true"
|
[exactMatch]="true"
|
||||||
>
|
>
|
||||||
|
<i
|
||||||
|
slot="end"
|
||||||
|
*ngIf="!activeOrganization.enabled"
|
||||||
|
class="bwi bwi-exclamation-triangle tw-my-auto !tw-text-danger"
|
||||||
|
></i>
|
||||||
<ng-container *ngIf="organizations$ | async as organizations">
|
<ng-container *ngIf="organizations$ | async as organizations">
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
*ngFor="let org of organizations"
|
*ngFor="let org of organizations"
|
||||||
@ -16,6 +21,11 @@
|
|||||||
(mainContentClicked)="toggle()"
|
(mainContentClicked)="toggle()"
|
||||||
[hideActiveStyles]="true"
|
[hideActiveStyles]="true"
|
||||||
>
|
>
|
||||||
|
<i
|
||||||
|
slot="end"
|
||||||
|
*ngIf="org.enabled == false"
|
||||||
|
class="bwi bwi-exclamation-triangle !tw-text-danger"
|
||||||
|
></i>
|
||||||
</bit-nav-item>
|
</bit-nav-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
|
@ -12,8 +12,14 @@ import type { Organization } from "@bitwarden/common/admin-console/models/domain
|
|||||||
export class OrgSwitcherComponent {
|
export class OrgSwitcherComponent {
|
||||||
protected organizations$: Observable<Organization[]> =
|
protected organizations$: Observable<Organization[]> =
|
||||||
this.organizationService.organizations$.pipe(
|
this.organizationService.organizations$.pipe(
|
||||||
map((orgs) => orgs.filter(this.filter).sort((a, b) => a.name.localeCompare(b.name)))
|
map((orgs) =>
|
||||||
|
orgs
|
||||||
|
.filter((org) => this.filter(org))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.sort((a, b) => (a.enabled ? -1 : 1))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
protected activeOrganization$: Observable<Organization> = combineLatest([
|
protected activeOrganization$: Observable<Organization> = combineLatest([
|
||||||
this.route.paramMap,
|
this.route.paramMap,
|
||||||
this.organizations$,
|
this.organizations$,
|
||||||
|
@ -70,6 +70,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
protected userIsAdmin: boolean;
|
protected userIsAdmin: boolean;
|
||||||
protected showOnboarding = false;
|
protected showOnboarding = false;
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
|
protected organizationEnabled = false;
|
||||||
|
|
||||||
protected view$: Observable<{
|
protected view$: Observable<{
|
||||||
allProjects: ProjectListView[];
|
allProjects: ProjectListView[];
|
||||||
@ -107,6 +108,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
this.organizationName = org.name;
|
this.organizationName = org.name;
|
||||||
this.userIsAdmin = org.isAdmin;
|
this.userIsAdmin = org.isAdmin;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.organizationEnabled = org.enabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
const projects$ = combineLatest([
|
const projects$ = combineLatest([
|
||||||
@ -208,6 +210,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -218,6 +221,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -227,6 +231,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -246,6 +251,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -256,6 +262,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
secretId: secretId,
|
secretId: secretId,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -273,6 +280,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ export enum OperationType {
|
|||||||
export interface ProjectOperation {
|
export interface ProjectOperation {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
operation: OperationType;
|
operation: OperationType;
|
||||||
|
organizationEnabled: boolean;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +64,15 @@ export class ProjectDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
if (!this.data.organizationEnabled) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("projectsCannotCreate")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.formGroup.markAllAsTouched();
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
|
@ -2,6 +2,7 @@ import { Component } from "@angular/core";
|
|||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs";
|
import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@ -31,6 +32,7 @@ export class ProjectSecretsComponent {
|
|||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
protected project$: Observable<ProjectView>;
|
protected project$: Observable<ProjectView>;
|
||||||
|
private organizationEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -38,7 +40,8 @@ export class ProjectSecretsComponent {
|
|||||||
private secretService: SecretService,
|
private secretService: SecretService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService
|
private i18nService: I18nService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -60,6 +63,7 @@ export class ProjectSecretsComponent {
|
|||||||
switchMap(async ([_, params]) => {
|
switchMap(async ([_, params]) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
this.projectId = params.projectId;
|
this.projectId = params.projectId;
|
||||||
|
this.organizationEnabled = this.organizationService.get(params.organizationId)?.enabled;
|
||||||
return await this.getSecretsByProject();
|
return await this.getSecretsByProject();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -75,6 +79,7 @@ export class ProjectSecretsComponent {
|
|||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
secretId: secretId,
|
secretId: secretId,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -93,6 +98,7 @@ export class ProjectSecretsComponent {
|
|||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
projectId: this.projectId,
|
projectId: this.projectId,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@ -33,7 +34,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
|
private organizationEnabled: boolean;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -42,7 +43,8 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService
|
private i18nService: I18nService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -69,6 +71,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
|||||||
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
this.projectId = params.projectId;
|
this.projectId = params.projectId;
|
||||||
|
this.organizationEnabled = this.organizationService.get(params.organizationId)?.enabled;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +85,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
projectId: this.projectId,
|
projectId: this.projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatest, lastValueFrom, Observable, startWith, switchMap } from "rxjs";
|
import { combineLatest, lastValueFrom, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { ProjectListView } from "../../models/view/project-list.view";
|
import { ProjectListView } from "../../models/view/project-list.view";
|
||||||
@ -32,12 +33,14 @@ export class ProjectsComponent implements OnInit {
|
|||||||
protected search: string;
|
protected search: string;
|
||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
|
private organizationEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private projectService: ProjectService,
|
private projectService: ProjectService,
|
||||||
private accessPolicyService: AccessPolicyService,
|
private accessPolicyService: AccessPolicyService,
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -48,6 +51,8 @@ export class ProjectsComponent implements OnInit {
|
|||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(async ([params]) => {
|
switchMap(async ([params]) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
|
this.organizationEnabled = this.organizationService.get(params.organizationId)?.enabled;
|
||||||
|
|
||||||
return await this.getProjects();
|
return await this.getProjects();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -62,6 +67,7 @@ export class ProjectsComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -72,6 +78,7 @@ export class ProjectsComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ export interface SecretOperation {
|
|||||||
operation: OperationType;
|
operation: OperationType;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
secretId?: string;
|
secretId?: string;
|
||||||
|
organizationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -163,6 +164,11 @@ export class SecretDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
if (!this.data.organizationEnabled) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("secretsCannotCreate"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.formGroup.markAllAsTouched();
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatestWith, Observable, startWith, switchMap } from "rxjs";
|
import { combineLatestWith, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@ -29,13 +30,15 @@ export class SecretsComponent implements OnInit {
|
|||||||
protected search: string;
|
protected search: string;
|
||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
|
private organizationEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private secretService: SecretService,
|
private secretService: SecretService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService
|
private i18nService: I18nService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -44,6 +47,8 @@ export class SecretsComponent implements OnInit {
|
|||||||
combineLatestWith(this.route.params),
|
combineLatestWith(this.route.params),
|
||||||
switchMap(async ([_, params]) => {
|
switchMap(async ([_, params]) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
|
this.organizationEnabled = this.organizationService.get(params.organizationId)?.enabled;
|
||||||
|
|
||||||
return await this.getSecrets();
|
return await this.getSecrets();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -63,6 +68,7 @@ export class SecretsComponent implements OnInit {
|
|||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
secretId: secretId,
|
secretId: secretId,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -80,6 +86,7 @@ export class SecretsComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ export interface ServiceAccountOperation {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
serviceAccountId?: string;
|
serviceAccountId?: string;
|
||||||
operation: OperationType;
|
operation: OperationType;
|
||||||
|
organizationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -62,6 +63,15 @@ export class ServiceAccountDialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
if (!this.data.organizationEnabled) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("serviceAccountsCannotCreate")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.formGroup.markAllAsTouched();
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatest, Observable, startWith, switchMap } from "rxjs";
|
import { combineLatest, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -30,12 +31,14 @@ export class ServiceAccountsComponent implements OnInit {
|
|||||||
protected search: string;
|
protected search: string;
|
||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
|
private organizationEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private accessPolicyService: AccessPolicyService,
|
private accessPolicyService: AccessPolicyService,
|
||||||
private serviceAccountService: ServiceAccountService
|
private serviceAccountService: ServiceAccountService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -46,6 +49,8 @@ export class ServiceAccountsComponent implements OnInit {
|
|||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(async ([params]) => {
|
switchMap(async ([params]) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
|
this.organizationEnabled = this.organizationService.get(params.organizationId)?.enabled;
|
||||||
|
|
||||||
return await this.getServiceAccounts();
|
return await this.getServiceAccounts();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -56,6 +61,7 @@ export class ServiceAccountsComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -66,6 +72,7 @@ export class ServiceAccountsComponent implements OnInit {
|
|||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
serviceAccountId: serviceAccountId,
|
serviceAccountId: serviceAccountId,
|
||||||
operation: OperationType.Edit,
|
operation: OperationType.Edit,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
|||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -24,13 +25,18 @@ import {
|
|||||||
})
|
})
|
||||||
export class NewMenuComponent implements OnInit, OnDestroy {
|
export class NewMenuComponent implements OnInit, OnDestroy {
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
|
private organizationEnabled: boolean;
|
||||||
private destroy$: Subject<void> = new Subject<void>();
|
private destroy$: Subject<void> = new Subject<void>();
|
||||||
|
constructor(
|
||||||
constructor(private route: ActivatedRoute, private dialogService: DialogService) {}
|
private route: ActivatedRoute,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params: any) => {
|
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params: any) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
|
this.organizationEnabled = this.organizationService.get(params.organizationId)?.enabled;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +50,7 @@ export class NewMenuComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -53,6 +60,7 @@ export class NewMenuComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -62,6 +70,7 @@ export class NewMenuComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organizationId,
|
organizationId: this.organizationId,
|
||||||
operation: OperationType.Add,
|
operation: OperationType.Add,
|
||||||
|
organizationEnabled: this.organizationEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
<sm-header [title]="organizationName$ | async">
|
||||||
|
<sm-new-menu></sm-new-menu>
|
||||||
|
</sm-header>
|
||||||
|
<bit-no-items [icon]="NoAccess">
|
||||||
|
<ng-container slot="title">{{ "organizationIsDisabled" | i18n }}</ng-container>
|
||||||
|
<ng-container slot="description">{{ "secretsAccessSuspended" | i18n }}</ng-container>
|
||||||
|
</bit-no-items>
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { map } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { Icon, Icons } from "@bitwarden/components";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./org-suspended.component.html",
|
||||||
|
})
|
||||||
|
export class OrgSuspendedComponent {
|
||||||
|
constructor(private organizationService: OrganizationService, private route: ActivatedRoute) {}
|
||||||
|
|
||||||
|
protected NoAccess: Icon = Icons.NoAccess;
|
||||||
|
protected organizationName$ = this.route.params.pipe(
|
||||||
|
map((params) => this.organizationService.get(params.organizationId)?.name)
|
||||||
|
);
|
||||||
|
}
|
@ -17,6 +17,7 @@ import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dia
|
|||||||
import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
|
import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
|
||||||
import { HeaderComponent } from "./header.component";
|
import { HeaderComponent } from "./header.component";
|
||||||
import { NewMenuComponent } from "./new-menu.component";
|
import { NewMenuComponent } from "./new-menu.component";
|
||||||
|
import { OrgSuspendedComponent } from "./org-suspended.component";
|
||||||
import { ProjectsListComponent } from "./projects-list.component";
|
import { ProjectsListComponent } from "./projects-list.component";
|
||||||
import { SecretsListComponent } from "./secrets-list.component";
|
import { SecretsListComponent } from "./secrets-list.component";
|
||||||
|
|
||||||
@ -55,6 +56,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
|||||||
ProjectsListComponent,
|
ProjectsListComponent,
|
||||||
SecretsListComponent,
|
SecretsListComponent,
|
||||||
AccessSelectorComponent,
|
AccessSelectorComponent,
|
||||||
|
OrgSuspendedComponent,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
|
@ -2,10 +2,10 @@ import { NgModule } from "@angular/core";
|
|||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AuthGuard } from "@bitwarden/angular/auth/guards";
|
import { AuthGuard } from "@bitwarden/angular/auth/guards";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
||||||
import { OrganizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
|
|
||||||
import { buildFlaggedRoute } from "@bitwarden/web-vault/app/oss-routing.module";
|
import { buildFlaggedRoute } from "@bitwarden/web-vault/app/oss-routing.module";
|
||||||
|
|
||||||
|
import { organizationEnabledGuard } from "./guards/sm-org-enabled.guard";
|
||||||
|
import { canActivateSM } from "./guards/sm.guard";
|
||||||
import { LayoutComponent } from "./layout/layout.component";
|
import { LayoutComponent } from "./layout/layout.component";
|
||||||
import { NavigationComponent } from "./layout/navigation.component";
|
import { NavigationComponent } from "./layout/navigation.component";
|
||||||
import { OverviewModule } from "./overview/overview.module";
|
import { OverviewModule } from "./overview/overview.module";
|
||||||
@ -13,7 +13,7 @@ import { ProjectsModule } from "./projects/projects.module";
|
|||||||
import { SecretsModule } from "./secrets/secrets.module";
|
import { SecretsModule } from "./secrets/secrets.module";
|
||||||
import { ServiceAccountsModule } from "./service-accounts/service-accounts.module";
|
import { ServiceAccountsModule } from "./service-accounts/service-accounts.module";
|
||||||
import { SettingsModule } from "./settings/settings.module";
|
import { SettingsModule } from "./settings/settings.module";
|
||||||
import { canActivateSM } from "./sm.guard";
|
import { OrgSuspendedComponent } from "./shared/org-suspended.component";
|
||||||
import { TrashModule } from "./trash/trash.module";
|
import { TrashModule } from "./trash/trash.module";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@ -29,16 +29,17 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: ":organizationId",
|
path: ":organizationId",
|
||||||
component: LayoutComponent,
|
component: LayoutComponent,
|
||||||
canActivate: [AuthGuard, OrganizationPermissionsGuard],
|
canActivate: [AuthGuard],
|
||||||
data: {
|
|
||||||
organizationPermissions: (org: Organization) => org.canAccessSecretsManager,
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: NavigationComponent,
|
component: NavigationComponent,
|
||||||
outlet: "sidebar",
|
outlet: "sidebar",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
canActivate: [organizationEnabledGuard],
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
path: "secrets",
|
path: "secrets",
|
||||||
loadChildren: () => SecretsModule,
|
loadChildren: () => SecretsModule,
|
||||||
@ -78,6 +79,12 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "organization-suspended",
|
||||||
|
component: OrgSuspendedComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from "./search";
|
export * from "./search";
|
||||||
|
export * from "./no-access";
|
||||||
|
12
libs/components/src/icon/icons/no-access.ts
Normal file
12
libs/components/src/icon/icons/no-access.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { svgIcon } from "../icon";
|
||||||
|
|
||||||
|
export const NoAccess = svgIcon`
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="154" height="130" fill="none">
|
||||||
|
<path class="tw-stroke-secondary-500" d="M60.795 112.1h55.135a4 4 0 0 0 4-4V59.65M32.9 51.766V6a4 4 0 0 1 4-4h79.03a4 4 0 0 1 4 4v19.992" stroke-width="4"/>
|
||||||
|
<path class="tw-stroke-secondary-500" d="M46.997 21.222h13.806M69.832 21.222h13.806M93.546 21.222h13.806M46.997 44.188h13.806M69.832 44.188h13.806M93.546 44.188h13.806M50.05 67.02h10.753M69.832 67.02h13.806M93.546 67.02h13.806M46.997 90.118h13.806M69.832 90.118h13.806M93.546 90.118h13.806" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path class="tw-stroke-secondary-500" d="M30.914 89.366c10.477 0 18.97-8.493 18.97-18.97 0-10.476-8.493-18.97-18.97-18.97-10.476 0-18.969 8.494-18.969 18.97 0 10.477 8.493 18.97 18.97 18.97ZM2.313 117.279c2.183-16.217 15.44-27.362 29.623-27.362 14.07 0 25.942 11.022 27.898 27.33.167 1.39-.988 2.753-2.719 2.753H5c-1.741 0-2.87-1.366-2.687-2.721Z" stroke-width="4"/>
|
||||||
|
<path class="tw-stroke-danger-500" d="m147.884 50.361-15.89-27.522c-2.31-4-8.083-4-10.392 0l-15.891 27.523c-2.309 4 .578 9 5.196 9h31.781c4.619 0 7.505-5 5.196-9Z" stroke-width="4"/>
|
||||||
|
<path class="tw-stroke-danger-500" d="M126.798 29.406v16.066" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path class="tw-fill-danger-500" d="M126.798 54.727a2.635 2.635 0 1 0 0-5.27 2.635 2.635 0 0 0 0 5.27Z" />
|
||||||
|
</svg>
|
||||||
|
`;
|
@ -17,7 +17,7 @@
|
|||||||
[bitIconButton]="
|
[bitIconButton]="
|
||||||
open ? 'bwi-angle-up' : variant === 'tree' ? 'bwi-angle-right' : 'bwi-angle-down'
|
open ? 'bwi-angle-up' : variant === 'tree' ? 'bwi-angle-right' : 'bwi-angle-down'
|
||||||
"
|
"
|
||||||
[buttonType]="'main'"
|
[buttonType]="'light'"
|
||||||
(click)="toggle($event)"
|
(click)="toggle($event)"
|
||||||
size="small"
|
size="small"
|
||||||
[title]="'toggleCollapse' | i18n"
|
[title]="'toggleCollapse' | i18n"
|
||||||
@ -32,9 +32,12 @@
|
|||||||
<ng-container slot="start" *ngIf="variant === 'tree'">
|
<ng-container slot="start" *ngIf="variant === 'tree'">
|
||||||
<ng-container *ngTemplateOutlet="button"></ng-container>
|
<ng-container *ngTemplateOutlet="button"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container slot="end" *ngIf="variant !== 'tree'">
|
<ng-container slot="end">
|
||||||
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
|
<ng-container *ngIf="variant !== 'tree'">
|
||||||
<ng-container *ngTemplateOutlet="button"></ng-container>
|
<ng-container *ngTemplateOutlet="button"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
</bit-nav-item>
|
</bit-nav-item>
|
||||||
|
|
||||||
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="tw-flex tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:!tw-text-alt2"
|
class="tw-flex tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
|
||||||
>
|
>
|
||||||
<ng-content select="[slot=end]"></ng-content>
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,7 +64,7 @@ export const WithChildButtons: Story = {
|
|||||||
slot="start"
|
slot="start"
|
||||||
class="tw-ml-auto"
|
class="tw-ml-auto"
|
||||||
[bitIconButton]="'bwi-clone'"
|
[bitIconButton]="'bwi-clone'"
|
||||||
[buttonType]="'contrast'"
|
[buttonType]="'light'"
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="option 1"
|
aria-label="option 1"
|
||||||
></button>
|
></button>
|
||||||
@ -72,7 +72,7 @@ export const WithChildButtons: Story = {
|
|||||||
slot="end"
|
slot="end"
|
||||||
class="tw-ml-auto"
|
class="tw-ml-auto"
|
||||||
[bitIconButton]="'bwi-pencil-square'"
|
[bitIconButton]="'bwi-pencil-square'"
|
||||||
[buttonType]="'contrast'"
|
[buttonType]="'light'"
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="option 2"
|
aria-label="option 2"
|
||||||
></button>
|
></button>
|
||||||
@ -80,7 +80,7 @@ export const WithChildButtons: Story = {
|
|||||||
slot="end"
|
slot="end"
|
||||||
class="tw-ml-auto"
|
class="tw-ml-auto"
|
||||||
[bitIconButton]="'bwi-check'"
|
[bitIconButton]="'bwi-check'"
|
||||||
[buttonType]="'contrast'"
|
[buttonType]="'light'"
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="option 3"
|
aria-label="option 3"
|
||||||
></button>
|
></button>
|
||||||
|
Loading…
Reference in New Issue
Block a user