Autorefresh stops working when service restart
[portal/nonrtric-controlpanel.git] / webapp-frontend / src / app / ei-coordinator / jobs-list / jobs-list.component.ts
1 /*-
2  * ========================LICENSE_START=================================
3  * O-RAN-SC
4  * %%
5  * Copyright (C) 2021 Nordix Foundation
6  * %%
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20 import { Component, OnInit, ViewChild } from "@angular/core";
21 import { FormControl, FormGroup } from "@angular/forms";
22 import { MatPaginator } from "@angular/material/paginator";
23 import { Sort } from "@angular/material/sort";
24 import { MatTableDataSource } from "@angular/material/table";
25 import { EMPTY, forkJoin, of, Subscription, concat } from "rxjs";
26 import { BehaviorSubject } from "rxjs/BehaviorSubject";
27 import { mergeMap, finalize, map, tap, concatMap, delay, skip, catchError } from "rxjs/operators";
28 import { ConsumerService } from "@services/ei/consumer.service";
29 import { UiService } from "@services/ui/ui.service";
30 import { OperationalState } from '@app/interfaces/consumer.types';
31
32 export interface Job {
33   jobId: string;
34   typeId: string;
35   targetUri: string;
36   owner: string;
37   prodIds: string[];
38   status: OperationalState;
39 }
40
41 @Component({
42   selector: "nrcp-jobs-list",
43   templateUrl: "./jobs-list.component.html",
44   styleUrls: ["./jobs-list.component.scss"],
45 })
46 export class JobsListComponent implements OnInit {
47   @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
48   jobsDataSource: MatTableDataSource<Job>;
49   jobForm: FormGroup;
50   darkMode: boolean;
51
52   private jobsSubject$ = new BehaviorSubject<Job[]>([]);
53   private refresh$ = new BehaviorSubject("");
54   private loadingSubject$ = new BehaviorSubject<boolean>(false);
55   private polling$ = new BehaviorSubject(0);
56   public loading$ = this.loadingSubject$.asObservable();
57   subscription: Subscription;
58   checked: boolean = false;
59   firstTime: boolean = true;
60   jobList: Job[] = [];
61
62   constructor(private consumerService: ConsumerService, private ui: UiService) {
63     this.jobForm = new FormGroup({
64       jobId: new FormControl(""),
65       typeId: new FormControl(""),
66       owner: new FormControl(""),
67       targetUri: new FormControl(""),
68       prodIds: new FormControl(""),
69       status: new FormControl("")
70     });
71   }
72
73   ngOnInit(): void {
74     this.subscription = this.dataSubscription();
75
76     this.jobsSubject$.subscribe((data) => {
77       this.jobsDataSource = new MatTableDataSource<Job>(data);
78       this.jobsDataSource.paginator = this.paginator;
79
80       this.jobsDataSource.filterPredicate = ((data: Job, filter) => {
81         let searchTerms = JSON.parse(filter);
82         return (
83           this.isDataIncluding(data.targetUri, searchTerms.targetUri) &&
84           this.isDataIncluding(data.jobId, searchTerms.jobId) &&
85           this.isDataIncluding(data.owner, searchTerms.owner) &&
86           this.isDataIncluding(data.typeId, searchTerms.typeId) &&
87           this.isArrayIncluding(data.prodIds, searchTerms.prodIds) &&
88           this.isDataIncluding(data.status, searchTerms.status)
89         );
90       }) as (data: Job, filter: any) => boolean;
91     });
92
93     this.jobForm.valueChanges.subscribe((value) => {
94       this.jobsDataSource.filter = JSON.stringify(value);
95     });
96
97     this.ui.darkModeState.subscribe((isDark) => {
98       this.darkMode = isDark;
99     });
100   }
101
102   dataSubscription(): Subscription {
103     const jobsInfo$ = this.consumerService.getJobIds().pipe(
104       catchError(_ => { return EMPTY }),
105       tap((_) => {
106         this.jobList = [] as Job[];
107       }),
108       mergeMap((jobIds) =>
109         forkJoin(jobIds.map((jobId) => {
110           return forkJoin([
111             of(jobId).pipe(
112               catchError(err => {
113                 return of([-1]);
114               })
115             ),
116             this.consumerService.getJobInfo(jobId).pipe(
117               catchError(err => {
118                 return of([-1]);
119               })),
120             this.consumerService.getConsumerStatus(jobId).pipe(
121               catchError(err => {
122                 return of([-1]);
123               })),
124           ])
125         }))
126       ),
127       finalize(() => {
128         this.loadingSubject$.next(false);
129         this.jobsSubject$.next(this.jobList);
130       })
131     );
132
133     const whenToRefresh$ = of('').pipe(
134       delay(10000),
135       tap((_) => this.refresh$.next('')),
136       skip(1),
137     );
138
139     const poll$ = concat(jobsInfo$, whenToRefresh$);
140
141     const refreshedJobs$ = this.refresh$.pipe(
142       tap((_) => {
143         this.loadingSubject$.next(true);
144       }),
145       concatMap((_) => this.checked ? poll$ : jobsInfo$),
146       map((response) => this.extractJobs(response))
147     );
148
149     return this.polling$
150       .pipe(
151         concatMap((value) => {
152           let pollCondition = value == 0 || this.checked;
153           return pollCondition ? refreshedJobs$ : EMPTY;
154         })
155       )
156       .subscribe();
157   }
158
159   ngOnDestroy() {
160     this.subscription.unsubscribe();
161   }
162
163   clearFilter() {
164     this.jobForm.get("jobId").setValue("");
165     this.jobForm.get("typeId").setValue("");
166     this.jobForm.get("owner").setValue("");
167     this.jobForm.get("targetUri").setValue("");
168     this.jobForm.get("prodIds").setValue("");
169     this.jobForm.get("status").setValue("");
170   }
171
172   sortJobs(sort: Sort) {
173     const data = this.jobsDataSource.data;
174     data.sort((a: Job, b: Job) => {
175       const isAsc = sort.direction === "asc";
176       switch (sort.active) {
177         case "jobId":
178           return this.compare(a.jobId, b.jobId, isAsc);
179         case "typeId":
180           return this.compare(a.typeId, b.typeId, isAsc);
181         case "owner":
182           return this.compare(a.owner, b.owner, isAsc);
183         case "targetUri":
184           return this.compare(a.targetUri, b.targetUri, isAsc);
185         case "prodIds":
186           return this.compare(a.prodIds, b.prodIds, isAsc);
187         case "status":
188           return this.compare(a.status, b.status, isAsc);
189         default:
190           return 0;
191       }
192     });
193     this.jobsDataSource.data = data;
194   }
195
196   stopPolling(checked) {
197     this.checked = checked;
198     this.polling$.next(this.jobs().length);
199     if (this.checked) {
200       this.refreshDataClick();
201     }
202   }
203
204   compare(a: any, b: any, isAsc: boolean) {
205     return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
206   }
207
208   stopSort(event: any) {
209     event.stopPropagation();
210   }
211
212   isDataIncluding(data: string, filter: string): boolean {
213     const transformedFilter = filter.trim().toLowerCase();
214     return data.toLowerCase().includes(transformedFilter);
215   }
216
217   isArrayIncluding(data: string[], filter: string): boolean {
218     if (!data)
219       return true;
220     for (let i = 0; i < data.length; i++) {
221       return this.isDataIncluding(data[i], filter);
222     }
223   }
224
225   getJobTypeId(job: Job): string {
226     if (job.typeId) {
227       return job.typeId;
228     }
229     return "< No type >";
230   }
231
232   getJobOwner(job: Job): string {
233     if (job.owner) {
234       return job.owner;
235     }
236     return "< No owner >";
237   }
238
239   public jobs(): Job[] {
240     return this.jobsSubject$.value;
241   }
242
243   private extractJobs(res: any) {
244     this.clearFilter();
245     res.forEach(element => {
246       if (element[0] != -1) {
247         if (element[1] != -1 && element[2] != -1) {
248           let jobObj = <Job>{};
249           jobObj.jobId = element[0];
250           jobObj.owner = element[1].job_owner;
251           jobObj.targetUri = element[1].job_result_uri;
252           jobObj.typeId = element[1].info_type_id;
253           jobObj.prodIds = (element[2].producers) ? element[2].producers : ["No Producers"];
254           jobObj.status = element[2].info_job_status;
255           this.jobList = this.jobList.concat(jobObj);
256         } else {
257           let jobObj = <Job>{};
258           jobObj.jobId = element[0];
259           if (element[1] == -1) {
260             jobObj.owner = "--Missing information--";
261             jobObj.targetUri = "--Missing information--";
262             jobObj.typeId = "--Missing information--";
263           }
264           if (element[2] == -1) {
265             jobObj.prodIds = "--Missing information--" as unknown as [];
266             jobObj.status = "--Missing information--" as OperationalState;
267           }
268           this.jobList = this.jobList.concat(jobObj);
269         }
270       }
271     });
272
273     if (this.firstTime && this.jobList.length > 0) {
274       this.polling$.next(this.jobList.length);
275       this.firstTime = false;
276     }
277     return this.jobList;
278   }
279
280   refreshDataClick() {
281     this.refresh$.next("");
282   }
283
284   jobsNumber() : number {
285     return this.jobsDataSource.data.length;
286   }
287
288   hasJobs(): boolean {
289     return this.jobsNumber() > 0;
290   }
291
292 }