diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 00fb0e9806b..cd19e083008 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -2099,6 +2099,12 @@
"featureFlags": {
"message": "Feature flags"
},
+ "flagName": {
+ "message": "Flag name"
+ },
+ "flagValue": {
+ "message": "Flag value"
+ },
"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
index e91f2abbbc4..550ef60efd5 100644
--- a/libs/angular/src/platform/feature-flags/feature-flags.component.html
+++ b/libs/angular/src/platform/feature-flags/feature-flags.component.html
@@ -3,20 +3,30 @@
} @else {
-
-
+
-
-
- | key |
- value |
-
+
-
-
- | {{ r.key }} |
- {{ r.value }} |
-
-
-
+
+
+
+ | {{ "flagName" | i18n }} |
+ {{ "flagValue" | i18n }} |
+
+
+
+
+ | {{ r.key }} |
+ {{ r.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
index e4d4d8eb5c9..70f7036fb44 100644
--- a/libs/angular/src/platform/feature-flags/feature-flags.component.ts
+++ b/libs/angular/src/platform/feature-flags/feature-flags.component.ts
@@ -1,21 +1,41 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { FormsModule } from "@angular/forms";
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";
+import {
+ ButtonModule,
+ FormFieldModule,
+ InputModule,
+ SearchModule,
+ TableDataSource,
+ TableModule,
+} from "@bitwarden/components";
+import { I18nPipe } from "@bitwarden/ui-common";
@Component({
selector: "app-feature-flags",
templateUrl: "./feature-flags.component.html",
- imports: [CommonModule, TableModule],
+ imports: [
+ CommonModule,
+ TableModule,
+ ButtonModule,
+ I18nPipe,
+ FormsModule,
+ FormFieldModule,
+ InputModule,
+ SearchModule,
+ ],
})
export class FeatureFlagsComponent implements OnInit {
loading = true;
tableDataSource = new TableDataSource<{ key: string; value: AllowedFeatureFlagTypes }>();
+ searchText = "";
+
constructor(
private destroyRef: DestroyRef,
private configService: ConfigService,
@@ -42,4 +62,13 @@ export class FeatureFlagsComponent implements OnInit {
this.loading = false;
});
}
+
+ onSearchTextChanged(searchText: string) {
+ this.searchText = searchText;
+ this.tableDataSource.filter = searchText;
+ }
+
+ refresh() {
+ this.configService.refreshServerConfig();
+ }
}
diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts
index b24cb6f7282..c378ea7461f 100644
--- a/libs/common/src/platform/abstractions/config/config.service.ts
+++ b/libs/common/src/platform/abstractions/config/config.service.ts
@@ -60,4 +60,9 @@ export abstract class ConfigService {
* Triggers a check that the config for the currently active user is up-to-date. If it is not, it will be fetched from the server and stored.
*/
abstract ensureConfigFetched(): Promise;
+
+ /**
+ * Refreshes the server config, forcing a new retrieval from the server.
+ */
+ abstract refreshServerConfig(): void;
}
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 8e36341aa8f..a0c826a8f01 100644
--- a/libs/common/src/platform/services/config/default-config.service.ts
+++ b/libs/common/src/platform/services/config/default-config.service.ts
@@ -9,6 +9,7 @@ import {
Observable,
of,
shareReplay,
+ startWith,
Subject,
switchMap,
tap,
@@ -57,6 +58,7 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record();
+ private manualRefresh$ = new Subject();
serverConfig$: Observable;
@@ -82,22 +84,28 @@ export class DefaultConfigService implements ConfigService {
userId$,
this.environmentService.environment$,
authStatus$,
+ // must have default value so combineLatest will emit once others have emitted
+ this.manualRefresh$.pipe(startWith(false)),
]).pipe(
- switchMap(([userId, environment, authStatus]) => {
+ switchMap(([userId, environment, authStatus, manualRefresh]) => {
if (userId == null || authStatus !== AuthenticationStatus.Unlocked) {
return this.globalConfigFor$(environment.getApiUrl()).pipe(
- map((config) => [config, null, environment] as const),
+ map((config) => [config, null, environment, manualRefresh] as const),
);
}
return this.userConfigFor$(userId).pipe(
- map((config) => [config, userId, environment] as const),
+ map((config) => [config, userId, environment, manualRefresh] as const),
);
}),
tap(async (rec) => {
- const [existingConfig, userId, environment] = rec;
+ const [existingConfig, userId, environment, manualRefresh] = rec;
// Grab new config if older retrieval interval
- if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) {
+ if (
+ !existingConfig ||
+ this.olderThanRetrievalInterval(existingConfig.utcDate) ||
+ manualRefresh
+ ) {
await this.renewConfig(existingConfig, userId, environment);
}
}),
@@ -128,6 +136,10 @@ export class DefaultConfigService implements ConfigService {
);
}
+ refreshServerConfig() {
+ this.manualRefresh$.next();
+ }
+
getFeatureFlag$(key: Flag) {
return this.serverConfig$.pipe(map((serverConfig) => getFeatureFlagValue(serverConfig, key)));
}