1 import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { Router } from '@angular/router';
4 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
5 import _ from 'lodash';
6 import { Subscription } from 'rxjs';
7 import { mergeMap } from 'rxjs/operators';
9 import { HostService } from '~/app/shared/api/host.service';
10 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
11 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
12 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
13 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
14 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
15 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
16 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
17 import { TableComponent } from '~/app/shared/datatable/table/table.component';
18 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
19 import { Icons } from '~/app/shared/enum/icons.enum';
20 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
21 import { CdTableAction } from '~/app/shared/models/cd-table-action';
22 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
23 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
24 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
25 import { FinishedTask } from '~/app/shared/models/finished-task';
26 import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
27 import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
28 import { Permissions } from '~/app/shared/models/permissions';
29 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
30 import { EmptyPipe } from '~/app/shared/pipes/empty.pipe';
31 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
32 import { ModalService } from '~/app/shared/services/modal.service';
33 import { NotificationService } from '~/app/shared/services/notification.service';
34 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
35 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
36 import { HostFormComponent } from './host-form/host-form.component';
38 const BASE_URL = 'hosts';
42 templateUrl: './hosts.component.html',
43 styleUrls: ['./hosts.component.scss'],
44 providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
46 export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit {
47 private sub = new Subscription();
49 @ViewChild(TableComponent)
50 table: TableComponent;
51 @ViewChild('servicesTpl', { static: true })
52 public servicesTpl: TemplateRef<any>;
53 @ViewChild('maintenanceConfirmTpl', { static: true })
54 maintenanceConfirmTpl: TemplateRef<any>;
55 @ViewChild('orchTmpl', { static: true })
56 orchTmpl: TemplateRef<any>;
57 @ViewChild('flashTmpl', { static: true })
58 flashTmpl: TemplateRef<any>;
59 @ViewChild('hostNameTpl', { static: true })
60 hostNameTpl: TemplateRef<any>;
63 hiddenColumns: string[] = [];
66 hideMaintenance = false;
69 hasTableDetails = true;
72 hideToolHeader = false;
75 showGeneralActionsOnly = false;
77 permissions: Permissions;
78 columns: Array<CdTableColumn> = [];
79 hosts: Array<object> = [];
80 isLoadingHosts = false;
81 cdParams = { fromLink: '/hosts' };
82 tableActions: CdTableAction[];
83 selection = new CdTableSelection();
84 modalRef: NgbModalRef;
87 enableMaintenanceBtn: boolean;
88 enableDrainBtn: boolean;
89 bsModalRef: NgbModalRef;
94 nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`
97 orchStatus: OrchestratorStatus;
98 actionOrchFeatures = {
99 add: [OrchestratorFeature.HOST_ADD],
100 edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE],
101 remove: [OrchestratorFeature.HOST_REMOVE],
103 OrchestratorFeature.HOST_MAINTENANCE_ENTER,
104 OrchestratorFeature.HOST_MAINTENANCE_EXIT
106 drain: [OrchestratorFeature.HOST_DRAIN]
110 private authStorageService: AuthStorageService,
111 private dimlessBinary: DimlessBinaryPipe,
112 private emptyPipe: EmptyPipe,
113 private hostService: HostService,
114 private actionLabels: ActionLabelsI18n,
115 private modalService: ModalService,
116 private taskWrapper: TaskWrapperService,
117 private router: Router,
118 private notificationService: NotificationService,
119 private orchService: OrchestratorService
122 this.permissions = this.authStorageService.getPermissions();
123 this.tableActions = [
125 name: this.actionLabels.ADD,
126 permission: 'create',
129 this.router.url.includes('/hosts')
130 ? this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }])
131 : (this.bsModalRef = this.modalService.show(HostFormComponent, {
132 hideMaintenance: this.hideMaintenance
134 disable: (selection: CdTableSelection) => this.getDisable('add', selection)
137 name: this.actionLabels.EDIT,
138 permission: 'update',
140 click: () => this.editAction(),
141 disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
144 name: this.actionLabels.START_DRAIN,
145 permission: 'update',
147 click: () => this.hostDrain(),
148 disable: (selection: CdTableSelection) =>
149 this.getDisable('drain', selection) || !this.enableDrainBtn,
150 visible: () => !this.showGeneralActionsOnly && this.enableDrainBtn
153 name: this.actionLabels.STOP_DRAIN,
154 permission: 'update',
156 click: () => this.hostDrain(true),
157 disable: (selection: CdTableSelection) =>
158 this.getDisable('drain', selection) || this.enableDrainBtn,
159 visible: () => !this.showGeneralActionsOnly && !this.enableDrainBtn
162 name: this.actionLabels.REMOVE,
163 permission: 'delete',
165 click: () => this.deleteAction(),
166 disable: (selection: CdTableSelection) => this.getDisable('remove', selection)
169 name: this.actionLabels.ENTER_MAINTENANCE,
170 permission: 'update',
172 click: () => this.hostMaintenance(),
173 disable: (selection: CdTableSelection) =>
174 this.getDisable('maintenance', selection) ||
176 this.enableMaintenanceBtn,
177 visible: () => !this.showGeneralActionsOnly && !this.enableMaintenanceBtn
180 name: this.actionLabels.EXIT_MAINTENANCE,
181 permission: 'update',
183 click: () => this.hostMaintenance(),
184 disable: (selection: CdTableSelection) =>
185 this.getDisable('maintenance', selection) ||
187 !this.enableMaintenanceBtn,
188 visible: () => !this.showGeneralActionsOnly && this.enableMaintenanceBtn
196 name: $localize`Hostname`,
199 cellTemplate: this.hostNameTpl
202 name: $localize`Service Instances`,
203 prop: 'service_instances',
205 cellTemplate: this.servicesTpl
208 name: $localize`Labels`,
211 cellTransformation: CellTemplate.badge,
212 customTemplateConfig: {
217 name: $localize`Status`,
220 cellTransformation: CellTemplate.badge,
221 customTemplateConfig: {
223 maintenance: { class: 'badge-warning' },
224 available: { class: 'badge-success' }
229 name: $localize`Model`,
234 name: $localize`CPUs`,
239 name: $localize`Cores`,
244 name: $localize`Total Memory`,
245 prop: 'memory_total_bytes',
246 pipe: this.dimlessBinary,
250 name: $localize`Raw Capacity`,
251 prop: 'raw_capacity',
252 pipe: this.dimlessBinary,
256 name: $localize`HDDs`,
261 name: $localize`Flash`,
263 headerTemplate: this.flashTmpl,
267 name: $localize`NICs`,
273 this.columns = this.columns.filter((col: any) => {
274 return !this.hiddenColumns.includes(col.prop);
279 this.sub.unsubscribe();
282 updateSelection(selection: CdTableSelection) {
283 this.selection = selection;
284 this.enableMaintenanceBtn = false;
285 this.enableDrainBtn = false;
286 if (this.selection.hasSelection) {
287 if (this.selection.first().status === 'maintenance') {
288 this.enableMaintenanceBtn = true;
291 if (!this.selection.first().labels.includes('_no_schedule')) {
292 this.enableDrainBtn = true;
298 this.hostService.getLabels().subscribe((resp: string[]) => {
299 const host = this.selection.first();
300 const labels = new Set(resp.concat(this.hostService.predefinedLabels));
301 const allLabels = Array.from(labels).map((label) => {
302 return { enabled: true, name: label };
304 this.modalService.show(FormModalComponent, {
305 titleText: $localize`Edit Host: ${host.hostname}`,
308 type: 'select-badges',
310 value: host['labels'],
311 label: $localize`Labels`,
315 messages: new SelectMessages({
316 empty: $localize`There are no labels.`,
317 filter: $localize`Filter or add labels`,
318 add: $localize`Add label`
323 submitButtonText: $localize`Edit Host`,
324 onSubmit: (values: any) => {
325 this.hostService.update(host['hostname'], true, values.labels).subscribe(() => {
326 this.notificationService.show(
327 NotificationType.success,
328 $localize`Updated Host "${host.hostname}"`
330 // Reload the data table content.
331 this.table.refreshBtn();
339 this.isExecuting = true;
340 const host = this.selection.first();
341 if (host['status'] !== 'maintenance') {
342 this.hostService.update(host['hostname'], false, [], true).subscribe(
344 this.isExecuting = false;
345 this.notificationService.show(
346 NotificationType.success,
347 $localize`"${host.hostname}" moved to maintenance`
349 this.table.refreshBtn();
352 this.isExecuting = false;
353 this.errorMessage = error.error['detail'].split(/\n/);
354 error.preventDefault();
356 error.error['detail'].includes('WARNING') &&
357 !error.error['detail'].includes('It is NOT safe to stop') &&
358 !error.error['detail'].includes('ALERT') &&
359 !error.error['detail'].includes('unsafe to stop')
361 const modalVariables = {
362 titleText: $localize`Warning`,
363 buttonText: $localize`Continue`,
365 bodyTpl: this.maintenanceConfirmTpl,
368 this.hostService.update(host['hostname'], false, [], true, true).subscribe(
370 this.modalRef.close();
372 () => this.modalRef.close()
376 this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
378 this.notificationService.show(
379 NotificationType.error,
380 $localize`"${host.hostname}" cannot be put into maintenance`,
381 $localize`${error.error['detail']}`
387 this.hostService.update(host['hostname'], false, [], true).subscribe(() => {
388 this.isExecuting = false;
389 this.notificationService.show(
390 NotificationType.success,
391 $localize`"${host.hostname}" has exited maintenance`
393 this.table.refreshBtn();
398 hostDrain(stop = false) {
399 const host = this.selection.first();
401 const index = host['labels'].indexOf('_no_schedule', 0);
402 host['labels'].splice(index, 1);
403 this.hostService.update(host['hostname'], true, host['labels']).subscribe(() => {
404 this.notificationService.show(
405 NotificationType.info,
406 $localize`"${host['hostname']}" stopped draining`
408 this.table.refreshBtn();
411 this.hostService.update(host['hostname'], false, [], false, false, true).subscribe(() => {
412 this.notificationService.show(
413 NotificationType.info,
414 $localize`"${host['hostname']}" started draining`
416 this.table.refreshBtn();
422 action: 'add' | 'edit' | 'remove' | 'maintenance' | 'drain',
423 selection: CdTableSelection
424 ): boolean | string {
426 action === 'remove' ||
428 action === 'maintenance' ||
431 if (!selection?.hasSingleSelection) {
434 if (!_.every(selection.selected, 'sources.orchestrator')) {
435 return this.messages.nonOrchHost;
438 return this.orchService.getTableActionDisableDesc(
440 this.actionOrchFeatures[action]
445 const hostname = this.selection.first().hostname;
446 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
447 itemDescription: 'Host',
448 itemNames: [hostname],
449 actionDescription: 'remove',
450 submitActionObservable: () =>
451 this.taskWrapper.wrapTaskAroundCall({
452 task: new FinishedTask('host/remove', { hostname: hostname }),
453 call: this.hostService.delete(hostname)
458 checkHostsFactsAvailable() {
459 const orchFeatures = this.orchStatus.features;
460 if (!_.isEmpty(orchFeatures)) {
461 if (orchFeatures.get_facts.available) {
469 transformHostsData() {
470 if (this.checkHostsFactsAvailable()) {
471 _.forEach(this.hosts, (hostKey) => {
472 hostKey['memory_total_bytes'] = this.emptyPipe.transform(hostKey['memory_total_kb'] * 1024);
473 hostKey['raw_capacity'] = this.emptyPipe.transform(
474 hostKey['hdd_capacity_bytes'] + hostKey['flash_capacity_bytes']
478 // mark host facts columns unavailable
479 for (let column = 4; column < this.columns.length; column++) {
480 this.columns[column]['cellTemplate'] = this.orchTmpl;
485 getHosts(context: CdTableFetchDataContext) {
486 if (this.isLoadingHosts) {
489 this.isLoadingHosts = true;
490 this.sub = this.orchService
493 mergeMap((orchStatus) => {
494 this.orchStatus = orchStatus;
495 const factsAvailable = this.checkHostsFactsAvailable();
496 return this.hostService.list(`${factsAvailable}`);
501 this.hosts = hostList;
502 this.hosts.forEach((host: object) => {
503 if (host['status'] === '') {
504 host['status'] = 'available';
507 this.transformHostsData();
508 this.isLoadingHosts = false;
511 this.isLoadingHosts = false;