1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +00:00

[AC-1192] Create new device approvals component for TDE (#5548)

* Add feature flag route guard and tests

* Add additional test for not showing error toast

* Strengthen error toast test with message check

* Cleanup leaking test state in platformService mock

* Negate if statement to reduce nesting

* Update return type to CanActivateFn

* Use null check instead of undefined

* Introduce interface to support different feature flag types

- Switch to observable pattern to access serverConfig$ subject
- Add catchError handler to allow navigation in case of unexpected exception
- Add additional tests

* Add additional test for missing feature flag

* Remove subscription to the serverConfig observable

Introduce type checking logic to determine the appropriately typed flag getter to use in configService

* [AC-1192] Create initial device approvals component and route

* [AC-1192] Introduce appIfFeature directive for conditionally rendering content based on feature flags

* [AC-1192] Add DeviceApprovals link in Settings navigation

* Remove align middle from bitCell directive

The bitRow directive supports alignment for the entire row and should be used instead

* [AC-1192] Add initial device approvals page template

* [AC-1192] Introduce fingerprint pipe

* [AC-1192] Create core organization module in bitwarden_license directory

* [AC-1192] Add support for new Devices icon to no items component

- Add new Devices svg
- Make icon property of bit-no-items an Input property

* [AC-1192] Introduce organization-auth-request.service.ts with related views/responses

* [AC-1192] Display pending requests on device approvals page

- Add support for loading spinner and no items component

* [AC-1192] Add method to bulk deny auth requests

* [AC-1192] Add functionality to deny requests from device approvals page

* [AC-1192] Add organizationUserId to pending-auth-request.view.ts

* [AC-1192] Add approvePendingRequest method to organization-auth-request.service.ts

* [AC-1192] Add logic to approve a device approval request

* [AC-1192] Change bitMenuItem directive into a component and implement ButtonLikeAbstraction

Update the bitMenuItem to be a component and implement the ButtonLikeAbstraction to support the bitAction directive.

* [AC-1192] Update menu items to use bitActions

* [AC-1192] Update device approvals description copy

* [AC-1192] Revert changes to bitMenuItem directive

* [AC-1192] Rework menus to use click handlers

- Wrap async actions to catch/log any exceptions, set an in-progress state, and refresh after completion
- Show a loading spinner in the header when an action is in progress
- Disable all menu items when an action is in progress

* [AC-1192] Move Devices icon into admin-console web directory

* [AC-1192] bit-no-items formatting

* [AC-1192] Update appIfFeature directive to hide content on error

* [AC-1192] Remove deprecated providedIn for OrganizationAuthRequestService

* [AC-1192] Rename key to encryptedUserKey to be more descriptive

* [AC-1192] Cleanup loading/spinner logic on data refresh

* [AC-1192] Set middle as the default bitRow.alignContent

* [AC-1192] Change default alignRowContent for table story

* [AC-1192] Rename userId to fingerprintMaterial to be more general

The fingerprint material is not always the userId so this name is more general

* [AC-1192] Remove redundant alignContent attribute

* [AC-1192] Move fingerprint pipe to platform
This commit is contained in:
Shane Melton
2023-06-15 14:53:21 -07:00
committed by GitHub
parent bec51c95f9
commit 0afbd90a2d
25 changed files with 746 additions and 9 deletions

View File

@@ -0,0 +1,137 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { IfFeatureDirective } from "./if-feature.directive";
const testBooleanFeature: FeatureFlag = "boolean-feature" as FeatureFlag;
const testStringFeature: FeatureFlag = "string-feature" as FeatureFlag;
const testStringFeatureValue = "test-value";
@Component({
template: `
<div *appIfFeature="testBooleanFeature">
<div data-testid="boolean-content">Hidden behind feature flag</div>
</div>
<div *appIfFeature="stringFeature; value: stringFeatureValue">
<div data-testid="string-content">Hidden behind feature flag</div>
</div>
<div *appIfFeature="missingFlag">
<div data-testid="missing-flag-content">
Hidden behind missing flag. Should not be visible.
</div>
</div>
`,
})
class TestComponent {
testBooleanFeature = testBooleanFeature;
stringFeature = testStringFeature;
stringFeatureValue = testStringFeatureValue;
missingFlag = "missing-flag" as FeatureFlag;
}
describe("IfFeatureDirective", () => {
let fixture: ComponentFixture<TestComponent>;
let content: HTMLElement;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: any) => {
if (typeof flagValue === "boolean") {
mockConfigService.getFeatureFlagBool.mockImplementation((f, defaultValue = false) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "string") {
mockConfigService.getFeatureFlagString.mockImplementation((f, defaultValue = "") =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "number") {
mockConfigService.getFeatureFlagNumber.mockImplementation((f, defaultValue = 0) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
}
};
const queryContent = (testId: string) =>
fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement;
beforeEach(async () => {
mockConfigService = mock<ConfigServiceAbstraction>();
await TestBed.configureTestingModule({
declarations: [IfFeatureDirective, TestComponent],
providers: [
{ provide: LogService, useValue: mock<LogService>() },
{
provide: ConfigServiceAbstraction,
useValue: mockConfigService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
});
it("renders content when the feature flag is enabled", async () => {
mockConfigFlagValue(testBooleanFeature, true);
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("boolean-content");
expect(content).toBeDefined();
});
it("renders content when the feature flag value matches the provided value", async () => {
mockConfigFlagValue(testStringFeature, testStringFeatureValue);
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("string-content");
expect(content).toBeDefined();
});
it("hides content when the feature flag is disabled", async () => {
mockConfigFlagValue(testBooleanFeature, false);
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("boolean-content");
expect(content).toBeUndefined();
});
it("hides content when the feature flag value does not match the provided value", async () => {
mockConfigFlagValue(testStringFeature, "wrong-value");
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("string-content");
expect(content).toBeUndefined();
});
it("hides content when the feature flag is missing", async () => {
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("missing-flag-content");
expect(content).toBeUndefined();
});
it("hides content when the directive throws an unexpected exception", async () => {
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("boolean-content");
expect(content).toBeUndefined();
});
});

View File

@@ -0,0 +1,67 @@
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
// Replace this with a type safe lookup of the feature flag values in PM-2282
type FlagValue = boolean | number | string;
/**
* Directive that conditionally renders the element when the feature flag is enabled and/or
* matches the value specified by {@link appIfFeatureValue}.
*
* When a feature flag is not found in the config service, the element is hidden.
*/
@Directive({
selector: "[appIfFeature]",
})
export class IfFeatureDirective implements OnInit {
/**
* The feature flag to check.
*/
@Input() appIfFeature: FeatureFlag;
/**
* Optional value to compare against the value of the feature flag in the config service.
* @default true
*/
@Input() appIfFeatureValue: FlagValue = true;
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private configService: ConfigServiceAbstraction,
private logService: LogService
) {}
async ngOnInit() {
try {
let flagValue: FlagValue;
if (typeof this.appIfFeatureValue === "boolean") {
flagValue = await this.configService.getFeatureFlagBool(this.appIfFeature);
} else if (typeof this.appIfFeatureValue === "number") {
flagValue = await this.configService.getFeatureFlagNumber(this.appIfFeature);
} else if (typeof this.appIfFeatureValue === "string") {
flagValue = await this.configService.getFeatureFlagString(this.appIfFeature);
}
if (this.appIfFeatureValue === flagValue) {
if (!this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
} else {
this.viewContainer.clear();
this.hasView = false;
}
} catch (e) {
this.logService.error(e);
this.viewContainer.clear();
this.hasView = false;
}
}
}