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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./developer-tools.component";
|
||||
@@ -2093,6 +2093,12 @@
|
||||
"default": {
|
||||
"message": "Default"
|
||||
},
|
||||
"developerTools": {
|
||||
"message": "Developer Tools"
|
||||
},
|
||||
"featureFlags": {
|
||||
"message": "Feature flags"
|
||||
},
|
||||
"domainRules": {
|
||||
"message": "Domain rules"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
1
libs/angular/src/platform/feature-flags/index.ts
Normal file
1
libs/angular/src/platform/feature-flags/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./feature-flags.component";
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user