id="name"
type="text"
formControlName="name"
- placeholder="Add issue title"
+ placeholder="Profile name"
+ i18n-placeholder
/>
</cds-text-label>
<ng-template #nameError>
</div>
}
- <!-- Technique -->
- @if ([PLUGIN.JERASURE, PLUGIN.ISA, PLUGIN.CLAY].includes(plugin)) {
- <div class="form-item">
- <cds-select
- label="Technique"
- id="technique"
- formControlName="technique"
- [helperText]="tooltips.plugins[plugin]?.technique"
- i18n
- >
- @for (technique of techniques; track technique) {
- <option [value]="technique">
- {{ technique }}
- </option>
- }
- </cds-select>
- </div>
- }
-
- <!-- Packetsize -->
- @if (plugin === PLUGIN.JERASURE) {
- <div
- cdsrow
- class="form-item"
- >
- <cds-number
- id="packetSize"
- label="Packetsize"
- [helperText]="tooltips.plugins.jerasure.packetSize"
- [invalid]="form.controls.packetSize.invalid && form.controls.packetSize.dirty"
- [invalidText]="packetSizeError"
- formControlName="packetSize"
- min="1"
- i18n
- >
- </cds-number>
- <ng-template #packetSizeError>
- @if (form.showError('packetSize', formDir, 'min')) {
- <span
- class="invalid-feedback"
- i18n
- >
- Must be equal to or greater than 1.
- </span>
- }
- </ng-template>
- </div>
- }
-
- <!-- Crush root -->
- @if ([PLUGIN.JERASURE, PLUGIN.ISA, PLUGIN.CLAY].includes(plugin)) {
- <div class="form-item">
- <cds-dropdown
- label="Crush root"
- id="crushRoot"
- formControlName="crushRoot"
- [placeholder]="'Select crush root...'"
- [helperText]="tooltips.crushRoot"
- i18n-label
- >
- <cds-dropdown-list [items]="buckets"> </cds-dropdown-list>
- </cds-dropdown>
- </div>
- }
-
<!-- Crush device class -->
<div class="form-item">
<cds-select
<cd-helper [html]="tooltips.crushDeviceClass"> </cd-helper>
</ng-template>
</div>
-
- <!-- Directory -->
- <div class="form-item">
- <cds-text-label [helperText]="tooltips.directory">
- Directory
- <input
- cdsText
- id="directory"
- type="text"
- formControlName="directory"
- placeholder="Path..."
- />
- </cds-text-label>
- </div>
</form>
</div>
<cd-form-button-panel
let fixtureHelper: FixtureHelper;
let data: { plugins: string[]; names: string[]; nodes: CrushNode[] };
- const expectTechnique = (current: string) =>
- expect(component.form.getValue('technique')).toBe(current);
-
- const expectTechniques = (techniques: string[], current: string) => {
- expect(component.techniques).toEqual(techniques);
- expectTechnique(current);
- };
-
const expectRequiredControls = (controlNames: string[]) => {
controlNames.forEach((name) => {
const value = component.form.getValue(name);
const showDefaults = (plugin: string) => {
formHelper.setValue('plugin', plugin);
fixtureHelper.expectIdElementsVisible(
- ['name', 'plugin', 'k', 'm', 'crushFailureDomain', 'crushDeviceClass', 'directory'],
+ ['name', 'plugin', 'k', 'm', 'crushFailureDomain', 'crushDeviceClass'],
true
);
};
showDefaults('isa');
});
- it('should change technique to default if not available in other plugin', () => {
- formHelper.setValue('plugin', 'jerasure');
- expectTechnique('reed_sol_van');
- formHelper.setValue('technique', 'blaum_roth');
- expectTechnique('blaum_roth');
- formHelper.setValue('plugin', 'isa');
- expectTechnique('reed_sol_van');
- formHelper.setValue('plugin', 'clay');
- formHelper.expectValidChange('scalar_mds', 'shec');
- expectTechnique('single');
- });
-
describe(`for 'jerasure' plugin (default)`, () => {
beforeEach(() => {
formHelper.setValue('plugin', 'jerasure');
expectRequiredControls(['k', 'm']);
});
- it(`should show 'packetSize' and 'technique'`, () => {
- fixture.detectChanges();
- expect(component.form.get('packetSize')).toBeTruthy();
- expect(component.form.get('technique')).toBeTruthy();
- });
-
- it('should show available techniques', () => {
- expectTechniques(
- [
- 'reed_sol_van',
- 'reed_sol_r6_op',
- 'cauchy_orig',
- 'cauchy_good',
- 'liberation',
- 'blaum_roth',
- 'liber8tion'
- ],
- 'reed_sol_van'
- );
- });
-
it(`should not show any other plugin specific form control`, () => {
fixtureHelper.expectIdElementsVisible(
['c', 'l', 'crushLocality', 'd', 'scalar_mds'],
expectRequiredControls(['k', 'm']);
});
- it(`should show 'technique'`, () => {
- fixture.detectChanges();
- expect(component.form.get('technique')).toBeTruthy();
- });
-
- it('should show available techniques', () => {
- expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
- });
-
it(`should not show any other plugin specific form control`, () => {
fixtureHelper.expectIdElementsVisible(
- ['c', 'l', 'crushLocality', 'packetSize', 'd', 'scalar_mds'],
+ ['c', 'l', 'crushLocality', 'd', 'scalar_mds'],
false
);
});
it(`should not show any other plugin specific form control`, () => {
fixture.detectChanges();
// Be tolerant to layout differences; verify core LRC-hidden fields
- ['c', 'packetSize', 'd', 'scalar_mds'].forEach((id) => {
+ ['c', 'd', 'scalar_mds'].forEach((id) => {
expect(fixture.debugElement.query(By.css(`#${id}`))).toBeNull();
});
});
it(`should not show any other plugin specific form control`, () => {
fixture.detectChanges();
- // Technique can be present in some layouts; focus on SHEC-specific hidden fields
- ['l', 'crushLocality', 'packetSize', 'd', 'scalar_mds'].forEach((id) => {
+ // Focus on SHEC-specific hidden fields
+ ['l', 'crushLocality', 'd', 'scalar_mds'].forEach((id) => {
expect(fixture.debugElement.query(By.css(`#${id}`))).toBeNull();
});
});
});
it(`should not show any other plugin specific form control`, () => {
- fixtureHelper.expectIdElementsVisible(['l', 'crushLocality', 'packetSize', 'c'], false);
+ fixtureHelper.expectIdElementsVisible(['l', 'crushLocality', 'c'], false);
});
it('should show default values for d and scalar_mds', () => {
);
});
- it('should have specific techniques for scalar_mds jerasure', () => {
- expectTechniques(
- ['reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig', 'cauchy_good', 'liber8tion'],
- 'reed_sol_van'
- );
- });
-
- it('should have specific techniques for scalar_mds isa', () => {
- formHelper.expectValidChange('scalar_mds', 'isa');
- expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
- });
-
- it('should have specific techniques for scalar_mds shec', () => {
- formHelper.expectValidChange('scalar_mds', 'shec');
- expectTechniques(['single', 'multiple'], 'single');
- });
-
describe('Validity of d', () => {
beforeEach(() => {
// Don't automatically change d - the only way to get d invalid
beforeEach(() => {
ecp = new ErasureCodeProfile();
submittedEcp = new ErasureCodeProfile();
- submittedEcp['crush-root'] = 'default';
submittedEcp['crush-failure-domain'] = CrushFailureDomains.Host;
- submittedEcp['packetsize'] = 2048;
- submittedEcp['technique'] = 'reed_sol_van';
const taskWrapper = TestBed.inject(TaskWrapperService);
spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
expect(ecpService.create).not.toHaveBeenCalled();
});
- it('should be able to create a profile with m, k, name, directory and packetSize', () => {
- ecpChange('m', 3);
- ecpChange('directory', '/different/ecp/path');
- formHelper.setMultipleValues(ecp, true);
- formHelper.setValue('packetSize', 8192, true);
- ecpChange('packetsize', 8192);
- testCreation();
- });
-
it('should not send the profile with unsupported fields', () => {
formHelper.setMultipleValues(ecp, true);
formHelper.setValue('crushLocality', 'osd', true);
ecpChange('plugin', 'isa');
submittedEcp.k = 7;
submittedEcp.m = 3;
- delete submittedEcp.packetsize;
});
it('should be able to create a profile with only plugin and name', () => {
testCreation();
});
- it('should send profile with plugin, name, failure domain and technique only', () => {
- ecpChange('technique', 'cauchy');
+ it('should send profile with plugin, name and failure domain only', () => {
formHelper.setMultipleValues(ecp, true);
formHelper.setValue('crushFailureDomain', 'osd', true);
formHelper.setValue('crushDeviceClass', 'ssd', true);
submittedEcp['crush-device-class'] = 'ssd';
testCreation();
});
-
- it('should not send the profile with unsupported fields', () => {
- formHelper.setMultipleValues(ecp, true);
- formHelper.setValue('packetSize', 'osd', true);
- testCreation();
- });
});
describe(`'lrc' usage`, () => {
submittedEcp.k = 4;
submittedEcp.m = 2;
submittedEcp.l = 3;
- delete submittedEcp.packetsize;
- delete submittedEcp.technique;
});
it('should be able to create a profile with only required fields', () => {
testCreation();
});
- it('should send profile with all required fields and crush root and locality', () => {
+ it('should send profile with all required fields and crush locality', () => {
ecpChange('l', '6');
formHelper.setMultipleValues(ecp, true);
- formHelper.setValue(
- 'crushRoot',
- component.buckets.find((bucket) => bucket.name === 'mix-host'),
- true
- );
- submittedEcp['crush-root'] = 'mix-host';
+ formHelper.setValue('crushFailureDomain', 'osd-rack', true);
submittedEcp['crush-failure-domain'] = 'osd-rack';
formHelper.setValue('crushLocality', 'osd-rack', true);
submittedEcp['crush-locality'] = 'osd-rack';
submittedEcp.k = 4;
submittedEcp.m = 3;
submittedEcp.c = 2;
- delete submittedEcp.packetsize;
- delete submittedEcp.technique;
});
it('should be able to create a profile with only plugin and name', () => {
submittedEcp.m = 2;
submittedEcp.d = 5;
submittedEcp.scalar_mds = 'jerasure';
- delete submittedEcp.packetsize;
});
it('should be able to create a profile with only plugin and name', () => {
ecpChange('scalar_mds', 'shec');
formHelper.setMultipleValues(ecp, true);
submittedEcp.scalar_mds = 'shec';
- submittedEcp.technique = 'single';
testCreation();
});
Output,
ViewChild
} from '@angular/core';
-import { AbstractControl, FormGroupDirective, ValidatorFn, Validators } from '@angular/forms';
+import {
+ AbstractControl,
+ FormControl,
+ FormGroupDirective,
+ ValidatorFn,
+ Validators
+} from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
form: CdFormGroup;
plugins: string[];
names: string[];
- techniques: string[];
action: string;
resource: string;
dCalc: boolean;
lrcGroups: number;
lrcMultiK: number;
- selectedCrushRoot: CrushNode;
+ private rootControl = new FormControl(null);
public CrushFailureDomains = CrushFailureDomains;
CdValidators.number(false)
]
],
- crushRoot: null, // Will be preselected
crushDeviceClass: '', // Will be preselected
- directory: '',
- // Only for 'jerasure', 'clay' and 'isa' use
- technique: 'reed_sol_van',
- // Only for 'jerasure' use
- packetSize: [2048],
// Only for 'lrc' use
l: [
3, // Will be overwritten with plugin defaults
this.form.get('l').updateValueAndValidity({ emitEvent: false });
});
this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin));
- this.form.get('scalar_mds').valueChanges.subscribe(() => this.setClayDefaultsForScalar());
this.form.get('crushFailureDomain').valueChanges.subscribe(() => {
this.form.get('crushNumFailureDomains').updateValueAndValidity();
this.form.get('crushOsdsPerFailureDomain').updateValueAndValidity();
}
private setJerasureDefaults() {
- this.techniques = [
- 'reed_sol_van',
- 'reed_sol_r6_op',
- 'cauchy_orig',
- 'cauchy_good',
- 'liberation',
- 'blaum_roth',
- 'liber8tion'
- ];
this.setDefaults({
k: 4,
- m: 2,
- technique: 'reed_sol_van'
+ m: 2
});
}
* if they are not set, therefore it's fine to mark them as required in order to get
* strange values that weren't set.
*/
- this.techniques = ['reed_sol_van', 'cauchy'];
this.setDefaults({
k: 7,
- m: 3,
- technique: 'reed_sol_van'
+ m: 3
});
}
// d: 5, <- Will be automatically update to 5
scalar_mds: this.PLUGIN.JERASURE
});
- this.setClayDefaultsForScalar();
- }
-
- private setClayDefaultsForScalar() {
- const plugin = this.form.getValue('scalar_mds');
- let defaultTechnique = 'reed_sol_van';
- if (plugin === this.PLUGIN.JERASURE) {
- this.techniques = [
- 'reed_sol_van',
- 'reed_sol_r6_op',
- 'cauchy_orig',
- 'cauchy_good',
- 'liber8tion'
- ];
- } else if (plugin === this.PLUGIN.ISA) {
- this.techniques = ['reed_sol_van', 'cauchy'];
- } else {
- // this.PLUGIN.SHEC
- defaultTechnique = 'single';
- this.techniques = ['single', 'multiple'];
- }
- this.setDefaults({ technique: defaultTechnique });
}
private setDefaults(defaults: object) {
*/
const overwrite =
control.pristine ||
- (controlName === 'technique' && !this.techniques.includes(value)) ||
(controlName === 'k' && [4, 7].includes(value)) ||
(controlName === 'm' && [2, 3].includes(value));
if (overwrite) {
this.ecpService
.getInfo()
.subscribe(
- ({
- plugins,
- names,
- directory,
- nodes
- }: {
- plugins: string[];
- names: string[];
- directory: string;
- nodes: CrushNode[];
- }) => {
+ ({ plugins, names, nodes }: { plugins: string[]; names: string[]; nodes: CrushNode[] }) => {
this.initCrushNodeSelection(
nodes,
- this.form.get('crushRoot'),
+ this.rootControl,
this.form.get('crushFailureDomain'),
this.form.get('crushDeviceClass'),
false
);
this.plugins = plugins;
this.names = names;
- this.form.silentSet('directory', directory);
this.preValidateNumericInputFields();
-
- setTimeout(() => {
- const selectElement = document.getElementById('crushRoot') as any;
- if (selectElement) {
- selectElement.value = this.form.get('crushRoot').value;
- }
- this.cdr.detectChanges();
- }, 0);
+ this.cdr.detectChanges();
if (this.plugins.includes(this.PLUGIN.ISA)) {
this.setIsaDefaults();
this.form.setErrors({ cdSubmitButton: true });
},
complete: () => {
- this.closeModal();
this.submitAction.emit(profile);
+ this.closeModal();
}
});
}
private createJson() {
const pluginControls = {
- technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE, this.PLUGIN.CLAY],
- packetSize: [this.PLUGIN.JERASURE],
l: [this.PLUGIN.LRC],
crushLocality: [this.PLUGIN.LRC],
c: [this.PLUGIN.SHEC],
crushFailureDomain: 'crush-failure-domain',
crushNumFailureDomains: 'crush-num-failure-domains',
crushOsdsPerFailureDomain: 'crush-osds-per-failure-domain',
- crushRoot: 'crush-root',
crushDeviceClass: 'crush-device-class',
- packetSize: 'packetsize',
crushLocality: 'crush-locality'
};
const value = this.form.getValue(name);
- ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value;
+ ecp[differentApiAttributes[name] || name] = value;
}
onCrushFailureDomainChane() {
<cd-table-key-value
[renderObjects]="true"
[hideKeys]="['name']"
- [data]="selectedEcp"
+ [data]="getEcpProfileDetails()"
+ [preserveOrder]="true"
[autoReload]="false"
>
</cd-table-key-value>
fixtureHelper.expectIdElementsVisible(['erasureProfile', 'ecp-info-block'], true);
});
+ it('should order profile details consistently with the creation form', () => {
+ component.selectedEcp = {
+ name: 'ecp1',
+ plugin: 'lrc',
+ k: 4,
+ m: 2,
+ l: 3,
+ 'crush-failure-domain': 'host',
+ 'crush-locality': 'rack'
+ } as ErasureCodeProfile;
+ expect(component.getEcpProfileDetails()).toEqual([
+ ['Plugin', 'lrc'],
+ ['Data chunks (k)', 4],
+ ['Coding chunks (m)', 2],
+ ['Locality (l)', 3],
+ ['Crush failure domain', 'host'],
+ ['Crush locality', 'rack']
+ ]);
+ });
+
+ it('should only show plugin-eligible fields with values in profile details', () => {
+ component.selectedEcp = {
+ name: 'ecp1',
+ plugin: 'shec',
+ k: 4,
+ m: 3,
+ c: 2,
+ l: 3,
+ d: 5,
+ scalar_mds: 'jerasure',
+ 'crush-failure-domain': 'host',
+ 'crush-num-failure-domains': 0,
+ 'crush-osds-per-failure-domain': 0
+ } as ErasureCodeProfile;
+ expect(component.getEcpProfileDetails()).toEqual([
+ ['Plugin', 'shec'],
+ ['Data chunks (k)', 4],
+ ['Coding chunks (m)', 3],
+ ['Durability estimator (c)', 2],
+ ['Crush failure domain', 'host']
+ ]);
+ });
+
it('should select the newly created profile', () => {
spyOn(ecpService, 'list').and.callFake(() => of(infoReturn.erasure_code_profiles));
expect(form.getValue('erasureProfile')).toBe('ecp1');
const name = 'awesomeProfile';
const modalCdsService = TestBed.inject(ModalCdsService);
- // Mock the show method to return a mock component with submitAction
+ const ecp2 = new ErasureCodeProfile();
+ ecp2.name = name;
+ ecp2.plugin = 'isa';
+ ecp2.k = 4;
+ ecp2.m = 2;
spyOn(modalCdsService, 'show').and.returnValue({
- submitAction: of({ name })
+ submitAction: of(ecp2)
} as any);
+ infoReturn.erasure_code_profiles.push(ecp2);
+ component.addErasureCodeProfile();
+ expect(form.getValue('erasureProfile')).toBe(name);
+ expect(component.selectedEcp).toEqual(ecp2);
+ });
+
+ it('should select the newly created profile before it appears in the listing', () => {
+ const name = 'pendingProfile';
const ecp2 = new ErasureCodeProfile();
ecp2.name = name;
- infoReturn.erasure_code_profiles.push(ecp2);
+ ecp2.plugin = 'jerasure';
+ ecp2.k = 4;
+ ecp2.m = 2;
+ spyOn(ecpService, 'list').and.callFake(() => of([infoReturn.erasure_code_profiles[0]]));
+ const modalCdsService = TestBed.inject(ModalCdsService);
+ spyOn(modalCdsService, 'show').and.returnValue({
+ submitAction: of(ecp2)
+ } as any);
+ component.addErasureCodeProfile();
+ expect(form.getValue('erasureProfile')).toBe(name);
+ expect(component.selectedEcp).toEqual(ecp2);
+ expect(component.ecProfiles.some((profile) => profile.name === name)).toBe(true);
+ });
+
+ it('should update the dropdown immediately without waiting for the listing API', () => {
+ const name = 'instantProfile';
+ const ecp2 = new ErasureCodeProfile();
+ ecp2.name = name;
+ ecp2.plugin = 'isa';
+ ecp2.k = 4;
+ ecp2.m = 2;
+ let emitList: (value: ErasureCodeProfile[]) => void;
+ spyOn(ecpService, 'list').and.returnValue(
+ new Observable((subscriber) => {
+ emitList = (profiles) => {
+ subscriber.next(profiles);
+ subscriber.complete();
+ };
+ })
+ );
+ const modalCdsService = TestBed.inject(ModalCdsService);
+ spyOn(modalCdsService, 'show').and.returnValue({
+ submitAction: of(ecp2)
+ } as any);
+
component.addErasureCodeProfile();
- // Form stores erasureProfile as string name, not full object
+
+ expect(component.ecProfiles.some((profile) => profile.name === name)).toBe(true);
+ expect(form.getValue('erasureProfile')).toBe(name);
+ expect(component.selectedEcp).toEqual(ecp2);
+
+ emitList([infoReturn.erasure_code_profiles[0], ecp2]);
expect(form.getValue('erasureProfile')).toBe(name);
+ expect(component.ecProfiles.some((profile) => profile.name === name)).toBe(true);
});
describe('ecp deletion', () => {
let modalSpy: jasmine.Spy;
let submitActionObservable: () => Observable<any>;
+ const mockDeletionModalShow = () => {
+ const modalCdsService = TestBed.inject(ModalCdsService);
+ const existingShowSpy = modalCdsService.show as jasmine.Spy;
+ modalSpy = existingShowSpy?.and ? existingShowSpy : spyOn(modalCdsService, 'show');
+ modalSpy.and.callFake((_deletionClass: any, config: any) => {
+ submitActionObservable = config.submitActionObservable;
+ return {} as any;
+ });
+ };
+
const callEcpDeletion = () => {
component.deleteErasureCodeProfile();
submitActionObservable().subscribe();
};
beforeEach(() => {
- const modalCdsService = TestBed.inject(ModalCdsService);
- modalSpy = spyOn(modalCdsService, 'show').and.callFake(
- (_deletionClass: any, config: any) => {
- submitActionObservable = config.submitActionObservable;
- return {} as any;
- }
- );
- deleteSpy = spyOn(ecpService, 'delete').and.callFake((name: string) => {
+ mockDeletionModalShow();
+
+ const listSpy = ecpService.list as jasmine.Spy;
+ if (listSpy?.and) {
+ listSpy.and.callFake(() => of(infoReturn.erasure_code_profiles));
+ }
+
+ const existingDeleteSpy = ecpService.delete as jasmine.Spy;
+ deleteSpy = existingDeleteSpy?.and ? existingDeleteSpy : spyOn(ecpService, 'delete');
+ deleteSpy.and.callFake((name: string) => {
const profiles = infoReturn.erasure_code_profiles;
const index = _.findIndex(profiles, (profile) => profile.name === name);
profiles.splice(index, 1);
return of({ status: 202 });
});
+
taskWrapper = TestBed.inject(TaskWrapperService);
- spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ const wrapSpy = taskWrapper.wrapTaskAroundCall as jasmine.Spy;
+ if (wrapSpy?.and) {
+ wrapSpy.and.callThrough();
+ } else {
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ }
const ecp2 = new ErasureCodeProfile();
ecp2.name = 'someEcpName';
const ecp3 = new ErasureCodeProfile();
ecp3.name = 'aDifferentEcpName';
infoReturn.erasure_code_profiles.push(ecp3);
+
+ component.ecProfiles = [...infoReturn.erasure_code_profiles];
+ component.form.get('erasureProfile').enable({ emitEvent: false });
});
it('should delete two different erasure code profiles', () => {
};
}
+interface EcpDetailField {
+ label: string;
+ plugins?: string[];
+}
+
@Component({
selector: 'cd-pool-form',
templateUrl: './pool-form.component.html',
})
export class PoolFormComponent extends CdForm implements OnInit {
private static readonly DEFAULT_RULE_NAME = 'replicated_rule';
+ private static readonly ECP_DETAIL_FIELDS = new Map<string, EcpDetailField>([
+ ['plugin', { label: $localize`Plugin` }],
+ ['k', { label: $localize`Data chunks (k)` }],
+ ['m', { label: $localize`Coding chunks (m)` }],
+ ['c', { label: $localize`Durability estimator (c)`, plugins: ['shec'] }],
+ ['d', { label: $localize`Helper chunks (d)`, plugins: ['clay'] }],
+ ['l', { label: $localize`Locality (l)`, plugins: ['lrc'] }],
+ ['crush-failure-domain', { label: $localize`Crush failure domain` }],
+ ['crush-num-failure-domains', { label: $localize`Crush num failure domain` }],
+ ['crush-osds-per-failure-domain', { label: $localize`Crush osds per failure domain` }],
+ ['crush-locality', { label: $localize`Crush locality`, plugins: ['lrc'] }],
+ ['scalar_mds', { label: $localize`Scalar mds`, plugins: ['clay'] }],
+ ['crush-device-class', { label: $localize`Crush device class` }]
+ ]);
@ViewChild('crushInfoTabs') crushInfoTabs: NgbNav;
@ViewChild('ecpInfoTabs') ecpInfoTabs: NgbNav;
this.initEcp(info.erasure_code_profiles);
}
- private initEcp(ecProfiles: ErasureCodeProfile[]) {
- this.setListControlStatus('erasureProfile', ecProfiles);
- this.ecProfiles = ecProfiles;
- this.erasureProfileChange();
+ private initEcp(ecProfiles: ErasureCodeProfile[], selectedName?: string) {
+ this.ecProfiles = [...ecProfiles];
+ this.setListControlStatus('erasureProfile', ecProfiles, selectedName);
+ this.erasureProfileChange(selectedName);
}
/**
* If more than one rule or profile exists the listing will be enabled,
* otherwise disabled.
*/
- private setListControlStatus(controlName: string, arr: any[]) {
+ private setListControlStatus(controlName: string, arr: any[], selectedName?: string) {
const control = this.form.get(controlName);
const value = control.value;
if (controlName === 'erasureProfile') {
- if (arr.length === 1 && (!value || !_.isEqual(value, arr[0]))) {
+ if (selectedName && arr.some((profile) => profile.name === selectedName)) {
+ control.setValue(selectedName);
+ } else if (arr.length === 1 && (!value || !_.isEqual(value, arr[0]))) {
control.setValue(arr[0].name);
} else if (arr.length === 0 && value) {
control.setValue(null);
}
addErasureCodeProfile() {
- this.addModal(ErasureCodeProfileFormModalComponent, (name) => this.reloadECPs(name));
+ this.addModal(ErasureCodeProfileFormModalComponent, (profile: ErasureCodeProfile) =>
+ this.reloadECPs(profile)
+ );
}
- private addModal(modalComponent: Type<any>, reload: (name: string) => void) {
+ private addModal(modalComponent: Type<any>, reload: (item: any) => void) {
this.hideOpenTooltips();
const modalRef = this.modalService.show(modalComponent);
modalRef.submitAction.subscribe((item: any) => {
- reload(item.name);
+ reload(item);
});
}
this.data.erasureInfo = false;
}
- private reloadECPs(profileName?: string) {
+ private reloadECPs(profileOrName?: ErasureCodeProfile | string) {
+ const profileName = typeof profileOrName === 'string' ? profileOrName : profileOrName?.name;
+ const createdProfile = typeof profileOrName === 'object' ? profileOrName : undefined;
+
+ if (createdProfile) {
+ this.applyCreatedEcp(createdProfile);
+ }
+
this.reloadList({
newItemName: profileName,
getInfo: () => this.ecpService.list(),
- initInfo: (profiles) => this.initEcp(profiles),
- findNewItem: () => this.ecProfiles.find((p: ErasureCodeProfile) => p.name === profileName),
+ initInfo: (profiles: ErasureCodeProfile[]) => {
+ if (createdProfile && !profiles.some((profile) => profile.name === createdProfile.name)) {
+ profiles = [...profiles, createdProfile];
+ }
+ this.initEcp(profiles, profileName);
+ },
+ findNewItem: () =>
+ this.ecProfiles.find((profile: ErasureCodeProfile) => profile.name === profileName) ||
+ createdProfile,
controlName: 'erasureProfile',
nameAttribute: 'name'
});
}
+ private applyCreatedEcp(profile: ErasureCodeProfile) {
+ let profiles = [...(this.ecProfiles || [])];
+ if (!profiles.some((existing) => existing.name === profile.name)) {
+ profiles = [...profiles, profile];
+ }
+ this.ecProfiles = profiles;
+
+ const control = this.form.get('erasureProfile');
+ control?.enable({ emitEvent: false });
+ control?.setValue(profile.name, { emitEvent: true });
+ this.erasureProfileChange(profile.name);
+ this.cdr.detectChanges();
+ }
+
private reloadList({
newItemName,
getInfo,
initInfo,
findNewItem,
controlName,
- nameAttribute
+ nameAttribute,
+ afterSelect
}: {
newItemName: string;
getInfo: () => Observable<any>;
findNewItem: () => any;
controlName: string;
nameAttribute?: string;
+ afterSelect?: (value: any) => void;
}) {
if (this.modalSubscription) {
this.modalSubscription.unsubscribe();
getInfo().subscribe((items: any) => {
initInfo(items);
if (!newItemName) {
+ this.cdr.detectChanges();
return;
}
const item = findNewItem();
if (item) {
+ const control = this.form.get(controlName);
const value = nameAttribute ? item[nameAttribute] : item;
- this.form.get(controlName)?.setValue(value);
+ control?.enable({ emitEvent: false });
+ control?.setValue(value, { emitEvent: true });
+ afterSelect?.(value);
}
+ this.cdr.detectChanges();
});
}
}
addCrushRule() {
- this.addModal(CrushRuleFormModalComponent, (name) => this.reloadCrushRules(name));
+ this.addModal(CrushRuleFormModalComponent, (item) => this.reloadCrushRules(item.name));
}
private reloadCrushRules(ruleName?: string) {
this.form.get('name').updateValueAndValidity({ emitEvent: false, onlySelf: true });
}
+ getEcpProfileDetails(): [string, any][] {
+ if (!this.selectedEcp) {
+ return [];
+ }
+ const plugin = this.selectedEcp.plugin;
+ const details: [string, any][] = [];
+
+ PoolFormComponent.ECP_DETAIL_FIELDS.forEach(({ label, plugins }, key) => {
+ if (!this.isEcpDetailFieldEligible(plugins, plugin)) {
+ return;
+ }
+ const value = this.selectedEcp[key];
+ if (!this.isEcpDetailValueVisible(key, value)) {
+ return;
+ }
+ details.push([label, value]);
+ });
+
+ return details;
+ }
+
+ private isEcpDetailFieldEligible(plugins: string[] | undefined, plugin: string): boolean {
+ return !plugins || plugins.includes(plugin);
+ }
+
+ private isEcpDetailValueVisible(key: string, value: any): boolean {
+ if (value === undefined || value === null || value === '') {
+ return false;
+ }
+ if (
+ (key === 'crush-num-failure-domains' || key === 'crush-osds-per-failure-domain') &&
+ value === 0
+ ) {
+ return false;
+ }
+ return true;
+ }
+
erasureProfileChange(selectedName?: string) {
if (!this.ecProfiles || this.ecProfiles.length === 0) {
return;
expect(component.tableData).toEqual([{ key: 'b', value: 2 }]);
});
+ it('should preserve the order of array data when requested', () => {
+ component.data = [
+ ['Plugin', 'isa'],
+ ['Data chunks (k)', 4],
+ ['Coding chunks (m)', 2]
+ ];
+ component.preserveOrder = true;
+ component.ngOnInit();
+ expect(component.tableData).toEqual([
+ { key: 'Plugin', value: 'isa' },
+ { key: 'Data chunks (k)', value: 4 },
+ { key: 'Coding chunks (m)', value: 2 }
+ ]);
+ });
+
it('should remove items with objects as values', () => {
component.data = [
[3, 'something'],
showMultiLineText = false; // If true, the value field will use a template that supports multi line text
@Input()
multilineTextKeys: string[]; // If set, the value field will use a template that supports multi line text for this key
+ @Input()
+ preserveOrder = false;
// If set, the classAddingTpl is used to enable different css for different values
@Input()
return item;
})
.filter((i) => i.value !== null);
- return _.sortBy(this.renderObjects ? this.insertFlattenObjects(result) : result, 'key');
+ const processed = this.renderObjects ? this.insertFlattenObjects(result) : result;
+ return this.preserveOrder ? processed : _.sortBy(processed, 'key');
}
private makePairsFromArray(data: any[]): KeyValueItem[] {