diff --git a/bridge/client/app/_components/ktb-approval-item/ktb-approval-item.component.ts b/bridge/client/app/_components/ktb-approval-item/ktb-approval-item.component.ts index a8cba06a98..130865846d 100644 --- a/bridge/client/app/_components/ktb-approval-item/ktb-approval-item.component.ts +++ b/bridge/client/app/_components/ktb-approval-item/ktb-approval-item.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { Trace } from '../../_models/trace'; import { DataService } from '../../_services/data.service'; import { DtOverlayConfig } from '@dynatrace/barista-components/overlay'; @@ -9,6 +9,7 @@ import { KeptnService } from '../../../../shared/models/keptn-service'; selector: 'ktb-approval-item[event]', templateUrl: './ktb-approval-item.component.html', styleUrls: ['./ktb-approval-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class KtbApprovalItemComponent { public _event?: Trace; @@ -39,7 +40,7 @@ export class KtbApprovalItemComponent { this.loadEvaluation(value); } - constructor(private dataService: DataService) {} + constructor(private dataService: DataService, private changeDetectorRef_: ChangeDetectorRef) {} private loadEvaluation(trace: Trace): void { this.dataService @@ -53,6 +54,7 @@ export class KtbApprovalItemComponent { .subscribe((evaluation) => { this.evaluation = evaluation[0]; this.evaluationExists = !!this.evaluation; + this.changeDetectorRef_.markForCheck(); }); } @@ -61,5 +63,6 @@ export class KtbApprovalItemComponent { this.approvalSent.emit(); }); this.approvalResult = result; + this.changeDetectorRef_.markForCheck(); } } diff --git a/bridge/client/app/_components/ktb-evaluation-info/ktb-evaluation-info.component.ts b/bridge/client/app/_components/ktb-evaluation-info/ktb-evaluation-info.component.ts index b6db0b2a43..75fa11f6b4 100644 --- a/bridge/client/app/_components/ktb-evaluation-info/ktb-evaluation-info.component.ts +++ b/bridge/client/app/_components/ktb-evaluation-info/ktb-evaluation-info.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, NgZone, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, NgZone, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; import { DtOverlay, DtOverlayConfig, DtOverlayRef } from '@dynatrace/barista-components/overlay'; import { Trace } from '../../_models/trace'; import { ResultTypes } from '../../../../shared/models/result-types'; @@ -83,7 +83,12 @@ export class KtbEvaluationInfoComponent implements OnDestroy { ); } - constructor(private dataService: DataService, private ngZone: NgZone, private _dtOverlay: DtOverlay) {} + constructor( + private dataService: DataService, + private ngZone: NgZone, + private _dtOverlay: DtOverlay, + private changeDetectorRef_: ChangeDetectorRef + ) {} private fetchEvaluationHistory(): void { const evaluation = this.evaluation; @@ -112,6 +117,7 @@ export class KtbEvaluationInfoComponent implements OnDestroy { } else { this._evaluationHistory = traces; } + this.changeDetectorRef_.markForCheck(); }); } } diff --git a/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.html b/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.html index dd7fc9eeec..8c90ffbbe0 100644 --- a/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.html +++ b/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.html @@ -1,4 +1,4 @@ - + subscribes to: diff --git a/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.spec.ts b/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.spec.ts index 00325fcb20..492b51b0ab 100644 --- a/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.spec.ts +++ b/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.spec.ts @@ -48,9 +48,8 @@ describe('KtbSubscriptionItemComponent', () => { subscription.id = 'mySubscriptionId'; }); - it('should create and have given project set', () => { + it('should create', () => { expect(component).toBeTruthy(); - expect(component.project?.projectName).toEqual('sockshop'); }); it('should navigate to subscription to edit', () => { @@ -58,6 +57,7 @@ describe('KtbSubscriptionItemComponent', () => { const router = TestBed.inject(Router); const routerSpy = jest.spyOn(router, 'navigate'); component.integrationId = 'myIntegrationId'; + component.projectName = 'sockshop'; // when component.editSubscription(subscription); diff --git a/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.ts b/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.ts index f047dff286..1a6e6b770c 100644 --- a/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.ts +++ b/bridge/client/app/_components/ktb-subscription-item/ktb-subscription-item.component.ts @@ -1,31 +1,17 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { UniformSubscription } from '../../_models/uniform-subscription'; -import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { DataService } from '../../_services/data.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { Project } from '../../_models/project'; -import { Subject } from 'rxjs'; import { DeleteDialogState } from '../_dialogs/ktb-delete-confirmation/ktb-delete-confirmation.component'; @Component({ - selector: 'ktb-subscription-item[subscription][integrationId][isWebhookService]', + selector: 'ktb-subscription-item[subscription][integrationId][isWebhookService][projectName]', templateUrl: './ktb-subscription-item.component.html', styleUrls: ['./ktb-subscription-item.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class KtbSubscriptionItemComponent implements OnInit, OnDestroy { +export class KtbSubscriptionItemComponent { private _subscription?: UniformSubscription; - public project?: Project; - private readonly unsubscribe$ = new Subject(); private currentSubscription?: UniformSubscription; public deleteState: DeleteDialogState = null; @@ -33,6 +19,7 @@ export class KtbSubscriptionItemComponent implements OnInit, OnDestroy { @Input() name?: string; @Input() integrationId?: string; @Input() isWebhookService = false; + @Input() projectName = ''; @Input() get subscription(): UniformSubscription | undefined { @@ -53,25 +40,11 @@ export class KtbSubscriptionItemComponent implements OnInit, OnDestroy { private router: Router ) {} - ngOnInit(): void { - this.route.paramMap - .pipe( - map((params) => params.get('projectName')), - filter((projectName: string | null): projectName is string => !!projectName), - switchMap((projectName) => this.dataService.getProject(projectName)), - filter((project: Project | undefined): project is Project => !!project), - takeUntil(this.unsubscribe$) - ) - .subscribe((project) => { - this.project = project; - }); - } - public editSubscription(subscription: UniformSubscription): void { this.router.navigate([ '/', 'project', - this.project?.projectName, + this.projectName, 'settings', 'uniform', 'integrations', @@ -97,9 +70,4 @@ export class KtbSubscriptionItemComponent implements OnInit, OnDestroy { }); } } - - ngOnDestroy(): void { - this.unsubscribe$.next(); - this.unsubscribe$.complete(); - } } diff --git a/bridge/client/app/_components/ktb-uniform-subscriptions/ktb-uniform-subscriptions.component.html b/bridge/client/app/_components/ktb-uniform-subscriptions/ktb-uniform-subscriptions.component.html index 973027c89c..c1bc53ef24 100644 --- a/bridge/client/app/_components/ktb-uniform-subscriptions/ktb-uniform-subscriptions.component.html +++ b/bridge/client/app/_components/ktb-uniform-subscriptions/ktb-uniform-subscriptions.component.html @@ -36,6 +36,7 @@ [subscription]="subscription" [integrationId]="uniformRegistration.id" [isWebhookService]="uniformRegistration.isWebhookService" + [projectName]="projectName" name="Subscription {{ index + 1 }}" (subscriptionDeleted)="deleteSubscription($event)" > diff --git a/bridge/client/app/_models/project.mock.ts b/bridge/client/app/_models/project.mock.ts deleted file mode 100644 index a1f55f98b5..0000000000 --- a/bridge/client/app/_models/project.mock.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Project } from './project'; -import { Stage } from './stage'; -import { Service } from './service'; - -const projectMock = { - projectName: 'sockshop', - stages: [ - { - stageName: 'development', - } as Stage, - { - stageName: 'staging', - } as Stage, - { - stageName: 'production', - } as Stage, - ], - services: [ - { - serviceName: 'cards', - } as Service, - { - serviceName: 'cards-db', - } as Service, - ], -} as Project; - -export { projectMock as ProjectMock }; diff --git a/bridge/client/app/_services/data.service.spec.ts b/bridge/client/app/_services/data.service.spec.ts index 1dc91fa19d..37a5e48953 100644 --- a/bridge/client/app/_services/data.service.spec.ts +++ b/bridge/client/app/_services/data.service.spec.ts @@ -149,6 +149,27 @@ describe('DataService', () => { expect(loadLogSpy).not.toHaveBeenCalled(); }); + it('should correctly set isTriggerSequenceOpen', async () => { + // given, when + let value = await firstValueFrom(dataService.isTriggerSequenceOpen); + // then + expect(value).toBe(false); + + // when + dataService.setIsTriggerSequenceOpen(true); + + // then + value = await firstValueFrom(dataService.isTriggerSequenceOpen); + expect(value).toBe(true); + + // when + dataService.setIsTriggerSequenceOpen(false); + value = await firstValueFrom(dataService.isTriggerSequenceOpen); + + // then + expect(value).toBe(false); + }); + function setGetTracesResponse(traces: Trace[]): void { const response = new HttpResponse({ body: { events: traces, totalCount: 0, pageSize: 1, nextPageKey: 1 } }); jest.spyOn(apiService, 'getTraces').mockReturnValue(of(response)); diff --git a/bridge/client/app/_services/data.service.ts b/bridge/client/app/_services/data.service.ts index 3fca4ef42a..2349c67d24 100644 --- a/bridge/client/app/_services/data.service.ts +++ b/bridge/client/app/_services/data.service.ts @@ -57,8 +57,7 @@ export class DataService { private readonly DEFAULT_NEXT_SEQUENCE_PAGE_SIZE = 10; private _isQualityGatesOnly = new BehaviorSubject(false); private _evaluationResults = new Subject(); - - public isTriggerSequenceOpen = false; + private _isTriggerSequenceOpen = new BehaviorSubject(false); constructor(private apiService: ApiService) {} @@ -86,6 +85,14 @@ export class DataService { return this._isQualityGatesOnly.asObservable(); } + get isTriggerSequenceOpen(): Observable { + return this._isTriggerSequenceOpen.asObservable(); + } + + public setIsTriggerSequenceOpen(status: boolean): void { + this._isTriggerSequenceOpen.next(status); + } + get projectName(): Observable { return this._projectName.asObservable(); } diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.html b/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.html index ccfdcc2acd..c256e8668c 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.html +++ b/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.html @@ -2,10 +2,20 @@ - + diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.spec.ts b/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.spec.ts index f0547ba9e9..06672a61c5 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.spec.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.spec.ts @@ -3,22 +3,106 @@ import { KtbEnvironmentViewComponent } from './ktb-environment-view.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { KtbEnvironmentViewModule } from './ktb-environment-view.module'; import { RouterTestingModule } from '@angular/router/testing'; +import { POLLING_INTERVAL_MILLIS } from '../../_utils/app.utils'; +import { Location } from '@angular/common'; +import { Stage } from '../../_models/stage'; +import { ActivatedRoute, convertToParamMap, ParamMap } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { DataService } from '../../_services/data.service'; +import { ApiService } from '../../_services/api.service'; +import { ApiServiceMock } from '../../_services/api.service.mock'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('KtbEnvironmentViewComponent', () => { let component: KtbEnvironmentViewComponent; let fixture: ComponentFixture; + let queryParamMap: BehaviorSubject; + let paramMap: BehaviorSubject; + let dataService: DataService; beforeEach(async () => { + queryParamMap = new BehaviorSubject(convertToParamMap({})); + paramMap = new BehaviorSubject(convertToParamMap({})); await TestBed.configureTestingModule({ - imports: [KtbEnvironmentViewModule, RouterTestingModule, HttpClientTestingModule], + imports: [KtbEnvironmentViewModule, RouterTestingModule, BrowserAnimationsModule, HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParamMap: queryParamMap.asObservable(), + paramMap: paramMap.asObservable(), + }, + }, + { + provide: ApiService, + useClass: ApiServiceMock, + }, + { provide: POLLING_INTERVAL_MILLIS, useValue: 0 }, + ], }).compileComponents(); fixture = TestBed.createComponent(KtbEnvironmentViewComponent); component = fixture.componentInstance; - fixture.detectChanges(); + dataService = TestBed.inject(DataService); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should set stage and the right location', () => { + // given + const locationSpy = jest.spyOn(TestBed.inject(Location), 'go'); + + // when + component.setSelectedStageInfo('sockshop', { stage: { stageName: 'myStage' } as Stage, filterType: 'approval' }); + + // then + expect(locationSpy).toHaveBeenCalledWith('/project/sockshop/environment/stage/myStage?filterType=approval'); + expect(component.selectedStageInfo).toEqual({ stage: { stageName: 'myStage' } as Stage, filterType: 'approval' }); + }); + + it('should set stage info and the right location without filterType', () => { + // given + const locationSpy = jest.spyOn(TestBed.inject(Location), 'go'); + + // when + component.setSelectedStageInfo('sockshop', { stage: { stageName: 'myStage' } as Stage, filterType: undefined }); + + // then + expect(locationSpy).toHaveBeenCalledWith('/project/sockshop/environment/stage/myStage'); + expect(component.selectedStageInfo).toEqual({ stage: { stageName: 'myStage' } as Stage, filterType: undefined }); + }); + + it('should load the project', () => { + // given + const loadSpy = jest.spyOn(dataService, 'loadProject'); + + // when + paramMap.next(convertToParamMap({ projectName: 'sockshop' })); + fixture.detectChanges(); + + // then + expect(loadSpy).toHaveBeenCalledWith('sockshop'); + }); + + it('should set the selected stage through params', () => { + // given, when + paramMap.next(convertToParamMap({ projectName: 'sockshop', stageName: 'dev' })); + + // then + expect(component.selectedStageInfo?.stage.stageName).toBe('dev'); + expect(component.selectedStageInfo?.filterType).toBeUndefined(); + }); + + it('should set the selected stage with filterType through params', () => { + // given, when + queryParamMap.next(convertToParamMap({ filterType: 'approval' })); + paramMap.next(convertToParamMap({ projectName: 'sockshop', stageName: 'dev' })); + + // then + expect(component.selectedStageInfo?.stage.stageName).toBe('dev'); + expect(component.selectedStageInfo?.filterType).toBe('approval'); + }); }); diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.ts b/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.ts index 9399148f26..4ca17b435f 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-environment-view.component.ts @@ -1,9 +1,18 @@ -import { Component, HostBinding } from '@angular/core'; -import { filter, map, switchMap } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; +import { Component, HostBinding, Inject, OnDestroy } from '@angular/core'; +import { distinctUntilChanged, filter, map, switchMap, takeUntil } from 'rxjs/operators'; +import { combineLatest, Observable, Subject } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; import { Project } from '../../_models/project'; import { DataService } from '../../_services/data.service'; +import { AppUtils, POLLING_INTERVAL_MILLIS } from '../../_utils/app.utils'; +import { ServiceFilterType } from './ktb-stage-details/ktb-stage-details.component'; +import { Stage } from '../../_models/stage'; +import { Location } from '@angular/common'; + +export interface ISelectedStageInfo { + stage: Stage; + filterType: ServiceFilterType; +} @Component({ selector: 'ktb-environment-view', @@ -11,19 +20,77 @@ import { DataService } from '../../_services/data.service'; styleUrls: ['./ktb-environment-view.component.scss'], preserveWhitespaces: false, }) -export class KtbEnvironmentViewComponent { +export class KtbEnvironmentViewComponent implements OnDestroy { @HostBinding('class') cls = 'ktb-environment-view'; - public project$: Observable; + public project$: Observable; + private readonly unsubscribe$ = new Subject(); + public selectedStageInfo?: ISelectedStageInfo; + public isQualityGatesOnly$: Observable; + public isTriggerSequenceOpen$: Observable; + + constructor( + private dataService: DataService, + private route: ActivatedRoute, + private router: Router, + private location: Location, + @Inject(POLLING_INTERVAL_MILLIS) private initialDelayMillis: number + ) { + this.isQualityGatesOnly$ = this.dataService.isQualityGatesOnly; + this.isTriggerSequenceOpen$ = this.dataService.isTriggerSequenceOpen; + this.dataService.setIsTriggerSequenceOpen(false); + const selectedStageName$ = this.route.paramMap.pipe( + map((params) => params.get('stageName')), + takeUntil(this.unsubscribe$) + ); - constructor(private dataService: DataService, private route: ActivatedRoute) { + const paramFilterType$ = this.route.queryParamMap.pipe(map((params) => params.get('filterType'))); const projectName$ = this.route.paramMap.pipe( map((params) => params.get('projectName')), - filter((projectName): projectName is string => !!projectName) + filter((projectName): projectName is string => !!projectName), + distinctUntilChanged() ); this.project$ = projectName$.pipe( switchMap((projectName) => this.dataService.getProject(projectName)), - map((project) => (project?.projectDetailsLoaded ? project : undefined)) + filter((project): project is Project => !!project?.projectDetailsLoaded) ); + + combineLatest([selectedStageName$, paramFilterType$, this.project$]) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(([stageName, filterType, project]) => { + const stage = project.stages.find((s) => s.stageName === stageName); + if (stage) { + this.selectedStageInfo = { stage: stage, filterType: (filterType as ServiceFilterType) ?? undefined }; + } + }); + + const projectTimer$ = AppUtils.createTimer(0, initialDelayMillis).pipe( + switchMap(() => projectName$), + takeUntil(this.unsubscribe$) + ); + + projectTimer$.subscribe((projectName) => { + this.dataService.loadProject(projectName); + }); + } + + public setSelectedStageInfo(projectName: string, stageInfo: ISelectedStageInfo): void { + this.selectedStageInfo = stageInfo; + this.setLocation(projectName, stageInfo); + } + + private setLocation(projectName: string, stageInfo: ISelectedStageInfo): void { + const url = this.router.createUrlTree( + ['/project', projectName, 'environment', 'stage', stageInfo.stage.stageName], + { + queryParams: { filterType: stageInfo.filterType }, + } + ); + this.location.go(url.toString()); + } + + public ngOnDestroy(): void { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); } } diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.html b/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.html index 6a7e958fff..dce058cd05 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.html +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.html @@ -1,18 +1,34 @@
- + -

- +

+ - + - + *ngIf="service.getOpenApprovals().length > 0" name="deploy" > - + -

+

Rollback to performed.

diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.spec.ts b/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.spec.ts index 8ad6ddbd2b..b940f9a8d2 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.spec.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.spec.ts @@ -3,6 +3,9 @@ import { KtbStageDetailsComponent } from './ktb-stage-details.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { KtbEnvironmentViewModule } from '../ktb-environment-view.module'; import { RouterTestingModule } from '@angular/router/testing'; +import { Stage } from '../../../_models/stage'; +import { DtToggleButtonChange } from '@dynatrace/barista-components/toggle-button-group'; +import { Service } from '../../../_models/service'; describe('KtbStageDetailsComponent', () => { let component: KtbStageDetailsComponent; @@ -21,4 +24,124 @@ describe('KtbStageDetailsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set filterType and stageInfo', () => { + // given + const stageInfoChangeSpy = jest.spyOn(component.selectedStageInfoChange, 'emit'); + + // when + component.selectedStageInfo = { stage: { stageName: 'dev' } as Stage, filterType: 'approval' }; + + // then + expect(component.filterEventType).toBe('approval'); + expect(component.selectedStageInfo).toEqual({ stage: { stageName: 'dev' } as Stage, filterType: 'approval' }); + expect(stageInfoChangeSpy).not.toHaveBeenCalled(); + }); + + it('should set and emit filter event type', () => { + // given + const stageInfoChangeSpy = jest.spyOn(component.selectedStageInfoChange, 'emit'); + + // when + component.selectFilterEvent( + { stageName: 'dev' } as Stage, + { + isUserInput: true, + value: 'approval', + source: { + get selected(): boolean { + return true; + }, + }, + } as DtToggleButtonChange + ); + + // then + expect(stageInfoChangeSpy).toHaveBeenCalledWith({ stage: { stageName: 'dev' }, filterType: 'approval' }); + }); + + it('should set and emit empty filter event type', () => { + // given + const stageInfoChangeSpy = jest.spyOn(component.selectedStageInfoChange, 'emit'); + + // when + component.selectFilterEvent( + { stageName: 'dev' } as Stage, + { + isUserInput: true, + value: 'approval', + source: { + get selected(): boolean { + return false; + }, + }, + } as DtToggleButtonChange + ); + + // then + expect(stageInfoChangeSpy).toHaveBeenCalledWith({ stage: { stageName: 'dev' }, filterType: undefined }); + }); + + it('should return service link', () => { + // given, when + const link = component.getServiceLink( + { + serviceName: 'carts', + stage: 'dev', + get deploymentContext(): string | undefined { + return 'keptnContext'; + }, + } as Service, + 'sockshop' + ); + + expect(link).toEqual(['/project', 'sockshop', 'service', 'carts', 'context', 'keptnContext', 'stage', 'dev']); + }); + + it('should filter services for certain filterType and should not reset filter', () => { + // given + component.filteredServices = ['carts']; + component.filterEventType = 'approval'; + + // when + const services = component.filterServices( + { stageName: 'dev' } as Stage, + [ + { + serviceName: 'carts', + }, + { + serviceName: 'carts-db', + }, + ] as Service[], + 'approval' + ); + + // then + expect(services).toEqual([{ serviceName: 'carts' }]); + expect(component.filterEventType).toBe('approval'); + }); + + it('should reset filter if the type does not have any services', () => { + const stageInfoChangeSpy = jest.spyOn(component.selectedStageInfoChange, 'emit'); + component.filteredServices = ['carts']; // => carts does not have an approval + component.filterEventType = 'approval'; + + // when + const services = component.filterServices( + { stageName: 'dev' } as Stage, + [ + { + serviceName: 'carts-db', + }, + ] as Service[], + 'approval' + ); + + // then + expect(services).toEqual([]); + expect(component.filterEventType).toBeUndefined(); + expect(stageInfoChangeSpy).toHaveBeenCalledWith({ stage: { stageName: 'dev' }, filterType: undefined }); + expect(component.selectedStageInfo).toEqual({ stage: { stageName: 'dev' }, filterType: undefined }); + }); }); diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.ts b/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.ts index 11d9c81619..5aed8be54c 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-details/ktb-stage-details.component.ts @@ -1,13 +1,10 @@ -import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { DtToggleButtonChange, DtToggleButtonItem } from '@dynatrace/barista-components/toggle-button-group'; import { DtOverlayConfig } from '@dynatrace/barista-components/overlay'; import { Project } from '../../../_models/project'; import { Stage } from '../../../_models/stage'; import { Service } from '../../../_models/service'; -import { DataService } from '../../../_services/data.service'; -import { takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { Router } from '@angular/router'; +import { ISelectedStageInfo } from '../ktb-environment-view.component'; export type ServiceFilterType = 'evaluation' | 'problem' | 'approval' | undefined; @@ -15,48 +12,32 @@ export type ServiceFilterType = 'evaluation' | 'problem' | 'approval' | undefine selector: 'ktb-stage-details', templateUrl: './ktb-stage-details.component.html', styleUrls: ['./ktb-stage-details.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class KtbStageDetailsComponent implements OnInit, OnDestroy { - public _project?: Project; - public selectedStage?: Stage; +export class KtbStageDetailsComponent { public filterEventType: ServiceFilterType; public overlayConfig: DtOverlayConfig = { pinnable: true, }; - public isQualityGatesOnly = false; public filteredServices: string[] = []; - private readonly unsubscribe$ = new Subject(); + private _selectedStageInfo?: ISelectedStageInfo; - @ViewChild('problemFilterEventButton') public problemFilterEventButton?: DtToggleButtonItem; - @ViewChild('evaluationFilterEventButton') public evaluationFilterEventButton?: DtToggleButtonItem; - @ViewChild('approvalFilterEventButton') public approvalFilterEventButton?: DtToggleButtonItem; - - @Input() - get project(): Project | undefined { - return this._project; - } - - set project(project: Project | undefined) { - if (this._project !== project) { - this._project = project; - this.selectedStage = undefined; + @Input() project?: Project; + @Input() set selectedStageInfo(stageInfo: ISelectedStageInfo | undefined) { + this._selectedStageInfo = stageInfo; + if (stageInfo && this.filterEventType !== stageInfo.filterType) { + this.resetFilter(stageInfo.filterType); } } - - constructor(private dataService: DataService, private router: Router) {} - - ngOnInit(): void { - this.dataService.isQualityGatesOnly.pipe(takeUntil(this.unsubscribe$)).subscribe((isQualityGatesOnly) => { - this.isQualityGatesOnly = isQualityGatesOnly; - }); + get selectedStageInfo(): ISelectedStageInfo | undefined { + return this._selectedStageInfo; } + @Output() selectedStageInfoChange = new EventEmitter(); + @Input() isQualityGatesOnly = false; - selectStage($event: { stage: Stage; filterType: ServiceFilterType }): void { - this.selectedStage = $event.stage; - if (this.filterEventType !== $event.filterType) { - this.resetFilter($event.filterType); - } - } + @ViewChild('problemFilterEventButton') public problemFilterEventButton?: DtToggleButtonItem; + @ViewChild('evaluationFilterEventButton') public evaluationFilterEventButton?: DtToggleButtonItem; + @ViewChild('approvalFilterEventButton') public approvalFilterEventButton?: DtToggleButtonItem; private resetFilter(eventType: ServiceFilterType): void { this.problemFilterEventButton?.deselect(); @@ -66,21 +47,19 @@ export class KtbStageDetailsComponent implements OnInit, OnDestroy { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - selectFilterEvent($event: DtToggleButtonChange): void { + selectFilterEvent(stage: Stage, $event: DtToggleButtonChange): void { if ($event.isUserInput) { - this.filterEventType = $event.source.selected ? $event.value : null; + this.filterEventType = $event.source.selected ? $event.value : undefined; - // Add filterType query parameter - this.router.navigate([], { - queryParams: { filterType: this.filterEventType }, - }); + this._selectedStageInfo = { stage, filterType: this.filterEventType }; + this.selectedStageInfoChange.emit(this.selectedStageInfo); } } - getServiceLink(service: Service): string[] { + getServiceLink(service: Service, projectName: string): string[] { return [ '/project', - this.project?.projectName ?? '', + projectName, 'service', service.serviceName, 'context', @@ -90,7 +69,7 @@ export class KtbStageDetailsComponent implements OnInit, OnDestroy { ]; } - public filterServices(services: Service[], type: ServiceFilterType): Service[] { + public filterServices(stage: Stage, services: Service[], type: ServiceFilterType): Service[] { const filteredServices = this.filteredServices.length === 0 ? services @@ -98,16 +77,9 @@ export class KtbStageDetailsComponent implements OnInit, OnDestroy { if (this.filterEventType && filteredServices.length === 0 && this.filterEventType === type) { this.resetFilter(undefined); - // Remove filterType query parameter - this.router.navigate([], { - queryParams: { filterType: null }, - }); + this._selectedStageInfo = { stage, filterType: undefined }; + this.selectedStageInfoChange.emit(this.selectedStageInfo); } return filteredServices; } - - ngOnDestroy(): void { - this.unsubscribe$.next(); - this.unsubscribe$.complete(); - } } diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.html b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.html index edf9b2dd6d..9bd7c63c67 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.html +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.html @@ -1,12 +1,12 @@ -
- +
+

Stages

@@ -53,7 +53,7 @@

fxLayoutAlign="start center" fxLayoutGap="5px" *ngIf="filterServices(stage.getServicesWithRemediations()) as problemServices" - (click)="problemServices.length > 0 && selectStage($event, project, stage, 'problem')" + (click)="problemServices.length > 0 && selectStage($event, stage, 'problem')" [attr.uitestid]="'filter-type-' + stage.stageName + '-problem'" > fxLayoutAlign="start center" fxLayoutGap="5px" *ngIf="filterServices(stage.getServicesWithFailedEvaluation()) as failedServices" - (click)="failedServices.length > 0 && selectStage($event, project, stage, 'evaluation')" + (click)="failedServices.length > 0 && selectStage($event, stage, 'evaluation')" [attr.uitestid]="'filter-type-' + stage.stageName + '-evaluation'" > fxLayoutAlign="start center" fxLayoutGap="5px" *ngIf="filterServices(stage.getServicesWithOpenApprovals()) as approvalServices" - (click)="approvalServices.length > 0 && selectStage($event, project, stage, 'approval')" + (click)="approvalServices.length > 0 && selectStage($event, stage, 'approval')" [attr.uitestid]="'filter-type-' + stage.stageName + '-approval'" > diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.spec.ts b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.spec.ts index 55205364da..fd62cce1ab 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.spec.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.spec.ts @@ -3,6 +3,13 @@ import { KtbStageOverviewComponent } from './ktb-stage-overview.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { KtbEnvironmentViewModule } from '../ktb-environment-view.module'; +import { Project } from '../../../_models/project'; +import { DtAutoComplete, DtFilter } from '../../../_models/dt-filter'; +import { ApiService } from '../../../_services/api.service'; +import { ProjectsMock } from '../../../_services/_mockData/projects.mock'; +import { DtFilterFieldChangeEvent } from '@dynatrace/barista-components/filter-field'; +import { Stage } from '../../../_models/stage'; +import { Service } from '../../../_models/service'; describe('KtbStageOverviewComponent', () => { let component: KtbStageOverviewComponent; @@ -21,4 +28,147 @@ describe('KtbStageOverviewComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set service filter on project change', () => { + // given + const emitServiceSpy = jest.spyOn(component.filteredServicesChange, 'emit'); + jest + .spyOn(TestBed.inject(ApiService), 'environmentFilter', 'get') + .mockReturnValue({ sockshop: { services: ['carts'] } }); + + // when + component.project = Project.fromJSON(ProjectsMock[0]); + + // then + expect(component._dataSource.data).toEqual({ + autocomplete: [ + { + name: 'Services', + autocomplete: [ + { + name: 'carts-db', + }, + { + name: 'carts', + }, + ], + } as DtAutoComplete, + ], + }); + expect(emitServiceSpy).toHaveBeenCalledWith(['carts']); + expect(component.filter).toEqual([ + [ + { + autocomplete: [ + { + name: 'carts-db', + }, + { + name: 'carts', + }, + ], + name: 'Services', + }, + { + name: 'carts', + }, + ], + ]); + }); + + it('should not emit service filter if it is not a new project', () => { + // given + component.project = Project.fromJSON(ProjectsMock[0]); + + // when + const emitServiceSpy = jest.spyOn(component.filteredServicesChange, 'emit'); + component.project = Project.fromJSON(ProjectsMock[0]); + + // then + expect(emitServiceSpy).not.toHaveBeenCalled(); + }); + + it('should emit and save filter on filter change', () => { + // given + const emitServiceSpy = jest.spyOn(component.filteredServicesChange, 'emit'); + + // when + component.filterChanged('sockshop', { + filters: [ + [ + { + autocomplete: [ + { + name: 'carts-db', + }, + { + name: 'carts', + }, + ], + }, + { name: 'carts' }, + ], + [ + { + autocomplete: [ + { + name: 'carts-db', + }, + { + name: 'carts', + }, + ], + }, + { name: 'carts-db' }, + ], + ], + } as DtFilterFieldChangeEvent); + + // then + expect(TestBed.inject(ApiService).environmentFilter).toEqual({ + sockshop: { + services: ['carts', 'carts-db'], + }, + }); + expect(emitServiceSpy).toHaveBeenCalledWith(['carts', 'carts-db']); + }); + + it('should emit the selected stage information', () => { + // given + const mouseEvent = new MouseEvent('click'); + const stopPropagationSpy = jest.spyOn(mouseEvent, 'stopPropagation'); + const emitStageInfoSpy = jest.spyOn(component.selectedStageInfoChange, 'emit'); + + // then + component.selectStage(mouseEvent, { stageName: 'dev' } as Stage, 'approval'); + + // then + expect(stopPropagationSpy).toHaveBeenCalled(); + expect(emitStageInfoSpy).toHaveBeenCalledWith({ stage: { stageName: 'dev' }, filterType: 'approval' }); + }); + + it('should return filtered services', () => { + // given + jest + .spyOn(TestBed.inject(ApiService), 'environmentFilter', 'get') + .mockReturnValue({ sockshop: { services: ['carts'] } }); + component.project = Project.fromJSON(ProjectsMock[0]); + + // when + const services = component.filterServices([{ serviceName: 'carts' }, { serviceName: 'carts-db' }] as Service[]); + + // then + expect(services).toEqual([{ serviceName: 'carts' }]); + }); + + it('should return unfiltered services', () => { + // given + component.project = Project.fromJSON(ProjectsMock[0]); + + // when + const services = component.filterServices([{ serviceName: 'carts' }, { serviceName: 'carts-db' }] as Service[]); + + // then + expect(services).toEqual([{ serviceName: 'carts' }, { serviceName: 'carts-db' }]); + }); }); diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.ts b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.ts index 4c2363bc25..1f39826e2f 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-stage-overview.component.ts @@ -1,73 +1,44 @@ -import { AfterContentInit, Component, EventEmitter, OnDestroy, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { Project } from '../../../_models/project'; import { Stage } from '../../../_models/stage'; -import { DataService } from '../../../_services/data.service'; import { DtFilterFieldChangeEvent, DtFilterFieldDefaultDataSource } from '@dynatrace/barista-components/filter-field'; import { ApiService } from '../../../_services/api.service'; import { Service } from '../../../_models/service'; import { DtAutoComplete, DtFilter, DtFilterArray } from '../../../_models/dt-filter'; -import { distinctUntilChanged, filter, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; -import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest, Subject } from 'rxjs'; import { DtFilterFieldDefaultDataSourceAutocomplete } from '@dynatrace/barista-components/filter-field/src/filter-field-default-data-source'; import { ServiceFilterType } from '../ktb-stage-details/ktb-stage-details.component'; +import { ISelectedStageInfo } from '../ktb-environment-view.component'; @Component({ selector: 'ktb-stage-overview', templateUrl: './ktb-stage-overview.component.html', styleUrls: ['./ktb-stage-overview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class KtbStageOverviewComponent implements AfterContentInit, OnDestroy { +export class KtbStageOverviewComponent { public _dataSource = new DtFilterFieldDefaultDataSource(); public filter: DtFilterArray[] = []; - public isTriggerSequenceOpen: boolean; private filteredServices: string[] = []; private globalFilter: { [projectName: string]: { services: string[] } } = {}; - private unsubscribe$: Subject = new Subject(); + private _project?: Project; - public project$ = this.route.params.pipe( - map((params) => params.projectName), - filter((projectName): projectName is string => !!projectName), - distinctUntilChanged(), - switchMap((projectName) => this.dataService.getProject(projectName)), - filter((project): project is Project => !!project) - ); + @Input() selectedStageInfo?: ISelectedStageInfo; + @Input() isTriggerSequenceOpen = false; - public readonly selectedStageName$ = this.route.paramMap.pipe( - map((params) => params.get('stageName')), - withLatestFrom(this.project$), - filter(([stageName, project]) => Boolean(stageName && project)), - map(([stageName]) => stageName) - ); - - private readonly paramFilterType$ = this.route.queryParamMap.pipe(map((params) => params.get('filterType'))); + @Input() set project(project: Project | undefined) { + if (project) { + this.setFilter(project, this._project?.projectName !== project.projectName); + } + this._project = project; + } + get project(): Project | undefined { + return this._project; + } - @Output() selectedStageChange: EventEmitter<{ stage: Stage; filterType: ServiceFilterType }> = new EventEmitter(); + @Output() selectedStageInfoChange: EventEmitter = new EventEmitter(); @Output() filteredServicesChange: EventEmitter = new EventEmitter(); - constructor( - private dataService: DataService, - private apiService: ApiService, - private route: ActivatedRoute, - private router: Router - ) { - this.isTriggerSequenceOpen = this.dataService.isTriggerSequenceOpen; - this.dataService.isTriggerSequenceOpen = false; - } - - ngAfterContentInit(): void { - combineLatest([this.selectedStageName$, this.paramFilterType$, this.project$]) - .pipe(takeUntil(this.unsubscribe$)) - .subscribe(([stageName, filterType, project]) => { - const stage = project.stages.find((s) => s.stageName === stageName); - if (stage) { - this.selectedStageChange.emit({ stage: stage, filterType: (filterType as ServiceFilterType) ?? undefined }); - } - }); - this.project$.pipe(takeUntil(this.unsubscribe$)).subscribe((project) => { - this.setFilter(project, true); - }); - } + constructor(private apiService: ApiService) {} private setFilter(project: Project | undefined, projectChanged: boolean): void { this._dataSource.data = { @@ -106,10 +77,10 @@ export class KtbStageOverviewComponent implements AfterContentInit, OnDestroy { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public filterChanged(project: Project, event: DtFilterFieldChangeEvent): void { + public filterChanged(projectName: string, event: DtFilterFieldChangeEvent): void { // can't set another type because of "is not assignable to..." this.filteredServices = this.getServicesOfFilter(event); - this.globalFilter[project.projectName] = { services: this.filteredServices }; + this.globalFilter[projectName] = { services: this.filteredServices }; this.apiService.environmentFilter = this.globalFilter; this.filteredServicesChange.emit(this.filteredServices); @@ -133,15 +104,8 @@ export class KtbStageOverviewComponent implements AfterContentInit, OnDestroy { return stage?.toString(); } - public selectStage($event: MouseEvent, project: Project, stage: Stage, filterType?: ServiceFilterType): void { - this.router.navigate(['/project', project.projectName, 'environment', 'stage', stage.stageName], { - queryParams: { filterType: filterType }, - }); + public selectStage($event: MouseEvent, stage: Stage, filterType?: ServiceFilterType): void { $event.stopPropagation(); - } - - public ngOnDestroy(): void { - this.unsubscribe$.next(); - this.unsubscribe$.complete(); + this.selectedStageInfoChange.emit({ stage, filterType }); } } diff --git a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-trigger-sequence/ktb-trigger-sequence.component.ts b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-trigger-sequence/ktb-trigger-sequence.component.ts index 8e536c4710..c10d176696 100644 --- a/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-trigger-sequence/ktb-trigger-sequence.component.ts +++ b/bridge/client/app/_views/ktb-environment-view/ktb-stage-overview/ktb-trigger-sequence/ktb-trigger-sequence.component.ts @@ -79,7 +79,7 @@ export class KtbTriggerSequenceComponent implements OnInit, OnDestroy, AfterView public customFormData: CustomSequenceFormData = {}; @Input() public projectName!: string; - @Input() public stage: string | null = null; + @Input() public stage?: string; @Input() public stages: string[] = []; @Input() diff --git a/bridge/client/app/_views/ktb-project-view/ktb-project-view-routing.module.ts b/bridge/client/app/_views/ktb-project-view/ktb-project-view-routing.module.ts index 7cff83a5eb..adc45ba29e 100644 --- a/bridge/client/app/_views/ktb-project-view/ktb-project-view-routing.module.ts +++ b/bridge/client/app/_views/ktb-project-view/ktb-project-view-routing.module.ts @@ -11,7 +11,7 @@ const routes: Routes = [ component: KtbProjectViewComponent, children: [ { path: '', pathMatch: 'full', loadChildren: lazyLoadEnvironmentView }, - { path: 'environment', pathMatch: 'full', loadChildren: lazyLoadEnvironmentView }, + { path: 'environment', loadChildren: lazyLoadEnvironmentView }, { path: 'environment/stage/:stageName', loadChildren: lazyLoadEnvironmentView }, { path: 'settings', diff --git a/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.spec.ts b/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.spec.ts index bb7f84ccae..991e1ce99b 100644 --- a/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.spec.ts +++ b/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.spec.ts @@ -9,6 +9,7 @@ import { ApiServiceMock } from '../../_services/api.service.mock'; import { KtbProjectViewCommonModule } from './ktb-project-view-common.module'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DataService } from '../../_services/data.service'; describe('ProjectBoardComponent', () => { let component: KtbProjectViewComponent; @@ -38,6 +39,7 @@ describe('ProjectBoardComponent', () => { fixture = TestBed.createComponent(KtbProjectViewComponent); component = fixture.componentInstance; fixture.detectChanges(); + TestBed.inject(DataService).loadProjects(); }); it('should have "project" error when project can not be found', (done) => { diff --git a/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.ts b/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.ts index b992672431..39b307f5b1 100644 --- a/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.ts +++ b/bridge/client/app/_views/ktb-project-view/ktb-project-view.component.ts @@ -1,5 +1,5 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { filter, map, switchMap, takeUntil, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, switchMap, takeUntil, tap } from 'rxjs/operators'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; import { DataService } from '../../_services/data.service'; @@ -13,7 +13,6 @@ import { AppUtils, POLLING_INTERVAL_MILLIS } from '../../_utils/app.utils'; }) export class KtbProjectViewComponent implements OnInit, OnDestroy { private readonly unsubscribe$ = new Subject(); - public logoInvertedUrl = environment?.config?.logoInvertedUrl; public hasProject$: Observable; private _errorSubject: BehaviorSubject = new BehaviorSubject(undefined); @@ -36,28 +35,21 @@ export class KtbProjectViewComponent implements OnInit, OnDestroy { filter((projectName: string | null): projectName is string => !!projectName) ); - const projectTimer$ = projectName$.pipe( - switchMap((projectName) => AppUtils.createTimer(0, initialDelayMillis).pipe(map(() => projectName))), - takeUntil(this.unsubscribe$) - ); - const uniformLogTimer$ = projectName$.pipe( switchMap(() => AppUtils.createTimer(0, uniformLogInterval)), takeUntil(this.unsubscribe$) ); - projectTimer$.subscribe((projectName) => { - // this is on project-board level because we need the project in environment, service, sequence and settings screen - // sequence screen because there is a check for the latest deployment context (lastEventTypes) - this.dataService.loadProject(projectName); - }); - uniformLogTimer$.subscribe(() => { this.dataService.loadUnreadUniformRegistrationLogs(); }); + this.hasProject$ = projectName$.pipe(switchMap((projectName) => this.dataService.projectExists(projectName))); - this.isCreateMode$ = this.route.data.pipe(map((data) => !!data.createMode)); + this.isCreateMode$ = this.route.data.pipe( + map((data) => !!data.createMode), + distinctUntilChanged() + ); } ngOnInit(): void { diff --git a/bridge/client/app/_views/ktb-sequence-view/ktb-sequence-view.component.ts b/bridge/client/app/_views/ktb-sequence-view/ktb-sequence-view.component.ts index 4ae8f6729b..230e811252 100644 --- a/bridge/client/app/_views/ktb-sequence-view/ktb-sequence-view.component.ts +++ b/bridge/client/app/_views/ktb-sequence-view/ktb-sequence-view.component.ts @@ -402,7 +402,7 @@ export class KtbSequenceViewComponent implements OnDestroy { } public navigateToTriggerSequence(projectName: string): void { - this.dataService.isTriggerSequenceOpen = true; + this.dataService.setIsTriggerSequenceOpen(true); this.router.navigate(['/project/' + projectName]); } diff --git a/bridge/client/app/_views/ktb-service-view/ktb-deployment-list/ktb-deployment-list.component.ts b/bridge/client/app/_views/ktb-service-view/ktb-deployment-list/ktb-deployment-list.component.ts index 3e3f8ebe13..f376d5ff89 100644 --- a/bridge/client/app/_views/ktb-service-view/ktb-deployment-list/ktb-deployment-list.component.ts +++ b/bridge/client/app/_views/ktb-service-view/ktb-deployment-list/ktb-deployment-list.component.ts @@ -1,8 +1,5 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { switchMap, takeUntil } from 'rxjs/operators'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest, Subject } from 'rxjs'; -import { DataService } from '../../../_services/data.service'; import { DtTableDataSource } from '@dynatrace/barista-components/table'; import { Location } from '@angular/common'; import { ServiceDeploymentInformation as sdi, ServiceState } from '../../../../../shared/models/service-state'; @@ -19,20 +16,19 @@ class DeploymentInformation implements sdi { } @Component({ - selector: 'ktb-deployment-list[service]', + selector: 'ktb-deployment-list[service][projectName]', templateUrl: './ktb-deployment-list.component.html', styleUrls: ['./ktb-deployment-list.component.scss'], }) -export class KtbDeploymentListComponent implements OnInit, OnDestroy { +export class KtbDeploymentListComponent { private _service?: ServiceState; - private projectName?: string; - private readonly unsubscribe$ = new Subject(); public _selectedDeploymentInfo?: DeploymentInformationSelection; public dataSource = new DtTableDataSource(); public loading = false; public DeploymentClass = DeploymentInformation; @Output() selectedDeploymentInfoChange: EventEmitter = new EventEmitter(); + @Input() projectName = ''; @Input() get service(): ServiceState | undefined { @@ -42,6 +38,7 @@ export class KtbDeploymentListComponent implements OnInit, OnDestroy { set service(service: ServiceState | undefined) { if (this._service !== service) { this._service = service; + this.updateDataSource(); } } @Input() @@ -54,30 +51,10 @@ export class KtbDeploymentListComponent implements OnInit, OnDestroy { } } - constructor( - private route: ActivatedRoute, - private dataService: DataService, - private router: Router, - private location: Location - ) {} - - public ngOnInit(): void { - const params$ = this.route.params.pipe(takeUntil(this.unsubscribe$)); - - const project$ = params$.pipe( - switchMap((params) => this.dataService.getProject(params.projectName)), - takeUntil(this.unsubscribe$) - ); - - combineLatest([project$, params$]).subscribe(([project]) => { - this.projectName = project?.projectName; - this.updateDataSource(); - }); - } + constructor(private route: ActivatedRoute, private router: Router, private location: Location) {} - private updateDataSource(count = -1): void { - this.dataSource.data = - (count !== -1 ? this.service?.deploymentInformation.slice(0, count) : this.service?.deploymentInformation) ?? []; + private updateDataSource(): void { + this.dataSource.data = this.service?.deploymentInformation ?? []; } public selectDeployment(deploymentInformation: DeploymentInformation, stageName?: string): void { @@ -106,9 +83,4 @@ export class KtbDeploymentListComponent implements OnInit, OnDestroy { $event.stopPropagation(); this.selectDeployment(deployment, stageName); } - - public ngOnDestroy(): void { - this.unsubscribe$.next(); - this.unsubscribe$.complete(); - } } diff --git a/bridge/client/app/_views/ktb-service-view/ktb-service-view.component.html b/bridge/client/app/_views/ktb-service-view/ktb-service-view.component.html index be631811a1..c11fa0f258 100644 --- a/bridge/client/app/_views/ktb-service-view/ktb-service-view.component.html +++ b/bridge/client/app/_views/ktb-service-view/ktb-service-view.component.html @@ -55,6 +55,7 @@

{ this.projectName = projectName; + this.selectedDeployment = undefined; }); if (this.initialDelayMillis !== 0) { diff --git a/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.html b/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.html index 5554259ce6..4767321d6e 100644 --- a/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.html +++ b/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.html @@ -37,7 +37,7 @@

{{ editMode ? 'Edit' : 'Create' }} subscription

class="mr-2 item" placeholder="Choose your task" aria-label="Choose your task" - (selectionChange)="selectedTaskChanged(data.project.projectName, data.subscription)" + (selectionChange)="selectedTaskChanged(data.projectName, data.subscription)" > @@ -49,7 +49,7 @@

{{ editMode ? 'Edit' : 'Create' }} subscription

class="mr-2 item" placeholder="Choose your task suffix" aria-label="Choose your task suffix" - (selectionChange)="selectedTaskChanged(data.project.projectName, data.subscription)" + (selectionChange)="selectedTaskChanged(data.projectName, data.subscription)" > {{ editMode ? 'Edit' : 'Create' }} subscription data.subscription.getFilter(_dataSource.isAutocomplete(_dataSource.data) ? _dataSource.data : undefined) " (filterChanges)=" - data.subscription.filterChanged($event, data.project.projectName); - subscriptionFilterChanged(data.subscription, data.project.projectName) + data.subscription.filterChanged($event, data.projectName); + subscriptionFilterChanged(data.subscription, data.projectName) " aria-label="Filter by stage and service" clearAllLabel="Clear all" @@ -81,7 +81,7 @@

{{ editMode ? 'Edit' : 'Create' }} subscription

@@ -98,7 +98,7 @@

{{ editMode ? 'Edit' : 'Create' }} subscription

class="mr-2" uitestid="updateSubscriptionButton" [disabled]="!isFormValid(data.subscription)" - (click)="updateSubscription(data.project.projectName, data.integrationId, data.subscription, data.webhook)" + (click)="updateSubscription(data.projectName, data.integrationId, data.subscription, data.webhook)" dt-button > { const dataService = TestBed.inject(DataService); // when - jest.spyOn(dataService, 'getProject').mockReturnValue(of(ProjectsMock[2])); + jest + .spyOn(dataService, 'getSequenceFilter') + .mockReturnValue(of({ stages: ['dev', 'staging', 'production'], services: ['carts-db'] })); component.data$.subscribe(); // then @@ -275,12 +276,16 @@ describe('KtbModifyUniformSubscriptionComponent', () => { it('should add new service to datasource', () => { // given const dataService = TestBed.inject(DataService); - jest.spyOn(dataService, 'getProject').mockReturnValue(of(ProjectsMock[2])); + jest + .spyOn(dataService, 'getSequenceFilter') + .mockReturnValue(of({ stages: ['dev', 'staging', 'production'], services: ['carts-db'] })); setSubscription(2, 0); // when - jest.spyOn(dataService, 'getProject').mockReturnValue(of(ProjectsMock[0])); + jest + .spyOn(dataService, 'getSequenceFilter') + .mockReturnValue(of({ stages: ['dev', 'staging', 'production'], services: ['carts-db', 'carts'] })); component.data$.subscribe(); // then diff --git a/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.ts b/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.ts index ac891a9081..c1236b3c49 100644 --- a/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.ts +++ b/bridge/client/app/_views/ktb-settings-view/ktb-integration-view/ktb-modify-uniform-subscription/ktb-modify-uniform-subscription.component.ts @@ -5,21 +5,20 @@ import { combineLatest, forkJoin, Observable, of, Subject, throwError } from 'rx import { catchError, filter, map, switchMap, take, takeUntil, tap } from 'rxjs/operators'; import { UniformSubscription } from '../../../../_models/uniform-subscription'; import { DtFilterFieldDefaultDataSource } from '@dynatrace/barista-components/filter-field'; -import { Project } from '../../../../_models/project'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { DtFilterFieldDefaultDataSourceAutocomplete } from '@dynatrace/barista-components/filter-field/src/filter-field-default-data-source'; -import { EventTypes } from '../../../../../../shared/interfaces/event-types'; -import { UniformRegistration } from '../../../../_models/uniform-registration'; +import { IWebhookConfigClient, PreviousWebhookConfig } from 'shared/interfaces/webhook-config'; +import { ISequencesFilter } from '../../../../../../shared/interfaces/sequencesFilter'; +import { IClientSecret } from '../../../../../../shared/interfaces/secret'; +import { EventState } from '../../../../../../shared/models/event-state'; import { AppUtils } from '../../../../_utils/app.utils'; -import { IWebhookConfigClient, PreviousWebhookConfig } from '../../../../../../shared/interfaces/webhook-config'; import { NotificationsService } from '../../../../_services/notifications.service'; -import { UniformRegistrationInfo } from '../../../../../../shared/interfaces/uniform-registration-info'; +import { HttpErrorResponse } from '@angular/common/http'; import { NotificationType } from '../../../../_models/notification'; +import { UniformRegistrationInfo } from '../../../../../../shared/interfaces/uniform-registration-info'; import { SecretScopeDefault } from '../../../../../../shared/interfaces/secret-scope'; -import { EventState } from '../../../../../../shared/models/event-state'; +import { EventTypes } from '../../../../../../shared/interfaces/event-types'; import { Trace } from '../../../../_models/trace'; -import { HttpErrorResponse } from '@angular/common/http'; -import { IClientSecret } from '../../../../../../shared/interfaces/secret'; @Component({ selector: 'ktb-modify-uniform-subscription', @@ -33,9 +32,10 @@ export class KtbModifyUniformSubscriptionComponent implements OnDestroy { public taskSuffixControl = new FormControl('', [Validators.required]); private isGlobalControl = new FormControl(); public data$: Observable<{ + projectName: string; taskNames: string[]; subscription: UniformSubscription; - project: Project; + filter: ISequencesFilter; integrationId: string; webhook?: IWebhookConfigClient; webhookSecrets?: IClientSecret[]; @@ -49,7 +49,6 @@ export class KtbModifyUniformSubscriptionComponent implements OnDestroy { isGlobal: this.isGlobalControl, }); private _previousFilter?: PreviousWebhookConfig; - public uniformRegistration?: UniformRegistration; public isWebhookFormValid = true; public isWebhookService = false; public suffixes: { value: string; displayValue: string }[] = [ @@ -152,10 +151,6 @@ export class KtbModifyUniformSubscriptionComponent implements OnDestroy { }), take(1) ); - const project$ = projectName$.pipe( - switchMap((projectName) => this.dataService.getProject(projectName)), - filter((project?: Project): project is Project => !!project) - ); const webhook$ = forkJoin({ subscription: subscription$, @@ -196,19 +191,30 @@ export class KtbModifyUniformSubscriptionComponent implements OnDestroy { }) ); - this.data$ = combineLatest([taskNames$, subscription$, project$, integrationId$, webhook$, webhookSecrets$]).pipe( - map(([taskNames, subscription, project, integrationId, webhook, webhookSecrets]) => { + const filter$ = projectName$.pipe(switchMap((projectName) => this.dataService.getSequenceFilter(projectName))); + + this.data$ = combineLatest([ + projectName$, + taskNames$, + subscription$, + filter$, + integrationId$, + webhook$, + webhookSecrets$, + ]).pipe( + map(([projectName, taskNames, subscription, filterData, integrationId, webhook, webhookSecrets]) => { return { taskNames, subscription, - project, + filter: filterData, + projectName, integrationId, webhook, webhookSecrets, }; }), tap((data) => { - this.updateDataSource(data.project, data.subscription); + this.updateDataSource(data.filter.stages, data.filter.services, data.subscription); }) ); } @@ -234,16 +240,8 @@ export class KtbModifyUniformSubscriptionComponent implements OnDestroy { window.location.reload(); } - private updateDataSource(project: Project, subscription: UniformSubscription): void { - const stages: { name: string }[] = project.stages.map((stage) => ({ - name: stage.stageName, - })); - const services: { name: string }[] = project.getServices().map((service) => ({ - name: service.serviceName, - })); - const availableServices = subscription.filter.services?.filter((service) => - services.some((s) => s.name === service) - ); + private updateDataSource(stages: string[], services: string[], subscription: UniformSubscription): void { + const availableServices = subscription.filter.services?.filter((service) => services.some((s) => s === service)); // check if services have been deleted if (availableServices && availableServices?.length !== subscription.filter.services?.length) { @@ -253,11 +251,11 @@ export class KtbModifyUniformSubscriptionComponent implements OnDestroy { autocomplete: [ { name: 'Stage', - autocomplete: stages, + autocomplete: stages.map((name) => ({ name })), }, { name: 'Service', - autocomplete: services, + autocomplete: services.map((name) => ({ name })), }, ], } as DtFilterFieldDefaultDataSourceAutocomplete; diff --git a/bridge/cypress/integration/notification.spec.ts b/bridge/cypress/integration/notification.spec.ts index 926cf5893d..763858b4fd 100644 --- a/bridge/cypress/integration/notification.spec.ts +++ b/bridge/cypress/integration/notification.spec.ts @@ -39,7 +39,7 @@ describe('Test notifications', () => { it('should test notification fade out', () => { const basePage = new BasePage(); - cy.visit('/project/dynatrace/settings/services/create').wait('@getApproval'); + cy.visit('/project/dynatrace/settings/services/create'); showSuccess(); basePage @@ -57,7 +57,7 @@ describe('Test notifications', () => { it('should test notification close', () => { const basePage = new BasePage(); - cy.visit('/project/dynatrace/settings/services/create').wait('@getApproval'); + cy.visit('/project/dynatrace/settings/services/create'); showSuccess(); const notification = basePage.notificationSuccessVisible(); @@ -66,7 +66,7 @@ describe('Test notifications', () => { }); it('should not show the same notifications', () => { - cy.visit('/project/dynatrace/settings/services').wait('@getApproval'); + cy.visit('/project/dynatrace/settings/services'); cy.byTestId('keptn-create-service-button').click(); showSuccess(); cy.byTestId('keptn-create-service-button').click(); diff --git a/bridge/cypress/integration/secret-add-delete.spec.ts b/bridge/cypress/integration/secret-add-delete.spec.ts index 0722b9c0c3..7976648bac 100644 --- a/bridge/cypress/integration/secret-add-delete.spec.ts +++ b/bridge/cypress/integration/secret-add-delete.spec.ts @@ -1,28 +1,32 @@ import { ProjectBoardPage } from '../support/pageobjects/ProjectBoardPage'; import SecretsPage from '../support/pageobjects/SecretsPage'; -import { interceptSecrets } from '../support/intercept'; +import DashboardPage from '../support/pageobjects/DashboardPage'; +import EnvironmentPage from '../support/pageobjects/EnvironmentPage'; describe('Keptn Secrets adding deleting test', () => { const basePage = new ProjectBoardPage(); + const dashboardPage = new DashboardPage(); + const environmentPage = new EnvironmentPage(); const secretsPage = new SecretsPage(); - const DYNATRACE_PROJECT = 'dynatrace'; + const project = 'sockshop'; beforeEach(() => { - interceptSecrets(); + dashboardPage.intercept(); + environmentPage.intercept(); + secretsPage.intercept(); }); it('should navigate to add secret page', () => { - cy.visit('/'); - cy.wait('@metadataCmpl'); - basePage.selectProject(DYNATRACE_PROJECT); + dashboardPage.visit(); + basePage.selectProject(project); basePage.goToUniformPage().goToSecretsPage(); secretsPage.clickAddSecret(); - cy.location('pathname').should('eq', `/project/${DYNATRACE_PROJECT}/settings/uniform/secrets/add`); + cy.location('pathname').should('eq', `/project/${project}/settings/uniform/secrets/add`); }); it('should add a secret', () => { secretsPage - .visitCreate(DYNATRACE_PROJECT) + .visitCreate(project) .setSecret('dynatrace-prod', 'dynatrace-service', 'DT_API_TOKEN', 'secretvalue!@#$%^&*(!@#$%^&*()') .assertScopesEnabled(true) .createSecret(); @@ -31,20 +35,20 @@ describe('Keptn Secrets adding deleting test', () => { it('should delete a secret', () => { const SECRET_NAME = 'dynatrace-prod'; - secretsPage.visit(DYNATRACE_PROJECT).deleteSecret(SECRET_NAME).secretExistsInList(SECRET_NAME, 1); + secretsPage.visit(project).deleteSecret(SECRET_NAME).secretExistsInList(SECRET_NAME, 1); }); it('should have a specific secret in the list', () => { - secretsPage.visit(DYNATRACE_PROJECT).assertSecretInList(1, 'dynatrace-prod', 'dynatrace-service', 'DT_API_TOKEN'); + secretsPage.visit(project).assertSecretInList(1, 'dynatrace-prod', 'dynatrace-service', 'DT_API_TOKEN'); }); it('should have disabled "remove key-value pair" icon-button if there is only one key-value pair', () => { - secretsPage.visitCreate(DYNATRACE_PROJECT).assertKeyValuePairLength(1).assertKeyValuePairEnabled(0, false); + secretsPage.visitCreate(project).assertKeyValuePairLength(1).assertKeyValuePairEnabled(0, false); }); it('should have enabled "remove key-value pair" icon-button if there is more than one key-value pair', () => { secretsPage - .visitCreate(DYNATRACE_PROJECT) + .visitCreate(project) .addKeyValuePair() .assertKeyValuePairLength(2) .assertKeyValuePairEnabled(0, true) @@ -60,7 +64,7 @@ describe('Keptn Secrets adding deleting test', () => { delay: 10_000, }); secretsPage - .visitCreate(DYNATRACE_PROJECT) + .visitCreate(project) .appendSecretName('my-secret') .appendSecretKey(0, 'my-key') .appendSecretValue(0, 'my-value') diff --git a/bridge/cypress/integration/uniform-integrations.spec.ts b/bridge/cypress/integration/uniform-integrations.spec.ts index 319cebd0a5..b42206ef46 100644 --- a/bridge/cypress/integration/uniform-integrations.spec.ts +++ b/bridge/cypress/integration/uniform-integrations.spec.ts @@ -479,14 +479,15 @@ describe('Add control plane subscription dynamic request', () => { body: { id: '0b77c90e-282d-4a7e-a96d-e23027265868', }, - delay: 5000, + delay: 10_000, }); uniformPage .visitAdd(integrationID) .setTaskPrefix('deployment') .setTaskSuffix('triggered') .update() - .assertIsUpdateButtonEnabled(false); + .assertIsUpdateButtonEnabled(false) + .waitForJmeterInfoRequest(); }); xit('should show an error message if can not parse shipyard.yaml', () => { diff --git a/bridge/cypress/support/intercept.ts b/bridge/cypress/support/intercept.ts index 6b9e5de782..522c9d0d63 100644 --- a/bridge/cypress/support/intercept.ts +++ b/bridge/cypress/support/intercept.ts @@ -21,6 +21,7 @@ export function interceptEmptyEnvironmentScreen(): void { export function interceptEnvironmentScreen(): void { const project = 'sockshop'; interceptProjectBoard(); + cy.intercept('/api/project/sockshop?approval=true&remediation=true', { fixture: 'project.mock' }).as('project'); cy.intercept('/api/project/sockshop/customSequences', { fixture: 'custom-sequences.mock' }).as('customSequences'); cy.intercept('POST', '/api/v1/event', { body: { keptnContext: '6c98fbb0-4c40-4bff-ba9f-b20556a57c8a' } }); cy.intercept('POST', '/api/controlPlane/v1/project/sockshop/stage/dev/service/carts/evaluation', { @@ -129,7 +130,6 @@ export function interceptDashboard(): void { export function interceptProjectBoard(): void { interceptMain(); - cy.intercept('/api/project/sockshop?approval=true&remediation=true', { fixture: 'project.mock' }).as('project'); cy.intercept('/api/hasUnreadUniformRegistrationLogs', { body: false }); } @@ -247,12 +247,7 @@ export function interceptSequencesPageWithSequenceThatIsNotLoaded(): void { } export function interceptIntegrations(): void { - interceptMain(); - cy.intercept('/api/project/sockshop?approval=true&remediation=true', { fixture: 'project.mock' }).as('project'); - cy.intercept('/api/hasUnreadUniformRegistrationLogs', { body: false }); - cy.intercept('/api/controlPlane/v1/project?disableUpstreamSync=true&pageSize=50', { fixture: 'projects.mock' }).as( - 'projects' - ); + interceptProjectBoard(); cy.intercept('/api/uniform/registration', { fixture: 'registration.mock' }); // jmeter-service cy.intercept('/api/controlPlane/v1/log?integrationId=355311a7bec3f35bf3abc2484ab09bcba8e2b297&pageSize=100', { @@ -281,12 +276,13 @@ export function interceptIntegrations(): void { ], }, }); + // jmeter-service uniform-info cy.intercept('/api/uniform/registration/355311a7bec3f35bf3abc2484ab09bcba8e2b297/info', { body: { isControlPlane: true, isWebhookService: false, }, - }); + }).as('jmeterUniformInfo'); cy.intercept('/api/uniform/registration/0f2d35875bbaa72b972157260a7bd4af4f2826df/info', { body: { isControlPlane: true, @@ -322,18 +318,12 @@ export function interceptIntegrations(): void { '/api/uniform/registration/355311a7bec3f35bf3abc2484ab09bcba8e2b297/subscription/0e021b71-1533-4cfe-875a-b756aa6107ba', { body: {} } ); + + cy.intercept('/api/project/sockshop/sequences/filter', { fixture: 'sequence.filter.mock' }).as('SequencesMetadata'); } export function interceptSecrets(): void { - cy.fixture('get.project.json').as('initProjectJSON'); - cy.fixture('metadata.json').as('initmetadata'); - - cy.intercept('/api/bridgeInfo', { fixture: 'bridgeInfo.mock' }); - cy.intercept('GET', 'api/v1/metadata', { fixture: 'metadata.json' }).as('metadataCmpl'); - cy.intercept('GET', 'api/controlPlane/v1/project?disableUpstreamSync=true&pageSize=50', { - fixture: 'get.project.json', - }).as('initProjects'); - cy.intercept('GET', 'api/controlPlane/v1/sequence/dynatrace?pageSize=5', { fixture: 'project.sequences.json' }); + interceptProjectBoard(); cy.intercept('POST', 'api/secrets/v1/secret', { statusCode: 200, @@ -351,11 +341,7 @@ export function interceptSecrets(): void { }, }).as('getSecrets'); - cy.intercept('GET', 'api/project/dynatrace?approval=true&remediation=true', { - statusCode: 200, - }).as('getApproval'); - - cy.intercept('GET', 'api/project/dynatrace', { + cy.intercept('GET', 'api/project/sockshop', { statusCode: 200, fixture: 'get.approval.json', }); @@ -428,7 +414,6 @@ export function interceptEvaluationBoardWithoutDeployment(): void { export function interceptHeatmapComponent(): void { cy.intercept('/api/v1/metadata', { fixture: 'metadata.mock' }); cy.intercept('/api/bridgeInfo', { fixture: 'bridgeInfoEnableD3Heatmap.mock.json' }); - cy.intercept('/api/project/sockshop?approval=true&remediation=true', { fixture: 'project.mock' }).as('project'); cy.intercept('/api/hasUnreadUniformRegistrationLogs', { body: false }); cy.intercept('/api/controlPlane/v1/project?disableUpstreamSync=true&pageSize=50', { fixture: 'projects.mock' }); cy.intercept('GET', '/api/project/sockshop/serviceStates', { diff --git a/bridge/cypress/support/pageobjects/ProjectSettingsPage.ts b/bridge/cypress/support/pageobjects/ProjectSettingsPage.ts index bbc25a8564..340ebeb3ba 100644 --- a/bridge/cypress/support/pageobjects/ProjectSettingsPage.ts +++ b/bridge/cypress/support/pageobjects/ProjectSettingsPage.ts @@ -56,7 +56,7 @@ class ProjectSettingsPage { } public visitSettings(project: string): this { - cy.visit(`/project/${project}/settings/project`).wait('@metadata').wait('@project'); + cy.visit(`/project/${project}/settings/project`).wait('@metadata').wait('@projectPlain'); return this; } diff --git a/bridge/cypress/support/pageobjects/SecretsPage.ts b/bridge/cypress/support/pageobjects/SecretsPage.ts index c29a41dcee..33f632488a 100644 --- a/bridge/cypress/support/pageobjects/SecretsPage.ts +++ b/bridge/cypress/support/pageobjects/SecretsPage.ts @@ -1,5 +1,7 @@ /// +import { interceptSecrets } from '../intercept'; + class SecretsPage { private readonly REMOVE_SECRET_KEY_VALUE_PAIR_ID = 'keptn-secret-remove-pair-button'; private readonly SECRET_NAME_ID = 'keptn-secret-name-input'; @@ -10,6 +12,11 @@ class SecretsPage { private readonly SECRET_VALUE_ID = 'keptn-secret-value-input'; private readonly ADD_KEY_VALUE_PAIR_ID = 'keptn-secret-add-pair-button'; + public intercept(): this { + interceptSecrets(); + return this; + } + public visit(projectName: string): this { cy.visit(`/project/${projectName}/settings/uniform/secrets`); return this; diff --git a/bridge/cypress/support/pageobjects/ServicesSettingsPage.ts b/bridge/cypress/support/pageobjects/ServicesSettingsPage.ts index 545ad5b875..a92f3652bf 100644 --- a/bridge/cypress/support/pageobjects/ServicesSettingsPage.ts +++ b/bridge/cypress/support/pageobjects/ServicesSettingsPage.ts @@ -7,7 +7,7 @@ export class ServicesSettingsPage { } public visitService(project: string, service: string): this { - cy.visit(`/project/${project}/settings/services/edit/${service}`).wait('@metadata').wait('@project'); + cy.visit(`/project/${project}/settings/services/edit/${service}`).wait('@metadata').wait('@projectPlain'); return this; } diff --git a/bridge/cypress/support/pageobjects/UniformPage.ts b/bridge/cypress/support/pageobjects/UniformPage.ts index bcb7074137..ef43039b77 100644 --- a/bridge/cypress/support/pageobjects/UniformPage.ts +++ b/bridge/cypress/support/pageobjects/UniformPage.ts @@ -352,5 +352,10 @@ class UniformPage { cy.byTestId(this.EDIT_WEBHOOK_FIELD_HEADER_VALUE_ID).find('input').eq(index).should('have.value', content); return this; } + + public waitForJmeterInfoRequest(): this { + cy.wait('@jmeterUniformInfo'); + return this; + } } export default UniformPage; diff --git a/bridge/shared/fixtures/service-state-response.mock.ts b/bridge/shared/fixtures/service-state-response.mock.ts index 82894977a1..dd55e851b4 100644 --- a/bridge/shared/fixtures/service-state-response.mock.ts +++ b/bridge/shared/fixtures/service-state-response.mock.ts @@ -67,26 +67,6 @@ const serviceStateResponseMock = [ }, ]; -const serviceStateResponseWithoutRemediationsMock = [ - { - deploymentInformation: [ - { - stages: [ - { - name: 'dev', - time: '2021-10-13T11:01:18.567Z', - }, - ], - name: 'carts-b', - image: 'carts-b', - version: '0.12.1', - keptnContext: '2c0e568b-8bd3-4726-a188-e528423813ef', - }, - ], - name: 'carts', - }, -]; - const serviceStateQualityGatesOnlyResponse = [ { deploymentInformation: [ diff --git a/bridge/tsconfig.app.json b/bridge/tsconfig.app.json index f1e332bcbb..17152061e8 100644 --- a/bridge/tsconfig.app.json +++ b/bridge/tsconfig.app.json @@ -6,5 +6,5 @@ }, "files": ["client/main.ts", "client/polyfills.ts"], "include": ["client/**/*.d.ts"], - "exclude": ["client/test.ts", "client/**/*.spec.ts", "client/**/*.mock.ts", "client/app/_utils/test.utils.ts"] + "exclude": [] } diff --git a/bridge/tsconfig.json b/bridge/tsconfig.json index 4b2d338de6..5da4792a09 100644 --- a/bridge/tsconfig.json +++ b/bridge/tsconfig.json @@ -19,7 +19,7 @@ "noImplicitReturns": true, "lib": ["es2018", "dom"] }, - "exclude": ["node_modules", "cypress/**/*.ts"], + "exclude": ["node_modules", "cypress/**/*.ts", "client/**/*.spec.ts"], "angularCompilerOptions": { "strictTemplates": true, "strictNullChecks": true, diff --git a/bridge/tsconfig.spec.json b/bridge/tsconfig.spec.json index c35d1f8408..3d38314223 100644 --- a/bridge/tsconfig.spec.json +++ b/bridge/tsconfig.spec.json @@ -5,5 +5,6 @@ "types": ["jest"] }, "files": ["client/polyfills.ts"], - "include": ["client/**/*.spec.ts", "shared/**/*.spec.ts", "client/**/*.d.ts"] + "include": ["client/**/*.spec.ts", "shared/**/*.spec.ts", "client/**/*.d.ts"], + "exclude": [] }