1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 14:34:02 +00:00

PM-23733 - BEEEP - Create dev tools feature flags view first draft.

This commit is contained in:
Jared Snider
2025-07-11 18:12:47 -04:00
parent c9f642e491
commit 800e10b8f8
11 changed files with 152 additions and 5 deletions

View File

@@ -25,6 +25,10 @@
route="settings/emergency-access"
></bit-nav-item>
<billing-free-families-nav-item></billing-free-families-nav-item>
<bit-nav-item
[text]="'developerTools' | i18n"
route="settings/developer-tools"
></bit-nav-item>
</bit-nav-group>
</app-side-nav>

View File

@@ -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,
),
},
],
},
],
},
{

View File

@@ -0,0 +1,9 @@
<app-header>
<bit-tab-nav-bar slot="tabs">
<bit-tab-link route="feature-flags">{{ "featureFlags" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>
</app-header>
<bit-container>
<router-outlet></router-outlet>
</bit-container>

View File

@@ -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 {}

View File

@@ -0,0 +1 @@
export * from "./developer-tools.component";

View File

@@ -2093,6 +2093,12 @@
"default": {
"message": "Default"
},
"developerTools": {
"message": "Developer Tools"
},
"featureFlags": {
"message": "Feature flags"
},
"domainRules": {
"message": "Domain rules"
},

View File

@@ -0,0 +1,17 @@
@if (loading) {
<div class="tw-flex tw-justify-center tw-items-center tw-p-4">
<i class="bwi bwi-spinner bwi-spin tw-text-2xl" aria-hidden="true"></i>
</div>
} @else {
<bit-table-scroll [dataSource]="tableDataSource" [rowSize]="50">
<ng-container header>
<th bitCell bitSortable="key" default>key</th>
<th bitCell bitSortable="value" default>value</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>{{ row.key }}</td>
<td bitCell>{{ row.value }}</td>
</ng-template>
</bit-table-scroll>
}

View File

@@ -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;
});
}
}

View File

@@ -0,0 +1 @@
export * from "./feature-flags.component";

View File

@@ -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<ServerConfig | null>;
/** The server settings of the currently active user */
/** The server settings of the environment or the currently active user */
serverSettings$: Observable<ServerSettings | null>;
/** 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<Region>;
/**
* Retrieves the value of a feature flag for the currently active user

View File

@@ -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<ServerConfig>;
featureStates$: Observable<{ [key: string]: AllowedFeatureFlagTypes } | undefined>;
serverSettings$: Observable<ServerSettings>;
cloudRegion$: Observable<Region>;
@@ -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$<Flag extends FeatureFlag>(key: Flag) {