import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Validators } from '@angular/forms';
import { I18n } from '@ngx-translate/i18n-polyfill';
import { BsModalRef } from 'ngx-bootstrap/modal';
import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
import { FinishedTask } from '../../../shared/models/finished-task';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
@Component({
selector: 'cd-erasure-code-profile-form',
templateUrl: './erasure-code-profile-form.component.html',
styleUrls: ['./erasure-code-profile-form.component.scss']
})
export class ErasureCodeProfileFormComponent implements OnInit {
@Output()
submitAction = new EventEmitter();
form: CdFormGroup;
failureDomains: string[];
plugins: string[];
names: string[];
techniques: string[];
requiredControls: string[] = [];
devices: string[] = [];
tooltips = this.ecpService.formTooltips;
PLUGIN = {
LRC: 'lrc', // Locally Repairable Erasure Code
SHEC: 'shec', // Shingled Erasure Code
JERASURE: 'jerasure', // default
ISA: 'isa' // Intel Storage Acceleration
};
plugin = this.PLUGIN.JERASURE;
action: string;
resource: string;
constructor(
private formBuilder: CdFormBuilder,
public bsModalRef: BsModalRef,
private taskWrapper: TaskWrapperService,
private ecpService: ErasureCodeProfileService,
private i18n: I18n,
public actionLabels: ActionLabelsI18n
) {
this.action = this.actionLabels.CREATE;
this.resource = this.i18n('EC Profile');
this.createForm();
this.setJerasureDefaults();
}
createForm() {
this.form = this.formBuilder.group({
name: [
null,
[
Validators.required,
Validators.pattern('[A-Za-z0-9_-]+'),
CdValidators.custom(
'uniqueName',
(value) => this.names && this.names.indexOf(value) !== -1
)
]
],
plugin: [this.PLUGIN.JERASURE, [Validators.required]],
k: [1], // Will be replaced by plugin defaults
m: [1], // Will be replaced by plugin defaults
crushFailureDomain: ['host'],
crushRoot: ['default'], // default for all - is a list possible???
crushDeviceClass: [''], // set none to empty at submit - get list from configs?
directory: [''],
// Only for 'jerasure' and 'isa' use
technique: ['reed_sol_van'],
// Only for 'jerasure' use
packetSize: [2048, [Validators.min(1)]],
// Only for 'lrc' use
l: [1, [Validators.required, Validators.min(1)]],
crushLocality: [''], // set to none at the end (same list as for failure domains)
// Only for 'shec' use
c: [1, [Validators.required, Validators.min(1)]]
});
this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin));
}
onPluginChange(plugin) {
this.plugin = plugin;
if (plugin === this.PLUGIN.JERASURE) {
this.setJerasureDefaults();
} else if (plugin === this.PLUGIN.LRC) {
this.setLrcDefaults();
} else if (plugin === this.PLUGIN.ISA) {
this.setIsaDefaults();
} else if (plugin === this.PLUGIN.SHEC) {
this.setShecDefaults();
}
}
private setNumberValidators(name: string, required: boolean) {
const validators = [Validators.min(1)];
if (required) {
validators.push(Validators.required);
}
this.form.get(name).setValidators(validators);
}
private setKMValidators(required: boolean) {
['k', 'm'].forEach((name) => this.setNumberValidators(name, required));
}
private setJerasureDefaults() {
this.requiredControls = ['k', 'm'];
this.setDefaults({
k: 4,
m: 2
});
this.setKMValidators(true);
this.techniques = [
'reed_sol_van',
'reed_sol_r6_op',
'cauchy_orig',
'cauchy_good',
'liberation',
'blaum_roth',
'liber8tion'
];
}
private setLrcDefaults() {
this.requiredControls = ['k', 'm', 'l'];
this.setKMValidators(true);
this.setNumberValidators('l', true);
this.setDefaults({
k: 4,
m: 2,
l: 3
});
}
private setIsaDefaults() {
this.requiredControls = [];
this.setKMValidators(false);
this.setDefaults({
k: 7,
m: 3
});
this.techniques = ['reed_sol_van', 'cauchy'];
}
private setShecDefaults() {
this.requiredControls = [];
this.setKMValidators(false);
this.setDefaults({
k: 4,
m: 3,
c: 2
});
}
private setDefaults(defaults: object) {
Object.keys(defaults).forEach((controlName) => {
if (this.form.get(controlName).pristine) {
this.form.silentSet(controlName, defaults[controlName]);
}
});
}
ngOnInit() {
this.ecpService
.getInfo()
.subscribe(
({
failure_domains,
plugins,
names,
directory,
devices
}: {
failure_domains: string[];
plugins: string[];
names: string[];
directory: string;
devices: string[];
}) => {
this.failureDomains = failure_domains;
this.plugins = plugins;
this.names = names;
this.devices = devices;
this.form.silentSet('directory', directory);
}
);
}
private createJson() {
const pluginControls = {
technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE],
packetSize: [this.PLUGIN.JERASURE],
l: [this.PLUGIN.LRC],
crushLocality: [this.PLUGIN.LRC],
c: [this.PLUGIN.SHEC]
};
const ecp = new ErasureCodeProfile();
const plugin = this.form.getValue('plugin');
Object.keys(this.form.controls)
.filter((name) => {
const pluginControl = pluginControls[name];
const control = this.form.get(name);
const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl;
return (
usable &&
(control.dirty || this.requiredControls.includes(name)) &&
this.form.getValue(name)
);
})
.forEach((name) => {
this.extendJson(name, ecp);
});
return ecp;
}
private extendJson(name: string, ecp: ErasureCodeProfile) {
const differentApiAttributes = {
crushFailureDomain: 'crush-failure-domain',
crushRoot: 'crush-root',
crushDeviceClass: 'crush-device-class',
packetSize: 'packetsize',
crushLocality: 'crush-locality'
};
ecp[differentApiAttributes[name] || name] = this.form.getValue(name);
}
onSubmit() {
if (this.form.invalid) {
this.form.setErrors({ cdSubmitButton: true });
return;
}
const profile = this.createJson();
this.taskWrapper
.wrapTaskAroundCall({
task: new FinishedTask('ecp/create', { name: profile.name }),
call: this.ecpService.create(profile)
})
.subscribe(
undefined,
() => {
this.form.setErrors({ cdSubmitButton: true });
},
() => {
this.bsModalRef.hide();
this.submitAction.emit(profile);
}
);
}
}
<div class="modal-header">
<h4 i18n="form title|Example: Create Pool@@formTitle"
class="modal-title pull-left">{{ action | titlecase }} {{ resource | upperFirst }}</h4>
<button type="button"
class="close pull-right"
aria-label="Close"
(click)="bsModalRef.hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<form class="form-horizontal"
#frm="ngForm"
[formGroup]="form"
novalidate>
<div class="modal-body">
<div class="form-group"
[ngClass]="{'has-error': form.showError('name', frm)}">
<label for="name"
class="control-label col-sm-3">
<ng-container i18n>Name</ng-container>
<span class="required"></span>
</label>
<div class="col-sm-9">
<input type="text"
id="name"
name="name"
class="form-control"
placeholder="Name..."
formControlName="name"
autofocus>
<span class="help-block"
*ngIf="form.showError('name', frm, 'required')"
i18n>This field is required!</span>
<span class="help-block"
*ngIf="form.showError('name', frm, 'pattern')"
i18n>The name can only consist of alphanumeric characters, dashes and underscores.</span>
<span class="help-block"
*ngIf="form.showError('name', frm, 'uniqueName')"
i18n>The chosen erasure code profile name is already in use.</span>
</div>
</div>
<div class="form-group">
<label for="plugin"
class="control-label col-sm-3">
<ng-container i18n>Plugin</ng-container>
<span class="required"></span>
<cd-helper [html]="tooltips.plugins[plugin].description">
</cd-helper>
</label>
<div class="col-sm-9">
<select class="form-control"
id="plugin"
name="plugin"
formControlName="plugin">
<option *ngIf="!plugins"
ngValue=""
i18n>Loading...</option>
<option *ngFor="let plugin of plugins"
[ngValue]="plugin">
{{ plugin }}
</option>
</select>
<span class="help-block"
*ngIf="form.showError('name', frm, 'required')"
i18n>This field is required!</span>
</div>
</div>
<div class="form-group"
[ngClass]="{'has-error': form.showError('k', frm)}">
<label for="k"
class="control-label col-sm-3">
<ng-container i18n>Data chunks (k)</ng-container>
<span class="required"
*ngIf="requiredControls.includes('k')"></span>
<cd-helper [html]="tooltips.k">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="number"
id="k"
name="k"
class="form-control"
ng-model="$ctrl.erasureCodeProfile.k"
placeholder="Data chunks..."
formControlName="k">
<span class="help-block"
*ngIf="form.showError('k', frm, 'required')"
i18n>This field is required!</span>
<span class="help-block"
*ngIf="form.showError('k', frm, 'min')"
i18n>Must be equal to or greater than 2.</span>
</div>
</div>
<div class="form-group"
[ngClass]="{'has-error': form.showError('m', frm)}">
<label for="m"
class="control-label col-sm-3">
<ng-container i18n>Coding chunks (m)</ng-container>
<span class="required"
*ngIf="requiredControls.includes('m')"></span>
<cd-helper [html]="tooltips.m">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="number"
id="m"
name="m"
class="form-control"
placeholder="Coding chunks..."
formControlName="m">
<span class="help-block"
*ngIf="form.showError('m', frm, 'required')"
i18n>This field is required!</span>
<span class="help-block"
*ngIf="form.showError('m', frm, 'min')"
i18n>Must be equal to or greater than 1.</span>
</div>
</div>
<div class="form-group"
*ngIf="plugin === 'shec'"
[ngClass]="{'has-error': form.showError('c', frm)}">
<label for="c"
class="control-label col-sm-3">
<ng-container i18n>Durability estimator (c)</ng-container>
<cd-helper [html]="tooltips.plugins.shec.c">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="number"
id="c"
name="c"
class="form-control"
placeholder="Coding chunks..."
formControlName="c">
<span class="help-block"
*ngIf="form.showError('c', frm, 'min')"
i18n>Must be equal to or greater than 1.</span>
</div>
</div>
<div class="form-group"
*ngIf="plugin === PLUGIN.LRC"
[ngClass]="{'has-error': form.showError('l', frm)}">
<label for="l"
class="control-label col-sm-3">
<ng-container i18n>Locality (l)</ng-container>
<span class="required"></span>
<cd-helper [html]="tooltips.plugins.lrc.l">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="number"
id="l"
name="l"
class="form-control"
placeholder="Coding chunks..."
formControlName="l">
<span class="help-block"
*ngIf="form.showError('l', frm, 'required')"
i18n>This field is required!</span>
<span class="help-block"
*ngIf="form.showError('l', frm, 'min')"
i18n>Must be equal to or greater than 1.</span>
</div>
</div>
<div class="form-group">
<label for="crushFailureDomain"
class="control-label col-sm-3">
<ng-container i18n>Crush failure domain</ng-container>
<cd-helper [html]="tooltips.crushFailureDomain">
</cd-helper>
</label>
<div class="col-sm-9">
<select class="form-control"
id="crushFailureDomain"
name="crushFailureDomain"
formControlName="crushFailureDomain">
<option *ngIf="!failureDomains"
ngValue=""
i18n>Loading...</option>
<option *ngFor="let domain of failureDomains"
[ngValue]="domain">
{{ domain }}
</option>
</select>
</div>
</div>
<div class="form-group"
*ngIf="plugin === PLUGIN.LRC">
<label for="crushLocality"
class="control-label col-sm-3">
<ng-container i18n>Crush Locality</ng-container>
<cd-helper [html]="tooltips.plugins.lrc.crushLocality">
</cd-helper>
</label>
<div class="col-sm-9">
<select class="form-control"
id="crushLocality"
name="crushLocality"
formControlName="crushLocality">
<option *ngIf="!failureDomains"
ngValue=""
i18n>Loading...</option>
<option *ngIf="failureDomains && failureDomains.length > 0"
ngValue=""
i18n>None</option>
<option *ngFor="let domain of failureDomains"
[ngValue]="domain">
{{ domain }}
</option>
</select>
</div>
</div>
<div class="form-group"
*ngIf="[PLUGIN.JERASURE, PLUGIN.ISA].includes(plugin)">
<label for="technique"
class="control-label col-sm-3">
<ng-container i18n>Technique</ng-container>
<cd-helper [html]="tooltips.plugins[plugin].technique">
</cd-helper>
</label>
<div class="col-sm-9">
<select class="form-control"
id="technique"
name="technique"
formControlName="technique">
<option *ngFor="let technique of techniques"
[ngValue]="technique">
{{ technique }}
</option>
</select>
</div>
</div>
<div class="form-group"
*ngIf="plugin === PLUGIN.JERASURE"
[ngClass]="{'has-error': form.showError('packetSize', frm)}">
<label for="packetSize"
class="control-label col-sm-3">
<ng-container i18n>Packetsize</ng-container>
<cd-helper [html]="tooltips.plugins.jerasure.packetSize">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="number"
id="packetSize"
name="packetSize"
class="form-control"
placeholder="Packetsize..."
formControlName="packetSize">
<span class="help-block"
*ngIf="form.showError('packetSize', frm, 'min')"
i18n>Must be equal to or greater than 1.</span>
</div>
</div>
<div class="form-group"
[ngClass]="{'has-error': form.showError('crushRoot', frm)}">
<label for="crushRoot"
class="control-label col-sm-3">
<ng-container i18n>Crush root</ng-container>
<cd-helper [html]="tooltips.crushRoot">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="text"
id="crushRoot"
name="crushRoot"
class="form-control"
placeholder="root..."
formControlName="crushRoot">
</div>
</div>
<div class="form-group">
<label for="crushDeviceClass"
class="control-label col-sm-3">
<ng-container i18n>Crush device class</ng-container>
<cd-helper [html]="tooltips.crushDeviceClass">
</cd-helper>
</label>
<div class="col-sm-9">
<select class="form-control"
id="crushDeviceClass"
name="crushDeviceClass"
formControlName="crushDeviceClass">
<option ngValue=""
i18n>any</option>
<option *ngFor="let deviceClass of devices"
[ngValue]="deviceClass">
{{ deviceClass }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label for="directory"
class="control-label col-sm-3">
<ng-container i18n>Directory</ng-container>
<cd-helper [html]="tooltips.directory">
</cd-helper>
</label>
<div class="col-sm-9">
<input type="text"
id="directory"
name="directory"
class="form-control"
placeholder="Path..."
formControlName="directory">
</div>
</div>
</div>
<div class="modal-footer">
<cd-submit-button
(submitAction)="onSubmit()"
i18n="form action button|Example: Create Pool@@formActionButton"
[form]="frm">{{ action | titlecase }} {{ resource | upperFirst }}</cd-submit-button>
<cd-back-button [back]="bsModalRef.hide"></cd-back-button>
</div>
</form>