Skip to content

Commit

Permalink
feat(Test) Fixed namespace-settings in global setting and added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
romdhanisam committed Oct 21, 2023
1 parent 7a059ac commit 218c8d1
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 71 deletions.
28 changes: 15 additions & 13 deletions modules/web/src/settings/global/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {ResourceService} from '@common/services/resource/resource';
import {SaveAnywayDialog} from './saveanywaysdialog/dialog';
import {SettingsHelperService} from './service';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Controls as NamespaceControls} from './namespace/component';

enum Controls {
ClusterName = 'clusterName',
Expand Down Expand Up @@ -65,18 +66,16 @@ export class GlobalSettingsComponent implements OnInit {
) {}

private get externalSettings_(): GlobalSettings {
const settings = {} as GlobalSettings;

settings.itemsPerPage = this.settingsService_.getItemsPerPage();
settings.labelsLimit = this.settingsService_.getLabelsLimit();
settings.clusterName = this.settingsService_.getClusterName();
settings.logsAutoRefreshTimeInterval = this.settingsService_.getLogsAutoRefreshTimeInterval();
settings.resourceAutoRefreshTimeInterval = this.settingsService_.getResourceAutoRefreshTimeInterval();
settings.disableAccessDeniedNotifications = this.settingsService_.getDisableAccessDeniedNotifications();
settings.defaultNamespace = this.settingsService_.getDefaultNamespace();
settings.namespaceFallbackList = this.settingsService_.getNamespaceFallbackList();

return settings;
return {
itemsPerPage: this.settingsService_.getItemsPerPage(),
labelsLimit: this.settingsService_.getLabelsLimit(),
clusterName: this.settingsService_.getClusterName(),
logsAutoRefreshTimeInterval: this.settingsService_.getLogsAutoRefreshTimeInterval(),
resourceAutoRefreshTimeInterval: this.settingsService_.getResourceAutoRefreshTimeInterval(),
disableAccessDeniedNotifications: this.settingsService_.getDisableAccessDeniedNotifications(),
defaultNamespace: this.settingsService_.getDefaultNamespace(),
namespaceFallbackList: this.settingsService_.getNamespaceFallbackList(),
};
}

ngOnInit(): void {
Expand All @@ -87,7 +86,10 @@ export class GlobalSettingsComponent implements OnInit {
[Controls.LogsAutorefreshInterval]: this.builder_.control(0),
[Controls.ResourceAutorefreshInterval]: this.builder_.control(0),
[Controls.DisableAccessDeniedNotification]: this.builder_.control(false),
[Controls.NamespaceSettings]: this.builder_.control(''),
[Controls.NamespaceSettings]: this.builder_.control({
[NamespaceControls.DefaultNamespace]: this.externalSettings_.defaultNamespace,
[NamespaceControls.FallbackList]: this.externalSettings_.namespaceFallbackList,
}),
});

this.load_();
Expand Down
154 changes: 154 additions & 0 deletions modules/web/src/settings/global/namespace/component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2017 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {ComponentFixture, TestBed} from '@angular/core/testing';

import {Controls, NamespaceSettings, NamespaceSettingsComponent} from './component';
import {MatFormFieldModule} from '@angular/material/form-field';
import {FormArray, FormBuilder, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule} from '@angular/forms';
import {SettingsEntryComponent} from '../../entry/component';
import {ResourceService} from '@common/services/resource/resource';
import {NamespaceList} from '@api/root.api';
import {SettingsHelperService} from '../service';
import {MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {BreakpointObserver} from '@angular/cdk/layout';
import {HttpClientModule} from '@angular/common/http';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {FilterByPipe} from '@common/pipes/filterby';
import {MatIconModule} from '@angular/material/icon';
import {of} from 'rxjs';
import {MatChipsModule} from '@angular/material/chips';
import {MatInputModule} from '@angular/material/input';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';

describe('NamespaceSettingsComponent', () => {
let component: NamespaceSettingsComponent;
let fixture: ComponentFixture<NamespaceSettingsComponent>;
const resourceServiceMock: any = {get: jest.fn()};
let settingsHelperServiceMock: SettingsHelperService;
let dialog: MatDialog;
beforeEach(async () => {
resourceServiceMock.get.mockReturnValue(of());
await TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
MatDialogModule,
MatAutocompleteModule,
MatIconModule,
MatChipsModule,
NoopAnimationsModule,
],
declarations: [NamespaceSettingsComponent, SettingsEntryComponent, FilterByPipe],
providers: [
{provide: ResourceService<NamespaceList>, useValue: resourceServiceMock},
{provide: SettingsHelperService},
{provide: MatDialog},
{provide: BreakpointObserver},
{provide: FormBuilder},
{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NamespaceSettingsComponent},
],
}).compileComponents();
dialog = TestBed.inject(MatDialog);
settingsHelperServiceMock = TestBed.inject(SettingsHelperService);
fixture = TestBed.createComponent(NamespaceSettingsComponent);
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

/**
* Test `NG_VALUE_ACCESSOR` binding from global setting
* <kd-namespace-settings
* >>>>>> [formControlName]="Controls.NamespaceSettings" <<<<<<<
* >
* </kd-namespace-settings>
*/
describe('should update form when ControlValueAccessor#writeValue', () => {
// Given
let setSettings: jest.SpyInstance;
beforeEach(() => {
setSettings = jest.spyOn(settingsHelperServiceMock, 'settings', 'set');
});
const cases: [any, NamespaceSettings, boolean][] = [
['', {defaultNamespace: '', fallbackList: []}, false],
[null, {defaultNamespace: '', fallbackList: []}, false],
[undefined, {defaultNamespace: '', fallbackList: []}, false],
[NaN, {defaultNamespace: '', fallbackList: []}, false],
[{}, {defaultNamespace: '', fallbackList: []}, false],
[
{defaultNamespace: 'default', fallbackList: ['default', 'kubernetes-dashboard']},
{defaultNamespace: 'default', fallbackList: ['default', 'kubernetes-dashboard']},
true,
],
];
it.each(cases)(
"when the input is '%p', the value should be %p",
(setting: any, expectedValue: NamespaceSettings, expectedCall: boolean) => {
// When
component.writeValue(setting);
// Then
expect(component.form.value).toEqual(expectedValue);
delete Object.assign(expectedValue, {namespaceFallbackList: expectedValue.fallbackList})['fallbackList'];
expectedCall
? expect(setSettings).toHaveBeenNthCalledWith(1, expectedValue)
: expect(setSettings).toHaveBeenCalledTimes(0);
}
);
});

it('should update setting when namespaces removed after edit()', () => {
// Given
component.form.get(Controls.DefaultNamespace).setValue('default');
(<FormArray>component.form.get(Controls.FallbackList)).push(new FormControl('default'));
(<FormArray>component.form.get(Controls.FallbackList)).push(new FormControl('kubernetes-dashboard'));
jest.spyOn(dialog, 'open').mockReturnValue({
afterClosed: () => of(['default']),
} as MatDialogRef<typeof component>);
const setSettings = jest.spyOn(settingsHelperServiceMock, 'settings', 'set');
// When
component.edit();
// Then
expect(component.form.value).toEqual({defaultNamespace: 'default', fallbackList: ['default']});
expect(setSettings).toHaveBeenNthCalledWith(1, {defaultNamespace: 'default', namespaceFallbackList: ['default']})
});

it('should update setting when namespace added after add()', () => {
// Given
component.form.get(Controls.DefaultNamespace).setValue('default');
(<FormArray>component.form.get(Controls.FallbackList)).push(new FormControl('default'));
(<FormArray>component.form.get(Controls.FallbackList)).push(new FormControl('kubernetes-dashboard'));
jest.spyOn(dialog, 'open').mockReturnValue({
afterClosed: () => of('newNamespace'),
} as MatDialogRef<typeof component>);
const setSettings = jest.spyOn(settingsHelperServiceMock, 'settings', 'set');
// When
component.add();
// Then
expect(component.form.value).toEqual({
defaultNamespace: 'default',
fallbackList: ['default', 'kubernetes-dashboard', 'newNamespace'],
});
expect(setSettings).toHaveBeenNthCalledWith(1,{
defaultNamespace: 'default',
namespaceFallbackList: ['default', 'kubernetes-dashboard', 'newNamespace'],
});
});
});
107 changes: 49 additions & 58 deletions modules/web/src/settings/global/namespace/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,10 @@

import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {Component, DestroyRef, forwardRef, inject, OnInit} from '@angular/core';
import {
ControlValueAccessor,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder, FormGroup, FormControl, FormArray} from '@angular/forms';
import {MatDialog, MatDialogConfig} from '@angular/material/dialog';
import {GlobalSettings, NamespaceList} from '@api/root.api';
import {map, take} from 'rxjs/operators';
import {map, take, tap} from 'rxjs/operators';
import {EndpointManager, Resource} from '@common/services/resource/endpoint';
import {ResourceService} from '@common/services/resource/resource';
import {SettingsHelperService} from '../service';
Expand All @@ -37,12 +31,12 @@ enum BreakpointElementCount {
Medium = 2,
}

enum Controls {
export enum Controls {
DefaultNamespace = 'defaultNamespace',
FallbackList = 'fallbackList',
}

interface NamespaceSettings {
export interface NamespaceSettings {
defaultNamespace: string;
fallbackList: string[];
}
Expand All @@ -64,7 +58,10 @@ export class NamespaceSettingsComponent implements OnInit, ControlValueAccessor

namespaces: string[] = [];
visibleNamespaces = 0;
form: UntypedFormGroup;
readonly form: FormGroup = new FormGroup({
[Controls.DefaultNamespace]: new FormControl(''),
[Controls.FallbackList]: new FormArray([]),
});

private settings_: GlobalSettings;
private readonly endpoint_ = EndpointManager.resource(Resource.namespace).list();
Expand All @@ -78,18 +75,13 @@ export class NamespaceSettingsComponent implements OnInit, ControlValueAccessor
return this.settings_.namespaceFallbackList ? this.settings_.namespaceFallbackList.filter(ns => ns) : [];
}

private get formArrayNamespaceFallbackList_(): string[] {
const arr = this.form.get(Controls.FallbackList).value as string[];
return arr ? arr.filter(ns => ns) : [];
}

private destroyRef = inject(DestroyRef);
constructor(
private readonly namespaceService_: ResourceService<NamespaceList>,
private readonly settingsHelperService_: SettingsHelperService,
private readonly dialog_: MatDialog,
private readonly breakpointObserver_: BreakpointObserver,
private readonly builder_: UntypedFormBuilder
private readonly builder_: FormBuilder
) {}

get invisibleCount(): number {
Expand All @@ -101,11 +93,6 @@ export class NamespaceSettingsComponent implements OnInit, ControlValueAccessor
ngOnInit(): void {
this.settings_ = this.settingsHelperService_.settings;

this.form = this.builder_.group({
[Controls.DefaultNamespace]: this.builder_.control(''),
[Controls.FallbackList]: this.builder_.array([]),
});

this.namespaceService_
.get(this.endpoint_)
.pipe(map(list => list.namespaces.map(ns => ns.objectMeta.name)))
Expand All @@ -120,10 +107,18 @@ export class NamespaceSettingsComponent implements OnInit, ControlValueAccessor
this.visibleNamespaces = breakpoint ? breakpoint[1] : BreakpointElementCount.Medium;
});

this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(this.onFormChange_.bind(this));
this.settingsHelperService_.onSettingsChange
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(this.onSettingsChange_.bind(this));
this.form.valueChanges
.pipe(
tap((next: NamespaceSettings) => {
this.settingsHelperService_.settings = {
...this.settingsHelperService_.settings,
defaultNamespace: next.defaultNamespace,
namespaceFallbackList: next.fallbackList,
};
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe();
}

add(): void {
Expand Down Expand Up @@ -155,53 +150,49 @@ export class NamespaceSettingsComponent implements OnInit, ControlValueAccessor
.pipe(take(1))
.subscribe((namespaces: string[] | undefined) => {
if (namespaces) {
this.settingsHelperService_.settings = {namespaceFallbackList: namespaces} as GlobalSettings;
this.removeNamespace_(namespaces);
}
});
}

// ControlValueAccessor interface implementation
writeValue(obj: NamespaceSettings): void {
if (!obj) {
return;
}

this.form.setValue(obj);
}

registerOnChange(fn: any): void {
this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fn);
private addNamespace_(ns: string): void {
(<FormArray>this.form.get(Controls.FallbackList)).push(this.builder_.control(ns), {emitEvent: true});
}

registerOnTouched(fn: any): void {
this.form.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fn);
private removeNamespace_(namespaces: string[]): void {
const fallbackList = <FormArray>this.form.get(Controls.FallbackList);
fallbackList.controls = namespaces.map(_ => this.builder_.control(''));
fallbackList.patchValue(
namespaces.map(ns => ns),
{emitEvent: true}
);
}

private addNamespace_(ns: string): void {
(this.form.get(Controls.FallbackList) as UntypedFormArray).push(this.builder_.control(ns));
private containsNamespace_(ns: string): boolean {
return !ns || (<FormArray>this.form.get(Controls.FallbackList)).controls.map(c => c.value).indexOf(ns) > -1;
}

private containsNamespace_(ns: string): boolean {
return (
!ns || (this.form.get(Controls.FallbackList) as UntypedFormArray).controls.map(c => c.value).indexOf(ns) > -1
writeValue(obj: any): void {
if (!obj || !this.isSetting(obj)) {
return;
}
this.form.get(Controls.DefaultNamespace).patchValue(obj.defaultNamespace, {emitEvent: false});
obj.fallbackList.map((namespace: any) =>
(<FormArray>this.form.get(Controls.FallbackList)).push(this.builder_.control(namespace), {emitEvent: false})
);
this.form.updateValueAndValidity({emitEvent: true});
}

private onFormChange_(): void {
this.settingsHelperService_.settings = {
defaultNamespace: this.form.get(Controls.DefaultNamespace).value,
namespaceFallbackList: this.formArrayNamespaceFallbackList_,
} as GlobalSettings;
registerOnChange(fn: (_: any) => void): void {
this.onChange(fn);
}

private onSettingsChange_(settings: GlobalSettings): void {
this.settings_ = settings;
registerOnTouched(fn: () => void): void {
this.onTouched(fn);
}

this.form.get(Controls.DefaultNamespace).setValue(this.settings_.defaultNamespace, {emitEvent: false});
onChange = (_: any) => {};
onTouched = (_: any) => {};

(this.form.get(Controls.FallbackList) as UntypedFormArray).controls = this.namespaceFallbackList_.map(_ =>
this.builder_.control('')
);
(this.form.get(Controls.FallbackList) as UntypedFormArray).reset(this.namespaceFallbackList_, {emitEvent: false});
}
private isSetting = (value: any): value is NamespaceSettings => !!value.defaultNamespace && !!value.fallbackList;
}

0 comments on commit 218c8d1

Please sign in to comment.