Refactor config-new component and add form validation

Refactors the config-new component form to add input constraints
and validation.

Change-Id: I930e6bf50def29d894c8ecc9cd5db9e3ed13d0cf
This commit is contained in:
Matthew Fuller 2020-12-10 20:57:46 +00:00
parent 61b9b19be6
commit d0ccf65d33
7 changed files with 261 additions and 66 deletions

View File

@ -62,8 +62,10 @@ export class ConfigManifestComponent implements OnInit {
}); });
const checkout = this.getCheckoutRef(repo); const checkout = this.getCheckoutRef(repo);
if (checkout !== null) {
repoGroup.controls.checkoutLabel.setValue(checkout[0]); repoGroup.controls.checkoutLabel.setValue(checkout[0]);
repoGroup.controls.checkoutReference.setValue(checkout[1]); repoGroup.controls.checkoutReference.setValue(checkout[1]);
}
repoArray.push(repoGroup); repoArray.push(repoGroup);
this.selectArray.push(name); this.selectArray.push(name);
} }

View File

@ -19,3 +19,7 @@
.text-input { .text-input {
width: 80%; width: 80%;
} }
mat-form-field {
width: 80%;
}

View File

@ -1,18 +1,171 @@
<h1 mat-dialog-title>New {{data.formName}} configuration</h1> <h1 mat-dialog-title>New {{data.formName}} configuration</h1>
<div mat-dialog-content class="form-content"> <div mat-dialog-content class="form-content" [ngSwitch]="data.formName">
<form [formGroup]="group"> <div *ngSwitchCase="'management'" [formGroup]="group">
<div *ngFor="let key of keys"> <mat-form-field appearance="fill">
<mat-form-field *ngIf="!isBool(dataObj[key])" appearance="fill"> <mat-label>Name</mat-label>
<mat-label>{{key}}</mat-label> <input matInput formControlName="Name">
<input class="text-input" formControlName="{{key}}" matInput> <mat-error *ngIf="group.controls.Name.hasError('required')">
Name is required
</mat-error>
</mat-form-field> </mat-form-field>
<p *ngIf="isBool(dataObj[key])"> <mat-form-field appearance="fill">
<mat-checkbox formControlName="{{key}}" labelPosition="before">{{key}} </mat-checkbox> <mat-label>Type</mat-label>
<input matInput formControlName="type">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>System Action Retries</mat-label>
<input matInput formControlName="systemActionRetries">
<mat-error *ngIf="group.controls.systemActionRetries.hasError('pattern')">
Value must be a number
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>System Reboot Delay</mat-label>
<input matInput formControlName="systemRebootDelay">
<mat-error *ngIf="group.controls.systemRebootDelay.hasError('pattern')">
Value must be a number
</mat-error>
</mat-form-field>
<p>
<mat-checkbox formControlName="useproxy" labelPosition="after">Use Proxy</mat-checkbox>
</p>
<p>
<mat-checkbox formControlName="insecure" labelPosition="after">Insecure</mat-checkbox>
</p>
</div>
<div *ngSwitchCase="'context'" [formGroup]="group">
<mat-form-field appearance="fill">
<mat-label>Name</mat-label>
<input formControlName="Name" matInput>
<mat-error *ngIf="group.controls.Name.hasError('required')">
Name is required
</mat-error>
</mat-form-field><br />
<mat-form-field>
<mat-label>Manifest</mat-label>
<mat-select formControlName="Manifest">
<mat-option *ngFor="let m of data.configs['manifests']" [value]="m">{{m}}</mat-option>
</mat-select>
</mat-form-field><br />
<mat-form-field>
<mat-label>Encryption Config</mat-label>
<mat-select formControlName="EncryptionConfig">
<!-- Encryption config isn't required, so allow a null option -->
<mat-option [value]="null">None</mat-option>
<mat-option *ngFor="let e of data.configs['encryption']" [value]="e">{{e}}</mat-option>
</mat-select>
</mat-form-field><br />
<mat-form-field>
<mat-label>Management Config</mat-label>
<mat-select formControlName="ManagementConfiguration">
<mat-option *ngFor="let m of data.configs['management']" [value]="m">{{m}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngSwitchCase="'encryption'" [formGroup]="group">
<mat-form-field appearance="fill">
<mat-label>Name</mat-label>
<input matInput formControlName="Name">
<mat-error *ngIf="group.controls.Name.hasError('required')">
Name is required
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Config Type</mat-label>
<mat-select [(value)]="encryptionType" (selectionChange)="onEncryptionChange($event)">
<mat-option value="encryption">Encrypt / Decrypt Key</mat-option>
<mat-option value="secret">Secret</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="encryptionType === 'encryption'">
<mat-form-field appearance="fill">
<mat-label>EncryptionKeyPath</mat-label>
<input matInput formControlName="EncryptionKeyPath">
<mat-error *ngIf="group.controls.EncryptionKeyPath.hasError('required')">
EncryptionKeyPath is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>DecryptionKeyPath</mat-label>
<input matInput formControlName="DecryptionKeyPath">
<mat-error *ngIf="group.controls.DecryptionKeyPath.hasError('required')">
DecryptionKeyPath is required
</mat-error>
</mat-form-field>
</div>
<div *ngIf="encryptionType === 'secret'">
<mat-form-field appearance="fill">
<mat-label>KeySecretName</mat-label>
<input matInput formControlName="KeySecretName">
<mat-error *ngIf="group.controls.KeySecretName.hasError('required')">
KeySecretName is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>KeySecretNamespace</mat-label>
<input matInput formControlName="KeySecretNamespace">
<mat-error *ngIf="group.controls.KeySecretNamespace.hasError('required')">
KeySecretNamespace is required
</mat-error>
</mat-form-field>
</div>
</div>
<div *ngSwitchCase="'manifest'" [formGroup]="group">
<mat-form-field appearance="fill">
<mat-label>Name</mat-label>
<input matInput formControlName="Name">
<mat-error *ngIf="group.controls.Name.hasError('required')">
Name is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Target Path</mat-label>
<input matInput formControlName="TargetPath">
<mat-error *ngIf="group.controls.TargetPath.hasError('required')">
TargetPath is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Metadata Path</mat-label>
<input matInput formControlName="MetadataPath">
<mat-error *ngIf="group.controls.MetadataPath.hasError('required')">
MetadataPath is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Repository Name</mat-label>
<input matInput formControlName="RepoName" readonly>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>URL</mat-label>
<input matInput formControlName="URL">
<mat-error *ngIf="group.controls.URL.hasError('required')">
URL is required
</mat-error>
</mat-form-field>
<mat-label>
<mat-select [(value)]="checkoutType">
<mat-option *ngFor="let type of checkoutTypes" [value]="type">{{type}}</mat-option>
</mat-select>
</mat-label>
<mat-form-field appearance="fill" *ngIf="checkoutType === 'Branch'">
<input matInput formControlName="Branch">
</mat-form-field>
<mat-form-field appearance="fill" *ngIf="checkoutType === 'Tag'">
<input matInput formControlName="Tag">
</mat-form-field>
<mat-form-field appearance="fill" *ngIf="checkoutType === 'CommitHash'">
<input matInput formControlName="CommitHash">
</mat-form-field>
<p>
<mat-checkbox formControlName="Force" labelPosition="after">Force</mat-checkbox>
</p>
<p>
<mat-checkbox formControlName="IsPhase" labelPosition="after">Is Phase</mat-checkbox>
</p> </p>
</div> </div>
</form>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<button mat-raised-button (click)="closeDialog()">Cancel</button> <button mat-raised-button (click)="closeDialog()">Cancel</button>
<button mat-raised-button color="primary" (click)="setConfig(data.formName)">Save</button> <button mat-raised-button color="primary" [disabled]="!group.valid" (click)="setConfig()">Save</button>
</div> </div>

View File

@ -18,6 +18,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr'; import { ToastrModule } from 'ngx-toastr';
import { ConfigNewComponent } from './config-new.component'; import { ConfigNewComponent } from './config-new.component';
@ -36,6 +37,7 @@ describe('ConfigNewComponent', () => {
MatInputModule, MatInputModule,
MatDialogModule, MatDialogModule,
MatCheckboxModule, MatCheckboxModule,
MatSelectModule,
ToastrModule.forRoot(), ToastrModule.forRoot(),
], ],
declarations: [ ConfigNewComponent ], declarations: [ ConfigNewComponent ],
@ -52,6 +54,11 @@ describe('ConfigNewComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
component.data.formName = 'context'; component.data.formName = 'context';
component.data.configs = {
manifests: ['default'],
encryption: ['default'],
management: ['default']
};
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -13,11 +13,10 @@
*/ */
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { WsService } from 'src/services/ws/ws.service'; import { WsService } from 'src/services/ws/ws.service';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ContextOptions, EncryptionConfigOptions, ManagementConfig, ManifestOptions } from '../config.models';
import { WsConstants, WsMessage } from 'src/services/ws/ws.models'; import { WsConstants, WsMessage } from 'src/services/ws/ws.models';
@Component({ @Component({
@ -27,77 +26,108 @@ import { WsConstants, WsMessage } from 'src/services/ws/ws.models';
}) })
export class ConfigNewComponent implements OnInit { export class ConfigNewComponent implements OnInit {
group: FormGroup; group: FormGroup;
subComponent: string;
dataObj: any; encryptionType: string;
keys: string[] = []; checkoutTypes = ['Branch', 'Tag', 'CommitHash'];
checkoutType = 'Branch';
dataObjs = {
context: new ContextOptions(),
manifest: new ManifestOptions(),
encryption: new EncryptionConfigOptions(),
management: new ManagementConfig()
};
constructor(private websocketService: WsService, constructor(private websocketService: WsService,
private fb: FormBuilder, private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA) public data: {formName: string}, @Inject(MAT_DIALOG_DATA) public data: {
formName: string,
configs: {}
},
public dialogRef: MatDialogRef<ConfigNewComponent>) { } public dialogRef: MatDialogRef<ConfigNewComponent>) { }
ngOnInit(): void { ngOnInit(): void {
const grp = {}; switch (this.data.formName) {
this.dataObj = this.dataObjs[this.data.formName];
for (const [key, val] of Object.entries(this.dataObj)) {
this.keys.push(key);
grp[key] = new FormControl(val);
}
this.group = new FormGroup(grp);
}
setConfig(type: string): void {
let subComponent = '';
switch (type) {
case 'context': case 'context':
subComponent = WsConstants.SET_CONTEXT; this.group = this.fb.group({
Name: new FormControl('', Validators.required),
Manifest: new FormControl(''),
EncryptionConfig: new FormControl(''),
ManagementConfiguration: new FormControl('')
});
this.subComponent = WsConstants.SET_CONTEXT;
break; break;
case 'manifest': case 'manifest':
subComponent = WsConstants.SET_MANIFEST; this.group = this.fb.group({
Name: new FormControl('', Validators.required),
TargetPath: new FormControl('', Validators.required),
MetadataPath: new FormControl('', Validators.required),
// new manifests seem to get an auto-generated repo named 'primary'
// that won't get configured properly unless it's done here, so
// don't let users modify this field
RepoName: new FormControl({value: 'primary', disabled: true}),
URL: new FormControl('', Validators.required),
Tag: new FormControl(''),
CommitHash: new FormControl(''),
Branch: new FormControl(''),
IsPhase: new FormControl(false),
Force: new FormControl(false)
});
this.subComponent = WsConstants.SET_MANIFEST;
break; break;
case 'encryption': case 'encryption':
subComponent = WsConstants.SET_ENCRYPTION_CONFIG; this.group = this.fb.group({
Name: new FormControl('', Validators.required),
EncryptionKeyPath: new FormControl('', Validators.required),
DecryptionKeyPath: new FormControl('', Validators.required),
KeySecretName: new FormControl('', Validators.required),
KeySecretNamespace: new FormControl('', Validators.required),
});
this.subComponent = WsConstants.SET_ENCRYPTION_CONFIG;
break; break;
case 'management': case 'management':
subComponent = WsConstants.SET_MANAGEMENT_CONFIG; // NOTE: capitalizations are different for management config due to
// inconsistent json definitions in airshipctl
this.group = this.fb.group({
Name: new FormControl('', Validators.required),
type: new FormControl(''),
insecure: new FormControl(false),
useproxy: new FormControl(false),
systemActionRetries: new FormControl(0, Validators.pattern('^[0-9]*$')),
systemRebootDelay: new FormControl(0, Validators.pattern('^[0-9]*$'))
});
this.subComponent = WsConstants.SET_MANAGEMENT_CONFIG;
break; break;
} }
}
for (const [key, control] of Object.entries(this.group.controls)) { setConfig(): void {
// TODO(mfuller): need to validate this within the form const msg = new WsMessage(WsConstants.CTL, WsConstants.CONFIG, this.subComponent);
if (typeof this.dataObj[key] === 'number') { const opts = {};
this.dataObj[key] = +control.value; for (const [key, val] of Object.entries(this.group.controls)) {
if (key === 'systemActionRetries' || key === 'systemRebootDelay') {
opts[key] = +val.value;
} else { } else {
this.dataObj[key] = control.value; opts[key] = val.value;
} }
} }
const name = 'Name';
const msg = new WsMessage(WsConstants.CTL, WsConstants.CONFIG, subComponent); msg.name = opts[name];
msg.data = JSON.parse(JSON.stringify(this.dataObj)); msg.data = JSON.parse(JSON.stringify(opts));
msg.name = this.dataObj.Name;
this.websocketService.sendMessage(msg); this.websocketService.sendMessage(msg);
this.dialogRef.close(); this.closeDialog();
} }
closeDialog(): void { closeDialog(): void {
this.dialogRef.close(); this.dialogRef.close();
} }
// annoying helper method because apparently I can't just test this natively onEncryptionChange(event: any): void {
// inside an *ngIf if (this.encryptionType === 'encryption') {
isBool(val: any): boolean { this.group.controls.EncryptionKeyPath.enable();
return typeof val === 'boolean'; this.group.controls.DecryptionKeyPath.enable();
this.group.controls.KeySecretName.disable();
this.group.controls.KeySecretNamespace.disable();
} else {
this.group.controls.EncryptionKeyPath.disable();
this.group.controls.DecryptionKeyPath.disable();
this.group.controls.KeySecretName.enable();
this.group.controls.KeySecretNamespace.enable();
}
} }
} }

View File

@ -19,8 +19,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { ContextOptions, EncryptionConfigOptions, ManagementConfig, ManifestOptions } from '../config.models'; import { MatSelectModule } from '@angular/material/select';
@NgModule({ @NgModule({
imports: [ imports: [
@ -31,10 +30,7 @@ import { ContextOptions, EncryptionConfigOptions, ManagementConfig, ManifestOpti
ReactiveFormsModule, ReactiveFormsModule,
MatCheckboxModule, MatCheckboxModule,
MatDialogModule, MatDialogModule,
ContextOptions, MatSelectModule
ManifestOptions,
ManagementConfig,
EncryptionConfigOptions
], ],
declarations: [ declarations: [
], ],

View File

@ -183,7 +183,10 @@ export class ConfigComponent implements WsReceiver, OnInit {
const dialogRef = this.dialog.open(ConfigNewComponent, { const dialogRef = this.dialog.open(ConfigNewComponent, {
width: '550px', width: '550px',
height: '650px', height: '650px',
data: { formName: configType} data: {
formName: configType,
configs: this.configs
}
}); });
} }
} }