1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00

PM-23733 - Add filter + force refresh

This commit is contained in:
Jared Snider
2025-07-14 15:25:37 -04:00
parent 0bb4b5a159
commit 749304e562
5 changed files with 83 additions and 21 deletions

View File

@@ -2099,6 +2099,12 @@
"featureFlags": {
"message": "Feature flags"
},
"flagName": {
"message": "Flag name"
},
"flagValue": {
"message": "Flag value"
},
"domainRules": {
"message": "Domain rules"
},

View File

@@ -3,20 +3,30 @@
<i class="bwi bwi-spinner bwi-spin tw-text-2xl" aria-hidden="true"></i>
</div>
} @else {
<!-- // TODO: add forced update to config -->
<!-- // TODO: add filtering inline for table -->
<button buttonType="primary" type="button" bitButton (click)="refresh()" class="tw-mb-4">
{{ "refresh" | i18n }}
</button>
<bit-table [dataSource]="tableDataSource">
<ng-container header>
<th bitCell bitSortable="key" default>key</th>
<th bitCell bitSortable="value" default>value</th>
</ng-container>
<bit-search
id="search"
placeholder="{{ 'filter' | i18n }}"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged($event)"
/>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>{{ r.key }}</td>
<td bitCell>{{ r.value }}</td>
</tr>
</ng-template>
</bit-table>
<div class="tw-flex tw-flex-col">
<bit-table [dataSource]="tableDataSource">
<ng-container header>
<th bitCell bitSortable="key" default>{{ "flagName" | i18n }}</th>
<th bitCell bitSortable="value" default>{{ "flagValue" | i18n }}</th>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>{{ r.key }}</td>
<td bitCell>{{ r.value }}</td>
</tr>
</ng-template>
</bit-table>
</div>
}

View File

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

View File

@@ -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<void>;
/**
* Refreshes the server config, forcing a new retrieval from the server.
*/
abstract refreshServerConfig(): void;
}

View File

@@ -9,6 +9,7 @@ import {
Observable,
of,
shareReplay,
startWith,
Subject,
switchMap,
tap,
@@ -57,6 +58,7 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record<ServerConfig, A
// FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it.
export class DefaultConfigService implements ConfigService {
private failedFetchFallbackSubject = new Subject<ServerConfig>();
private manualRefresh$ = new Subject<void>();
serverConfig$: Observable<ServerConfig>;
@@ -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$<Flag extends FeatureFlag>(key: Flag) {
return this.serverConfig$.pipe(map((serverConfig) => getFeatureFlagValue(serverConfig, key)));
}