Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion libs/angular/src/platform/utils/feature-flagged-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ import { componentRouteSwap } from "../../utils/component-route-swap";
* @param flaggedComponent The component to be used when the feature flag is on.
* @param featureFlag The feature flag to evaluate
* @param routeOptions The shared route options to apply to both components.
* @param flaggedRouteProviders Optional providers scoped only to the flagged route. Use this to
* register services that should only be instantiated when the feature flag is on.
* @param defaultRouteProviders Optional providers scoped only to the default route. Use this to
* register services that should only be instantiated when the feature flag is off.
*/
type FeatureFlaggedRouteConfig = {
defaultComponent: Type<any>;
flaggedComponent: Type<any>;
featureFlag: FeatureFlag;
routeOptions: Omit<Route, "component">;
flaggedRouteProviders?: NonNullable<Route["providers"]>;
defaultRouteProviders?: NonNullable<Route["providers"]>;
};

/**
Expand All @@ -26,6 +32,7 @@ type FeatureFlaggedRouteConfig = {
* @param config See {@link FeatureFlaggedRouteConfig}
* @returns A tuple containing the conditional configuration for the two routes. This should be unpacked into your existing Routes array.
* @example
* // Basic usage β€” shared route options, no scoped providers:
* const routes: Routes = [
* ...featureFlaggedRoute({
* defaultComponent: GroupsComponent,
Expand All @@ -37,17 +44,54 @@ type FeatureFlaggedRouteConfig = {
* },
* }),
* ]
*
* @example
* // Scoped providers β€” each route only instantiates the services it needs:
* const routes: Routes = [
* ...featureFlaggedRoute({
* defaultComponent: GroupsComponent,
* flaggedComponent: GroupsNewComponent,
* featureFlag: FeatureFlag.GroupsComponentRefactor,
* routeOptions: {
* path: "groups",
* canActivate: [OrganizationPermissionsGuard],
* },
* defaultRouteProviders: [
* // Only instantiated when the feature flag is OFF
* safeProvider({ provide: LegacyGroupsService, useClass: LegacyGroupsService, deps: [...] }),
* ],
* flaggedRouteProviders: [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ What's the specific use case for this? Couldn't the existing and flagged component just create their own providers? As for example:
https://github.com/bitwarden/clients/blob/main/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts#L59

Then using this or the https://github.com/bitwarden/clients/blob/main/libs/angular/src/platform/guard/feature-flag.guard.ts would be sufficient.

I still wonder why we have two of these 🀷

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example is in this pull request for Access Intelligence refactoring in the routing module.

Two main reason: performance and unintended background execution

Sever Access Intelligence services have been refactored that start observable pipelines in the constructor. Route level providers scope the instantiation to when that route is actually activated, so only when the feature flag is on. Many of these services are also needed in many components rather than just one. The main service that benefits from this approach is the AccessIntelligenceDataService replacing the RiskInsightsOrchestratorService.

We could work around this by changing the services by exposing an initialize method to start the service, but I feel this adds a lot of overhead. The proposed change felt clean and clear to me

* // Only instantiated when the feature flag is ON
* safeProvider({ provide: GroupsService, useClass: GroupsService, deps: [...] }),
* ],
* }),
* ]
*/
export function featureFlaggedRoute(config: FeatureFlaggedRouteConfig): Routes {
const canMatch$ = () =>
inject(ConfigService)
.getFeatureFlag$(config.featureFlag)
.pipe(map((flagValue) => flagValue === true));

const defaultRouteOptions = config.defaultRouteProviders
? { ...config.routeOptions, providers: config.defaultRouteProviders }
: config.routeOptions;

// When defaultRouteProviders is set, defaultRouteOptions carries those providers as part of
// `options` passed to componentRouteSwap. Without an explicit flaggedRouteOptions, componentRouteSwap
// would fall back to `options` for the flagged route, unintentionally inheriting the default
// route's providers. Passing config.routeOptions directly avoids that.
const flaggedRouteOptions: Route | undefined = config.flaggedRouteProviders
? { ...config.routeOptions, providers: config.flaggedRouteProviders }
: config.defaultRouteProviders
? config.routeOptions
: undefined;

return componentRouteSwap(
config.defaultComponent,
config.flaggedComponent,
canMatch$,
config.routeOptions,
defaultRouteOptions,
flaggedRouteOptions,
);
}
Loading