Skip to content

Commit 49433db

Browse files
nabramovitznorman-abramovitz
authored andcommitted
App detail: signal-native flat shapes for org/space/domain
Drops the last three v2 proxy URL fetches in AppDetailDataService (spaces, organizations, organization domains) in favor of the Stratos native handlers. The Stratos<->backend interface is now independent of V2/V3 wire shape — the backend owns that translation. Flips _space/_org/_domains signal types from APIResource<I*> v2 envelopes to flat StSpace/StOrg/StDomain. ApplicationService façade appOrg$/appSpace$/orgDomains$ types follow; wrapDomain compatibility adapter is dropped. Nine consumer sites rewrite .metadata.guid -> .guid, .entity.name -> .name, .entity.organization_guid -> .orgGuid, .entity.allow_ssh -> .allowSsh. Verified end-to-end on the app detail Summary tab against a live adepttech CF: backend hits new getNativeSpaceDetail with allowSsh resolved via the SSH feature endpoint, Cloud Foundry side panel renders correct org/space names and org/space deep links.
1 parent ca29c1c commit 49433db

17 files changed

Lines changed: 153 additions & 143 deletions

src/frontend/packages/cloud-foundry/src/features/applications/app-detail-data.service.spec.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,42 @@ const MOCK_ENV = {
9797
};
9898

9999
const MOCK_SPACE = {
100-
metadata: { guid: 'sp-1', created_at: '', updated_at: '', url: '' },
101-
entity: { name: 'my-space', organization_guid: 'org-1' },
100+
guid: 'sp-1',
101+
name: 'my-space',
102+
orgGuid: 'org-1',
103+
createdAt: '',
104+
updatedAt: '',
105+
cnsiGuid: CNSI,
106+
appCount: 0,
107+
routeCount: 0,
102108
};
103109

104110
const MOCK_ORG = {
105-
metadata: { guid: 'org-1', created_at: '', updated_at: '', url: '' },
106-
entity: { name: 'my-org' },
111+
guid: 'org-1',
112+
name: 'my-org',
113+
status: 'active',
114+
quotaGuid: '',
115+
labels: {},
116+
annotations: {},
117+
createdAt: '',
118+
updatedAt: '',
119+
cnsiGuid: CNSI,
107120
};
108121

109122
const MOCK_DOMAINS_RESPONSE = {
110123
resources: [
111-
{ metadata: { guid: 'd-1', created_at: '', updated_at: '', url: '' }, entity: { name: 'example.com' } },
124+
{
125+
guid: 'd-1',
126+
name: 'example.com',
127+
internal: false,
128+
supportedProtocols: ['http'],
129+
sharedOrgGuids: [],
130+
cnsiGuid: CNSI,
131+
createdAt: '',
132+
updatedAt: '',
133+
},
112134
],
135+
pagination: { totalResults: 1 },
113136
};
114137

115138
/**
@@ -277,8 +300,8 @@ describe('AppDetailDataService', () => {
277300
// Phase 1a (parallel): app + envVars (no separate /summary fetch — the
278301
// composed StAppDetail envelope carries every Summary-tab field).
279302
// Phase 1b: stats (conditional on app.state === STARTED).
280-
// Phase 2: space (V2 proxy — until org/space migration).
281-
// Phase 3: org → domains (sequential).
303+
// Phase 2: space (Jetstream native handler).
304+
// Phase 3: org → domains (sequential, Jetstream native handlers).
282305
// -------------------------------------------------------------------------
283306

284307
it('refresh("all") phase 1: app + envVars in parallel, then stats, then space/org/domains', async () => {
@@ -295,15 +318,15 @@ describe('AppDetailDataService', () => {
295318

296319
// Phase 2: space request needs app.spaceGuid
297320
await tick();
298-
httpMock.expectOne(`/pp/v1/proxy/v2/spaces/sp-1`).flush(MOCK_SPACE);
321+
httpMock.expectOne(`/pp/v1/cf/spaces/cnsi-1/sp-1`).flush(MOCK_SPACE);
299322

300-
// Phase 3a: org needs space.organization_guid
323+
// Phase 3a: org needs space.orgGuid
301324
await tick();
302-
httpMock.expectOne(`/pp/v1/proxy/v2/organizations/org-1`).flush(MOCK_ORG);
325+
httpMock.expectOne(`/pp/v1/cf/org/cnsi-1/org-1`).flush(MOCK_ORG);
303326

304-
// Phase 3b: domains needs org.metadata.guid
327+
// Phase 3b: domains needs org.guid
305328
await tick();
306-
httpMock.expectOne(`/pp/v1/proxy/v2/organizations/org-1/domains`).flush(MOCK_DOMAINS_RESPONSE);
329+
httpMock.expectOne(`/pp/v1/cf/org/cnsi-1/org-1/private_domains`).flush(MOCK_DOMAINS_RESPONSE);
307330

308331
await promise;
309332

@@ -327,13 +350,13 @@ describe('AppDetailDataService', () => {
327350
httpMock.expectOne(STATS_URL).flush(MOCK_STATS_RESPONSE);
328351

329352
await tick();
330-
httpMock.expectOne(`/pp/v1/proxy/v2/spaces/sp-1`).flush(MOCK_SPACE);
353+
httpMock.expectOne(`/pp/v1/cf/spaces/cnsi-1/sp-1`).flush(MOCK_SPACE);
331354

332355
await tick();
333-
httpMock.expectOne(`/pp/v1/proxy/v2/organizations/org-1`).flush(MOCK_ORG);
356+
httpMock.expectOne(`/pp/v1/cf/org/cnsi-1/org-1`).flush(MOCK_ORG);
334357

335358
await tick();
336-
httpMock.expectOne(`/pp/v1/proxy/v2/organizations/org-1/domains`).flush(MOCK_DOMAINS_RESPONSE);
359+
httpMock.expectOne(`/pp/v1/cf/org/cnsi-1/org-1/private_domains`).flush(MOCK_DOMAINS_RESPONSE);
337360

338361
await promise;
339362

@@ -360,7 +383,7 @@ describe('AppDetailDataService', () => {
360383

361384
expect(svc.space()).toBeUndefined();
362385
expect(svc.org()).toBeUndefined();
363-
httpMock.expectNone(`/pp/v1/proxy/v2/spaces/sp-1`);
386+
httpMock.expectNone(`/pp/v1/cf/spaces/cnsi-1/sp-1`);
364387
});
365388

366389
it('sets lastPolledAt after a successful refresh("all")', async () => {
@@ -375,13 +398,13 @@ describe('AppDetailDataService', () => {
375398
httpMock.expectOne(STATS_URL).flush(MOCK_STATS_RESPONSE);
376399

377400
await tick();
378-
httpMock.expectOne(`/pp/v1/proxy/v2/spaces/sp-1`).flush(MOCK_SPACE);
401+
httpMock.expectOne(`/pp/v1/cf/spaces/cnsi-1/sp-1`).flush(MOCK_SPACE);
379402

380403
await tick();
381-
httpMock.expectOne(`/pp/v1/proxy/v2/organizations/org-1`).flush(MOCK_ORG);
404+
httpMock.expectOne(`/pp/v1/cf/org/cnsi-1/org-1`).flush(MOCK_ORG);
382405

383406
await tick();
384-
httpMock.expectOne(`/pp/v1/proxy/v2/organizations/org-1/domains`).flush(MOCK_DOMAINS_RESPONSE);
407+
httpMock.expectOne(`/pp/v1/cf/org/cnsi-1/org-1/private_domains`).flush(MOCK_DOMAINS_RESPONSE);
385408

386409
await promise;
387410

src/frontend/packages/cloud-foundry/src/features/applications/app-detail-data.service.ts

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import { Injectable, computed, effect, inject, signal, Signal, WritableSignal } from '@angular/core';
2-
import { HttpClient, HttpHeaders } from '@angular/common/http';
1+
import { Injectable, computed, effect, inject, signal, Signal } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
33
import { firstValueFrom } from 'rxjs';
44

55
import { AppDetailPrefs } from './app-detail-prefs.service';
66
import { AppLifecycleStateService } from './app-lifecycle-state.service';
77
import { ApplicationStateService, ApplicationStateData } from '../../shared/services/application-state.service';
8-
import { IApp, IAppSummary, IDomain, IOrganization, ISpace } from '../../cf-api.types';
8+
import { IApp, IAppSummary } from '../../cf-api.types';
99
import { APIResource } from '@stratosui/store';
1010
import { EnvVarStratosProject } from './application/application-tabs-base/tabs/build-tab/application-env-vars.service';
1111
import {
1212
StAppDetail,
1313
StAppRoutesResponse,
1414
StAppStat,
15+
StDomain,
16+
StratosPagedResponse,
1517
StEnvVars,
18+
StOrg,
1619
StRoute,
1720
StServiceCredentialBinding,
1821
StServiceCredentialBindingsResponse,
22+
StSpace,
1923
StratosError,
2024
} from '../../services/endpoint-data/stratos-types';
2125
import { stToLegacy } from '../../services/v3-to-legacy-adapter';
@@ -36,8 +40,8 @@ interface StAppStatsResponse {
3640
* signals via the `stToLegacy` adapter so unmigrated cards/tabs keep
3741
* reading their familiar shape during the migration.
3842
*
39-
* Space / org / domains stay v2-shaped for now — those are addressed in
40-
* a later slice when the org/space detail pages migrate.
43+
* Space / org / domains are sourced from Jetstream native handlers and
44+
* exposed as Stratos-native shapes (`StSpace`, `StOrg`, `StDomain[]`).
4145
*
4246
* Provide at application-base.component so signals are torn down when the
4347
* user navigates away from the app detail page. DO NOT add `providedIn:'root'`.
@@ -59,11 +63,12 @@ export class AppDetailDataService {
5963
private readonly _envVars = signal<StEnvVars | undefined>(undefined);
6064
private readonly _stats = signal<StAppStat[]>([]);
6165

62-
// Space / org / domains stay V2-shaped — out of scope for slice 1 commit 4.
63-
// These switch to V3 native handlers when the org/space detail pages migrate.
64-
private readonly _space = signal<APIResource<ISpace> | undefined>(undefined);
65-
private readonly _org = signal<APIResource<IOrganization> | undefined>(undefined);
66-
private readonly _domains = signal<IDomain[]>([]);
66+
// Space / org / domains — Stratos-native shapes from Jetstream native
67+
// handlers. Backend translates v2/v3 under the hood; the frontend never
68+
// touches `/pp/v1/proxy/v2/...` directly.
69+
private readonly _space = signal<StSpace | undefined>(undefined);
70+
private readonly _org = signal<StOrg | undefined>(undefined);
71+
private readonly _domains = signal<StDomain[]>([]);
6772

6873
// Per-app routes — V3-shaped via the native handler. Null until first load
6974
// so consumers can distinguish "haven't fetched yet" from "fetched, empty".
@@ -117,9 +122,9 @@ export class AppDetailDataService {
117122
/** Trimmed V3 stats — one row per running instance with `{ index, state }`. */
118123
readonly stats: Signal<StAppStat[]> = this._stats.asReadonly();
119124

120-
readonly space: Signal<APIResource<ISpace> | undefined> = this._space.asReadonly();
121-
readonly org: Signal<APIResource<IOrganization> | undefined> = this._org.asReadonly();
122-
readonly domains: Signal<IDomain[]> = this._domains.asReadonly();
125+
readonly space: Signal<StSpace | undefined> = this._space.asReadonly();
126+
readonly org: Signal<StOrg | undefined> = this._org.asReadonly();
127+
readonly domains: Signal<StDomain[]> = this._domains.asReadonly();
123128
readonly loading: Signal<Record<EntityKind, boolean>> = this._loading.asReadonly();
124129
readonly errors: Signal<Record<EntityKind, StratosError | null>> = this._errors.asReadonly();
125130
readonly lastPolledAt: Signal<Date | null> = this._lastPolledAt.asReadonly();
@@ -302,7 +307,7 @@ export class AppDetailDataService {
302307
* Phase 1b (conditional): stats — only when state is STARTED to avoid
303308
* a noisy 400 from CF's CF-AppStoppedStatsError on stopped apps.
304309
* Phase 2 (sequential): space (needs app.spaceGuid), then org (needs
305-
* space.organization_guid), then domains (needs org.guid).
310+
* space.orgGuid), then domains (needs org.guid).
306311
*/
307312
async refresh(scope: EntityKind | 'all' = 'all'): Promise<void> {
308313
if (scope === 'all') {
@@ -340,9 +345,9 @@ export class AppDetailDataService {
340345
// ---------------------------------------------------------------------------
341346
// URL helpers
342347
//
343-
// App detail / env / stats now hit Jetstream native handlers — the cnsi
344-
// is in the path, no x-cap-passthrough header needed. Space / org /
345-
// domains still hit the V2 proxy until those pages migrate (slice 2+).
348+
// All entity fetches hit Jetstream native handlers — cnsi is in the
349+
// path and responses come back as Stratos-native shapes. No
350+
// `x-cap-passthrough` header needed; the frontend never talks v2 wire.
346351
// ---------------------------------------------------------------------------
347352

348353
private nativeAppDetailUrl(): string {
@@ -362,29 +367,15 @@ export class AppDetailDataService {
362367
}
363368

364369
private spaceUrl(spaceGuid: string): string {
365-
return `/pp/v1/proxy/v2/spaces/${spaceGuid}`;
370+
return `/pp/v1/cf/spaces/${this.cnsiGuid}/${spaceGuid}`;
366371
}
367372

368373
private orgUrl(orgGuid: string): string {
369-
return `/pp/v1/proxy/v2/organizations/${orgGuid}`;
374+
return `/pp/v1/cf/org/${this.cnsiGuid}/${orgGuid}`;
370375
}
371376

372377
private orgDomainsUrl(orgGuid: string): string {
373-
return `/pp/v1/proxy/v2/organizations/${orgGuid}/domains`;
374-
}
375-
376-
/**
377-
* Headers required by the V2 proxy paths (space / org / domains). Native
378-
* handlers don't need these — cnsi is in the path and the response is
379-
* the raw shape, no envelope.
380-
*/
381-
private v2ProxyHeaders(): { headers: HttpHeaders } {
382-
return {
383-
headers: new HttpHeaders({
384-
'x-cap-cnsi-list': this.cnsiGuid,
385-
'x-cap-passthrough': 'true',
386-
}),
387-
};
378+
return `/pp/v1/cf/org/${this.cnsiGuid}/${orgGuid}/private_domains`;
388379
}
389380

390381
// ---------------------------------------------------------------------------
@@ -478,7 +469,7 @@ export class AppDetailDataService {
478469
this._errors.update(m => ({ ...m, space: null }));
479470
try {
480471
const value = await firstValueFrom(
481-
this.http.get<APIResource<ISpace>>(this.spaceUrl(spaceGuid), this.v2ProxyHeaders())
472+
this.http.get<StSpace>(this.spaceUrl(spaceGuid))
482473
);
483474
this._space.set(value);
484475
} catch (err: unknown) {
@@ -489,15 +480,15 @@ export class AppDetailDataService {
489480
}
490481

491482
private async fetchOrg(): Promise<void> {
492-
const orgGuid = this._space()?.entity?.organization_guid;
483+
const orgGuid = this._space()?.orgGuid;
493484
if (!orgGuid) {
494485
return;
495486
}
496487
this._loading.update(m => ({ ...m, org: true }));
497488
this._errors.update(m => ({ ...m, org: null }));
498489
try {
499490
const value = await firstValueFrom(
500-
this.http.get<APIResource<IOrganization>>(this.orgUrl(orgGuid), this.v2ProxyHeaders())
491+
this.http.get<StOrg>(this.orgUrl(orgGuid))
501492
);
502493
this._org.set(value);
503494
} catch (err: unknown) {
@@ -613,15 +604,15 @@ export class AppDetailDataService {
613604
}
614605

615606
private async fetchDomains(): Promise<void> {
616-
const orgGuid = this._org()?.metadata?.guid;
607+
const orgGuid = this._org()?.guid;
617608
if (!orgGuid) {
618609
return;
619610
}
620611
this._loading.update(m => ({ ...m, domains: true }));
621612
this._errors.update(m => ({ ...m, domains: null }));
622613
try {
623614
const result = await firstValueFrom(
624-
this.http.get<{ resources: IDomain[] }>(this.orgDomainsUrl(orgGuid), this.v2ProxyHeaders())
615+
this.http.get<StratosPagedResponse<StDomain>>(this.orgDomainsUrl(orgGuid))
625616
);
626617
this._domains.set(result?.resources ?? []);
627618
} catch (err: unknown) {

src/frontend/packages/cloud-foundry/src/features/applications/application-delete/application-delete.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ export class ApplicationDeleteComponent {
6363
this.seed?.appName || this.dataService.app()?.entity?.name || ''
6464
);
6565
public readonly orgName = computed(() =>
66-
this.seed?.orgName || this.dataService.org()?.entity?.name || ''
66+
this.seed?.orgName || this.dataService.org()?.name || ''
6767
);
6868
public readonly spaceName = computed(() =>
69-
this.seed?.spaceName || this.dataService.space()?.entity?.name || ''
69+
this.seed?.spaceName || this.dataService.space()?.name || ''
7070
);
7171
public readonly endpointName = computed(() =>
7272
this.seed?.endpointName || this.cfEndpointService.endpoint()?.entity?.name || ''

src/frontend/packages/cloud-foundry/src/features/applications/application.service.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ describe('ApplicationService (facade shim)', () => {
326326
expect(count).toBe(0); // filter blocks undefined (initial value)
327327

328328
dataStub._org.set({
329-
metadata: { guid: 'org-1', created_at: '', updated_at: '', url: '' },
330-
entity: { name: 'my-org' },
329+
guid: 'org-1',
330+
name: 'my-org',
331331
});
332332
await tick();
333333
expect(count).toBe(1);
@@ -342,8 +342,8 @@ describe('ApplicationService (facade shim)', () => {
342342
expect(count).toBe(0); // filter blocks undefined (initial value)
343343

344344
dataStub._space.set({
345-
metadata: { guid: 'sp-1', created_at: '', updated_at: '', url: '' },
346-
entity: { name: 'my-space' },
345+
guid: 'sp-1',
346+
name: 'my-space',
347347
});
348348
await tick();
349349
expect(count).toBe(1);
@@ -360,13 +360,14 @@ describe('ApplicationService (facade shim)', () => {
360360
expect(v.length).toBe(0);
361361
});
362362

363-
it('orgDomains$ wraps IDomain entries into APIResource shape', async () => {
363+
it('orgDomains$ passes through StDomain entries unchanged', async () => {
364364
dataStub._domains.set([
365365
{ guid: 'd-1', name: 'example.com' } as any,
366366
]);
367367
const v = await firstValueFrom(svc.orgDomains$.pipe(take(1)));
368368
expect(v.length).toBe(1);
369-
expect(v[0].metadata?.guid).toBe('d-1');
369+
expect(v[0].guid).toBe('d-1');
370+
expect(v[0].name).toBe('example.com');
370371
});
371372

372373
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)