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