]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/blob
7687318f953c5c141a8ead38dff20bbb24a38e4c
[ceph.git] /
1 import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { ActivatedRoute, Router } from '@angular/router';
3 import { NvmeofService, GroupsComboboxItem } from '~/app/shared/api/nvmeof.service';
4 import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
5 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
6 import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum';
7 import { Icons } from '~/app/shared/enum/icons.enum';
8
9 import { CdTableAction } from '~/app/shared/models/cd-table-action';
10 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
11 import { FinishedTask } from '~/app/shared/models/finished-task';
12 import { NvmeofSubsystemNamespace } from '~/app/shared/models/nvmeof';
13 import { Permission } from '~/app/shared/models/permissions';
14 import { CephServiceSpec } from '~/app/shared/models/service.interface';
15 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
16 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
17 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
18
19 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
20 import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
21 import { catchError, map, switchMap, takeUntil } from 'rxjs/operators';
22
23 const BASE_URL = 'block/nvmeof/subsystems';
24 const DEFAULT_PLACEHOLDER = $localize`Enter group name`;
25
26 @Component({
27   selector: 'cd-nvmeof-namespaces-list',
28   templateUrl: './nvmeof-namespaces-list.component.html',
29   styleUrls: ['./nvmeof-namespaces-list.component.scss'],
30   standalone: false
31 })
32 export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
33   @Input()
34   subsystemNQN: string;
35   @Input()
36   group: string;
37   @ViewChild('deleteTpl', { static: true })
38   deleteTpl: TemplateRef<any>;
39   namespacesColumns: any;
40   tableActions: CdTableAction[];
41   selection = new CdTableSelection();
42   permission: Permission;
43   namespaces$: Observable<NvmeofSubsystemNamespace[]>;
44   private namespaceSubject = new BehaviorSubject<void>(undefined);
45
46   // Gateway group dropdown properties
47   gwGroups: GroupsComboboxItem[] = [];
48   gwGroupsEmpty: boolean = false;
49   gwGroupPlaceholder: string = DEFAULT_PLACEHOLDER;
50
51   private destroy$ = new Subject<void>();
52
53   constructor(
54     public actionLabels: ActionLabelsI18n,
55     private router: Router,
56     private route: ActivatedRoute,
57     private modalService: ModalCdsService,
58     private authStorageService: AuthStorageService,
59     private taskWrapper: TaskWrapperService,
60     private nvmeofService: NvmeofService,
61     private dimlessBinaryPipe: DimlessBinaryPipe
62   ) {
63     this.permission = this.authStorageService.getPermissions().nvmeof;
64   }
65
66   ngOnInit() {
67     this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
68       if (params?.['group']) this.onGroupSelection({ content: params?.['group'] });
69     });
70     this.setGatewayGroups();
71     this.namespacesColumns = [
72       {
73         name: $localize`Namespace ID`,
74         prop: 'nsid'
75       },
76       {
77         name: $localize`Size`,
78         prop: 'rbd_image_size',
79         pipe: this.dimlessBinaryPipe
80       },
81       {
82         name: $localize`Pool`,
83         prop: 'rbd_pool_name'
84       },
85       {
86         name: $localize`Image`,
87         prop: 'rbd_image_name'
88       },
89       {
90         name: $localize`Subsystem`,
91         prop: 'ns_subsystem_nqn'
92       }
93     ];
94     this.tableActions = [
95       {
96         name: this.actionLabels.CREATE,
97         permission: 'create',
98         icon: Icons.add,
99         click: () => {
100           this.router.navigate(['block/nvmeof/namespaces/create'], {
101             queryParams: {
102               group: this.group,
103               subsystem_nqn: this.subsystemNQN
104             }
105           });
106         },
107         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
108         disable: () => !this.group
109       },
110       {
111         name: this.actionLabels.EDIT,
112         permission: 'update',
113         icon: Icons.edit,
114         click: () =>
115           this.router.navigate(
116             [
117               BASE_URL,
118               URLVerbs.EDIT,
119               this.selection.first().ns_subsystem_nqn,
120               'namespace',
121               this.selection.first().nsid
122             ],
123             { queryParams: { group: this.group } }
124           )
125       },
126       {
127         name: this.actionLabels.DELETE,
128         permission: 'delete',
129         icon: Icons.destroy,
130         click: () => this.deleteNamespaceModal()
131       }
132     ];
133
134     this.namespaces$ = this.namespaceSubject.pipe(
135       switchMap(() => {
136         if (!this.group) {
137           return of([]);
138         }
139         return this.nvmeofService.listNamespaces(this.group).pipe(
140           map((res: NvmeofSubsystemNamespace[] | { namespaces: NvmeofSubsystemNamespace[] }) => {
141             const namespaces = Array.isArray(res) ? res : res.namespaces || [];
142             // Deduplicate by nsid + subsystem NQN (API with wildcard can return duplicates per gateway)
143             const seen = new Set<string>();
144             return namespaces.filter((ns) => {
145               const key = `${ns.nsid}_${ns['ns_subsystem_nqn']}`;
146               if (seen.has(key)) return false;
147               seen.add(key);
148               return true;
149             });
150           }),
151           catchError(() => of([]))
152         );
153       }),
154       takeUntil(this.destroy$)
155     );
156   }
157
158   updateSelection(selection: CdTableSelection) {
159     this.selection = selection;
160   }
161
162   listNamespaces() {
163     this.namespaceSubject.next();
164   }
165
166   fetchData() {
167     this.namespaceSubject.next();
168   }
169
170   // Gateway groups methods
171   onGroupSelection(selected: GroupsComboboxItem) {
172     selected.selected = true;
173     this.group = selected.content;
174     this.listNamespaces();
175   }
176
177   onGroupClear() {
178     this.group = null;
179     this.listNamespaces();
180   }
181
182   setGatewayGroups() {
183     this.nvmeofService
184       .listGatewayGroups()
185       .pipe(takeUntil(this.destroy$))
186       .subscribe({
187         next: (response: CephServiceSpec[][]) => this.handleGatewayGroupsSuccess(response),
188         error: (error) => this.handleGatewayGroupsError(error)
189       });
190   }
191
192   handleGatewayGroupsSuccess(response: CephServiceSpec[][]) {
193     if (response?.[0]?.length) {
194       this.gwGroups = this.nvmeofService.formatGwGroupsList(response);
195     } else {
196       this.gwGroups = [];
197     }
198     this.updateGroupSelectionState();
199   }
200
201   updateGroupSelectionState() {
202     if (!this.group && this.gwGroups.length) {
203       this.onGroupSelection(this.gwGroups[0]);
204       this.gwGroupsEmpty = false;
205       this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER;
206     } else if (!this.gwGroups.length) {
207       this.gwGroupsEmpty = true;
208       this.gwGroupPlaceholder = $localize`No groups available`;
209     }
210   }
211
212   handleGatewayGroupsError(error: any) {
213     this.gwGroups = [];
214     this.gwGroupsEmpty = true;
215     this.gwGroupPlaceholder = $localize`Unable to fetch Gateway groups`;
216     if (error?.preventDefault) {
217       error?.preventDefault?.();
218     }
219   }
220
221   deleteNamespaceModal() {
222     const namespace = this.selection.first();
223     const subsystemNqn = namespace.ns_subsystem_nqn;
224     this.modalService.show(DeleteConfirmationModalComponent, {
225       itemDescription: $localize`Namespace`,
226       impact: DeletionImpact.high,
227       bodyTemplate: this.deleteTpl,
228       itemNames: [namespace.nsid],
229       actionDescription: 'delete',
230       bodyContext: {
231         deletionMessage: $localize`Deleting the namespace <strong>${namespace.nsid}</strong> will permanently remove all resources, services, and configurations within it. This action cannot be undone.`
232       },
233       submitActionObservable: () =>
234         this.taskWrapper.wrapTaskAroundCall({
235           task: new FinishedTask('nvmeof/namespace/delete', {
236             nqn: subsystemNqn,
237             nsid: namespace.nsid
238           }),
239           call: this.nvmeofService.deleteNamespace(subsystemNqn, namespace.nsid, this.group)
240         })
241     });
242   }
243
244   ngOnDestroy() {
245     this.destroy$.next();
246     this.destroy$.complete();
247   }
248 }