Repository navigation
refactor(frontend): replace RxJS 7 deprecated APIs - #6044
Merged
norman-abramovitz merged 13 commits intoOct 22, 2026
Merged
norman-abramovitz merged 13 commits into
norman-abramovitz merged 13 commits into
Conversation
The app runs with provideZonelessChangeDetection(), so NgZone.run() and NgZone.runOutsideAngular() just call their callback. Remove the 12 calls and the 9 NgZone injections that existed only for them, in the session-expiry poll, the endpoint health-check interval, the metrics time-window poll, the Kubernetes summary poll, the blur and focus directives and the markdown content observer. The metrics poll keeps its explicit ApplicationRef.tick(). ApplicationTabsBaseComponent injected NgZone without using it. Delete code that never did anything: - core/src/leaveEnterAngularZone.ts: two pass-through scheduler helpers with no importers. - DashboardBaseComponent.redrawSideNav() and the (changedMode) binding that called it. It poked _modeChanged on a Material drawer, but #sidenav is now a plain <div>, so the property is undefined and the method returned at its first check. - The @ViewChild('sidenav') setter in the same component. It subscribed to drawer.closedStart, which a <div>'s ElementRef does not have, so the subscription was never made.
HttpClientModule is deprecated. AppModule already configures the HTTP client with provideHttpClient(withXhr(), withInterceptors([...])), and that configuration is the one the app uses. The HttpClientModule imported by AppStoreModule and StratosThemeModule, both loaded into the same root injector, only appended a second copy of Angular's XSRF interceptor and the adapter for class-based HTTP_INTERCEPTORS to the interceptor chain. No class-based interceptor is registered at the root, so that adapter had nothing to run. The built-in XSRF interceptor that sends the X-XSRF-TOKEN header still comes from provideHttpClient. Also drop it from HelmBaseTestModules and WorkloadsBaseTestingModule, two exported test arrays that no spec uses.
RxJS 7 deprecates several call forms that RxJS 8 removes. Replace
them with the forms RxJS 7 recommends; behaviour is unchanged.
- subscribe(next, error[, complete]) with positional callbacks becomes
subscribe({ next, error, complete }) at 7 sites: the EULA page,
backup and restore of endpoints, the markdown preview, the SSH
viewer, the kubeconfig certificate check and the Kubernetes
dashboard configuration.
- throwError(value) becomes throwError(() => value) in the monocular
ChartsService error handler.
- Observable.create(fn) becomes new Observable(fn) for ChartsService's
cached-charts branch. It still emits the cached list and does not
complete, as before.
- EventEmitter.observers.length > 0 becomes EventEmitter.observed in
CardCfRecentAppsComponent.
Delete the monocular ReposService and its provider entry. Nothing
injects it, and its error handler called error.json(), which an
HttpErrorResponse does not have, so it would have thrown a TypeError
instead of reporting the failure. Its throwError(value) was the other
deprecated call of that form.
RxJS 7 deprecates two forms of combineLatest that RxJS 8 removes: - the creation function called with each source as its own argument, combineLatest(a, b). 32 calls, 9 of them through the observableCombineLatest import alias, now pass one array, combineLatest([a, b]). The emitted tuple is the same. - the pipeable operator imported from rxjs/operators. Its 8 calls in the log viewer, the services helper, CfRolesService and the build tab now use combineLatestWith, which emits the same [source, ...others] tuple. MetaCardComponent built its combineLatest input as (Observable<boolean> | undefined)[], which only matched the deprecated overload. Every menu item's `can` is set to of(true) earlier in the same setter when missing, so map it with `?? of(true)` to give the array a non-optional element type.
Observable.toPromise() is deprecated in RxJS 7 and removed in RxJS 8. - StratosBrandingService loads /assets/company-config.json, then /assets/config/company.json, then falls back to the built-in default. Both requests are HttpClient GETs, which emit once or error, so firstValueFrom() resolves and rejects exactly as toPromise() did. New specs flush each of the three outcomes through HttpTestingController and check the company name that results; they passed before the change and still pass. - LoginPageComponent reads the current auth state once before an auto-redirect. auth$ comes from toObservable() and can complete without a value if the page is torn down mid-redirect; the code already returns early on undefined. firstValueFrom() with defaultValue: undefined keeps that, where a bare firstValueFrom() would reject with EmptyError. It also takes the first value itself, so the take(1) goes.
The application log stream tab reconnects its web socket with
retryWhen(), which RxJS 7 deprecates, and the way it limited the
retries had two faults:
- take(3) sat on the retry notifier, so the third failure still
produced a retry signal. retryWhen() opened a fourth socket on it
and then, because the notifier had completed, ended the stream and
closed that socket at once. The "failed permanently" handler never
ran.
- take(3) counted retries over the life of the tab. A successful
connection reset the attempt counter used for the back-off and the
messages, but not take(3). After two failures, a connection, and
one more failure, the stream ended silently, still in the
"retrying" state, and stopped reconnecting.
Use retry({ count: maxRetries - 1, delay, resetOnSuccess: true }):
three attempts in all (the first connection and two retries, 2 s and
4 s apart), counted afresh after each successful connection. The
delay callback reports each retry; the third failure reaches the
catchError, which marks the stream failed. The retry count comes from
retry() itself, so the connectionAttempts field goes.
New specs replace the web socket with a fake that counts connections.
Before this change, three failures opened 4 sockets instead of 3, and
the socket opened after fail, fail, connect, fail was closed at once.
Also convert the existing spec in this file from detectChanges() to
await fixture.whenStable().
publishReplay() and refCount() are deprecated in RxJS 7 and removed in
RxJS 8. All 29 uses were the pair publishReplay(1), refCount().
The replacement is not exactly equivalent. publishReplay(1) keeps its
replay buffer when the subscriber count drops to zero, so a later
subscriber gets the old value at once and the source is subscribed
again. shareReplay({ bufferSize: 1, refCount: true }) drops the
buffer and starts over. The two only differ for a source that has
not completed and whose subscriber count can reach zero and recover.
28 sites use shareReplay({ bufferSize: 1, refCount: true }). At each
one the source completes after one value (take(1) or an HTTP call),
or a subscriber that lives as long as the owning component keeps the
count above zero, or the source replays its current value on
subscribe (toObservable, observeSignal, startWith), so a reset buffer
is refilled at once.
GlobalEventService is the exception. It is a root service; the
page-header banner and the events page subscribe to events$ and
leave on every navigation, and its event stream is debounced by
100 ms. With refCount: true each new page would see no events (no
banner, no unread count) for the first 100 ms. It uses
refCount: false, which keeps the subscription and the last value for
the life of the app. A new spec subscribes, unsubscribes and
subscribes again, and expects the events at once: it passed before
this change, fails with refCount: true and passes with
refCount: false.
A comment in AppDeleteSelectionService that named the publishReplay
caches now says shareReplay.
The previous commits removed every use of the RxJS 7 deprecations listed below and of NgZone and HttpClientModule. Two ESLint rules, both at "error", stop them coming back: - no-restricted-imports: publishReplay, refCount, publish, publishLast, publishBehavior, multicast and retryWhen from rxjs or rxjs/operators; the combineLatest operator from rxjs/operators; HttpClientModule; NgZone. - no-restricted-syntax: .toPromise(); subscribe() with more than one argument; combineLatest() or observableCombineLatest() with more than one argument; throwError() whose argument is not an inline function. typescript-eslint's no-deprecated rule would catch these without a list, but it needs type-aware linting and also reports hundreds of other deprecations (NoopAnimationsModule, RouterTestingModule and others) that are outside this change. The throwError rule cannot tell a function variable from an error value, so write-with-job.ts now passes its abort error factory as throwError(() => abortError()) instead of throwError(abortError). Behaviour is the same. Add src/frontend/packages/theme/ to the lint paths. It was not linted before, and lints clean.
The application Log Stream tab never told the user it was retrying or that it had given up. The log viewer already has messages for both, but the status the tab passes it only ever said "connecting" and then "connected". The retrying and failed codes went to a subject and a signal that nothing read, so after a failed connection the tab said "Connecting to log stream..." for good. The subject has been write-only since 2019, when the status passed to the viewer became a value derived from the socket. The retry and its codes came later, in 2025, written to that subject. Pass the subject itself to the viewer, debounced as before so the "connecting" message does not flash. The tab now shows, in turn: - "Connecting to log stream..." - "Connection lost. Retrying (attempt 1/2)..." and "(attempt 2/2)..." - "Unable to connect to log stream. Please check that the backend service is running and try refreshing the page." and clears the message once connected. The retry count starts again after each good connection. The viewer counted retries out of a fixed 5; it now takes the number as an input, which the tab sets to its own 2. Its message sits in a role="status" region so screen readers announce it, and errors show in the danger colour. The unread signal goes.
Add the log stream tab's retrying and failed messages to the RxJS 7 fragment's bug fixes.
HelmBaseTestModules, HelmBaseTestProviders and WorkloadsBaseTestingModule had no importers anywhere in the frontend. The only mention was a commented-out line in the Helm release values tab spec, which goes too, along with the imports only these lists used. Both files keep their other exports, which specs still import.
The metrics range selector polls every 10 seconds when a time window is chosen, re-requesting the window so the chart keeps moving, and ticks the app because the poll runs from a plain setInterval. This branch removed the NgZone wrappers around that interval. The manager's spec only checked that it could be created, and the selector spec checks a single tick and the stop on destroy. Check that every poll sends a new request whose one-hour window has moved on by the poll interval, and ticks the app each time; and that choosing a custom range stops the poll. Removing the request, the tick or the stop each turns one of these red.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses #6022.
Removes every use of the RxJS APIs that RxJS 7 deprecates and RxJS 8 removes, plus two Angular leftovers that do nothing in this zoneless app:
HttpClientModule(duplicatingprovideHttpClient) and theNgZonewrappers. ESLint rules then reject all of them, so they can't come back. Two user-visible log stream bugs turned up in the code being replaced, and they are fixed here in their own commits.User-visible fixes
The log stream stops after three attempts. The application Log Stream tab limited its retries with
take(3)on theretryWhennotifier. The third failure still opened a fourth socket and closed it at once, so the "failed" handler never ran. The count also never reset after a good connection: after fail, fail, connect, fail, the stream ended silently and stopped reconnecting. It now usesretry({ count: maxRetries - 1, delay, resetOnSuccess: true }), which gives three attempts in all, 2 s and 4 s apart, counted afresh after each successful connection.The log stream says when it is retrying or has given up. The log viewer already had messages for both, but since 2019 the status the tab passed it only ever said "connecting" or "connected". The retrying and failed codes went to a subject that nothing read, so after a failed connection the tab said "Connecting to log stream..." for good. The tab now shows:
The message clears once the stream connects. It sits in a
role="status"region so screen readers announce it, and the failure shows in the danger colour.Replacements
NgZonerun/runOutsideAngularwrappersHttpClientModuleprovideHttpClient.subscribe(next, error, complete),Observable.create,.observers,throwError(value)new Observable,.observed,throwError(() => value)ReposService, which had no callers, is deleted.combineLatest, thecombineLatestoperatorcombineLatest,combineLatestWithcancheck now defaults toof(true)instead of relying on a single-array overload.toPromise()firstValueFromdefaultValue: undefinedwhere the source may complete without a value.retryWhenretry({ … })publishReplay(1), refCount()(29 uses)shareReplaypublishReplay(1), refCount()keeps its last value when the subscriber count drops to zero, whileshareReplay({ bufferSize: 1, refCount: true })drops it. That only matters for a source that never completes and whose subscribers can all leave and come back.refCount: true. At each one the source completes after one value, a subscriber keeps the count above zero, or the source replays its current value on subscribe.GlobalEventServiceis the exception. It is a root service whose page-header banner and events page leave and resubscribe on every navigation, behind a 100 ms debounce. WithrefCount: true, each new page would show no banner and no unread count for its first 100 ms, so it usesrefCount: false. A new spec subscribes, unsubscribes and subscribes again: it passes on develop, fails withrefCount: true, and passes withrefCount: false.Lint
The rules are
no-restricted-importsandno-restricted-syntax, both at error. They cover the operators above,HttpClientModule,NgZone,.toPromise(), multi-argumentsubscribe()andcombineLatest(), andthrowError()without an inline function. typescript-eslint'sno-deprecatedwould need type-aware linting and also reports hundreds of unrelated deprecations, so it is not used here.src/frontend/packages/theme/is added to the lint paths; it was not linted before and lints clean. Reverse test: putting a deprecated call back gives the expected lint errors.Also in this PR
HelmBaseTestModules,HelmBaseTestProvidersandWorkloadsBaseTestingModule, test setup lists with no importers, are deleted.Tests
[status]binding.StratosBrandingServicespecs cover all three config load outcomes. They pass both before and after thefirstValueFromchange.GlobalEventService: the resubscribe spec described above.6a34127aaa); each of 3 mutations turns it red.make check gate: passed on this branch (4411 tests), and again on develop f88c08f + fix(core): show the Metrics tab for covered endpoints #6050 + this branch (4595 passed, 1 skipped; Go lint 0 issues).Live checks
Against a Cloud Foundry lab in a browser, since unit tests don't cover these paths. Each failure says whether it was real or faked.
Release note:
changelog.d/0021-rxjs7-deprecations.md.AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.