from_orchestrator = 'orchestrator' in _sources
return get_hosts(from_ceph, from_orchestrator)
- @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_CREATE])
+ @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_ADD])
@handle_orchestrator_error('host')
@host_task('add', {'hostname': '{hostname}'})
@EndpointDoc('',
status: Optional[str] = None): # pragma: no cover - requires realtime env
add_host(hostname, addr, labels, status)
- @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE])
+ @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_REMOVE])
@handle_orchestrator_error('host')
- @host_task('delete', {'hostname': '{hostname}'})
+ @host_task('remove', {'hostname': '{hostname}'})
@allow_empty_body
def delete(self, hostname): # pragma: no cover - requires realtime env
orch_client = OrchClient.instance()
status: 3
};
+ serviceColumnIndex = {
+ service_name: 1,
+ placement: 2
+ };
+
createCluster() {
cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
cy.get('[name=expand-cluster]').click();
add(hostname: string, exist?: boolean, maintenance?: boolean) {
cy.get('.btn.btn-accent').first().click({ force: true });
-
cy.get('cd-modal').should('exist');
cy.get('cd-modal').within(() => {
cy.get('#hostname').type(hostname);
}
delete(hostname: string) {
- super.delete(hostname, this.columnIndex.hostname);
+ super.delete(hostname, this.columnIndex.hostname, 'hosts');
}
// Add or remove labels on a host, then verify labels in the table
cy.get('@addButton').click();
});
}
+
+ private selectServiceType(serviceType: string) {
+ return this.selectOption('service_type', serviceType);
+ }
+
+ addService(serviceType: string) {
+ cy.get('.btn.btn-accent').first().click({ force: true });
+ cy.get('cd-modal').should('exist');
+ cy.get('cd-modal').within(() => {
+ this.selectServiceType(serviceType);
+ if (serviceType === 'rgw') {
+ cy.get('#service_id').type('rgw');
+ cy.get('#count').type('1');
+ } else if (serviceType === 'ingress') {
+ this.selectOption('backend_service', 'rgw.rgw');
+ cy.get('#service_id').should('have.value', 'rgw.rgw');
+ cy.get('#virtual_ip').type('192.168.20.1/24');
+ cy.get('#frontend_port').type('8081');
+ cy.get('#monitor_port').type('8082');
+ }
+
+ cy.get('cd-submit-button').click();
+ });
+ }
+
+ checkServiceExist(serviceName: string, exist: boolean) {
+ this.getTableCell(this.serviceColumnIndex.service_name, serviceName).should(($elements) => {
+ const services = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(services).to.include(serviceName);
+ } else {
+ expect(services).to.not.include(serviceName);
+ }
+ });
+ }
+
+ deleteService(serviceName: string, wait: number) {
+ const getRow = this.getTableCell.bind(this, this.serviceColumnIndex.service_name);
+ getRow(serviceName).click();
+
+ // Clicks on table Delete button
+ this.clickActionButton('delete');
+
+ // Confirms deletion
+ cy.get('cd-modal .custom-control-label').click();
+ cy.contains('cd-modal button', 'Delete').click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+
+ // wait for delete operation to complete: tearing down the service daemons
+ cy.wait(wait);
+
+ this.checkServiceExist(serviceName, false);
+ }
}
@PageHelper.restrictTo(pages.index.url)
delete(hostname: string) {
- super.delete(hostname, this.columnIndex.hostname);
+ super.delete(hostname, this.columnIndex.hostname, 'hosts');
}
// Add or remove labels on a host, then verify labels in the table
const pages = {
index: { url: '#/services', id: 'cd-services' },
- create: { url: '#/services/create', id: 'cd-service-form' }
+ create: { url: '#/services/(modal:create)', id: 'cd-service-form' }
};
export class ServicesPageHelper extends PageHelper {
cy.get('button[aria-label="Next"]').click();
cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
});
});
});
+++ /dev/null
-import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
-
-describe('Create Cluster Review page', () => {
- const createCluster = new CreateClusterWizardHelper();
-
- beforeEach(() => {
- cy.login();
- Cypress.Cookies.preserveOnce('token');
- createCluster.navigateTo();
- createCluster.createCluster();
-
- cy.get('button[aria-label="Next"]').click();
- cy.get('button[aria-label="Next"]').click();
- });
-
- describe('navigation link and title test', () => {
- it('should check if nav-link and title contains Review', () => {
- cy.get('.nav-link').should('contain.text', 'Review');
- });
- });
-
- describe('fields check', () => {
- it('should check cluster resources table is present', () => {
- // check for table header 'Cluster Resources'
- createCluster.getLegends().its(0).should('have.text', 'Cluster Resources');
-
- // check for fields in table
- createCluster.getStatusTables().should('contain.text', 'Hosts');
- createCluster.getStatusTables().should('contain.text', 'Storage Capacity');
- });
-
- it('should check Hosts by Label and Host Details tables are present', () => {
- // check for there to be two tables
- createCluster.getDataTables().should('have.length', 2);
-
- // check for table header 'Hosts by Label'
- createCluster.getLegends().its(1).should('have.text', 'Hosts by Label');
-
- // check for table header 'Host Details'
- createCluster.getLegends().its(2).should('have.text', 'Host Details');
-
- // verify correct columns on Hosts by Label table
- createCluster.getDataTableHeaders(0).contains('Label');
-
- createCluster.getDataTableHeaders(0).contains('Number of Hosts');
-
- // verify correct columns on Host Details table
- createCluster.getDataTableHeaders(1).contains('Host Name');
-
- createCluster.getDataTableHeaders(1).contains('Labels');
- });
-
- it('should check hosts count and default host name are present', () => {
- createCluster.getStatusTables().contains(2);
-
- createCluster.check_for_host();
- });
- });
-});
+++ /dev/null
-import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
-import { HostsPageHelper } from 'cypress/integration/cluster/hosts.po';
-import { OSDsPageHelper } from 'cypress/integration/cluster/osds.po';
-
-describe('when cluster creation is completed', () => {
- const createCluster = new CreateClusterWizardHelper();
-
- beforeEach(() => {
- cy.login();
- Cypress.Cookies.preserveOnce('token');
- });
-
- it('should redirect to dashboard landing page after cluster creation', () => {
- createCluster.navigateTo();
- createCluster.createCluster();
-
- cy.get('button[aria-label="Next"]').click();
- cy.get('button[aria-label="Next"]').click();
- cy.get('button[aria-label="Next"]').click();
-
- cy.get('cd-dashboard').should('exist');
- });
-
- describe('Hosts page', () => {
- const hosts = new HostsPageHelper();
- const hostnames = ['ceph-node-00.cephlab.com', 'ceph-node-02.cephlab.com'];
-
- beforeEach(() => {
- hosts.navigateTo();
- });
- it('should have removed "_no_schedule" label', () => {
- for (let host = 0; host < hostnames.length; host++) {
- cy.get('datatable-row-wrapper').should('not.have.text', '_no_schedule');
- }
- });
-
- it('should display inventory', () => {
- hosts.clickHostTab(hostnames[1], 'Physical Disks');
- cy.get('cd-host-details').within(() => {
- hosts.getTableCount('total').should('be.gte', 0);
- });
- });
-
- it('should display daemons', () => {
- hosts.clickHostTab(hostnames[1], 'Daemons');
- cy.get('cd-host-details').within(() => {
- hosts.getTableCount('total').should('be.gte', 0);
- });
- });
- });
-
- describe('OSDs page', () => {
- const osds = new OSDsPageHelper();
-
- beforeEach(() => {
- osds.navigateTo();
- });
-
- it('should check if osds are created', { retries: 1 }, () => {
- osds.expectTableCount('total', 2);
- });
- });
-});
--- /dev/null
+import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
+
+describe('Create cluster create services page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+ });
+
+ it('should check if nav-link and title contains Create Services', () => {
+ cy.get('.nav-link').should('contain.text', 'Create Services');
+
+ cy.get('.title').should('contain.text', 'Create Services');
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create an rgw service', () => {
+ createCluster.addService('rgw');
+
+ createCluster.checkExist('rgw.rgw', true);
+ });
+
+ it('should create and delete an ingress service', () => {
+ createCluster.addService('ingress');
+
+ createCluster.checkExist('ingress.rgw.rgw', true);
+
+ createCluster.deleteService('ingress.rgw.rgw', 60000);
+ });
+ });
+});
--- /dev/null
+import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
+
+describe('Create Cluster Review page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+ });
+
+ describe('navigation link and title test', () => {
+ it('should check if nav-link and title contains Review', () => {
+ cy.get('.nav-link').should('contain.text', 'Review');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check cluster resources table is present', () => {
+ // check for table header 'Cluster Resources'
+ createCluster.getLegends().its(0).should('have.text', 'Cluster Resources');
+
+ // check for fields in table
+ createCluster.getStatusTables().should('contain.text', 'Hosts');
+ createCluster.getStatusTables().should('contain.text', 'Storage Capacity');
+ });
+
+ it('should check Hosts by Services and Host Details tables are present', () => {
+ // check for there to be two tables
+ createCluster.getDataTables().should('have.length', 2);
+
+ // check for table header 'Hosts by Services'
+ createCluster.getLegends().its(1).should('have.text', 'Hosts by Services');
+
+ // check for table header 'Host Details'
+ createCluster.getLegends().its(2).should('have.text', 'Host Details');
+
+ // verify correct columns on Hosts by Services table
+ createCluster.getDataTableHeaders(0).contains('Services');
+
+ createCluster.getDataTableHeaders(0).contains('Number of Hosts');
+
+ // verify correct columns on Host Details table
+ createCluster.getDataTableHeaders(1).contains('Host Name');
+
+ createCluster.getDataTableHeaders(1).contains('Labels');
+ });
+
+ it('should check hosts count and default host name are present', () => {
+ createCluster.getStatusTables().contains(2);
+
+ createCluster.check_for_host();
+ });
+ });
+});
--- /dev/null
+import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
+import { HostsPageHelper } from 'cypress/integration/cluster/hosts.po';
+import { OSDsPageHelper } from 'cypress/integration/cluster/osds.po';
+import { ServicesPageHelper } from 'cypress/integration/cluster/services.po';
+
+describe('when cluster creation is completed', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ });
+
+ it('should redirect to dashboard landing page after cluster creation', () => {
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+
+ cy.get('cd-dashboard').should('exist');
+ });
+
+ describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+ const hostnames = ['ceph-node-00.cephlab.com', 'ceph-node-02.cephlab.com'];
+
+ beforeEach(() => {
+ hosts.navigateTo();
+ });
+ it('should have removed "_no_schedule" label', () => {
+ for (let host = 0; host < hostnames.length; host++) {
+ cy.get('datatable-row-wrapper').should('not.have.text', '_no_schedule');
+ }
+ });
+
+ it('should display inventory', () => {
+ hosts.clickHostTab(hostnames[1], 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should display daemons', () => {
+ hosts.clickHostTab(hostnames[1], 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+ });
+
+ describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ osds.navigateTo();
+ });
+
+ it('should check if osds are created', { retries: 1 }, () => {
+ osds.expectTableCount('total', 2);
+ });
+ });
+
+ describe('Services page', () => {
+ const services = new ServicesPageHelper();
+
+ beforeEach(() => {
+ services.navigateTo();
+ });
+
+ it('should check if services are created', () => {
+ services.checkExist('rgw.rgw', true);
+ });
+ });
+});
* @param name The string to search in table cells.
* @param columnIndex If provided, search string in columnIndex column.
*/
- delete(name: string, columnIndex?: number) {
+ delete(name: string, columnIndex?: number, section?: string) {
// Selects row
const getRow = columnIndex
? this.getTableCell.bind(this, columnIndex)
: this.getFirstTableCell.bind(this);
getRow(name).click();
+ let action: string;
+ section === 'hosts' ? (action = 'remove') : (action = 'delete');
- // Clicks on table Delete button
- this.clickActionButton('delete');
+ // Clicks on table Delete/Remove button
+ this.clickActionButton(action);
- // Confirms deletion
+ // Convert action to SentenceCase and Confirms deletion
+ const actionUpperCase = action.charAt(0).toUpperCase() + action.slice(1);
cy.get('cd-modal .custom-control-label').click();
- cy.contains('cd-modal button', 'Delete').click();
+ cy.contains('cd-modal button', actionUpperCase).click();
// Wait for modal to close
cy.get('cd-modal').should('not.exist');
},
{
path: 'services',
- canActivateChild: [ModuleStatusGuardService],
+ component: ServicesComponent,
+ canActivate: [ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'orchestrator',
breadcrumbs: 'Cluster/Services'
},
children: [
- { path: '', component: ServicesComponent },
{
path: URLVerbs.CREATE,
component: ServiceFormComponent,
- data: { breadcrumbs: ActionLabels.CREATE }
+ outlet: 'modal'
}
]
},
<tr>
<td i18n
class="bold">Storage Capacity</td>
- <td><span i18n
- *ngIf="filteredDevices && capacity">Number of devices: {{ filteredDevices.length }}. Raw capacity:
- {{ capacity | dimlessBinary }}.</span></td>
+ <td><span i18n>Number of devices: {{ totalDevices }}. Raw capacity:
+ {{ totalCapacity | dimlessBinary }}.</span></td>
</tr>
</table>
</fieldset>
<div class="col-lg-8">
<legend i18n
- class="cd-header">Hosts by Label</legend>
- <cd-table [data]="hostsByLabel['data']"
- [columns]="hostsByLabel['columns']"
+ class="cd-header">Hosts by Services</legend>
+ <cd-table [data]="hostsByService['data']"
+ [columns]="hostsByService['columns']"
[toolHeader]="false">
</cd-table>
import { CephModule } from '~/app/ceph/ceph.module';
import { CoreModule } from '~/app/core/core.module';
-import { HostService } from '~/app/shared/api/host.service';
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
import { CreateClusterReviewComponent } from './create-cluster-review.component';
describe('CreateClusterReviewComponent', () => {
let component: CreateClusterReviewComponent;
let fixture: ComponentFixture<CreateClusterReviewComponent>;
- let hostService: HostService;
- let hostListSpy: jasmine.Spy;
+ let cephServiceService: CephServiceService;
+ let serviceListSpy: jasmine.Spy;
configureTestBed({
imports: [HttpClientTestingModule, SharedModule, CoreModule, CephModule]
beforeEach(() => {
fixture = TestBed.createComponent(CreateClusterReviewComponent);
component = fixture.componentInstance;
- hostService = TestBed.inject(HostService);
- hostListSpy = spyOn(hostService, 'list');
+ cephServiceService = TestBed.inject(CephServiceService);
+ serviceListSpy = spyOn(cephServiceService, 'list');
});
it('should create', () => {
const payload = [
{
hostname: hostnames[0],
- ceph_version: 'ceph version Development',
- labels: ['foo', 'bar']
+ service_type: ['mgr', 'mon']
},
{
hostname: hostnames[1],
- ceph_version: 'ceph version Development',
- labels: ['foo1', 'bar1']
+ service_type: ['mgr', 'alertmanager']
}
];
- hostListSpy.and.callFake(() => of(payload));
+ serviceListSpy.and.callFake(() => of(payload));
fixture.detectChanges();
- expect(hostListSpy).toHaveBeenCalled();
+ expect(serviceListSpy).toHaveBeenCalled();
- expect(component.hostsCount).toBe(2);
- expect(component.uniqueLabels.size).toBe(4);
- const labels = ['foo', 'bar', 'foo1', 'bar1'];
-
- labels.forEach((label) => {
- expect(component.labelOccurrences[label]).toBe(1);
- });
+ expect(component.serviceCount).toBe(2);
+ expect(component.uniqueServices.size).toBe(2);
});
});
import _ from 'lodash';
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CephServiceSpec } from '~/app/shared/models/service.interface';
import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
-import { InventoryDevice } from '../inventory/inventory-devices/inventory-device.model';
@Component({
selector: 'cd-create-cluster-review',
export class CreateClusterReviewComponent implements OnInit {
hosts: object[] = [];
hostsDetails: object;
- hostsByLabel: object;
+ hostsByService: object;
hostsCount: number;
- labelOccurrences = {};
- hostsCountPerLabel: object[] = [];
- uniqueLabels: Set<string> = new Set();
- filteredDevices: InventoryDevice[] = [];
- capacity = 0;
+ serviceCount: number;
+ serviceOccurrences = {};
+ hostsCountPerService: object[] = [];
+ uniqueServices: Set<string> = new Set();
+ totalDevices: number;
+ totalCapacity = 0;
+ services: Array<CephServiceSpec> = [];
- constructor(private hostService: HostService, public wizardStepService: WizardStepsService) {}
+ constructor(
+ public wizardStepsService: WizardStepsService,
+ public cephServiceService: CephServiceService,
+ public hostService: HostService,
+ private osdService: OsdService
+ ) {}
ngOnInit() {
+ let dataDevices = 0;
+ let dataDeviceCapacity = 0;
+ let walDevices = 0;
+ let walDeviceCapacity = 0;
+ let dbDevices = 0;
+ let dbDeviceCapacity = 0;
this.hostsDetails = {
columns: [
{
]
};
- this.hostsByLabel = {
+ this.hostsByService = {
columns: [
{
- prop: 'label',
- name: $localize`Labels`,
+ prop: 'service_type',
+ name: $localize`Services`,
flexGrow: 1,
cellTransformation: CellTemplate.badge,
customTemplateConfig: {
},
{
name: $localize`Number of Hosts`,
- prop: 'hosts_per_label',
+ prop: 'hosts_per_service',
flexGrow: 1
}
]
};
- this.hostService.list().subscribe((resp: object[]) => {
- this.hosts = resp;
- this.hostsCount = this.hosts.length;
+ this.cephServiceService.list().subscribe((resp: Array<CephServiceSpec>) => {
+ this.services = resp;
+ this.serviceCount = this.services.length;
- _.forEach(this.hosts, (hostKey) => {
- const labels = hostKey['labels'];
- _.forEach(labels, (label) => {
- this.labelOccurrences[label] = (this.labelOccurrences[label] || 0) + 1;
- this.uniqueLabels.add(label);
- });
+ _.forEach(this.services, (serviceKey) => {
+ this.serviceOccurrences[serviceKey['service_type']] =
+ (this.serviceOccurrences[serviceKey['service_type']] || 0) + 1;
+ this.uniqueServices.add(serviceKey['service_type']);
});
- this.uniqueLabels.forEach((label) => {
- this.hostsCountPerLabel.push({
- label: label,
- hosts_per_label: this.labelOccurrences[label]
+ this.uniqueServices.forEach((serviceType) => {
+ this.hostsCountPerService.push({
+ service_type: serviceType,
+ hosts_per_service: this.serviceOccurrences[serviceType]
});
});
- this.hostsByLabel['data'] = [...this.hostsCountPerLabel];
+ this.hostsByService['data'] = [...this.hostsCountPerService];
+ });
+
+ this.hostService.list().subscribe((resp: object[]) => {
+ this.hosts = resp;
+ this.hostsCount = this.hosts.length;
this.hostsDetails['data'] = [...this.hosts];
});
- this.filteredDevices = this.wizardStepService.osdDevices;
- this.capacity = this.wizardStepService.osdCapacity;
+ if (this.osdService.osdDevices['data']) {
+ dataDevices = this.osdService.osdDevices['data']?.length;
+ dataDeviceCapacity = this.osdService.osdDevices['data']['capacity'];
+ }
+
+ if (this.osdService.osdDevices['wal']) {
+ walDevices = this.osdService.osdDevices['wal']?.length;
+ walDeviceCapacity = this.osdService.osdDevices['wal']['capacity'];
+ }
+
+ if (this.osdService.osdDevices['db']) {
+ dbDevices = this.osdService.osdDevices['db']?.length;
+ dbDeviceCapacity = this.osdService.osdDevices['db']['capacity'];
+ }
+
+ this.totalDevices = dataDevices + walDevices + dbDevices;
+ this.osdService.osdDevices['totalDevices'] = this.totalDevices;
+ this.totalCapacity = dataDeviceCapacity + walDeviceCapacity + dbDeviceCapacity;
}
}
<h4 class="title"
i18n>Add Hosts</h4>
<br>
- <cd-hosts [clusterCreation]="true"></cd-hosts>
+ <cd-hosts [hiddenColumns]="['services', 'ceph_version']"
+ [hideTitle]="true"
+ [hideSubmitBtn]="true"
+ [hasTableDetails]="false"
+ [showGeneralActionsOnly]="true"></cd-hosts>
</div>
<div *ngSwitchCase="'2'"
class="ml-5">
i18n>Create OSDs</h4>
<br>
<div class="alignForm">
- <cd-osd-form [clusterCreation]="true"></cd-osd-form>
+ <cd-osd-form [hideTitle]="true"
+ [hideSubmitBtn]="true"
+ (emitDriveGroup)="getDriveGroup($event)"></cd-osd-form>
</div>
</div>
<div *ngSwitchCase="'3'"
class="ml-5">
+ <h4 class="title"
+ i18n>Create Services</h4>
+ <br>
+ <cd-services [hasDetails]="false"
+ [hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
+ [hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
+ [modal]="false"></cd-services>
+ </div>
+ <div *ngSwitchCase="'4'"
+ class="ml-5">
<cd-create-cluster-review></cd-create-cluster-review>
</div>
</ng-container>
import { CephModule } from '~/app/ceph/ceph.module';
import { CoreModule } from '~/app/core/core.module';
import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
import { AppConstants } from '~/app/shared/constants/app.constants';
let fixture: ComponentFixture<CreateClusterComponent>;
let wizardStepService: WizardStepsService;
let hostService: HostService;
+ let osdService: OsdService;
let modalServiceShowSpy: jasmine.Spy;
const projectConstants: typeof AppConstants = AppConstants;
component = fixture.componentInstance;
wizardStepService = TestBed.inject(WizardStepsService);
hostService = TestBed.inject(HostService);
+ osdService = TestBed.inject(OsdService);
modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({
// mock the close function, it might be called if there are async tests.
close: jest.fn()
it('should move to next step and show the second page', () => {
const wizardStepServiceSpy = spyOn(wizardStepService, 'moveToNextStep').and.callThrough();
- const hostServiceSpy = spyOn(hostService, 'list').and.callThrough();
component.createCluster();
fixture.detectChanges();
component.onNextStep();
fixture.detectChanges();
expect(wizardStepServiceSpy).toHaveBeenCalledTimes(1);
- expect(hostServiceSpy).toBeCalledTimes(1);
});
it('should show the button labels correctly', () => {
cancelBtnLabel = component.showCancelButtonLabel();
expect(cancelBtnLabel).toEqual('Back');
+ component.onNextStep();
+ fixture.detectChanges();
+ submitBtnLabel = component.showSubmitButtonLabel();
+ expect(submitBtnLabel).toEqual('Next');
+ cancelBtnLabel = component.showCancelButtonLabel();
+ expect(cancelBtnLabel).toEqual('Back');
+
// Last page of the wizard
component.onNextStep();
fixture.detectChanges();
cancelBtnLabel = component.showCancelButtonLabel();
expect(cancelBtnLabel).toEqual('Back');
});
+
+ it('should ensure osd creation did not happen when no devices are selected', () => {
+ const osdServiceSpy = spyOn(osdService, 'create').and.callThrough();
+ component.onSubmit();
+ fixture.detectChanges();
+ expect(osdServiceSpy).toBeCalledTimes(0);
+ });
+
+ it('should ensure osd creation did happen when devices are selected', () => {
+ const osdServiceSpy = spyOn(osdService, 'create').and.callThrough();
+ osdService.osdDevices['totalDevices'] = 1;
+ component.onSubmit();
+ fixture.detectChanges();
+ expect(osdServiceSpy).toBeCalledTimes(1);
+ });
+
+ it('should ensure host list call happened', () => {
+ const hostServiceSpy = spyOn(hostService, 'list').and.callThrough();
+ component.onSubmit();
+ expect(hostServiceSpy).toHaveBeenCalledTimes(1);
+ });
});
currentStepSub: Subscription;
permissions: Permissions;
projectConstants: typeof AppConstants = AppConstants;
- stepTitles = ['Add Hosts', 'Create OSDs', 'Review'];
+ stepTitles = ['Add Hosts', 'Create OSDs', 'Create Services', 'Review'];
startClusterCreation = false;
observables: any = [];
modalRef: NgbModalRef;
constructor(
private authStorageService: AuthStorageService,
- private stepsService: WizardStepsService,
+ private wizardStepsService: WizardStepsService,
private router: Router,
private hostService: HostService,
private notificationService: NotificationService,
private clusterService: ClusterService,
private modalService: ModalService,
private taskWrapper: TaskWrapperService,
- private osdService: OsdService,
- private wizardStepService: WizardStepsService
+ private osdService: OsdService
) {
this.permissions = this.authStorageService.getPermissions();
- this.currentStepSub = this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
- this.currentStep = step;
- });
+ this.currentStepSub = this.wizardStepsService
+ .getCurrentStep()
+ .subscribe((step: WizardStepModel) => {
+ this.currentStep = step;
+ });
this.currentStep.stepIndex = 1;
}
}
onSubmit() {
- forkJoin(this.observables)
- .pipe(
- finalize(() =>
- this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => {
- this.notificationService.show(
- NotificationType.success,
- $localize`Cluster expansion was successful`
- );
- this.router.navigate(['/dashboard']);
- })
- )
- )
- .subscribe({
- error: (error) => error.preventDefault()
- });
-
- this.taskWrapper
- .wrapTaskAroundCall({
- task: new FinishedTask('osd/' + URLVerbs.CREATE, {
- tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ')
- }),
- call: this.osdService.create(this.driveGroups)
- })
- .subscribe({
- error: (error) => error.preventDefault(),
- complete: () => {
- this.submitAction.emit();
+ this.hostService.list().subscribe((hosts) => {
+ hosts.forEach((host) => {
+ const index = host['labels'].indexOf('_no_schedule', 0);
+ if (index > -1) {
+ host['labels'].splice(index, 1);
+ this.observables.push(this.hostService.update(host['hostname'], true, host['labels']));
}
});
- }
+ forkJoin(this.observables)
+ .pipe(
+ finalize(() =>
+ this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Cluster expansion was successful`
+ );
+ this.router.navigate(['/dashboard']);
+ })
+ )
+ )
+ .subscribe({
+ error: (error) => error.preventDefault()
+ });
+ });
+ if (this.driveGroup) {
+ const user = this.authStorageService.getUsername();
+ this.driveGroup.setName(`dashboard-${user}-${_.now()}`);
+ this.driveGroups.push(this.driveGroup.spec);
+ }
- onNextStep() {
- if (!this.stepsService.isLastStep()) {
- this.hostService.list().subscribe((hosts) => {
- hosts.forEach((host) => {
- const index = host['labels'].indexOf('_no_schedule', 0);
- if (index > -1) {
- host['labels'].splice(index, 1);
- this.observables.push(this.hostService.update(host['hostname'], true, host['labels']));
+ if (this.osdService.osdDevices['totalDevices'] > 0) {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+ tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ')
+ }),
+ call: this.osdService.create(this.driveGroups)
+ })
+ .subscribe({
+ error: (error) => error.preventDefault(),
+ complete: () => {
+ this.submitAction.emit();
+ this.osdService.osdDevices = [];
}
});
- });
- this.driveGroup = this.wizardStepService.sharedData;
- this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
+ }
+ }
+
+ getDriveGroup(driveGroup: DriveGroup) {
+ this.driveGroup = driveGroup;
+ }
+
+ onNextStep() {
+ if (!this.wizardStepsService.isLastStep()) {
+ this.wizardStepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
this.currentStep = step;
});
- if (this.currentStep.stepIndex === 2 && this.driveGroup) {
- const user = this.authStorageService.getUsername();
- this.driveGroup.setName(`dashboard-${user}-${_.now()}`);
- this.driveGroups.push(this.driveGroup.spec);
- }
- this.stepsService.moveToNextStep();
+ this.wizardStepsService.moveToNextStep();
} else {
this.onSubmit();
}
}
onPreviousStep() {
- if (!this.stepsService.isFirstStep()) {
- this.stepsService.moveToPreviousStep();
+ if (!this.wizardStepsService.isFirstStep()) {
+ this.wizardStepsService.moveToPreviousStep();
} else {
this.router.navigate(['/dashboard']);
}
}
showSubmitButtonLabel() {
- return !this.stepsService.isLastStep() ? this.actionLabels.NEXT : $localize`Expand Cluster`;
+ return !this.wizardStepsService.isLastStep()
+ ? this.actionLabels.NEXT
+ : $localize`Expand Cluster`;
}
showCancelButtonLabel() {
- return !this.stepsService.isFirstStep() ? this.actionLabels.BACK : this.actionLabels.CANCEL;
+ return !this.wizardStepsService.isFirstStep()
+ ? this.actionLabels.BACK
+ : this.actionLabels.CANCEL;
}
ngOnDestroy(): void {
columnMode="flex"
(fetchData)="getHosts($event)"
selectionType="single"
- [hasDetails]="!clusterCreation"
+ [hasDetails]="hasTableDetails"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
<div class="table-actions btn-toolbar">
</ng-template>
</li>
<li ngbNavItem
- *ngIf="permissions.grafana.read && !clusterCreation">
+ *ngIf="permissions.grafana.read">
<a ngbNavLink
i18n>Overall Performance</a>
<ng-template ngbNavContent>
showForceMaintenanceModal = new MockShowForceMaintenanceModal();
fixture = TestBed.createComponent(HostsComponent);
component = fixture.componentInstance;
- component.clusterCreation = false;
hostListSpy = spyOn(TestBed.inject(HostService), 'list');
orchService = TestBed.inject(OrchestratorService);
});
expectResults: {
Add: { disabled: false, disableDesc: '' },
Edit: { disabled: true, disableDesc: '' },
- Delete: { disabled: true, disableDesc: '' }
+ Remove: { disabled: true, disableDesc: '' }
}
},
{
expectResults: {
Add: { disabled: false, disableDesc: '' },
Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
- Delete: { disabled: true, disableDesc: component.messages.nonOrchHost }
+ Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
}
},
{
expectResults: {
Add: { disabled: false, disableDesc: '' },
Edit: { disabled: false, disableDesc: '' },
- Delete: { disabled: false, disableDesc: '' }
+ Remove: { disabled: false, disableDesc: '' }
}
}
];
const features = [
- OrchestratorFeature.HOST_CREATE,
+ OrchestratorFeature.HOST_ADD,
OrchestratorFeature.HOST_LABEL_ADD,
- OrchestratorFeature.HOST_DELETE,
+ OrchestratorFeature.HOST_REMOVE,
OrchestratorFeature.HOST_LABEL_REMOVE
];
await testTableActions(true, features, tests);
expectResults: {
Add: resultNoOrchestrator,
Edit: { disabled: true, disableDesc: '' },
- Delete: { disabled: true, disableDesc: '' }
+ Remove: { disabled: true, disableDesc: '' }
}
},
{
expectResults: {
Add: resultNoOrchestrator,
Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
- Delete: { disabled: true, disableDesc: component.messages.nonOrchHost }
+ Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
}
},
{
expectResults: {
Add: resultNoOrchestrator,
Edit: resultNoOrchestrator,
- Delete: resultNoOrchestrator
+ Remove: resultNoOrchestrator
}
}
];
expectResults: {
Add: resultMissingFeatures,
Edit: { disabled: true, disableDesc: '' },
- Delete: { disabled: true, disableDesc: '' }
+ Remove: { disabled: true, disableDesc: '' }
}
},
{
expectResults: {
Add: resultMissingFeatures,
Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
- Delete: { disabled: true, disableDesc: component.messages.nonOrchHost }
+ Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
}
},
{
expectResults: {
Add: resultMissingFeatures,
Edit: resultMissingFeatures,
- Delete: resultMissingFeatures
+ Remove: resultMissingFeatures
}
}
];
public servicesTpl: TemplateRef<any>;
@ViewChild('maintenanceConfirmTpl', { static: true })
maintenanceConfirmTpl: TemplateRef<any>;
+
+ @Input()
+ hiddenColumns: string[] = [];
+
+ @Input()
+ hideTitle = false;
+
+ @Input()
+ hideSubmitBtn = false;
+
+ @Input()
+ hasTableDetails = true;
+
@Input()
- clusterCreation = false;
+ showGeneralActionsOnly = false;
permissions: Permissions;
columns: Array<CdTableColumn> = [];
isExecuting = false;
errorMessage: string;
enableButton: boolean;
- pageURL: string;
bsModalRef: NgbModalRef;
icons = Icons;
orchStatus: OrchestratorStatus;
actionOrchFeatures = {
- add: [OrchestratorFeature.HOST_CREATE],
+ add: [OrchestratorFeature.HOST_ADD],
edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE],
- delete: [OrchestratorFeature.HOST_DELETE],
+ remove: [OrchestratorFeature.HOST_REMOVE],
maintenance: [
OrchestratorFeature.HOST_MAINTENANCE_ENTER,
OrchestratorFeature.HOST_MAINTENANCE_EXIT
super();
this.permissions = this.authStorageService.getPermissions();
this.tableActions = [
+ {
+ name: this.actionLabels.ADD,
+ permission: 'create',
+ icon: Icons.add,
+ click: () =>
+ this.router.url.includes('/hosts')
+ ? this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }])
+ : (this.bsModalRef = this.modalService.show(HostFormComponent)),
+ disable: (selection: CdTableSelection) => this.getDisable('add', selection)
+ },
{
name: this.actionLabels.EDIT,
permission: 'update',
disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
},
{
- name: this.actionLabels.DELETE,
+ name: this.actionLabels.REMOVE,
permission: 'delete',
icon: Icons.destroy,
click: () => this.deleteAction(),
- disable: (selection: CdTableSelection) => this.getDisable('delete', selection)
+ disable: (selection: CdTableSelection) => this.getDisable('remove', selection)
},
{
name: this.actionLabels.ENTER_MAINTENANCE,
click: () => this.hostMaintenance(),
disable: (selection: CdTableSelection) =>
this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
- visible: () => !this.clusterCreation
+ visible: () => !this.showGeneralActionsOnly
},
{
name: this.actionLabels.EXIT_MAINTENANCE,
icon: Icons.exit,
click: () => this.hostMaintenance(),
disable: (selection: CdTableSelection) =>
- this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
- visible: () => !this.clusterCreation
+ this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton,
+ visible: () => !this.showGeneralActionsOnly
}
];
}
ngOnInit() {
- this.tableActions.unshift({
- name: this.actionLabels.ADD,
- permission: 'create',
- icon: Icons.add,
- click: () =>
- this.clusterCreation
- ? (this.bsModalRef = this.modalService.show(HostFormComponent))
- : this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }]),
- disable: (selection: CdTableSelection) => this.getDisable('add', selection)
- });
this.columns = [
{
name: $localize`Hostname`,
{
name: $localize`Services`,
prop: 'services',
- isHidden: this.clusterCreation,
flexGrow: 3,
cellTemplate: this.servicesTpl
},
{
name: $localize`Version`,
prop: 'ceph_version',
- isHidden: this.clusterCreation,
flexGrow: 1,
pipe: this.cephShortVersionPipe
}
this.orchStatus = status;
});
- if (this.clusterCreation) {
- const hiddenColumns = ['services', 'ceph_version'];
- this.columns = this.columns.filter((col: any) => {
- return !hiddenColumns.includes(col.prop);
- });
- }
+ this.columns = this.columns.filter((col: any) => {
+ return !this.hiddenColumns.includes(col.prop);
+ });
}
updateSelection(selection: CdTableSelection) {
}
getDisable(
- action: 'add' | 'edit' | 'delete' | 'maintenance',
+ action: 'add' | 'edit' | 'remove' | 'maintenance',
selection: CdTableSelection
): boolean | string {
- if (action === 'delete' || action === 'edit' || action === 'maintenance') {
+ if (action === 'remove' || action === 'edit' || action === 'maintenance') {
if (!selection?.hasSingleSelection) {
return true;
}
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
itemDescription: 'Host',
itemNames: [hostname],
- actionDescription: 'delete',
+ actionDescription: 'remove',
submitActionObservable: () =>
this.taskWrapper.wrapTaskAroundCall({
- task: new FinishedTask('host/delete', { hostname: hostname }),
+ task: new FinishedTask('host/remove', { hostname: hostname }),
call: this.hostService.delete(hostname)
})
});
(click)="showSelectionModal()"
data-toggle="tooltip"
[title]="addButtonTooltip"
- [disabled]="availDevices.length === 0 || !canSelect">
+ [disabled]="availDevices.length === 0 || !canSelect || expansionCanSelect">
<i [ngClass]="[icons.add]"></i>
<ng-container i18n>Add</ng-container>
</button>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
import { ToastrModule } from 'ngx-toastr';
FormsModule,
HttpClientTestingModule,
SharedModule,
- ToastrModule.forRoot()
+ ToastrModule.forRoot(),
+ RouterTestingModule
],
declarations: [OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
});
describe('with devices selected', () => {
beforeEach(() => {
+ component.isOsdPage = true;
component.availDevices = [];
component.devices = devices;
+ component.ngOnInit();
fixture.detectChanges();
});
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
+import { Router } from '@angular/router';
import _ from 'lodash';
import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { OsdService } from '~/app/shared/api/osd.service';
import { Icons } from '~/app/shared/enum/icons.enum';
import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
import { ModalService } from '~/app/shared/services/modal.service';
icons = Icons;
devices: InventoryDevice[] = [];
capacity = 0;
- appliedFilters: any[] = [];
+ appliedFilters = new Array();
+ expansionCanSelect = false;
+ isOsdPage: boolean;
addButtonTooltip: String;
tooltips = {
addByFilters: $localize`Add devices by using filters`
};
- constructor(private modalService: ModalService) {}
+ constructor(
+ private modalService: ModalService,
+ public osdService: OsdService,
+ private router: Router
+ ) {
+ this.isOsdPage = this.router.url.includes('/osd');
+ }
ngOnInit() {
+ if (!this.isOsdPage) {
+ this.osdService?.osdDevices[this.type]
+ ? (this.devices = this.osdService.osdDevices[this.type])
+ : (this.devices = []);
+ this.capacity = _.sumBy(this.devices, 'sys_api.size');
+ this.osdService?.osdDevices
+ ? (this.expansionCanSelect = this.osdService?.osdDevices['disableSelect'])
+ : (this.expansionCanSelect = false);
+ }
this.updateAddButtonTooltip();
}
this.capacity = _.sumBy(this.devices, 'sys_api.size');
this.appliedFilters = result.filters;
const event = _.assign({ type: this.type }, result);
+ if (!this.isOsdPage) {
+ this.osdService.osdDevices[this.type] = this.devices;
+ this.osdService.osdDevices['disableSelect'] =
+ this.canSelect || this.devices.length === this.availDevices.length;
+ this.osdService.osdDevices[this.type]['capacity'] = this.capacity;
+ }
this.selected.emit(event);
});
}
}
clearDevices() {
+ if (!this.isOsdPage) {
+ this.expansionCanSelect = false;
+ this.osdService.osdDevices['disableSelect'] = false;
+ this.osdService.osdDevices = [];
+ }
const event = {
type: this.type,
clearedDevices: [...this.devices]
this.filteredDevices = event.data;
this.capacity = _.sumBy(this.filteredDevices, 'sys_api.size');
this.event = event;
- this.wizardStepService.osdDevices = this.filteredDevices;
- this.wizardStepService.osdCapacity = this.capacity;
}
}
<div class="card">
<div i18n="form title|Example: Create Pool@@formTitle"
class="card-header"
- *ngIf="!clusterCreation">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}</div>
<div class="card-body">
<fieldset>
<cd-osd-devices-selection-groups #dataDeviceSelectionGroups
</fieldset>
</div>
<div class="card-footer"
- *ngIf="!clusterCreation">
+ *ngIf="!hideSubmitBtn">
<cd-form-button-panel #previewButtonPanel
(submitActionEvent)="submit()"
[form]="form"
-import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Router } from '@angular/router';
@ViewChild('previewButtonPanel')
previewButtonPanel: FormButtonPanelComponent;
+ @Input()
+ hideTitle = false;
+
+ @Input()
+ hideSubmitBtn = false;
+
+ @Output() emitDriveGroup: EventEmitter<DriveGroup> = new EventEmitter();
+
icons = Icons;
form: CdFormGroup;
featureList: OsdFeature[] = [];
hasOrchestrator = true;
- @Input()
- clusterCreation = false;
constructor(
public actionLabels: ActionLabelsI18n,
this.enableFeatures();
}
this.driveGroup.setDeviceSelection(event.type, event.filters);
- if (this.clusterCreation) {
- this.wizardStepService.sharedData = this.driveGroup;
- }
+
+ this.emitDriveGroup.emit(this.driveGroup);
}
onDevicesCleared(event: DevicesSelectionClearEvent) {
-<div class="cd-col-form">
- <form #frm="ngForm"
- [formGroup]="serviceForm"
- novalidate>
- <div class="card">
- <div i18n="form title"
- class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+<cd-modal [pageURL]="pageURL"
+ [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="serviceForm"
+ novalidate>
+ <div class="modal-body">
- <div class="card-body">
<!-- Service type -->
<div class="form-group row">
<label class="cd-col-form-label required"
</ng-container>
</div>
- <div class="card-footer">
+ <div class="modal-footer">
<div class="text-right">
<cd-form-button-panel (submitActionEvent)="onSubmit()"
[form]="serviceForm"
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
</div>
</div>
- </div>
- </form>
-</div>
+ </form>
+ </ng-container>
+</cd-modal>
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
-import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import _ from 'lodash';
import { ToastrModule } from 'ngx-toastr';
configureTestBed({
declarations: [ServiceFormComponent],
+ providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
NgbTypeaheadModule,
beforeEach(() => {
fixture = TestBed.createComponent(ServiceFormComponent);
component = fixture.componentInstance;
+ component.ngOnInit();
form = component.serviceForm;
formHelper = new FormHelper(form);
fixture.detectChanges();
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, Validators } from '@angular/forms';
import { Router } from '@angular/router';
-import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import _ from 'lodash';
import { merge, Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
@ViewChild(NgbTypeahead, { static: false })
typeahead: NgbTypeahead;
+ @Input() public hiddenServices: string[] = [];
+
serviceForm: CdFormGroup;
action: string;
resource: string;
labelFocus = new Subject<string>();
pools: Array<object>;
services: Array<CephServiceSpec> = [];
+ pageURL: string;
constructor(
public actionLabels: ActionLabelsI18n,
private hostService: HostService,
private poolService: PoolService,
private router: Router,
- private taskWrapperService: TaskWrapperService
+ private taskWrapperService: TaskWrapperService,
+ public activeModal: NgbActiveModal
) {
super();
this.resource = $localize`service`;
}
ngOnInit(): void {
+ if (this.router.url.includes('services')) {
+ this.pageURL = 'services';
+ }
this.action = this.actionLabels.CREATE;
this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
// Remove service types:
// osd - This is deployed a different way.
// container - This should only be used in the CLI.
- this.serviceTypes = _.difference(resp, ['container', 'osd']).sort();
+ this.hiddenServices.push('osd', 'container');
+
+ this.serviceTypes = _.difference(resp, this.hiddenServices).sort();
});
this.hostService.list().subscribe((resp: object[]) => {
const options: SelectOption[] = [];
});
}
- goToListView() {
- this.router.navigate(['/services']);
- }
-
searchLabels = (text$: Observable<string>) => {
return merge(
text$.pipe(debounceTime(200), distinctUntilChanged()),
error() {
self.serviceForm.setErrors({ cdSubmitButton: true });
},
- complete() {
- self.goToListView();
+ complete: () => {
+ this.pageURL === 'services'
+ ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
+ : this.activeModal.close();
}
});
}
selectionType="single"
[autoReload]="5000"
(fetchData)="getServices($event)"
- [hasDetails]="true"
+ [hasDetails]="hasDetails"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
<cd-table-actions class="table-actions"
</cd-service-details>
</cd-table>
</ng-container>
+<router-outlet name="modal"></router-outlet>
import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { delay } from 'rxjs/operators';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
import { PlacementPipe } from './placement.pipe';
+import { ServiceFormComponent } from './service-form/service-form.component';
const BASE_URL = 'services';
// Do not display these columns
@Input() hiddenColumns: string[] = [];
+ @Input() hiddenServices: string[] = [];
+
+ @Input() hasDetails = true;
+
+ @Input() modal = true;
+
permissions: Permissions;
tableActions: CdTableAction[];
showDocPanel = false;
+ bsModalRef: NgbModalRef;
orchStatus: OrchestratorStatus;
actionOrchFeatures = {
private cephServiceService: CephServiceService,
private relativeDatePipe: RelativeDatePipe,
private taskWrapperService: TaskWrapperService,
- private urlBuilder: URLBuilderService
+ private router: Router
) {
super();
this.permissions = this.authStorageService.getPermissions();
{
permission: 'create',
icon: Icons.add,
- routerLink: () => this.urlBuilder.getCreate(),
+ click: () => this.openModal(),
name: this.actionLabels.CREATE,
canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
disable: (selection: CdTableSelection) => this.getDisable('create', selection)
];
}
+ openModal() {
+ if (this.modal) {
+ this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
+ } else {
+ this.bsModalRef = this.modalService.show(ServiceFormComponent);
+ this.bsModalRef.componentInstance.hiddenServices = this.hiddenServices;
+ }
+ }
+
ngOnInit() {
const columns = [
{
this.cephServiceService.list().subscribe(
(services: CephServiceSpec[]) => {
this.services = services;
+ this.services = this.services.filter((col: any) => {
+ return !this.hiddenServices.includes(col.service_name);
+ });
this.isLoadingServices = false;
},
() => {
import { map } from 'rxjs/operators';
import { CdDevice } from '../models/devices';
+import { InventoryDeviceType } from '../models/inventory-device-type.model';
import { SmartDataResponseV1 } from '../models/smart';
import { DeviceService } from '../services/device.service';
})
export class OsdService {
private path = 'api/osd';
+ osdDevices: InventoryDeviceType[] = [];
osdRecvSpeedModalPriorities = {
KNOWN_PRIORITIES: [
--- /dev/null
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+
+export interface InventoryDeviceType {
+ type: string;
+ capacity: number;
+ devices: InventoryDevice[];
+ canSelect: boolean;
+ totalDevices: number;
+}
export enum OrchestratorFeature {
HOST_LIST = 'get_hosts',
- HOST_CREATE = 'add_host',
- HOST_DELETE = 'remove_host',
+ HOST_ADD = 'add_host',
+ HOST_REMOVE = 'remove_host',
HOST_LABEL_ADD = 'add_host_label',
HOST_LABEL_REMOVE = 'remove_host_label',
HOST_MAINTENANCE_ENTER = 'enter_host_maintenance',
messages = {
// Host tasks
'host/add': this.newTaskMessage(this.commonOperations.add, (metadata) => this.host(metadata)),
- 'host/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ 'host/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
this.host(metadata)
),
'host/identify_device': this.newTaskMessage(
import { BehaviorSubject, Observable } from 'rxjs';
-import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
-import { DriveGroup } from '~/app/ceph/cluster/osd/osd-form/drive-group.model';
import { WizardStepModel } from '~/app/shared/models/wizard-steps';
const initialStep = [{ stepIndex: 1, isComplete: false }];
export class WizardStepsService {
steps$: BehaviorSubject<WizardStepModel[]>;
currentStep$: BehaviorSubject<WizardStepModel> = new BehaviorSubject<WizardStepModel>(null);
- sharedData = new DriveGroup();
- osdDevices: InventoryDevice[] = [];
- osdCapacity = 0;
constructor() {
this.steps$ = new BehaviorSubject<WizardStepModel[]>(initialStep);
class OrchFeature(object):
HOST_LIST = 'get_hosts'
- HOST_CREATE = 'add_host'
- HOST_DELETE = 'remove_host'
+ HOST_ADD = 'add_host'
+ HOST_REMOVE = 'remove_host'
HOST_LABEL_ADD = 'add_host_label'
HOST_LABEL_REMOVE = 'remove_host_label'
HOST_MAINTENANCE_ENTER = 'enter_host_maintenance'