Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
5c6535e
refactor(store): add subscription-scoped signal helpers
nabramovitz Oct 18, 2026
fc82378
fix(cf): stop leaking feature-flag signal watches
nabramovitz Oct 18, 2026
2595e2f
fix(autoscaler): stop leaking availability watches
nabramovitz Oct 18, 2026
6dca372
fix(git): stop leaking endpoint watches per SCM call
nabramovitz Oct 18, 2026
f70dc6e
fix(core): end the permission fallback endpoint watch
nabramovitz Oct 18, 2026
387031d
fix(kube): end helm release watches with the tab
nabramovitz Oct 18, 2026
e9bceed
fix(kube): keep a kube config import moving
nabramovitz Oct 18, 2026
04f4dbb
fix(store): return one observable from a signal wrapper
nabramovitz Oct 18, 2026
c2252aa
fix(deploy): keep deploy updates after leaving the page
nabramovitz Oct 18, 2026
2678253
refactor(kube): use the shared signal wrapper
nabramovitz Oct 18, 2026
830712a
fix(permissions): answer checks over no endpoints
nabramovitz Oct 18, 2026
4001eea
fix(cf): check endpoint scopes on each connected CF
nabramovitz Oct 18, 2026
cc1ddf6
fix(setup): check UAA credentials before going on
nabramovitz Oct 18, 2026
6f3cd73
fix(setup): clear the local account saving overlay
nabramovitz Oct 18, 2026
812a72c
refactor(core): drop the unused UAA setup signal facade
nabramovitz Oct 18, 2026
8e72dba
fix(setup): stop waiting on a setup request that hangs
nabramovitz Oct 18, 2026
dfc6070
fix(setup): word a failed UAA check as a check
nabramovitz Oct 18, 2026
c538099
fix(kube): skip a connect when its register fails
nabramovitz Oct 18, 2026
44cd64c
fix(kube): go on past an unsupported auth type
nabramovitz Oct 18, 2026
fd289f1
fix(kube): always destroy the auth form component
nabramovitz Oct 18, 2026
88438a9
test(kube): give the import spec a real kube user
nabramovitz Oct 18, 2026
cfe2335
fix(kube): say why a kube config connect failed
nabramovitz Oct 18, 2026
eaaffdc
fix(cf): stop the role picker loading forever
nabramovitz Oct 18, 2026
b74bd84
fix(cf): fetch org names again after a failure
nabramovitz Oct 18, 2026
effc623
fix(cf): start each roles dialog without old roles
nabramovitz Oct 18, 2026
32fcc88
fix(cf): say so when there are no orgs to manage
nabramovitz Oct 18, 2026
b31600a
fix(cf): stop the role picker space spinner hanging
nabramovitz Oct 18, 2026
ce54f4c
refactor(cf): remove the orphaned remove-user page
nabramovitz Oct 18, 2026
a3fb9e8
fix(cf): load manage-users past a bad user guid
nabramovitz Oct 18, 2026
d6d920e
test(cf): give the user service test provider a backend
nabramovitz Oct 18, 2026
0bce4ac
test(cf): stop the endpoint test provider breaking HTTP
nabramovitz Oct 18, 2026
d22b55c
fix(git): key git lookups by host as well
nabramovitz Oct 18, 2026
7b7be41
fix(git): retry a git lookup that failed
nabramovitz Oct 18, 2026
3eaa3b3
fix(git): read a branch head afresh on open
nabramovitz Oct 18, 2026
1eebb37
fix(git): end git waits when the lookup fails
nabramovitz Oct 18, 2026
44ecac4
test(cf): request the commit list once per open
nabramovitz Oct 18, 2026
75d50fe
fix(core): end the routing bar on a failed navigation
nabramovitz Oct 18, 2026
a4fff6d
fix(core): drop the stepper's 5 ms first-step poll
nabramovitz Oct 18, 2026
1417a14
fix(core): refilter steps when a step handle hides
nabramovitz Oct 18, 2026
10606d5
docs(cf): correct the create app step content note
nabramovitz Oct 18, 2026
bb2e640
fix(core): keep the stepper on its step after refilter
nabramovitz Oct 18, 2026
294131b
fix(core): keep the stepper oriented when all steps hide
nabramovitz Oct 18, 2026
22c63bc
fix(cf): keep a running file deploy open on leave
nabramovitz Oct 18, 2026
e1de84d
docs(store): note observeSignal view injector end
nabramovitz Oct 18, 2026
8dc22b0
fix(cf): build the deployer on the root injector
nabramovitz Oct 18, 2026
44b42f7
fix(cf): redeploy the recorded commit
nabramovitz Oct 18, 2026
920e525
fix(cf): show the deploy title once the manifest is in
nabramovitz Oct 18, 2026
dfec517
fix(cf): report every end of a deploy after leaving
nabramovitz Oct 18, 2026
5b14868
fix(cf): open the app from a notice after leaving
nabramovitz Oct 18, 2026
feb5f9c
test(cf): match only the app lookup in a title spec
nabramovitz Oct 18, 2026
4a3ef5b
fix(cf): upload afresh when coming back to a step
nabramovitz Oct 18, 2026
5a8d37e
fix(cf): count each uploaded file only once
nabramovitz Oct 19, 2026
3c7074c
fix(cf): drop a file read from a closed upload
nabramovitz Oct 19, 2026
fa08498
fix(cf): look up the app of each new deploy
nabramovitz Oct 19, 2026
9d9db2e
fix(cf): keep Previous locked until a deploy ends
nabramovitz Oct 19, 2026
aa2e33f
fix(core): start a new log for a new stream
nabramovitz Oct 19, 2026
e3a01ee
fix(cf): start an empty log for each deploy run
nabramovitz Oct 19, 2026
9ced565
fix(cf): upload again when retrying a file deploy
nabramovitz Oct 19, 2026
b86d589
fix(cf): allow closing when the app is not found
nabramovitz Oct 19, 2026
2a734af
fix(cf): close and report a failed app lookup
nabramovitz Oct 19, 2026
c8abda6
fix(cf): keep deploy errors boolean
nabramovitz Oct 19, 2026
b255b7f
refactor(cf): drop unused deploy step state
nabramovitz Oct 19, 2026
f8dbbc8
chore(e2e): drop the removed users page from ratchet
nabramovitz Oct 19, 2026
a218f48
test(cf): match git lookups with a predicate
nabramovitz Oct 19, 2026
69c595b
refactor(cf): bind the redeploy app without alias
nabramovitz Oct 19, 2026
e0565c2
test(cf): take the file reader from the spy
nabramovitz Oct 19, 2026
f1c94da
fix(cf): hide a stale commit card on redeploy
nabramovitz Oct 19, 2026
44631a2
refactor(cf): drop dead description markup
nabramovitz Oct 19, 2026
3741e49
docs(cf): correct the app GUID fallback comment
nabramovitz Oct 19, 2026
b5fc8fb
fix(cf): render git tab lookups as signals
nabramovitz Oct 19, 2026
f2dab3f
fix(autoscaler): load gauges once the policy loads
nabramovitz Oct 19, 2026
d960b7e
fix(core): show SSH progress, errors and state
nabramovitz Oct 19, 2026
09f54d5
fix(cf): keep recent apps in step with app changes
nabramovitz Oct 19, 2026
09556e9
fix(cf): follow the compact app card's input
nabramovitz Oct 19, 2026
73c7c6c
fix(cf): honour noStats in the recent apps card
nabramovitz Oct 19, 2026
64739e5
fix(cf): follow app changes on the home card
nabramovitz Oct 19, 2026
d8ccf33
fix(kube): render the resource viewer header
nabramovitz Oct 19, 2026
e29c852
fix(helm): show the catalog repository sidebar
nabramovitz Oct 19, 2026
0a1d549
fix(kube): read the dashboard flag, not the terminal
nabramovitz Oct 19, 2026
7fa5170
fix(helm): show the chart and readme spinners
nabramovitz Oct 19, 2026
19b8490
docs(kube): match release endpoint comments to code
nabramovitz Oct 19, 2026
a37a61f
fix(core): render meta card items added later
nabramovitz Oct 19, 2026
1ad7184
fix(cf): show the broker card's space and link
nabramovitz Oct 19, 2026
81a794e
fix(cf): make the broker cell's space a signal
nabramovitz Oct 19, 2026
fbf11ab
fix(cf): follow a plan that arrives after render
nabramovitz Oct 19, 2026
3e48893
fix(core): show succeeded stacked input rows
nabramovitz Oct 19, 2026
3c47913
fix(autoscaler): make the policy tab template build
nabramovitz Oct 19, 2026
0eae219
fix(autoscaler): register the Autoscale app tab
nabramovitz Oct 19, 2026
5d00495
fix(autoscaler): keep the autoscaler's reason
nabramovitz Oct 19, 2026
e5dc8b0
fix(autoscaler): show policy actions and refusals
nabramovitz Oct 19, 2026
06ed5b3
fix(deploy): label a second deploy "Deploy again"
nabramovitz Oct 19, 2026
8fa453b
fix(cf): drop dead registry release hooks
nabramovitz Oct 19, 2026
776c603
fix(endpoints): derive the name filter once
nabramovitz Oct 19, 2026
be8bc33
fix(endpoints): end permission watch with page
nabramovitz Oct 19, 2026
64fa70d
fix(core): stop guards leaking root watchers
nabramovitz Oct 19, 2026
24260b9
fix(cf): drop stale quota bridges on re-emit
nabramovitz Oct 19, 2026
23c9c66
fix(metrics): stop the window poll with its host
nabramovitz Oct 19, 2026
96e4fcb
fix(metrics): end the observe effect on stop
nabramovitz Oct 19, 2026
48ca127
fix(cf): fetch a quota again only on change
nabramovitz Oct 19, 2026
7b28342
fix(endpoints): render the custom empty component
nabramovitz Oct 19, 2026
4472356
fix(endpoints): end connect bridges on destroy
nabramovitz Oct 19, 2026
ae3585f
fix(endpoints): end the connect on step re-entry
nabramovitz Oct 19, 2026
851ae6d
fix(cf): drop unread refCount from cf registries
nabramovitz Oct 19, 2026
4bf5f14
fix(cf): name the service broker card link
nabramovitz Oct 19, 2026
de36047
fix(kubernetes): evict domain caches on disconnect
nabramovitz Oct 19, 2026
3df38cd
fix(apps): stop app-stats poll with its page
nabramovitz Oct 19, 2026
9d5d926
fix(endpoints): build the name cell once
nabramovitz Oct 19, 2026
15e95fb
fix(helm): clear release data on a failed load
nabramovitz Oct 19, 2026
03babc0
fix(kubernetes): keep newest errors, evict on leave
nabramovitz Oct 19, 2026
dbc25d3
fix(endpoints): keep recent apps in step with apps
nabramovitz Oct 19, 2026
f9e8ad3
refactor(cf): drop the recent apps self-load path
nabramovitz Oct 19, 2026
60027ee
docs(cf): correct the endpoint data load() note
nabramovitz Oct 19, 2026
c74b7c9
fix(cf): end broker space loads on swap/destroy
nabramovitz Oct 19, 2026
611472f
fix(kubernetes): drop responses after eviction
nabramovitz Oct 19, 2026
a9f3fec
fix(kubernetes): close eviction and refresh races
nabramovitz Oct 19, 2026
c995a7d
fix(core): track activity again after a blip
nabramovitz Oct 19, 2026
5d8dd8a
fix(metrics): subscribe to range with DestroyRef
nabramovitz Oct 19, 2026
4f6fe72
fix(auth): settle verifying when auth resets
nabramovitz Oct 19, 2026
4e9b78f
docs(changelog): note the audit fixes
nabramovitz Oct 19, 2026
399d1c3
fix(autoscaler): forward only sent history params
nabramovitz Oct 19, 2026
5858eb4
fix(autoscaler): provide app services to pages
nabramovitz Oct 19, 2026
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
41 changes: 41 additions & 0 deletions changelog.d/0013-audit-leaks-hangs-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[BugFixes]
- Fixed pages that waited forever:
- The UAA setup wizard checks the credentials in step 1 and shows an
error instead of an endless "Saving configuration" overlay. The local
account wizard gets the same overlay fix.
- Kubernetes config import no longer stalls at the first Connect, and
moves on past a cluster that fails to register or connect.
- The role picker in Add User and Manage Roles no longer shows
"Loading organizations..." forever; a failure shows Retry.
- The navigation progress bar no longer stays at 95% after a page
fails to load.
- After a failed session check near expiry, navigation no longer stops
working until a reload.
- Fixed views that did not show or showed out-of-date content:
- SSH sessions show their progress and server errors.
- Deploy keeps its completion notice after you leave the page and keeps
every log line. A second deploy from the same wizard is labelled
"Deploy again".
- The application Autoscale tab is back. It shows the reason when the
autoscaler refuses a request, and Create Policy only when there is no
policy. Its scaling history and metric charts load again; every
request for them had failed with an HTTP 400 error.
- The git tab no longer stays on "Retrieving Git Details" after a
failed lookup.
- Service broker cards show the Space row, and the broker URL as the
link text.
- Home page application cards follow start, stop and delete.
- Helm release pages no longer show earlier data after a failed reload,
and the Helm catalog sidebar shows.
- The Kubernetes resource viewer shows its header, and Delete works.
- Metrics charts with a time range selector render.
- An active user is no longer warned and logged out as idle after a
short session check failure.
- Stopped background work that kept running after you left a page:
- the application stats poll (one request per Cloud Foundry every 30
seconds, until a reload);
- the metrics polls;
- watches on endpoints, permissions and quotas.
- Disconnecting a Kubernetes endpoint clears its cached data and errors.
- Removed the `users/remove` page, which nothing linked to any more. The
users tabs remove users directly.
41 changes: 0 additions & 41 deletions e2e/pages/cloud-foundry/remove-users.page.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, it, expect } from 'vitest';

import { getTabsFromExtensions, StratosTabType } from '../../core/src/core/extension/extension-service';
import { CfAutoscalerPackageModule } from './cf-autoscaler-package.module';

describe('CfAutoscalerPackageModule', () => {
// The app imports only this module from the package, so it must load the
// tab component for its decorator to register the Autoscale tab
it('registers the Autoscale application tab', () => {
expect(CfAutoscalerPackageModule).toBeDefined();
expect(getTabsFromExtensions(StratosTabType.Application).map(t => t.link)).toContain('autoscale');
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injector, NgModule, inject } from '@angular/core';

import { EntityCatalogModule } from '../../store/src/entity-catalog.module';
import { CfAutoscalerModule } from './cf-autoscaler.module';
import { setAutoscalerHelperInjector } from './core/autoscaler-helpers/autoscaler-available';
import { generateASEntities } from './store/autoscaler-entity-generator';

Expand All @@ -17,6 +18,8 @@ import { generateASEntities } from './store/autoscaler-entity-generator';
@NgModule({
imports: [
EntityCatalogModule.forFeature(generateASEntities),
// Loads the tab component, whose decorator registers the Autoscale tab
CfAutoscalerModule,
],
})
export class CfAutoscalerPackageModule {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';

// Things in this path include add/edit policy stepper, policy metrics page and policy events page
// They're include like this to ensure those parts are lazy loaded.
// The core application policy tab is included via CfAutoscalerModule, which is imported in the Application Module
// The core application policy tab is included via CfAutoscalerModule, which CfAutoscalerPackageModule imports
const customRoutes: Routes = [
{
path: 'autoscaler',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { computed, Injector, provideZonelessChangeDetection, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { AutoscalerInfoDataService } from '../../services/domain-data/autoscaler-info-data.service';
import { fetchAutoscalerInfo, isAutoscalerEnabled, setAutoscalerHelperInjector } from './autoscaler-available';

// The helpers run off the root injector captured by the package module, so
// whatever watches the data service signals must end with the subscription,
// not with the (permanent) root injector.
describe('autoscaler-available helpers', () => {
// Each data service signal is read through a computed that counts its
// evaluations, so a still-running watcher shows up as extra reads after a
// write to the backing signal.
let available: ReturnType<typeof signal<boolean>>;
let info: ReturnType<typeof signal<unknown>>;
let reads: number;

beforeEach(() => {
available = signal(false);
info = signal<unknown>(null);
reads = 0;
const counted = <T>(read: () => T) => computed(() => {
reads++;
return read();
});
TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
{
provide: AutoscalerInfoDataService,
useValue: {
load: vi.fn().mockResolvedValue(undefined),
isAvailable: () => counted(() => available()),
info: () => counted(() => info()),
error: () => computed(() => null),
loading: () => computed(() => false),
},
},
],
});
setAutoscalerHelperInjector(TestBed.inject(Injector));
});

describe('isAutoscalerEnabled', () => {
it('does not read the availability signal until subscribed', () => {
isAutoscalerEnabled('ep-1');
TestBed.tick();
available.set(true);
TestBed.tick();

expect(reads).toBe(0);
});

it('stops reading the availability signal after unsubscribe', () => {
const values: boolean[] = [];
const sub = isAutoscalerEnabled('ep-1').subscribe(v => values.push(v));
TestBed.tick();
available.set(true);
TestBed.tick();
// startWith(false) sits after distinctUntilChanged, so the seed and the
// first signal value both come through.
expect(values).toEqual([false, false, true]);
const readsWhileSubscribed = reads;

sub.unsubscribe();
available.set(false);
TestBed.tick();

expect(reads).toBe(readsWhileSubscribed);
expect(values).toEqual([false, false, true]);
});
});

describe('fetchAutoscalerInfo', () => {
it('stops reading the info signal after unsubscribe', () => {
const builds: unknown[] = [];
const sub = fetchAutoscalerInfo('ep-1').subscribe(v => builds.push(v.entity?.entity));
TestBed.tick();
info.set({ build: '1' });
TestBed.tick();
expect(builds).toEqual([undefined, { build: '1' }]);
const readsWhileSubscribed = reads;

sub.unsubscribe();
info.set({ build: '2' });
TestBed.tick();

expect(reads).toBe(readsWhileSubscribed);
expect(builds).toEqual([undefined, { build: '1' }]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injector, computed, runInInjectionContext } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { Injector, computed } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, startWith } from 'rxjs/operators';

import { observeSignal } from '@stratosui/store';
import { APIResource, EntityInfo } from '../../../../store/src/types/api.types';
import { AutoscalerInfoDataService } from '../../services/domain-data/autoscaler-info-data.service';
import { AutoscalerInfo } from '../../store/app-autoscaler.types';
Expand All @@ -18,6 +18,11 @@ import { AutoscalerInfo } from '../../store/app-autoscaler.types';
// args) and the cloud-foundry summary card. The `esf` arg is retained
// for backwards compat but is unused — the data service is fetched via
// the module-level injector captured by CfAutoscalerPackageModule.
//
// That injector is the root one, which is never destroyed, so the signals
// are watched with observeSignal: the watcher starts on subscribe and stops
// on unsubscribe. toObservable would start one at call time that runs for
// the rest of the session.

let helperInjector: Injector | null = null;

Expand Down Expand Up @@ -67,7 +72,7 @@ function asEntityInfoObservable(
: null,
} as EntityInfo<APIResource<AutoscalerInfo>>;
});
return runInInjectionContext(requireInjector(), () => toObservable(envelope));
return observeSignal(envelope, requireInjector());
}

export const fetchAutoscalerInfo = (
Expand All @@ -90,9 +95,7 @@ export const isAutoscalerEnabled = (
): Observable<boolean> => {
const svc = getDataService();
void svc.load(endpointGuid);
return runInInjectionContext(requireInjector(), () =>
toObservable(svc.isAvailable(endpointGuid)),
).pipe(
return observeSignal(svc.isAvailable(endpointGuid), requireInjector()).pipe(
distinctUntilChanged(),
map(value => !!value),
catchError(() => of(false)),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpErrorResponse } from '@angular/common/http';
import { describe, it, expect } from 'vitest';
import {
isEqual, buildLegendData, shiftArray,
autoscalerErrorMessage, isEqual, buildLegendData, shiftArray,
} from './autoscaler-util';

describe('Autoscaler Util Helper', () => {
Expand Down Expand Up @@ -71,4 +72,10 @@ describe('Autoscaler Util Helper', () => {
expect(shiftArray([0, 2, 3], 1)).toEqual([1, 3, 4]);
expect(shiftArray([1, 6, 9], -1)).toEqual([0, 5, 8]);
});
it('autoscalerErrorMessage', () => {
const refusal = new HttpErrorResponse({ status: 403, error: { error: 'The application is not bound to Auto-Scaling service' } });
expect(autoscalerErrorMessage(refusal)).toBe('The application is not bound to Auto-Scaling service');
const other = new HttpErrorResponse({ status: 500, error: 'boom', url: '/x' });
expect(autoscalerErrorMessage(other)).toBe(other.message);
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HttpErrorResponse } from '@angular/common/http';
import { addDays, format, setHours, setMinutes } from 'date-fns';

import {
Expand Down Expand Up @@ -142,6 +143,13 @@ export const PolicyAlert = {
alertInvalidPolicyTriggerScheduleEmpty: 'At least one Scaling Rule or Schedule should be defined.',
};

// The autoscaler API gives its reason as {"error": "..."}; the request's own
// message only names the URL and the status
export function autoscalerErrorMessage(err: unknown): string {
const reason = (err as HttpErrorResponse)?.error?.error;
return typeof reason === 'string' ? reason : (err as HttpErrorResponse)?.message ?? String(err);
}

export function isEqual(a: any, b: any): boolean {
if (typeof a !== typeof b) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,11 @@ describe('AutoscalerBaseComponent', () => {
it('should be created', () => {
expect(component).toBeTruthy();
});

// The policy editor and the other autoscaler pages get ApplicationService
// from this component. It needs the app-detail services the app page
// provides; without them every autoscaler page failed with NG0201.
it('provides an ApplicationService its pages can use', () => {
expect(() => fixture.debugElement.injector.get(ApplicationService)).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute, RouterOutlet } from '@angular/router';
import { RouterOutlet } from '@angular/router';

import { ApplicationService, getGuids } from '@stratosui/cloud-foundry';
import { APP_GUID, CF_GUID } from '@stratosui/core';
import { APP_DETAIL_PROVIDERS } from '@stratosui/cloud-foundry';

@Component({
selector: 'app-autoscaler-base',
templateUrl: './autoscaler-base.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
ApplicationService,
{
provide: CF_GUID,
useFactory: getGuids('cf'),
deps: [ActivatedRoute]
},
{
provide: APP_GUID,
useFactory: getGuids(),
deps: [ActivatedRoute]
},
],
// The same app-detail providers as the app page: ApplicationService
// needs AppDetailDataService and the services it depends on.
providers: APP_DETAIL_PROVIDERS,
standalone: true,
imports: [RouterOutlet]
})
Expand Down
Loading
Loading