Refactor manifests to support multiple repos and validation

Refactors the config-manifest component to support multiple
repos, and provides frontend validation for edits to
existing manifests and repos. New repos can also be added
to existing manifests.

Change-Id: Ia486d08c3a14ebc537294725694946305d94e221
This commit is contained in:
Matthew Fuller
2020-12-07 23:43:42 +00:00
parent 82155c82dc
commit 61b9b19be6
11 changed files with 390 additions and 132 deletions

View File

@@ -4,48 +4,43 @@
<h4>{{manifest.name}}</h4>
</mat-panel-title>
</mat-expansion-panel-header>
RepoName:
<mat-form-field appearance="fill">
<input matInput [formControl]="RepoName">
</mat-form-field>
URL:
<mat-form-field appearance="fill">
<input matInput [formControl]="URL">
</mat-form-field>
Branch:
<mat-form-field appearance="fill">
<input matInput [formControl]="Branch">
</mat-form-field>
CommitHash:
<mat-form-field appearance="fill">
<input matInput [formControl]="CommitHash">
</mat-form-field>
Tag:
<mat-form-field appearance="fill">
<input matInput [formControl]="Tag">
</mat-form-field>
RemoteRef:
<mat-form-field appearance="fill">
<input matInput [formControl]="RemoteRef">
</mat-form-field>
<p>
<mat-checkbox [formControl]="Force" labelPosition="before">Force: </mat-checkbox>
</p>
<p>
<mat-checkbox [formControl]="IsPhase" labelPosition="before">IsPhase: </mat-checkbox>
</p>
SubPath:
<mat-form-field appearance="fill">
<input matInput [formControl]="SubPath">
</mat-form-field>
TargetPath:
<mat-form-field appearance="fill">
<input matInput [formControl]="TargetPath">
</mat-form-field>
MetadataPath:
<mat-form-field appearance="fill">
<input matInput [formControl]="MetadataPath">
</mat-form-field>
<div [formGroup]="group">
<mat-form-field appearance="fill">
<mat-label>Target Path</mat-label>
<input matInput formControlName="targetPath">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Metadata Path</mat-label>
<input matInput formControlName="metadataPath">
</mat-form-field>
<h5>Repositories</h5>
<mat-form-field appearance="fill">
<mat-select [(value)]="selectedIndex">
<mat-option *ngFor="let repoName of selectArray; let i = index" [value]="i">{{repoName}}</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngFor="let repo of repoArray.controls; let i = index">
<div *ngIf="i === selectedIndex" [formGroup]="repo">
<mat-form-field appearance="fill">
<mat-label>URL</mat-label>
<input matInput formControlName="url">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>{{repo.controls.checkoutLabel.value}}</mat-label>
<input matInput formControlName="checkoutReference">
</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>
</div>
</ng-container>
<button mat-icon-button (click)="newRepoDialog()">
<mat-icon class="grey-icon" svgIcon="add"></mat-icon>New Repository
</button>
</div>
<mat-action-row>
<div class="edit-btn-container">
<button mat-icon-button (click)="toggleLock()">
@@ -55,7 +50,7 @@
</ng-template>
Edit</button>
</div>
<button mat-raised-button class="set-button" [disabled]="locked" (click)="setManifest()" color="primary">Set</button>
<button mat-raised-button class="set-button" [disabled]="locked || !group.valid" (click)="setManifest(selectedIndex)" color="primary">Set</button>
</mat-action-row>
</mat-expansion-panel>
<br />

View File

@@ -16,9 +16,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 { CtlManifest, Manifest, RepoCheckout, Repository } from '../config.models';
@@ -41,7 +43,13 @@ describe('ConfigManifestComponent', () => {
MatButtonModule,
ReactiveFormsModule,
ToastrModule.forRoot(),
MatExpansionModule
MatExpansionModule,
MatSelectModule,
MatDialogModule
],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {name: 'default'}},
{provide: MatDialogRef, useValue: {}}
]
})
.compileComponents();

View File

@@ -14,9 +14,11 @@
import { Component, Input, OnInit } from '@angular/core';
import { Manifest, ManifestOptions, Repository } from '../config.models';
import { FormControl } from '@angular/forms';
import { FormControl, FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
import { WsService } from 'src/services/ws/ws.service';
import { WsMessage, WsConstants } from 'src/services/ws/ws.models';
import { RepositoryComponent } from './repository/repository.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
@@ -31,97 +33,118 @@ export class ConfigManifestComponent implements OnInit {
component = WsConstants.CONFIG;
locked = true;
Name = new FormControl({value: '', disabled: true});
RepoName = new FormControl({value: '', disabled: true});
URL = new FormControl({value: '', disabled: true});
Branch = new FormControl({value: '', disabled: true});
CommitHash = new FormControl({value: '', disabled: true});
Tag = new FormControl({value: '', disabled: true});
RemoteRef = new FormControl({value: '', disabled: true});
Force = new FormControl({value: false, disabled: true});
IsPhase = new FormControl({value: false, disabled: true});
SubPath = new FormControl({value: '', disabled: true});
TargetPath = new FormControl({value: '', disabled: true});
MetadataPath = new FormControl({value: '', disabled: true});
group: FormGroup;
repoArray = new FormArray([]);
selectArray: string[] = [];
selectedIndex = 0;
checkoutTypes = ['Branch', 'CommitHash', 'Tag'];
controlsArray = [
this.Name,
this.RepoName,
this.URL,
this.Branch,
this.CommitHash,
this.Tag,
this.RemoteRef,
this.Force,
this.IsPhase,
this.SubPath,
this.TargetPath,
this.MetadataPath
];
constructor(private websocketService: WsService) { }
ngOnInit(): void {
this.Name.setValue(this.manifest.name);
// TODO(mfuller): not sure yet how to handle multiple repositories,
// so for now, I'm just showing the phase repository (primary)
const repoName = this.manifest.manifest.phaseRepositoryName;
this.RepoName.setValue(repoName);
const primaryRepo: Repository = this.manifest.manifest.repositories[repoName];
this.URL.setValue(primaryRepo.url);
this.Branch.setValue(primaryRepo.checkout.branch);
this.CommitHash.setValue(primaryRepo.checkout.commitHash);
this.Tag.setValue(primaryRepo.checkout.tag);
this.RemoteRef.setValue(primaryRepo.checkout.remoteRef);
this.Force.setValue(primaryRepo.checkout.force);
// TODO(mfuller): this value doesn't come from the config file, but if set to true,
// it appears to set the phaseRepositoryName key, and since that's
// the only repo I'm showing, set to true for now
this.IsPhase.setValue(true);
this.SubPath.setValue(this.manifest.manifest.subPath);
this.TargetPath.setValue(this.manifest.manifest.targetPath);
this.MetadataPath.setValue(this.manifest.manifest.metadataPath);
constructor(private websocketService: WsService,
private fb: FormBuilder,
public dialog: MatDialog) {
this.group = this.fb.group({
name: new FormControl({value: '', disabled: true}),
repositories: this.repoArray,
targetPath: new FormControl({value: '', disabled: true}, Validators.required),
metadataPath: new FormControl({value: '', disabled: true}, Validators.required)
});
}
toggleLock(): void {
for (const control of this.controlsArray) {
if (this.locked) {
control.enable();
} else {
control.disable();
addRepo(name: string, repo: Repository): void {
const repoArray = this.group.controls.repositories as FormArray;
const repoGroup = new FormGroup({
repoName: new FormControl({value: name, disabled: true}),
url: new FormControl({value: repo.url, disabled: true}, Validators.required),
checkoutLabel: new FormControl(''),
checkoutReference: new FormControl({value: '', disabled: true}, Validators.required),
force: new FormControl({value: repo.checkout.force, disabled: true}),
isPhase: new FormControl({value: this.manifest.manifest.phaseRepositoryName === name, disabled: true}),
});
const checkout = this.getCheckoutRef(repo);
repoGroup.controls.checkoutLabel.setValue(checkout[0]);
repoGroup.controls.checkoutReference.setValue(checkout[1]);
repoArray.push(repoGroup);
this.selectArray.push(name);
}
getCheckoutRef(repo: Repository): string[] {
for (const t of this.checkoutTypes) {
const key = t[0].toLowerCase() + t.substring(1);
if (repo.checkout[key] !== null && repo.checkout[key] !== '') {
return [t, repo.checkout[key]];
}
}
return null;
}
newRepoDialog(): void {
const dialogRef = this.dialog.open(RepositoryComponent, {
width: '400px',
height: '520px',
data: {
name: this.manifest.name,
}
});
}
ngOnInit(): void {
this.group.controls.name.setValue(this.manifest.name);
this.group.controls.targetPath.setValue(this.manifest.manifest.targetPath);
this.group.controls.metadataPath.setValue(this.manifest.manifest.metadataPath);
for (const [name, repo] of Object.entries(this.manifest.manifest.repositories)) {
this.addRepo(name, repo);
}
}
// once set to true, 'isPhase' and 'force' cannot be set to false using airshipctl's
// setters, so those controls won't be enabled if true. 'isPhase' can only be
// set to false by setting it to true for another repo
toggleLock(): void {
this.toggleControl(this.group.controls.targetPath as FormControl);
this.toggleControl(this.group.controls.metadataPath as FormControl);
for (const grp of this.repoArray.controls as FormGroup[]) {
Object.keys(grp.controls).forEach(key => {
this.toggleControl(grp.controls[key] as FormControl);
});
}
this.locked = !this.locked;
}
setManifest(): void {
const msg = new WsMessage(this.type, this.component, WsConstants.SET_MANIFEST);
msg.name = this.manifest.name;
toggleControl(ctrl: FormControl): void {
if (ctrl.disabled && ctrl.value !== true) {
ctrl.enable();
} else {
ctrl.disable();
}
}
// TODO(mfuller): since "Force" and "IsPhase" can only be set by passing in
// CLI flags rather than passing in values, there doesn't appear to be a way
// to unset them once they're true without manually editing the config file.
// Open a bug for this? Or is this intentional? I may have to write a custom
// setter to set the value directly in the Config struct
const opts: ManifestOptions = {
Name: this.Name.value,
RepoName: this.RepoName.value,
URL: this.URL.value,
Branch: this.Branch.value,
CommitHash: this.CommitHash.value,
Tag: this.Tag.value,
RemoteRef: this.RemoteRef.value,
Force: this.Force.value,
IsPhase: this.IsPhase.value,
SubPath: this.SubPath.value,
TargetPath: this.TargetPath.value,
MetadataPath: this.MetadataPath.value
};
setManifest(index: number): void {
const m = this.repoArray.at(index) as FormGroup;
const controls = m.controls;
if (controls !== undefined) {
const msg = new WsMessage(this.type, this.component, WsConstants.SET_MANIFEST);
msg.name = this.manifest.name;
msg.data = JSON.parse(JSON.stringify(opts));
this.websocketService.sendMessage(msg);
this.toggleLock();
const opts: ManifestOptions = {
Name: this.manifest.name,
RepoName: controls.repoName.value,
URL: controls.url.value,
Branch: null,
CommitHash: null,
Tag: null,
RemoteRef: null,
Force: controls.force.value,
IsPhase: controls.isPhase.value,
TargetPath: this.group.controls.targetPath.value,
MetadataPath: this.group.controls.metadataPath.value
};
opts[controls.checkoutLabel.value] = controls.checkoutReference.value;
msg.data = JSON.parse(JSON.stringify(opts));
this.websocketService.sendMessage(msg);
this.toggleLock();
}
}
}

View File

@@ -18,7 +18,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { RepositoryModule } from './repository/repository.module';
@NgModule({
imports: [
@@ -27,10 +28,11 @@ import { MatInputModule } from '@angular/material/input';
MatCardModule,
MatButtonModule,
ReactiveFormsModule,
MatCheckboxModule
],
declarations: [
MatCheckboxModule,
MatSelectModule,
RepositoryModule
],
declarations: [],
providers: []
})
export class ConfigManifestModule { }

View File

@@ -0,0 +1,21 @@
/*
# 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.
*/
mat-form-field {
width: 100%;
}
.set-btn {
margin-left: 5px;
}

View File

@@ -0,0 +1,29 @@
<h2>New Repository</h2>
<div [formGroup]="group">
<mat-form-field appearance="fill">
<mat-label>Repository Name</mat-label>
<input matInput formControlName="repoName">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>URL</mat-label>
<input matInput formControlName="url">
</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">
<input matInput formControlName="checkoutReference">
</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>
</div>
<div class="action-buttons">
<button mat-raised-button (click)="cancel()">Cancel</button>
<button class="set-btn" mat-raised-button color="primary" [disabled]="!group.valid" (click)="setRepo()">Set</button>
</div>

View File

@@ -0,0 +1,62 @@
/*
# 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 { RepositoryComponent } from './repository.component';
describe('RepositoryComponent', () => {
let component: RepositoryComponent;
let fixture: ComponentFixture<RepositoryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RepositoryComponent ],
imports: [
BrowserAnimationsModule,
FormsModule,
MatButtonModule,
MatCheckboxModule,
MatInputModule,
MatSelectModule,
ReactiveFormsModule,
MatDialogModule,
ToastrModule.forRoot()
],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {name: 'default'}},
{provide: MatDialogRef, useValue: {}}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,79 @@
/*
# 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 { Component, Inject, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { WsConstants, WsMessage } from 'src/services/ws/ws.models';
import { WsService } from 'src/services/ws/ws.service';
import { ManifestOptions } from '../../config.models';
@Component({
selector: 'app-repository',
templateUrl: './repository.component.html',
styleUrls: ['./repository.component.css']
})
export class RepositoryComponent implements OnInit{
group: FormGroup;
checkoutTypes = ['Branch', 'CommitHash', 'Tag'];
checkoutType = 'Branch';
constructor(
public dialogRef: MatDialogRef<RepositoryComponent>,
@Inject(MAT_DIALOG_DATA) public data: {
name: string,
},
private ws: WsService) {}
ngOnInit(): void {
this.group = new FormGroup({
repoName: new FormControl('', Validators.required),
url: new FormControl('', Validators.required),
checkoutReference: new FormControl('', Validators.required),
force: new FormControl(false),
isPhase: new FormControl(false)
});
}
cancel(): void {
this.dialogRef.close();
}
setRepo(): void {
const msg = new WsMessage(WsConstants.CTL, WsConstants.CONFIG, WsConstants.SET_MANIFEST);
msg.name = this.data.name;
const opts: ManifestOptions = {
Name: this.data.name,
RepoName: this.group.controls.repoName.value,
URL: this.group.controls.url.value,
Branch: null,
CommitHash: null,
Tag: null,
RemoteRef: null,
Force: this.group.controls.force.value,
IsPhase: this.group.controls.isPhase.value,
TargetPath: null,
MetadataPath: null
};
opts[this.checkoutType] = this.group.controls.checkoutReference.value;
msg.data = JSON.parse(JSON.stringify(opts));
this.ws.sendMessage(msg);
this.dialogRef.close();
}
}

View File

@@ -0,0 +1,36 @@
/*
# 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 { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { RepositoryComponent } from './repository.component';
@NgModule({
imports: [
FormsModule,
MatInputModule,
MatCheckboxModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule
],
declarations: [],
providers: []
})
export class RepositoryModule { }

View File

@@ -12,6 +12,8 @@
# limitations under the License.
*/
import { FormControl } from '@angular/forms';
export class EncryptionConfig {
name: string;
encryptionKeyPath: string;
@@ -101,7 +103,6 @@ export class ManifestOptions {
RemoteRef = '';
Force = false;
IsPhase = false;
SubPath = '';
TargetPath = '';
MetadataPath = '';
}

View File

@@ -30,6 +30,7 @@ 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';
import { RepositoryComponent } from './config-manifest/repository/repository.component';
@NgModule({
imports: [
@@ -52,7 +53,8 @@ import { MatSelectModule } from '@angular/material/select';
ConfigManagementComponent,
ConfigManifestComponent,
ConfigInitComponent,
ConfigNewComponent
ConfigNewComponent,
RepositoryComponent
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]