From 800e10b8f852acde1b8fdfb27562d3669e3a0f7d Mon Sep 17 00:00:00 2001 From: Jared Snider Date: Fri, 11 Jul 2025 18:12:47 -0400 Subject: [PATCH] PM-23733 - BEEEP - Create dev tools feature flags view first draft. --- .../app/layouts/user-layout.component.html | 4 ++ apps/web/src/app/oss-routing.module.ts | 37 +++++++++++++++ .../developer-tools.component.html | 9 ++++ .../developer-tools.component.ts | 11 +++++ .../settings/developer-tools/index.ts | 1 + apps/web/src/locales/en/messages.json | 6 +++ .../feature-flags.component.html | 17 +++++++ .../feature-flags/feature-flags.component.ts | 45 +++++++++++++++++++ .../src/platform/feature-flags/index.ts | 1 + .../abstractions/config/config.service.ts | 14 ++++-- .../services/config/default-config.service.ts | 12 ++++- 11 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/platform/settings/developer-tools/developer-tools.component.html create mode 100644 apps/web/src/app/platform/settings/developer-tools/developer-tools.component.ts create mode 100644 apps/web/src/app/platform/settings/developer-tools/index.ts create mode 100644 libs/angular/src/platform/feature-flags/feature-flags.component.html create mode 100644 libs/angular/src/platform/feature-flags/feature-flags.component.ts create mode 100644 libs/angular/src/platform/feature-flags/index.ts diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 530d4caca03..37c4ba67e0a 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -25,6 +25,10 @@ route="settings/emergency-access" > + diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index d3e7fc495ca..83f2f7d188f 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -176,6 +176,22 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ + // TODO: consider adding guard to prevent access to this route if env is not dev or qa. + // TODO: figure out why this doesn't work when other one does. + // this is for anon-web scenario + { + path: "feature-flags", + data: { + pageTitle: { + key: "featureFlags", + }, + maxWidth: "3xl", + hideIcon: true, // TODO: log bug with UIF or offer a PR to fix where this isn't reset to false upon navigation + } satisfies RouteDataProperties & AnonLayoutWrapperData, + loadComponent: () => + import("@bitwarden/angular/platform/feature-flags").then((m) => m.FeatureFlagsComponent), + }, + { path: "signup", canActivate: [unauthGuardFn()], @@ -700,6 +716,27 @@ const routes: Routes = [ component: SponsoredFamiliesComponent, data: { titleId: "sponsoredFamilies" } satisfies RouteDataProperties, }, + // TODO: consider adding guard to prevent access to this route if env is not dev or qa. + { + path: "developer-tools", + data: { titleId: "developerTools" } satisfies RouteDataProperties, + loadComponent: () => + import("./platform/settings/developer-tools").then((m) => m.DeveloperToolsComponent), + children: [ + { + path: "", + redirectTo: "feature-flags", + pathMatch: "full", + }, + { + path: "feature-flags", + loadComponent: () => + import("@bitwarden/angular/platform/feature-flags").then( + (m) => m.FeatureFlagsComponent, + ), + }, + ], + }, ], }, { diff --git a/apps/web/src/app/platform/settings/developer-tools/developer-tools.component.html b/apps/web/src/app/platform/settings/developer-tools/developer-tools.component.html new file mode 100644 index 00000000000..3861aae3028 --- /dev/null +++ b/apps/web/src/app/platform/settings/developer-tools/developer-tools.component.html @@ -0,0 +1,9 @@ + + + {{ "featureFlags" | i18n }} + + + + + + diff --git a/apps/web/src/app/platform/settings/developer-tools/developer-tools.component.ts b/apps/web/src/app/platform/settings/developer-tools/developer-tools.component.ts new file mode 100644 index 00000000000..3b3fd15188c --- /dev/null +++ b/apps/web/src/app/platform/settings/developer-tools/developer-tools.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; + +@Component({ + selector: "app-developer-tools", + templateUrl: "./developer-tools.component.html", + imports: [SharedModule, HeaderModule], +}) +export class DeveloperToolsComponent {} diff --git a/apps/web/src/app/platform/settings/developer-tools/index.ts b/apps/web/src/app/platform/settings/developer-tools/index.ts new file mode 100644 index 00000000000..3fea3c4e3b0 --- /dev/null +++ b/apps/web/src/app/platform/settings/developer-tools/index.ts @@ -0,0 +1 @@ +export * from "./developer-tools.component"; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9150028f4d6..00fb0e9806b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2093,6 +2093,12 @@ "default": { "message": "Default" }, + "developerTools": { + "message": "Developer Tools" + }, + "featureFlags": { + "message": "Feature flags" + }, "domainRules": { "message": "Domain rules" }, diff --git a/libs/angular/src/platform/feature-flags/feature-flags.component.html b/libs/angular/src/platform/feature-flags/feature-flags.component.html new file mode 100644 index 00000000000..d900490a4cb --- /dev/null +++ b/libs/angular/src/platform/feature-flags/feature-flags.component.html @@ -0,0 +1,17 @@ +@if (loading) { +
+ +
+} @else { + + + key + value + + + + {{ row.key }} + {{ row.value }} + + +} diff --git a/libs/angular/src/platform/feature-flags/feature-flags.component.ts b/libs/angular/src/platform/feature-flags/feature-flags.component.ts new file mode 100644 index 00000000000..e4d4d8eb5c9 --- /dev/null +++ b/libs/angular/src/platform/feature-flags/feature-flags.component.ts @@ -0,0 +1,45 @@ +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + +import { AllowedFeatureFlagTypes } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { TableDataSource, TableModule } from "@bitwarden/components"; + +@Component({ + selector: "app-feature-flags", + templateUrl: "./feature-flags.component.html", + imports: [CommonModule, TableModule], +}) +export class FeatureFlagsComponent implements OnInit { + loading = true; + tableDataSource = new TableDataSource<{ key: string; value: AllowedFeatureFlagTypes }>(); + + constructor( + private destroyRef: DestroyRef, + private configService: ConfigService, + ) {} + + ngOnInit() { + this.configService.featureStates$ + .pipe( + takeUntilDestroyed(this.destroyRef), + map((states) => { + if (!states) { + return []; + } + + // Convert the feature states object into an array of key-value pairs + return Object.entries(states).map(([key, value]) => ({ + key, + value, + })); + }), + ) + .subscribe((featureStates) => { + this.tableDataSource.data = featureStates; + this.loading = false; + }); + } +} diff --git a/libs/angular/src/platform/feature-flags/index.ts b/libs/angular/src/platform/feature-flags/index.ts new file mode 100644 index 00000000000..7d4448120d2 --- /dev/null +++ b/libs/angular/src/platform/feature-flags/index.ts @@ -0,0 +1 @@ +export * from "./feature-flags.component"; diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts index 04f150838e4..b24cb6f7282 100644 --- a/libs/common/src/platform/abstractions/config/config.service.ts +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -3,7 +3,11 @@ import { Observable } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum"; +import { + AllowedFeatureFlagTypes, + FeatureFlag, + FeatureFlagValueType, +} from "../../../enums/feature-flag.enum"; import { UserId } from "../../../types/guid"; import { ServerSettings } from "../../models/domain/server-settings"; import { Region } from "../environment.service"; @@ -11,11 +15,13 @@ import { Region } from "../environment.service"; import { ServerConfig } from "./server-config"; export abstract class ConfigService { - /** The server config of the currently active user */ + /** The server config of the environment or the currently active user */ serverConfig$: Observable; - /** The server settings of the currently active user */ + /** The server settings of the environment or the currently active user */ serverSettings$: Observable; - /** The cloud region of the currently active user */ + /** The feature states of the environment or the currently active user */ + featureStates$: Observable<{ [key: string]: AllowedFeatureFlagTypes } | undefined>; + /** The cloud region of the environment or the currently active user */ cloudRegion$: Observable; /** * Retrieves the value of a feature flag for the currently active user diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 33f86d30885..8e36341aa8f 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -17,7 +17,11 @@ import { SemVer } from "semver"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum"; +import { + AllowedFeatureFlagTypes, + FeatureFlag, + getFeatureFlagValue, +} from "../../../enums/feature-flag.enum"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigService } from "../../abstractions/config/config.service"; @@ -56,6 +60,8 @@ export class DefaultConfigService implements ConfigService { serverConfig$: Observable; + featureStates$: Observable<{ [key: string]: AllowedFeatureFlagTypes } | undefined>; + serverSettings$: Observable; cloudRegion$: Observable; @@ -116,6 +122,10 @@ export class DefaultConfigService implements ConfigService { this.serverSettings$ = this.serverConfig$.pipe( map((config) => config?.settings ?? new ServerSettings()), ); + + this.featureStates$ = this.serverConfig$.pipe( + map((config) => config?.featureStates ?? undefined), + ); } getFeatureFlag$(key: Flag) {