]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/blob
42f50767e755154a76c192929417cbe222c3b32d
[ceph-ci.git] /
1 import {
2   Component,
3   EventEmitter,
4   Input,
5   OnDestroy,
6   OnInit,
7   Output,
8   TemplateRef,
9   ViewChild
10 } from '@angular/core';
11 import { ActivatedRoute } from '@angular/router';
12 import { Observable, Subject, Subscription } from 'rxjs';
13 import { finalize } from 'rxjs/operators';
14
15 import { TableComponent } from '~/app/shared/datatable/table/table.component';
16 import { HostStatus } from '~/app/shared/enum/host-status.enum';
17 import { Icons } from '~/app/shared/enum/icons.enum';
18 import { CdTableAction } from '~/app/shared/models/cd-table-action';
19 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
20 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
21 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
22 import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
23 import { Permission } from '~/app/shared/models/permissions';
24
25 import { Host } from '~/app/shared/models/host.interface';
26 import { CephServiceSpec } from '~/app/shared/models/service.interface';
27 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
28 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
29 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
30 import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component';
31
32 @Component({
33   selector: 'cd-nvmeof-gateway-node',
34   templateUrl: './nvmeof-gateway-node.component.html',
35   styleUrls: ['./nvmeof-gateway-node.component.scss'],
36   standalone: false
37 })
38 export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
39   @ViewChild(TableComponent, { static: true })
40   table!: TableComponent;
41
42   @ViewChild('hostNameTpl', { static: true })
43   hostNameTpl!: TemplateRef<any>;
44
45   @ViewChild('statusTpl', { static: true })
46   statusTpl!: TemplateRef<any>;
47
48   @ViewChild('addrTpl', { static: true })
49   addrTpl!: TemplateRef<any>;
50
51   @ViewChild('labelsTpl', { static: true })
52   labelsTpl!: TemplateRef<any>;
53
54   @Output() selectionChange = new EventEmitter<CdTableSelection>();
55   @Output() hostsLoaded = new EventEmitter<number>();
56
57   @Input() groupName: string | undefined;
58   @Input() mode: 'selector' | 'details' = 'selector';
59
60   usedHostnames: Set<string> = new Set();
61   serviceSpec: CephServiceSpec | undefined;
62   hasAvailableHosts = false;
63
64   permission: Permission;
65   columns: CdTableColumn[] = [];
66   hosts: Host[] = [];
67   isLoadingHosts = false;
68   tableActions: CdTableAction[] = [];
69   selectionType: 'single' | 'multiClick' | 'none' = 'single';
70
71   selection = new CdTableSelection();
72   icons = Icons;
73   HostStatus = HostStatus;
74   private tableContext: CdTableFetchDataContext | undefined;
75   count = 0;
76   orchStatus: OrchestratorStatus | undefined;
77   private destroy$ = new Subject<void>();
78   private sub: Subscription | undefined;
79
80   constructor(
81     private authStorageService: AuthStorageService,
82     private nvmeofService: NvmeofService,
83     private route: ActivatedRoute,
84     private modalService: ModalCdsService
85   ) {
86     this.permission = this.authStorageService.getPermissions().nvmeof;
87   }
88
89   ngOnInit(): void {
90     const routeData = this.route.snapshot.data;
91     if (routeData?.['mode']) {
92       this.mode = routeData['mode'];
93     }
94
95     this.selectionType = this.mode === 'selector' ? 'multiClick' : 'single';
96
97     if (this.mode === 'details') {
98       this.route.parent?.params.subscribe((params: any) => {
99         this.groupName = params.group;
100       });
101       this.setTableActions();
102     }
103
104     this.columns = [
105       {
106         name: $localize`Hostname`,
107         prop: 'hostname',
108         flexGrow: 1,
109         cellTemplate: this.hostNameTpl
110       },
111       {
112         name: $localize`IP address`,
113         prop: 'addr',
114         flexGrow: 0.8,
115         cellTemplate: this.addrTpl
116       },
117       {
118         name: $localize`Status`,
119         prop: 'status',
120         flexGrow: 0.8,
121         cellTemplate: this.statusTpl
122       },
123       {
124         name: $localize`Labels (tags)`,
125         prop: 'labels',
126         flexGrow: 1,
127         cellTemplate: this.labelsTpl
128       }
129     ];
130   }
131
132   private setTableActions() {
133     this.tableActions = [
134       {
135         permission: 'create',
136         icon: Icons.add,
137         click: () => this.addGateway(),
138         name: $localize`Add`,
139         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
140         disable: () => (!this.hasAvailableHosts ? $localize`No available nodes to add` : false)
141       },
142       {
143         permission: 'delete',
144         icon: Icons.destroy,
145         click: () => this.removeGateway(),
146         name: $localize`Remove`,
147         disable: (selection: CdTableSelection) => !selection.hasSelection
148       }
149     ];
150   }
151
152   addGateway(): void {
153     const modalRef = this.modalService.show(NvmeofGatewayNodeAddModalComponent, {
154       groupName: this.groupName,
155       usedHostnames: Array.from(this.usedHostnames),
156       serviceSpec: this.serviceSpec
157     });
158
159     modalRef.gatewayAdded.subscribe(() => {
160       this.table.refreshBtn();
161     });
162   }
163
164   removeGateway(): void {
165     // TODO
166   }
167
168   ngOnDestroy(): void {
169     this.destroy$.next();
170     this.destroy$.complete();
171     if (this.sub) {
172       this.sub.unsubscribe();
173     }
174   }
175
176   updateSelection(selection: CdTableSelection): void {
177     this.selection = selection;
178     this.selectionChange.emit(selection);
179   }
180
181   getSelectedHostnames(): string[] {
182     return this.selection.selected.map((host: Host) => host.hostname);
183   }
184
185   getHosts(context: CdTableFetchDataContext): void {
186     this.tableContext =
187       context || this.tableContext || new CdTableFetchDataContext(() => undefined);
188     if (this.isLoadingHosts) {
189       return;
190     }
191     this.isLoadingHosts = true;
192
193     if (this.sub) {
194       this.sub.unsubscribe();
195     }
196
197     const fetchData$: Observable<any> =
198       this.mode === 'details'
199         ? this.nvmeofService.fetchHostsAndGroups()
200         : this.nvmeofService.getAvailableHosts(this.tableContext?.toParams());
201
202     this.sub = fetchData$
203       .pipe(
204         finalize(() => {
205           this.isLoadingHosts = false;
206         })
207       )
208       .subscribe({
209         next: (result: any) => {
210           if (this.mode === 'details') {
211             this.processDetailsData(result.groups, result.hosts);
212           } else {
213             this.hosts = result;
214             this.count = this.hosts.length;
215             this.hostsLoaded.emit(this.count);
216           }
217         },
218         error: () => context?.error()
219       });
220   }
221
222   private processDetailsData(groups: any[][], hostList: Host[]) {
223     const groupList = groups?.[0] ?? [];
224
225     const allUsedHostnames = new Set<string>();
226     groupList.forEach((group: CephServiceSpec) => {
227       const hosts = group.placement?.hosts || (group.spec as any)?.placement?.hosts || [];
228       hosts.forEach((hostname: string) => allUsedHostnames.add(hostname));
229     });
230
231     this.usedHostnames = allUsedHostnames;
232
233     // Check if there are any available hosts globally (not used by any group)
234     this.hasAvailableHosts = (hostList || []).some(
235       (host: Host) => !this.usedHostnames.has(host.hostname)
236     );
237     this.setTableActions();
238
239     const currentGroup = groupList.find((group: CephServiceSpec) => {
240       return (
241         group.spec?.group === this.groupName ||
242         group.service_id === `nvmeof.${this.groupName}` ||
243         group.service_id.endsWith(`.${this.groupName}`)
244       );
245     });
246
247     this.serviceSpec = currentGroup as CephServiceSpec;
248
249     if (!this.serviceSpec) {
250       this.hosts = [];
251     } else {
252       const placementHosts =
253         this.serviceSpec.placement?.hosts || (this.serviceSpec.spec as any)?.placement?.hosts || [];
254       const currentGroupHosts = new Set<string>(placementHosts);
255
256       this.hosts = (hostList || []).filter((host: Host) => {
257         return currentGroupHosts.has(host.hostname);
258       });
259     }
260
261     this.count = this.hosts.length;
262     this.hostsLoaded.emit(this.count);
263   }
264 }