Skip to content

Commit f7b8e9c

Browse files
authored
Merge pull request #5304 from cloudfoundry/feature/angular-21-changes
Angular 21 upgrade and post-merge improvements
2 parents e13a2cb + d3d199d commit f7b8e9c

56 files changed

Lines changed: 1486 additions & 941 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stratos",
3-
"version": "v5.0.0-dev.8+build.20260415.52d8eaf981",
3+
"version": "v5.0.0-dev.10",
44
"type": "module",
55
"description": "Stratos Console",
66
"main": "index.js",

postcss.config.cjs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
module.exports = {
2-
plugins: {
3-
tailwindcss: {},
4-
autoprefixer: {},
1+
const postcss = require('postcss');
2+
const nested = require('postcss-nested');
3+
4+
// postcss-nested uses PostCSS v8 visitor API (phase 2), but Tailwind's
5+
// detectNesting runs in phase 1 (runOnRoot). Wrapping in an Once handler
6+
// forces nesting to be expanded in phase 1, before Tailwind sees it.
7+
const nestedOnce = {
8+
postcssPlugin: 'postcss-nested-once',
9+
Once(root) {
10+
postcss([nested]).process(root).sync();
511
},
6-
}
12+
};
13+
14+
module.exports = {
15+
plugins: [
16+
nestedOnce,
17+
require('tailwindcss'),
18+
require('autoprefixer'),
19+
],
20+
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
LoadingPageComponent,
5757
],
5858
providers: [
59+
DatePipe,
5960
CfAppRoutesListConfigService,
6061
AppServiceBindingListConfigService
6162
]

src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/instances-tab/instances-tab.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { provideHttpClient } from '@angular/common/http';
33
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
44
import { provideZonelessChangeDetection, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
5+
import { provideRouter } from '@angular/router';
56
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
67
import { of } from 'rxjs';
78
import { Store } from '@ngrx/store';
@@ -54,6 +55,7 @@ describe('InstancesTabComponent', () => {
5455
provideHttpClient(),
5556
provideHttpClientTesting(),
5657
provideZonelessChangeDetection(),
58+
provideRouter([]),
5759
{ provide: Store, useValue: mockStore },
5860
{ provide: PaginationMonitorFactory, useValue: mockPmf },
5961
{ provide: ApplicationService, useClass: ApplicationServiceMock },

src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2-1/commit-list-wrapper/commit-list-wrapper.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DatePipe } from '@angular/common';
12
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
23
import { GitCommit } from '@stratosui/git';
34
import { Observable } from 'rxjs';
@@ -18,6 +19,7 @@ import {
1819
ListComponent
1920
],
2021
providers: [
22+
DatePipe,
2123
{
2224
provide: ListConfig,
2325
useFactory: () => new GithubCommitsListConfigServiceDeploy(),
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { provideZonelessChangeDetection } from '@angular/core';
3-
import { provideMockStore } from '@ngrx/store/testing';
4-
import { describe, it, expect, beforeEach } from 'vitest';
3+
import { provideHttpClient } from '@angular/common/http';
4+
import { provideHttpClientTesting } from '@angular/common/http/testing';
5+
import { ActivatedRoute } from '@angular/router';
6+
import { describe, it, expect, beforeEach, vi } from 'vitest';
7+
import { Subject, of } from 'rxjs';
8+
import { StoreModule } from '@ngrx/store';
9+
import { deployAppReducer } from '../../../../store/reducers/deploy-app.reducer';
510

6-
import { getGitHubAPIURL, GITHUB_API_URL, GitSCMService } from '@stratosui/git';
7-
import { STORE_TEST_PROVIDERS } from '@stratosui/store/testing';
11+
import { getGitHubAPIURL, GITHUB_API_URL, GitSCMService, GitHubSCM } from '@stratosui/git';
12+
import {
13+
EntityCatalogHelper,
14+
EntityCatalogHelpers,
15+
EntityCatalogTestModule,
16+
TEST_CATALOGUE_ENTITIES,
17+
generateStratosEntities,
18+
} from '@stratosui/store';
19+
import { createBasicStoreModule, STORE_TEST_PROVIDERS } from '@stratosui/store/testing';
20+
import { generateCFEntities } from '../../../../cf-entity-generator';
821
import { ApplicationDeploySourceTypes } from '../deploy-application-steps.types';
922
import { DeployApplicationStep2Component } from './deploy-application-step2.component';
1023

@@ -15,22 +28,148 @@ describe('DeployApplicationStep2Component', () => {
1528
beforeEach(() => {
1629
TestBed.configureTestingModule({
1730
imports: [
18-
DeployApplicationStep2Component
31+
DeployApplicationStep2Component,
32+
createBasicStoreModule(),
33+
StoreModule.forFeature('deployApplication', deployAppReducer),
34+
EntityCatalogTestModule,
1935
],
2036
providers: [
21-
provideMockStore(),
2237
...STORE_TEST_PROVIDERS,
38+
{
39+
provide: TEST_CATALOGUE_ENTITIES,
40+
useValue: [
41+
...generateStratosEntities(),
42+
...generateCFEntities(),
43+
],
44+
},
2345
GitSCMService,
2446
ApplicationDeploySourceTypes,
2547
{ provide: GITHUB_API_URL, useFactory: getGitHubAPIURL },
26-
provideZonelessChangeDetection()
27-
]
48+
{
49+
provide: ActivatedRoute,
50+
useValue: { snapshot: { queryParams: {}, data: {}, params: {} } },
51+
},
52+
provideZonelessChangeDetection(),
53+
provideHttpClient(),
54+
provideHttpClientTesting(),
55+
],
2856
});
57+
58+
const helper = TestBed.inject(EntityCatalogHelper);
59+
EntityCatalogHelpers.SetEntityCatalogHelper(helper);
2960
});
3061

31-
// TODO: Fix EntityCatalogHelper initialization to enable component creation test
32-
// The component requires EntityCatalogHelper to be initialized, which needs proper entity catalog setup
3362
it('should be defined', () => {
3463
expect(DeployApplicationStep2Component).toBeDefined();
3564
});
65+
66+
it('creates the component', () => {
67+
fixture = TestBed.createComponent(DeployApplicationStep2Component);
68+
component = fixture.componentInstance;
69+
expect(component).toBeTruthy();
70+
});
71+
72+
describe('applyGithubEnterpriseAndToken (GHE + PAT port)', () => {
73+
let scmSpy: {
74+
setPublicApi: ReturnType<typeof vi.fn>;
75+
setAccessToken: ReturnType<typeof vi.fn>;
76+
clearAccessToken: ReturnType<typeof vi.fn>;
77+
getType: ReturnType<typeof vi.fn>;
78+
};
79+
80+
beforeEach(() => {
81+
fixture = TestBed.createComponent(DeployApplicationStep2Component);
82+
component = fixture.componentInstance;
83+
84+
scmSpy = {
85+
setPublicApi: vi.fn(),
86+
setAccessToken: vi.fn(),
87+
clearAccessToken: vi.fn(),
88+
getType: vi.fn().mockReturnValue('github'),
89+
};
90+
// Inject a mock SCM so applyGithubEnterpriseAndToken's side effects are observable
91+
// without driving the full deploy-step form.
92+
component.scm = scmSpy as unknown as GitHubSCM;
93+
});
94+
95+
const invoke = (url: string | undefined, token: string | undefined) =>
96+
(component as unknown as {
97+
applyGithubEnterpriseAndToken(u?: string, t?: string): void;
98+
}).applyGithubEnterpriseAndToken(url, token);
99+
100+
it('flags an invalid enterprise URL and does not set the public API', () => {
101+
invoke('not-a-url', undefined);
102+
103+
expect(component.isInvalidGithubEnterpriseUrl).toBe(true);
104+
expect(scmSpy.setPublicApi).not.toHaveBeenCalled();
105+
});
106+
107+
it('sets the public API endpoint for a valid enterprise URL', () => {
108+
invoke('https://github.example.com/api/v3', undefined);
109+
110+
expect(component.isInvalidGithubEnterpriseUrl).toBe(false);
111+
expect(scmSpy.setPublicApi).toHaveBeenCalledWith('https://github.example.com/api/v3');
112+
});
113+
114+
it('sets the access token when one is provided', () => {
115+
invoke('https://github.example.com/api/v3', 'pat-abc123');
116+
117+
expect(scmSpy.setAccessToken).toHaveBeenCalledWith('pat-abc123');
118+
expect(scmSpy.clearAccessToken).not.toHaveBeenCalled();
119+
});
120+
121+
it('clears the access token when none is provided', () => {
122+
invoke('https://github.example.com/api/v3', '');
123+
124+
expect(scmSpy.clearAccessToken).toHaveBeenCalled();
125+
expect(scmSpy.setAccessToken).not.toHaveBeenCalled();
126+
});
127+
128+
it('ignores an empty enterprise URL and leaves the public API untouched', () => {
129+
invoke('', 'pat-abc123');
130+
131+
expect(component.isInvalidGithubEnterpriseUrl).toBe(false);
132+
expect(scmSpy.setPublicApi).not.toHaveBeenCalled();
133+
});
134+
135+
it('skips token handling when the active SCM is not GitHub (e.g. GitLab)', () => {
136+
scmSpy.getType.mockReturnValue('gitlab');
137+
invoke('https://gitlab.example.com/api/v4', 'pat-should-be-ignored');
138+
139+
expect(scmSpy.setAccessToken).not.toHaveBeenCalled();
140+
expect(scmSpy.clearAccessToken).not.toHaveBeenCalled();
141+
});
142+
143+
it('wires applyGithubEnterpriseAndToken to the sourceSelectionForm valueChanges stream', () => {
144+
// Mock the NgForm ViewChild with a Subject so we can emit form values
145+
// without standing up the full template-driven reactive form. Also mock
146+
// setupForGit's other upstream collaborators just enough to reach the
147+
// suggestedRepos$ assignment where the tap callback is registered.
148+
const valueChanges = new Subject<Record<string, string | undefined>>();
149+
(component as unknown as { sourceSelectionForm: { valueChanges: Subject<unknown> } }).sourceSelectionForm = {
150+
valueChanges,
151+
};
152+
// updateSuggestedRepositories is called inside switchMap; stub it to
153+
// return an empty result so the pipe completes without network work.
154+
vi.spyOn(
155+
component as unknown as { updateSuggestedRepositories: (n: string) => unknown },
156+
'updateSuggestedRepositories',
157+
).mockReturnValue(of([]));
158+
159+
// Invoke setupForGit so the valueChanges subscription gets wired up.
160+
(component as unknown as { setupForGit(): void }).setupForGit();
161+
// Subscribe to keep the pipe hot; the tap callback only fires on
162+
// subscribed observables.
163+
component.suggestedRepos$.subscribe();
164+
165+
valueChanges.next({
166+
githubEnterpriseUrl: 'https://github.example.com/api/v3',
167+
githubAccessToken: 'pat-from-form',
168+
projectName: 'org/repo',
169+
});
170+
171+
expect(scmSpy.setPublicApi).toHaveBeenCalledWith('https://github.example.com/api/v3');
172+
expect(scmSpy.setAccessToken).toHaveBeenCalledWith('pat-from-form');
173+
});
174+
});
36175
});

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,23 @@ export class DeployApplicationStep3Component implements OnDestroy {
207207
}
208208

209209
onEnter = (fsDeployer: DeployApplicationDeployer) => {
210+
// Re-entry after Previous: tear down the prior deployer's subscriptions
211+
// and websocket so its stale status$ cannot replay the earlier error
212+
// snackbar on top of the fresh attempt.
213+
this.destroyDeployer();
214+
if (this.deployer && this.deployer !== fsDeployer) {
215+
this.deployer.close();
216+
}
217+
218+
// Reset UI state so buttons reflect "deploy not yet started" rather
219+
// than carrying over the prior attempt's terminal state (e.g. "Go to
220+
// App Summary" staying active from a previous successful push).
221+
this.validSubject.next(false);
222+
this.closeableSubject.next(false);
223+
this.showOverlaySubject.next(false);
224+
this.disablePreviousSubject.next(true);
225+
this.error.set(false);
226+
210227
// If we were passed data, then we came from the File System step
211228
if (fsDeployer) {
212229
this.deployer = fsDeployer;

src/frontend/packages/cloud-foundry/src/features/services/detach-service-instance/detach-service-instance.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ import { DetachAppsComponent } from './detach-apps/detach-apps.component';
3838
StepComponent,
3939
DetachAppsComponent,
4040
AppActionMonitorComponent
41-
]
41+
],
42+
providers: [DatePipe]
4243
})
4344
export class DetachServiceInstanceComponent {
4445
private store = inject<Store<CFAppState>>(Store);

0 commit comments

Comments
 (0)