},
{
path: 'osd',
- component: OsdListComponent,
canActivate: [AuthGuardService],
- data: { breadcrumbs: 'Cluster/OSDs' }
+ canActivateChild: [AuthGuardService],
+ data: { breadcrumbs: 'Cluster/OSDs' },
+ children: [
+ {
+ path: '',
+ component: OsdListComponent
+ }
+ ]
},
{
path: 'configuration',
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
+import { AlertModule } from 'ngx-bootstrap/alert';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { SharedModule } from '../../shared/shared.module';
import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
import { OsdFlagsModalComponent } from './osd/osd-flags-modal/osd-flags-modal.component';
import { OsdListComponent } from './osd/osd-list/osd-list.component';
import { OsdPerformanceHistogramComponent } from './osd/osd-performance-histogram/osd-performance-histogram.component';
+import { OsdReweightModalComponent } from './osd/osd-reweight-modal/osd-reweight-modal.component';
import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.component';
@NgModule({
- entryComponents: [OsdDetailsComponent, OsdScrubModalComponent, OsdFlagsModalComponent],
+ entryComponents: [
+ OsdDetailsComponent,
+ OsdScrubModalComponent,
+ OsdFlagsModalComponent,
+ OsdReweightModalComponent
+ ],
imports: [
CommonModule,
PerformanceCounterModule,
FormsModule,
ReactiveFormsModule,
BsDropdownModule.forRoot(),
- ModalModule.forRoot()
+ ModalModule.forRoot(),
+ AlertModule.forRoot(),
+ TooltipModule.forRoot()
],
declarations: [
HostsComponent,
OsdFlagsModalComponent,
HostDetailsComponent,
ConfigurationDetailsComponent,
- ConfigurationFormComponent
+ ConfigurationFormComponent,
+ OsdReweightModalComponent
]
})
export class ClusterModule {}
i18n>
{{ patternHelpText }}
</span>
+ <span class="help-block"
+ *ngIf="configForm.showError(section, formDir, 'invalidUuid')"
+ i18n>
+ {{ patternHelpText }}
+ </span>
<span class="help-block"
*ngIf="configForm.showError(section, formDir, 'max')"
i18n>
selectionType="single"
(updateSelection)="updateSelection($event)"
[updateSelectionOnRefresh]="'never'">
- <div class="table-actions btn-toolbar" *ngIf="permission.update">
+ <div class="table-actions btn-toolbar">
<cd-table-actions [permission]="permission"
[selection]="selection"
- onlyDropDown="Perform Task"
class="btn-group"
[tableActions]="tableActions">
</cd-table-actions>
</cd-grafana>
</tab>
</tabset>
+
+<ng-template #osdUsageTpl
+ let-row="row">
+ <cd-usage-bar [totalBytes]="row.stats.stat_bytes"
+ [usedBytes]="row.stats.stat_bytes_used"></cd-usage-bar>
+</ng-template>
+
+<ng-template #markOsdConfirmationTpl i18n let-markActionDescription="markActionDescription">
+ <strong>OSD {{ selection.first().id }}</strong> will be marked
+ <strong>{{ markActionDescription }}</strong> if you proceed.
+</ng-template>
+
+<ng-template #criticalConfirmationTpl
+ i18n
+ let-safeToDestroyResult="result"
+ let-actionDescription="actionDescription">
+ <div *ngIf="!safeToDestroyResult['safe-to-destroy']"
+ class="danger">
+ <cd-warning-panel>
+ {{ safeToDestroyResult.message }}
+ </cd-warning-panel>
+ </div>
+ <strong>OSD {{ selection.first().id }}</strong> will be
+ <strong>{{ actionDescription }}</strong> if you proceed.
+</ng-template>
import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DebugElement } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
+import { BsModalService } from 'ngx-bootstrap/modal';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { EMPTY, of } from 'rxjs';
import { configureTestBed, PermissionHelper } from '../../../../../testing/unit-test-helper';
+import { OsdService } from '../../../../shared/api/osd.service';
+import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permissions } from '../../../../shared/models/permissions';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
import { SharedModule } from '../../../../shared/shared.module';
import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
import { OsdDetailsComponent } from '../osd-details/osd-details.component';
import { OsdPerformanceHistogramComponent } from '../osd-performance-histogram/osd-performance-histogram.component';
+import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
import { OsdListComponent } from './osd-list.component';
describe('OsdListComponent', () => {
let component: OsdListComponent;
let fixture: ComponentFixture<OsdListComponent>;
+ let modalServiceShowSpy: jasmine.Spy;
const fakeAuthStorageService = {
getPermissions: () => {
PerformanceCounterModule,
TabsModule.forRoot(),
SharedModule,
+ ReactiveFormsModule,
RouterTestingModule
],
declarations: [OsdListComponent, OsdDetailsComponent, OsdPerformanceHistogramComponent],
- providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
+ providers: [
+ { provide: AuthStorageService, useValue: fakeAuthStorageService },
+ TableActionsComponent,
+ BsModalService
+ ]
});
beforeEach(() => {
fixture = TestBed.createComponent(OsdListComponent);
+ fixture.detectChanges();
component = fixture.componentInstance;
+ modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
});
+ const setFakeSelection = () => {
+ // Default data and selection
+ const selection = [{ id: 1 }];
+ const data = [{ id: 1 }];
+
+ // Table data and selection
+ component.selection = new CdTableSelection();
+ component.selection.selected = selection;
+ component.selection.update();
+ component.osds = data;
+ };
+
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
getTableActionComponent()
);
scenario = {
- fn: () => tableActions.getCurrentButton(),
- single: undefined,
- empty: undefined
+ fn: () => tableActions.getCurrentButton().name,
+ single: 'Scrub',
+ empty: 'Scrub'
};
tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1);
});
- it('shows no action button', () => permissionHelper.testScenarios(scenario));
+ it('shows action button', () => permissionHelper.testScenarios(scenario));
it('shows all actions', () => {
- expect(tableActions.tableActions.length).toBe(2);
+ expect(tableActions.tableActions.length).toBe(9);
expect(tableActions.tableActions).toEqual(component.tableActions);
});
+ });
+
+ describe('test table actions in submenu', () => {
+ beforeEach(
+ fakeAsync(() => {
+ // The menu needs a click to render the dropdown!
+ const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
+ dropDownToggle.triggerEventHandler('click', null);
+ tick();
+ fixture.detectChanges();
+ })
+ );
+
+ /**
+ * Helper function to retrieve menu item
+ * @param selector
+ */
+ const getMenuItem = (selector: string): DebugElement => {
+ return fixture.debugElement
+ .query(By.directive(TableActionsComponent))
+ .query(By.css(selector));
+ };
+
+ it('has menu entries disabled for entries without create permission', () => {
+ component.tableActions
+ .filter((tableAction) => tableAction.permission !== 'create')
+ .map((tableAction) => tableAction.name)
+ .map(TestBed.get(TableActionsComponent).toClassName)
+ .map((className) => getMenuItem(`.${className}`))
+ .forEach((debugElement) => {
+ expect(debugElement.classes.disabled).toBe(true);
+ });
+ });
+ });
+
+ describe('tests if all modals are opened correctly', () => {
+ /**
+ * Helper function to check if a function opens a modal
+ * @param fn
+ * @param modalClass - The expected class of the modal
+ */
+ const expectOpensModal = (fn, modalClass): void => {
+ setFakeSelection();
+ fn();
+
+ expect(modalServiceShowSpy.calls.any()).toBe(true, 'modalService.show called');
+ expect(modalServiceShowSpy.calls.first()).toBeTruthy();
+ expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
+
+ modalServiceShowSpy.calls.reset();
+ };
+
+ it('opens the appropriate modal', () => {
+ expectOpensModal(() => component.reweight(), OsdReweightModalComponent);
+ expectOpensModal(() => component.markOut(), ConfirmationModalComponent);
+ expectOpensModal(() => component.markIn(), ConfirmationModalComponent);
+ expectOpensModal(() => component.markDown(), ConfirmationModalComponent);
+
+ // The following modals are called after the information about their
+ // safety to destroy/remove/mark them lost has been retrieved, hence
+ // we will have to fake its request to be able to open those modals.
+ spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
+ of({ 'safe-to-destroy': true })
+ );
+
+ expectOpensModal(() => component.markLost(), CriticalConfirmationModalComponent);
+ expectOpensModal(() => component.remove(), CriticalConfirmationModalComponent);
+ expectOpensModal(() => component.destroy(), CriticalConfirmationModalComponent);
+ });
+ });
+
+ describe('tests if the correct methods are called on confirmation', () => {
+ const expectOsdServiceMethodCalled = (fn: Function, osdServiceMethodName: string): void => {
+ setFakeSelection();
+ const osdServiceSpy = spyOn(TestBed.get(OsdService), osdServiceMethodName).and.callFake(
+ () => EMPTY
+ );
+
+ modalServiceShowSpy.calls.reset();
+ fn(); // calls show on BsModalService
+ // Calls onSubmit given to `bsModalService.show()`
+ const initialState = modalServiceShowSpy.calls.first().args[1].initialState;
+ const action = initialState.onSubmit || initialState.submitAction;
+ action.call(component);
+
+ expect(osdServiceSpy.calls.count()).toBe(1);
+ expect(osdServiceSpy.calls.first().args[0]).toBe(1);
+ modalServiceShowSpy.calls.reset();
+ osdServiceSpy.calls.reset();
+ };
+
+ it('calls the corresponding service methods', () => {
+ // Purposely `reweight`
+ expectOsdServiceMethodCalled(() => component.markOut(), 'markOut');
+ expectOsdServiceMethodCalled(() => component.markIn(), 'markIn');
+ expectOsdServiceMethodCalled(() => component.markDown(), 'markDown');
+
+ spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
+ of({ 'safe-to-destroy': true })
+ );
- it(`shows 'Perform task' as drop down`, () => {
- expect(
- fixture.debugElement.query(By.directive(TableActionsComponent)).query(By.css('button'))
- .nativeElement.textContent
- ).toBe('Perform Task');
+ expectOsdServiceMethodCalled(() => component.markLost(), 'markLost');
+ expectOsdServiceMethodCalled(() => component.remove(), 'remove');
+ expectOsdServiceMethodCalled(() => component.destroy(), 'destroy');
});
});
});
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
+import { Observable } from 'rxjs';
import { OsdService } from '../../../../shared/api/osd.service';
import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { TableComponent } from '../../../../shared/datatable/table/table.component';
import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
import { CdTableAction } from '../../../../shared/models/cd-table-action';
import { DimlessBinaryPipe } from '../../../../shared/pipes/dimless-binary.pipe';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
import { OsdFlagsModalComponent } from '../osd-flags-modal/osd-flags-modal.component';
+import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
import { OsdScrubModalComponent } from '../osd-scrub-modal/osd-scrub-modal.component';
@Component({
statusColor: TemplateRef<any>;
@ViewChild('osdUsageTpl')
osdUsageTpl: TemplateRef<any>;
+ @ViewChild('markOsdConfirmationTpl')
+ markOsdConfirmationTpl: TemplateRef<any>;
+ @ViewChild('criticalConfirmationTpl')
+ criticalConfirmationTpl: TemplateRef<any>;
@ViewChild(TableComponent)
tableComponent: TableComponent;
+ @ViewChild('reweightBodyTpl')
+ reweightBodyTpl: TemplateRef<any>;
+ @ViewChild('safeToDestroyBodyTpl')
+ safeToDestroyBodyTpl: TemplateRef<any>;
permission: Permission;
tableActions: CdTableAction[];
bsModalRef: BsModalRef;
- osds = [];
columns: CdTableColumn[];
+
+ osds = [];
selection = new CdTableSelection();
protected static collectStates(osd) {
- return [
- osd['in'] ? 'in' : 'out',
- osd['up'] ? 'up' : 'out',
- ];
+ return [osd['in'] ? 'in' : 'out', osd['up'] ? 'up' : 'down'];
}
constructor(
private modalService: BsModalService
) {
this.permission = this.authStorageService.getPermissions().osd;
- const scrubAction: CdTableAction = {
- permission: 'update',
- icon: 'fa-stethoscope',
- click: () => this.scrubAction(false),
- name: 'Scrub'
- };
- const deleteAction: CdTableAction = {
- permission: 'update',
- icon: 'fa-cog',
- click: () => this.scrubAction(true),
- name: 'Deep Scrub'
- };
- this.tableActions = [scrubAction, deleteAction];
+ this.tableActions = [
+ {
+ name: 'Scrub',
+ permission: 'update',
+ icon: 'fa-stethoscope',
+ click: () => this.scrubAction(false),
+ disable: () => !this.hasOsdSelected
+ },
+ {
+ name: 'Deep Scrub',
+ permission: 'update',
+ icon: 'fa-cog',
+ click: () => this.scrubAction(true),
+ disable: () => !this.hasOsdSelected
+ },
+ {
+ name: 'Reweight',
+ permission: 'update',
+ click: () => this.reweight(),
+ disable: () => !this.hasOsdSelected,
+ icon: 'fa-balance-scale'
+ },
+ {
+ name: 'Mark Out',
+ permission: 'update',
+ click: () => this.markOut(),
+ disable: () => this.isNotSelectedOrInState('out'),
+ icon: 'fa-arrow-left'
+ },
+ {
+ name: 'Mark In',
+ permission: 'update',
+ click: () => this.markIn(),
+ disable: () => this.isNotSelectedOrInState('in'),
+ icon: 'fa-arrow-right'
+ },
+ {
+ name: 'Mark Down',
+ permission: 'update',
+ click: () => this.markDown(),
+ disable: () => this.isNotSelectedOrInState('down'),
+ icon: 'fa-arrow-down'
+ },
+ {
+ name: 'Mark lost',
+ permission: 'delete',
+ click: () => this.markLost(),
+ disable: () => this.isNotSelectedOrInState('up'),
+ icon: 'fa-unlink'
+ },
+ {
+ name: 'Remove',
+ permission: 'delete',
+ click: () => this.remove(),
+ disable: () => this.isNotSelectedOrInState('up'),
+ icon: 'fa-remove'
+ },
+ {
+ name: 'Destroy',
+ permission: 'delete',
+ click: () => this.destroy(),
+ disable: () => this.isNotSelectedOrInState('up'),
+ icon: 'fa-eraser'
+ }
+ ];
}
ngOnInit() {
];
}
+ get hasOsdSelected() {
+ if (this.selection.hasSelection) {
+ const osdId = this.selection.first().id;
+ const osd = this.osds.filter((o) => o.id === osdId).pop();
+ return !!osd;
+ }
+ return false;
+ }
+
updateSelection(selection: CdTableSelection) {
this.selection = selection;
}
+ /**
+ * Returns true if no row is selected or if the selected row is in the given
+ * state. Useful for deactivating the corresponding menu entry.
+ */
+ isNotSelectedOrInState(state: 'in' | 'up' | 'down' | 'out'): boolean {
+ if (!this.hasOsdSelected) {
+ return true;
+ }
+
+ const osdId = this.selection.first().id;
+ const osd = this.osds.filter((o) => o.id === osdId).pop();
+
+ if (!osd) {
+ // `osd` is undefined if the selected OSD has been removed.
+ return true;
+ }
+
+ switch (state) {
+ case 'in':
+ return osd.in === 1;
+ case 'out':
+ return osd.in !== 1;
+ case 'down':
+ return osd.up !== 1;
+ case 'up':
+ return osd.up === 1;
+ }
+ }
+
getOsdList() {
this.osdService.getList().subscribe((data: any[]) => {
this.osds = data;
}
scrubAction(deep) {
- if (!this.tableComponent.selection.hasSelection) {
+ if (!this.hasOsdSelected) {
return;
}
configureClusterAction() {
this.bsModalRef = this.modalService.show(OsdFlagsModalComponent, {});
}
+
+ showConfirmationModal(markAction: string, onSubmit: (id: number) => Observable<any>) {
+ this.bsModalRef = this.modalService.show(ConfirmationModalComponent, {
+ initialState: {
+ titleText: `Mark OSD ${markAction}`,
+ buttonText: `Mark ${markAction}`,
+ bodyTpl: this.markOsdConfirmationTpl,
+ bodyContext: {
+ markActionDescription: markAction
+ },
+ onSubmit: () => {
+ onSubmit
+ .call(this.osdService, this.selection.first().id)
+ .subscribe(() => this.bsModalRef.hide());
+ }
+ }
+ });
+ }
+
+ markOut() {
+ this.showConfirmationModal('out', this.osdService.markOut);
+ }
+
+ markIn() {
+ this.showConfirmationModal('in', this.osdService.markIn);
+ }
+
+ markDown() {
+ this.showConfirmationModal('down', this.osdService.markDown);
+ }
+
+ reweight() {
+ const selectedOsd = this.osds.filter((o) => o.id === this.selection.first().id).pop();
+ this.modalService.show(OsdReweightModalComponent, {
+ initialState: {
+ currentWeight: selectedOsd.weight,
+ osdId: selectedOsd.id
+ }
+ });
+ }
+
+ showCriticalConfirmationModal(
+ actionDescription: string,
+ itemDescription: string,
+ templateItemDescription: string,
+ action: (id: number) => Observable<any>
+ ): void {
+ this.osdService.safeToDestroy(this.selection.first().id).subscribe((result) => {
+ const modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ initialState: {
+ actionDescription: actionDescription,
+ itemDescription: itemDescription,
+ bodyTemplate: this.criticalConfirmationTpl,
+ bodyContext: {
+ result: result,
+ actionDescription: templateItemDescription
+ },
+ submitAction: () => {
+ action
+ .call(this.osdService, this.selection.first().id)
+ .subscribe(() => modalRef.hide());
+ }
+ }
+ });
+ });
+ }
+
+ markLost() {
+ this.showCriticalConfirmationModal('Mark', 'OSD lost', 'marked lost', this.osdService.markLost);
+ }
+
+ remove() {
+ this.showCriticalConfirmationModal('Remove', 'OSD', 'removed', this.osdService.remove);
+ }
+
+ destroy() {
+ this.showCriticalConfirmationModal('destroy', 'OSD', 'destroyed', this.osdService.destroy);
+ }
}
--- /dev/null
+<cd-modal [modalRef]="bsModalRef">
+ <ng-container class="modal-title"
+ i18n>
+ Reweight OSD
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form class="form-horizontal"
+ [formGroup]="reweightForm">
+ <div class="modal-body" [ngClass]="{'has-error': weight.errors}">
+ <div class="row">
+ <label for="weight" class="col-sm-2 control-label">Weight</label>
+ <div class="col-sm-10">
+ <input id="weight" class="form-control" type="number"
+ step="0.1" formControlName="weight" min="0" max="1"
+ [value]="currentWeight">
+ <span i18n
+ class="help-block"
+ *ngIf="weight.errors">
+ <span *ngIf="weight.errors?.required" i18n>
+ This field is required.
+ </span>
+ <span *ngIf="weight.errors?.max || weight.errors?.min" i18n>
+ The value needs to be between 0 and 1.
+ </span>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-submit-button (submitAction)="reweight()"
+ [form]="reweightForm"
+ [disabled]="reweightForm.invalid"
+ i18n>
+ Reweight
+ </cd-submit-button>
+
+ <button class="btn btn-link btn-sm"
+ (click)="bsModalRef.hide()"
+ i18n>
+ Cancel
+ </button>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { BsModalRef } from 'ngx-bootstrap/modal';
+import { of } from 'rxjs';
+
+import { configureTestBed } from '../../../../../testing/unit-test-helper';
+import { OsdService } from '../../../../shared/api/osd.service';
+import { ModalComponent } from '../../../../shared/components/modal/modal.component';
+import { SubmitButtonComponent } from '../../../../shared/components/submit-button/submit-button.component';
+import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder';
+import { OsdReweightModalComponent } from './osd-reweight-modal.component';
+
+describe('OsdReweightModalComponent', () => {
+ let component: OsdReweightModalComponent;
+ let fixture: ComponentFixture<OsdReweightModalComponent>;
+
+ configureTestBed({
+ imports: [ReactiveFormsModule, HttpClientTestingModule],
+ declarations: [OsdReweightModalComponent, ModalComponent, SubmitButtonComponent],
+ providers: [OsdService, BsModalRef, CdFormBuilder]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdReweightModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call OsdService::reweight() on submit', () => {
+ component.osdId = 1;
+ component.reweightForm.get('weight').setValue(0.5);
+
+ const osdServiceSpy = spyOn(TestBed.get(OsdService), 'reweight').and.callFake(() => of(true));
+ component.reweight();
+
+ expect(osdServiceSpy.calls.count()).toBe(1);
+ expect(osdServiceSpy.calls.first().args).toEqual([1, 0.5]);
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { BsModalRef } from 'ngx-bootstrap/modal';
+
+import { OsdService } from '../../../../shared/api/osd.service';
+import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder';
+import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-osd-reweight-modal',
+ templateUrl: './osd-reweight-modal.component.html',
+ styleUrls: ['./osd-reweight-modal.component.scss']
+})
+export class OsdReweightModalComponent implements OnInit {
+ currentWeight = 1;
+ osdId: number;
+ reweightForm: CdFormGroup;
+
+ constructor(
+ public bsModalRef: BsModalRef,
+ private osdService: OsdService,
+ private fb: CdFormBuilder
+ ) {}
+
+ get weight() {
+ return this.reweightForm.get('weight');
+ }
+
+ ngOnInit() {
+ this.reweightForm = this.fb.group({
+ weight: this.fb.control(this.currentWeight, [
+ Validators.required,
+ Validators.max(1),
+ Validators.min(0)
+ ])
+ });
+ }
+
+ reweight() {
+ this.osdService
+ .reweight(this.osdId, this.reweightForm.value.weight)
+ .subscribe(() => this.bsModalRef.hide());
+ }
+}
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { PoolService } from '../../../shared/api/pool.service';
+import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { TableComponent } from '../../../shared/datatable/table/table.component';
import { CellTemplate } from '../../../shared/enum/cell-template.enum';
import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
import { TaskListService } from '../../../shared/services/task-list.service';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
import { Pool } from '../pool';
-import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
@Component({
selector: 'cd-pool-list',
expect(req.request.method).toBe('PUT');
expect(req.request.body).toEqual({ flags: ['foo'] });
});
+
+ it('should mark the OSD out', () => {
+ service.markOut(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark_out');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should mark the OSD in', () => {
+ service.markIn(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark_in');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should mark the OSD down', () => {
+ service.markDown(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark_down');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should reweight an OSD', () => {
+ service.reweight(1, 0.5).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/reweight');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ weight: 0.5 });
+ });
+
+ it('should mark an OSD lost', () => {
+ service.markLost(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark_lost');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should remove an OSD', () => {
+ service.remove(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/remove');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should destroy an OSD', () => {
+ service.destroy(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/destroy');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should return if it is safe to destroy an OSD', () => {
+ service.safeToDestroy(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/safe_to_destroy');
+ expect(req.request.method).toBe('GET');
+ });
});
updateFlags(flags: string[]) {
return this.http.put(`${this.path}/flags`, { flags: flags });
}
+
+ markOut(id: number) {
+ return this.http.post(`${this.path}/${id}/mark_out`, null);
+ }
+
+ markIn(id: number) {
+ return this.http.post(`${this.path}/${id}/mark_in`, null);
+ }
+
+ markDown(id: number) {
+ return this.http.post(`${this.path}/${id}/mark_down`, null);
+ }
+
+ reweight(id: number, weight: number) {
+ return this.http.post(`${this.path}/${id}/reweight`, { weight: weight });
+ }
+
+ markLost(id: number) {
+ return this.http.post(`${this.path}/${id}/mark_lost`, null);
+ }
+
+ remove(id: number) {
+ return this.http.post(`${this.path}/${id}/remove`, null);
+ }
+
+ destroy(id: number) {
+ return this.http.post(`${this.path}/${id}/destroy`, null);
+ }
+
+ safeToDestroy(id: number) {
+ interface SafeToDestroyResponse {
+ 'safe-to-destroy': boolean;
+ message?: string;
+ }
+ return this.http.get<SafeToDestroyResponse>(`${this.path}/${id}/safe_to_destroy`);
+ }
}
WarningPanelComponent,
GrafanaComponent
],
- entryComponents: [
- ModalComponent,
- CriticalConfirmationModalComponent,
- ConfirmationModalComponent
- ]
+ entryComponents: [ModalComponent, CriticalConfirmationModalComponent, ConfirmationModalComponent]
})
export class ComponentsModule {}
<div class="button-group text-right">
<cd-submit-button i18n
[form]="confirmationForm"
- (submitAction)="submit()">
+ (submitAction)="onSubmit(confirmationForm.value)">
{{ buttonText }}
</cd-submit-button>
<button i18n
}
ngOnInit() {
- this.bodyContext = {
- $implicit: this.bodyData
- };
- }
-
- submit() {
- this.onSubmit();
+ this.bodyContext = this.bodyContext || {};
+ this.bodyContext['$implicit'] = this.bodyData;
}
cancel() {
.modal-body .question {
font-weight: bold;
+ margin-top: 1em;
}
.modal-body .question .checkbox {
padding-top: 7px;
}
}
-describe('DeletionModalComponent', () => {
+describe('CriticalConfirmationModalComponent', () => {
let mockComponent: MockComponent;
let component: CriticalConfirmationModalComponent;
let mockFixture: ComponentFixture<MockComponent>;
@ViewChild(SubmitButtonComponent)
submitButton: SubmitButtonComponent;
bodyTemplate: TemplateRef<any>;
- bodyContext: any;
+ bodyContext: object;
submitActionObservable: () => Observable<any>;
submitAction: Function;
deletionForm: CdFormGroup;
<button [type]="type"
class="btn btn-sm btn-primary tc_submitButton"
- [disabled]="loading"
+ [disabled]="loading || disabled"
(click)="submit($event)">
<ng-content></ng-content>
<span *ngIf="loading">
type = 'submit';
@Output()
submitAction = new EventEmitter();
+ @Input()
+ disabled = false;
loading = false;
<ul *dropdownMenu class="dropdown-menu" role="menu">
<ng-container *ngFor="let action of dropDownActions">
<li role="menuitem"
+ class="{{ toClassName(action['name']) }}"
[ngClass]="{'disabled': disableSelectionAction(action)}">
<a class="dropdown-item"
(click)="useClickAction(action)"
expect(component.getCurrentButton()).toBeFalsy();
});
});
+
+ it('should convert any name to a proper CSS class', () => {
+ expect(component.toClassName('Create')).toBe('create');
+ expect(component.toClassName('Mark x down')).toBe('mark-x-down');
+ expect(component.toClassName('?Su*per!')).toBe('super');
+ });
});
this.updateDropDownActions();
}
+ toClassName(name: string): string {
+ return name
+ .replace(/ /g, '-')
+ .replace(/[^a-z-]/gi, '')
+ .toLowerCase();
+ }
+
/**
* Removes all actions from 'tableActions' that need a permission the user doesn't have.
*/
beforeEach(() => {
form = new FormGroup({
- x: new FormControl()
+ x: new FormControl(null, CdValidators.uuid())
});
- form.controls['x'].setValidators(CdValidators.uuid());
});
it('should accept empty value', () => {
it('should error on uuid containing too many blocks', () => {
const x = form.get('x');
+ x.markAsDirty();
x.setValue('e33bbcb6-fcc3-40b1-ae81-3f81706a35d5-23d3');
expect(x.valid).toBeFalsy();
- expect(x.hasError('pattern')).toBeTruthy();
+ expect(x.hasError('invalidUuid')).toBeTruthy();
});
it('should error on uuid containing too many chars in block', () => {
const x = form.get('x');
+ x.markAsDirty();
x.setValue('aae33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
expect(x.valid).toBeFalsy();
- expect(x.hasError('pattern')).toBeTruthy();
+ expect(x.hasError('invalidUuid')).toBeTruthy();
});
it('should error on uuid containing invalid char', () => {
const x = form.get('x');
+ x.markAsDirty();
x.setValue('x33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
expect(x.valid).toBeFalsy();
- expect(x.hasError('pattern')).toBeTruthy();
+ expect(x.hasError('invalidUuid')).toBeTruthy();
x.setValue('$33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
expect(x.valid).toBeFalsy();
- expect(x.hasError('pattern')).toBeTruthy();
+ expect(x.hasError('invalidUuid')).toBeTruthy();
});
});
}
}
- /**
- * Validator function in order to validate uuids.
- * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
- * if the validation failed, otherwise `null`.
- */
- static uuid(): ValidatorFn {
- const uuidRgx = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
- return Validators.pattern(uuidRgx);
- }
-
/**
* Validator function in order to validate numbers.
* @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
);
};
}
+
+ /**
+ * Validator function for UUIDs.
+ * @param required - Defines if it is mandatory to fill in the UUID
+ * @return Validator function that returns an error object containing `invalidUuid` if the
+ * validation failed, `null` otherwise.
+ */
+ static uuid(required = false): ValidatorFn {
+ const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ return (control: AbstractControl): { [key: string]: any } | null => {
+ if (control.pristine && control.untouched) {
+ return null;
+ } else if (!required && !control.value) {
+ return null;
+ } else if (uuidRe.test(control.value)) {
+ return null;
+ }
+ return { invalidUuid: 'This is not a valid UUID' };
+ };
+ }
}
// This is the function that will be triggered on a click event if defined
click?: Function;
- // Only change permissions are allowed
- permission: 'create' | 'update' | 'delete';
+ permission: 'create' | 'update' | 'delete' | 'read';
// The name of the action
name: string;
// if one selection is made and no task is running on the selected item.
disable?: (_: CdTableSelection) => boolean;
- // You can define the condition to disable the action.
- // By default all 'create' actions can be the action button if no or multiple items are selected
- // By default all 'update' and 'delete' actions can be the action button if one item is selected
+ /**
+ * Defines if the button can become 'primary' (displayed as button and not
+ * 'hidden' in the menu). Only one button can be primary at a time. By
+ * default all 'create' actions can be the action button if no or multiple
+ * items are selected. Also, all 'update' and 'delete' actions can be the
+ * action button by default, provided only one item is selected.
+ */
canBePrimary?: (_: CdTableSelection) => boolean;
// In some rare cases you want to hide a action that can be used by the user for example
@import '../defaults';
::ng-deep .popover-content {
- padding: 0px 0px;
+ padding: 0.5em;
height: auto;
max-height: 70vh;
overflow-x: hidden;
singleExecuting?: any; // uses 'single' if not defined
multiple?: any; // uses 'empty' if not defined
}) {
- this.testScenario( // 'multiple selections'
+ this.testScenario(
+ // 'multiple selections'
[{}, {}],
fn,
_.isUndefined(multiple) ? empty : multiple
);
- this.testScenario( // 'select executing item'
+ this.testScenario(
+ // 'select executing item'
[{ cdExecuting: 'someAction' }],
fn,
_.isUndefined(singleExecuting) ? single : singleExecuting
this.testScenario([], fn, empty); // 'no selection'
}
- private testScenario(
- selection: object[],
- fn: () => any,
- expected: any
- ) {
+ private testScenario(selection: object[], fn: () => any, expected: any) {
this.setSelection(selection);
expect(fn()).toBe(expected);
}