Frontend validation for config edits

Validates and enforces input constraints for edits to existing
config sections for encryption configs, contexts, and
management configs. This is first of several patches dealing
with validation.

Remaining work: validate edits to manifests, refactor New
config component form to include validation for all new config
items.

Change-Id: If1b8dc1e0e1263f86f7bf0b7455ca3640ae6a127
This commit is contained in:
Matthew Fuller
2020-12-03 20:11:02 +00:00
parent 888640a614
commit 82155c82dc
10 changed files with 155 additions and 93 deletions

View File

@@ -4,22 +4,32 @@
<h4>{{context.name}}</h4>
</mat-panel-title>
</mat-expansion-panel-header>
ContextKubeconf:
<!-- Modifying the 'name' field makes Set treat it as a new context, which
is why it's omitted from this form. New configs are handled with the 'New' button-->
<mat-form-field appearance="fill">
<input [formControl]="contextKubeconf" matInput>
</mat-form-field>
Manifest:
<mat-form-field appearance="fill">
<input [formControl]="manifest" matInput>
</mat-form-field>
EncryptionConfig:
<mat-form-field appearance="fill">
<input [formControl]="encryptionConfig" matInput>
</mat-form-field>
ManagementConfiguration:
<mat-form-field appearance="fill">
<input [formControl]="managementConfiguration" matInput>
</mat-form-field>
<mat-label>Kubeconfig Context</mat-label>
<input [formControl]="contextKubeconf" matInput readonly>
</mat-form-field><br />
<mat-form-field>
<mat-label>Manifest</mat-label>
<mat-select [formControl]="manifest" [(value)]="context.manifest">
<mat-option *ngFor="let m of configs.manifests" [value]="m">{{m}}</mat-option>
</mat-select>
</mat-form-field><br />
<mat-form-field>
<mat-label>Encryption Config</mat-label>
<mat-select [formControl]="encryptionConfig" [(value)]="context.encryptionConfig">
<!-- Encryption config isn't required, so allow a null option -->
<mat-option [value]="null">None</mat-option>
<mat-option *ngFor="let e of configs.encryption" [value]="e">{{e}}</mat-option>
</mat-select>
</mat-form-field><br />
<mat-form-field>
<mat-label>Management Config</mat-label>
<mat-select [formControl]="managementConfiguration" [(value)]="context.managementConfiguration">
<mat-option *ngFor="let m of configs.management" [value]="m">{{m}}</mat-option>
</mat-select>
</mat-form-field><br />
<mat-action-row>
<div class="edit-btn-container">
<button mat-icon-button (click)="toggleLock()">

View File

@@ -12,6 +12,7 @@
# limitations under the License.
*/
import { CommonModule } from '@angular/common';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@@ -19,6 +20,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { Context } from '../config.models';
@@ -42,6 +44,8 @@ describe('ConfigContextComponent', () => {
ReactiveFormsModule,
ToastrModule.forRoot(),
MatExpansionModule,
MatSelectModule,
CommonModule
]
})
.compileComponents();
@@ -52,6 +56,11 @@ describe('ConfigContextComponent', () => {
component = fixture.componentInstance;
component.context = new Context();
component.configs = {
manifests: ['default'],
encryption: ['default'],
management: ['default']
};
fixture.detectChanges();
});

View File

@@ -25,6 +25,12 @@ import { WsMessage, WsConstants } from 'src/services/ws/ws.models';
})
export class ConfigContextComponent implements OnInit {
@Input() context: Context;
@Input() configs: {
manifests: string[],
encryption: string[],
management: string[]
};
type = WsConstants.CTL;
component = WsConstants.CONFIG;

View File

@@ -4,26 +4,28 @@
<h4>{{config.name}}</h4>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="config.hasOwnProperty('encryptionKeyPath')">
EncryptionKeyPath:
<form [formGroup]="group">
<div *ngIf="config.hasOwnProperty('encryptionKeyPath')">
<mat-form-field appearance="fill">
<input matInput [formControl]="encryptionKeyPath">
<mat-label>EncryptionKeyPath</mat-label>
<input matInput formControlName="encryptionKeyPath">
</mat-form-field>
DecryptionKeyPath:
<mat-form-field appearance="fill">
<input matInput [formControl]="decryptionKeyPath">
<mat-label>DecryptionKeyPath</mat-label>
<input matInput formControlName="decryptionKeyPath">
</mat-form-field>
</div>
<div *ngIf="config.hasOwnProperty('keySecretName')">
KeySecretName:
</div>
<div *ngIf="config.hasOwnProperty('keySecretName')">
<mat-form-field appearance="fill">
<input matInput [formControl]="keySecretName">
<mat-label>KeySecretName</mat-label>
<input matInput formControlName="keySecretName">
</mat-form-field>
KeySecretNamespace:
<mat-form-field appearance="fill">
<input matInput [formControl]="keySecretNamespace">
<mat-label>KeySecretNamespace</mat-label>
<input matInput formControlName="keySecretNamespace">
</mat-form-field>
</div>
</div>
</form>
<mat-action-row>
<div class="edit-btn-container">
<button mat-icon-button (click)="toggleLock()">
@@ -33,7 +35,7 @@
</ng-template>
Edit</button>
</div>
<button mat-raised-button class="set-button" [disabled]="locked" (click)="setEncryptionConfig()" color="primary">Set</button>
<button mat-raised-button class="set-button" [disabled]="locked || !group.valid" (click)="setEncryptionConfig()" color="primary">Set</button>
</mat-action-row>
</mat-expansion-panel>
<br />

View File

@@ -13,7 +13,7 @@
*/
import { Component, OnInit, Input } from '@angular/core';
import { FormControl } from '@angular/forms';
import { FormControl, FormGroup, Validators, AbstractControl } from '@angular/forms';
import { EncryptionConfig, EncryptionConfigOptions } from '../config.models';
import { WsService } from 'src/services/ws/ws.service';
import { WsMessage, WsConstants } from 'src/services/ws/ws.models';
@@ -29,26 +29,35 @@ export class ConfigEncryptionComponent implements OnInit {
component = WsConstants.CONFIG;
locked = true;
name = new FormControl({value: '', disabled: true});
encryptionKeyPath = new FormControl({value: '', disabled: true});
decryptionKeyPath = new FormControl({value: '', disabled: true});
keySecretName = new FormControl({value: '', disabled: true});
keySecretNamespace = new FormControl({value: '', disabled: true});
group: FormGroup;
controlsArray = [this.encryptionKeyPath, this.decryptionKeyPath, this.keySecretName, this.keySecretNamespace];
configOptions: AbstractControl[] = [];
constructor(private websocketService: WsService) {}
ngOnInit(): void {
this.name.setValue(this.config.name);
this.encryptionKeyPath.setValue(this.config.encryptionKeyPath);
this.decryptionKeyPath.setValue(this.config.decryptionKeyPath);
this.keySecretName.setValue(this.config.keySecretName);
this.keySecretNamespace.setValue(this.config.keySecretNamespace);
this.group = new FormGroup({
encryptionKeyPath: new FormControl({value: this.config.encryptionKeyPath, disabled: true},
Validators.required),
decryptionKeyPath: new FormControl({value: this.config.decryptionKeyPath, disabled: true},
Validators.required),
keySecretName: new FormControl({value: this.config.keySecretName, disabled: true},
Validators.required),
keySecretNamespace: new FormControl({value: this.config.keySecretNamespace, disabled: true},
Validators.required)
});
if (this.config.hasOwnProperty('encryptionKeyPath')) {
this.configOptions.push(this.group.controls.encryptionKeyPath);
this.configOptions.push(this.group.controls.decryptionKeyPath);
} else {
this.configOptions.push(this.group.controls.keySecretName);
this.configOptions.push(this.group.controls.keySecretNamespace);
}
}
toggleLock(): void {
for (const control of this.controlsArray) {
for (const control of this.configOptions) {
if (this.locked) {
control.enable();
} else {
@@ -61,16 +70,16 @@ export class ConfigEncryptionComponent implements OnInit {
setEncryptionConfig(): void {
const opts: EncryptionConfigOptions = {
Name: this.name.value,
EncryptionKeyPath: this.encryptionKeyPath.value,
DecryptionKeyPath: this.decryptionKeyPath.value,
KeySecretName: this.keySecretName.value,
KeySecretNamespace: this.keySecretNamespace.value,
Name: this.config.name,
EncryptionKeyPath: this.group.controls.encryptionKeyPath.value,
DecryptionKeyPath: this.group.controls.decryptionKeyPath.value,
KeySecretName: this.group.controls.keySecretName.value,
KeySecretNamespace: this.group.controls.keySecretNamespace.value,
};
const msg = new WsMessage(this.type, this.component, WsConstants.SET_ENCRYPTION_CONFIG);
msg.data = JSON.parse(JSON.stringify(opts));
msg.name = this.name.value;
msg.name = this.config.name;
this.websocketService.sendMessage(msg);
this.toggleLock();

View File

@@ -4,24 +4,26 @@
<h4>{{config.Name}}</h4>
</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="group">
<p>
<mat-checkbox [formControl]="insecure" labelPosition="before">Insecure: </mat-checkbox>
<mat-checkbox formControlName="insecure" labelPosition="after">Insecure</mat-checkbox>
</p>
SystemActionRetries:
<mat-form-field appearance="fill">
<input matInput [formControl]="systemActionRetries" pattern="[0-9]*">
</mat-form-field>
SystemRebootDelay:
<mat-label>System Action Retries</mat-label>
<input matInput formControlName="systemActionRetries" placeholder="Value in seconds, e.g. 30">
</mat-form-field><br />
<mat-form-field appearance="fill">
<input matInput [formControl]="systemRebootDelay" pattern="[0-9]*">
</mat-form-field>
Type:
<mat-label>System Reboot Delay</mat-label>
<input matInput formControlName="systemRebootDelay" placeholder="Value in seconds, e.g. 30">
</mat-form-field><br />
<mat-form-field appearance="fill">
<input matInput [formControl]="type">
<mat-label>Type</mat-label>
<input matInput formControlName="type">
</mat-form-field>
<p>
<mat-checkbox [formControl]="useproxy" labelPosition="before">UseProxy: </mat-checkbox>
<mat-checkbox formControlName="useproxy" labelPosition="after">UseProxy</mat-checkbox>
</p>
</form>
<mat-action-row>
<div class="edit-btn-container">
<button mat-icon-button (click)="toggleLock()">
@@ -31,7 +33,7 @@
</ng-template>
Edit</button>
</div>
<button mat-raised-button class="set-button" [disabled]="locked" (click)="setManagementConfig()" color="primary">Set</button>
<button mat-raised-button class="set-button" [disabled]="locked || !group.valid" (click)="setManagementConfig()" color="primary">Set</button>
</mat-action-row>
</mat-expansion-panel>
<br />

View File

@@ -14,7 +14,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { ManagementConfig } from '../config.models';
import { FormControl, Validators } from '@angular/forms';
import { FormControl, FormGroup, Validators, AbstractControl } from '@angular/forms';
import { WsService } from 'src/services/ws/ws.service';
import { WsMessage, WsConstants } from 'src/services/ws/ws.models';
@@ -30,50 +30,43 @@ export class ConfigManagementComponent implements OnInit {
locked = true;
name = new FormControl({value: '', disabled: true});
insecure = new FormControl({value: false, disabled: true});
systemActionRetries = new FormControl({value: '', disabled: true}, Validators.pattern('[0-9]*'));
systemRebootDelay = new FormControl({value: '', disabled: true}, Validators.pattern('[0-9]*'));
type = new FormControl({value: '', disabled: true});
useproxy = new FormControl({value: false, disabled: true});
controlsArray = [this.name, this.insecure, this.systemRebootDelay, this.systemActionRetries, this.type, this.useproxy];
group: FormGroup;
constructor(private websocketService: WsService) { }
ngOnInit(): void {
this.name.setValue(this.config.Name);
this.insecure.setValue(this.config.insecure);
this.systemActionRetries.setValue(this.config.systemActionRetries);
this.systemRebootDelay.setValue(this.config.systemRebootDelay);
this.type.setValue(this.config.type);
this.useproxy.setValue(this.config.useproxy);
this.group = new FormGroup({
name: new FormControl({value: this.config.Name, disabled: true}),
insecure: new FormControl({value: this.config.insecure, disabled: true}),
systemActionRetries: new FormControl({value: this.config.systemActionRetries, disabled: true},
Validators.pattern('^[0-9]*$')),
systemRebootDelay: new FormControl({value: this.config.systemRebootDelay, disabled: true},
Validators.pattern('^[0-9]*$')),
type: new FormControl({value: this.config.type, disabled: true}),
useproxy: new FormControl({value: this.config.useproxy, disabled: true})
});
}
toggleLock(): void {
for (const control of this.controlsArray) {
if (this.locked) {
control.enable();
} else {
control.disable();
}
if (this.group.disabled) {
this.group.enable();
} else {
this.group.disable();
}
this.locked = !this.locked;
}
setManagementConfig(): void {
const msg = new WsMessage(this.msgType, this.component, WsConstants.SET_MANAGEMENT_CONFIG);
msg.name = this.name.value;
msg.name = this.group.controls.name.value;
const cfg: ManagementConfig = {
Name: this.name.value,
insecure: this.insecure.value,
// TODO(mfuller): need to validate these are numerical values in the form
systemActionRetries: +this.systemActionRetries.value,
systemRebootDelay: +this.systemRebootDelay.value,
type: this.type.value,
useproxy: this.useproxy.value
Name: this.group.controls.name.value,
insecure: this.group.controls.insecure.value,
systemActionRetries: +this.group.controls.systemActionRetries.value,
systemRebootDelay: +this.group.controls.systemRebootDelay.value,
type: this.group.controls.type.value,
useproxy: this.group.controls.useproxy.value
};
msg.data = JSON.parse(JSON.stringify(cfg));

View File

@@ -12,7 +12,7 @@
</mat-panel-description>
</mat-expansion-panel-header>
<mat-accordion *ngFor="let context of contexts">
<app-config-context [context]="context"></app-config-context>
<app-config-context [context]="context" [configs]="configs"></app-config-context>
</mat-accordion>
<button mat-icon-button (click)="newConfig('context')">
<mat-icon class="grey-icon" svgIcon="add"></mat-icon>New Context

View File

@@ -39,6 +39,8 @@ export class ConfigComponent implements WsReceiver, OnInit {
managementConfigs: ManagementConfig[] = [];
encryptionConfigs: EncryptionConfig[] = [];
configs = {};
constructor(private websocketService: WsService,
public dialog: MatDialog) {
this.websocketService.registerFunctions(this);
@@ -71,13 +73,13 @@ export class ConfigComponent implements WsReceiver, OnInit {
Object.assign(this.contexts, message.data);
break;
case WsConstants.GET_MANIFESTS:
Object.assign(this.manifests, message.data);
this.handleGetManifests(message);
break;
case WsConstants.GET_ENCRYPTION_CONFIGS:
Object.assign(this.encryptionConfigs, message.data);
this.handleGetEncryptionConfigs(message);
break;
case WsConstants.GET_MANAGEMENT_CONFIGS:
Object.assign(this.managementConfigs, message.data);
this.handleGetManagementConfigs(message);
break;
case WsConstants.USE_CONTEXT:
this.getCurrentContext();
@@ -105,6 +107,33 @@ export class ConfigComponent implements WsReceiver, OnInit {
}
}
handleGetManifests(message: WsMessage): void {
Object.assign(this.manifests, message.data);
const manifests = 'manifests';
this.configs[manifests] = [];
for (const m of this.manifests) {
this.configs[manifests].push(m.name);
}
}
handleGetEncryptionConfigs(message: WsMessage): void {
Object.assign(this.encryptionConfigs, message.data);
const encryption = 'encryption';
this.configs[encryption] = [];
for (const e of this.encryptionConfigs) {
this.configs[encryption].push(e.name);
}
}
handleGetManagementConfigs(message: WsMessage): void {
Object.assign(this.managementConfigs, message.data);
const management = 'management';
this.configs[management] = [];
for (const m of this.managementConfigs) {
this.configs[management].push(m.Name);
}
}
getConfig(): void {
this.getAirshipConfigPath();
this.getCurrentContext();

View File

@@ -29,6 +29,7 @@ import { ConfigInitComponent } from './config-init/config-init.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatExpansionModule } from '@angular/material/expansion';
import { ConfigNewComponent } from './config-new/config-new.component';
import { MatSelectModule } from '@angular/material/select';
@NgModule({
imports: [
@@ -41,7 +42,8 @@ import { ConfigNewComponent } from './config-new/config-new.component';
MatButtonModule,
MatIconModule,
MatCheckboxModule,
MatExpansionModule
MatExpansionModule,
MatSelectModule
],
declarations: [
ConfigComponent,