]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
8006bf721843f8e421fe99e52552bd796149377a
[ceph-ci.git] /
1 import {
2   Component,
3   EventEmitter,
4   Input,
5   OnChanges,
6   OnInit,
7   Output,
8   ViewChild
9 } from '@angular/core';
10
11 import * as _ from 'lodash';
12
13 import { CellTemplate } from '../../enum/cell-template.enum';
14 import { CdTableColumn } from '../../models/cd-table-column';
15 import { CdDatePipe } from '../../pipes/cd-date.pipe';
16 import { TableComponent } from '../table/table.component';
17
18 interface KeyValueItem {
19   key: string;
20   value: any;
21 }
22
23 /**
24  * Display the given data in a 2 column data table. The left column
25  * shows the 'key' attribute, the right column the 'value' attribute.
26  * The data table has the following characteristics:
27  * - No header and footer is displayed
28  * - The relation of the width for the columns 'key' and 'value' is 1:3
29  * - The 'key' column is displayed in bold text
30  */
31 @Component({
32   selector: 'cd-table-key-value',
33   templateUrl: './table-key-value.component.html',
34   styleUrls: ['./table-key-value.component.scss']
35 })
36 export class TableKeyValueComponent implements OnInit, OnChanges {
37   @ViewChild(TableComponent, { static: true })
38   table: TableComponent;
39
40   @Input()
41   data: any;
42   @Input()
43   autoReload: any = 5000;
44   @Input()
45   renderObjects = false;
46   // Only used if objects are rendered
47   @Input()
48   appendParentKey = true;
49   @Input()
50   hideEmpty = false;
51   @Input()
52   hideKeys = []; // Keys of pairs not to be displayed
53
54   // If set, the classAddingTpl is used to enable different css for different values
55   @Input()
56   customCss?: { [css: string]: number | string | ((any) => boolean) };
57
58   columns: Array<CdTableColumn> = [];
59   tableData: KeyValueItem[];
60
61   /**
62    * The function that will be called to update the input data.
63    */
64   @Output()
65   fetchData = new EventEmitter();
66
67   constructor(private datePipe: CdDatePipe) {}
68
69   ngOnInit() {
70     this.columns = [
71       {
72         prop: 'key',
73         flexGrow: 1,
74         cellTransformation: CellTemplate.bold
75       },
76       {
77         prop: 'value',
78         flexGrow: 3
79       }
80     ];
81     if (this.customCss) {
82       this.columns[1].cellTransformation = CellTemplate.classAdding;
83     }
84     // We need to subscribe the 'fetchData' event here and not in the
85     // HTML template, otherwise the data table will display the loading
86     // indicator infinitely if data is only bound via '[data]="xyz"'.
87     // See for 'loadingIndicator' in 'TableComponent::ngOnInit()'.
88     if (this.fetchData.observers.length > 0) {
89       this.table.fetchData.subscribe(() => {
90         // Forward event triggered by the 'cd-table' data table.
91         this.fetchData.emit();
92       });
93     }
94     this.useData();
95   }
96
97   ngOnChanges() {
98     this.useData();
99   }
100
101   useData() {
102     if (!this.data) {
103       return; // Wait for data
104     }
105     let pairs = this.makePairs(this.data);
106     if (this.hideKeys) {
107       pairs = pairs.filter((pair) => !this.hideKeys.includes(pair.key));
108     }
109     this.tableData = pairs;
110   }
111
112   private makePairs(data: any): KeyValueItem[] {
113     let result: KeyValueItem[] = [];
114     if (!data) {
115       return; // Wait for data
116     } else if (_.isArray(data)) {
117       result = this.makePairsFromArray(data);
118     } else if (_.isObject(data)) {
119       result = this.makePairsFromObject(data);
120     } else {
121       throw new Error('Wrong data format');
122     }
123     result = result
124       .map((item) => {
125         item.value = this.convertValue(item.value);
126         return item;
127       })
128       .filter((i) => i.value !== null);
129     return _.sortBy(this.renderObjects ? this.insertFlattenObjects(result) : result, 'key');
130   }
131
132   private makePairsFromArray(data: any[]): KeyValueItem[] {
133     let temp = [];
134     const first = data[0];
135     if (_.isArray(first)) {
136       if (first.length === 2) {
137         temp = data.map((a) => ({
138           key: a[0],
139           value: a[1]
140         }));
141       } else {
142         throw new Error(
143           `Array contains too many elements (${first.length}). ` +
144             `Needs to be of type [string, any][]`
145         );
146       }
147     } else if (_.isObject(first)) {
148       if (_.has(first, 'key') && _.has(first, 'value')) {
149         temp = [...data];
150       } else {
151         temp = data.reduce(
152           (previous: any[], item) => previous.concat(this.makePairsFromObject(item)),
153           temp
154         );
155       }
156     }
157     return temp;
158   }
159
160   private makePairsFromObject(data: object): KeyValueItem[] {
161     return Object.keys(data).map((k) => ({
162       key: k,
163       value: data[k]
164     }));
165   }
166
167   private insertFlattenObjects(data: KeyValueItem[]): any[] {
168     return _.flattenDeep(
169       data.map((item) => {
170         const value = item.value;
171         const isObject = _.isObject(value);
172         if (!isObject || _.isEmpty(value)) {
173           if (isObject) {
174             item.value = '';
175           }
176           return item;
177         }
178         return this.splitItemIntoItems(item);
179       })
180     );
181   }
182
183   /**
184    * Split item into items will call _makePairs inside _makePairs (recursion), in oder to split
185    * the object item up into items as planned.
186    */
187   private splitItemIntoItems(data: { key: string; value: object }): KeyValueItem[] {
188     return this.makePairs(data.value).map((item) => {
189       if (this.appendParentKey) {
190         item.key = data.key + ' ' + item.key;
191       }
192       return item;
193     });
194   }
195
196   private convertValue(value: any): KeyValueItem {
197     if (_.isArray(value)) {
198       if (_.isEmpty(value) && this.hideEmpty) {
199         return null;
200       }
201       value = value.map((item) => (_.isObject(item) ? JSON.stringify(item) : item)).join(', ');
202     } else if (_.isObject(value)) {
203       if ((this.hideEmpty && _.isEmpty(value)) || !this.renderObjects) {
204         return null;
205       }
206     } else if (_.isString(value)) {
207       if (value === '' && this.hideEmpty) {
208         return null;
209       }
210       if (this.isDate(value)) {
211         value = this.datePipe.transform(value) || value;
212       }
213     }
214
215     return value;
216   }
217
218   private isDate(s) {
219     const sep = '[ -:.TZ]';
220     const n = '\\d{2}' + sep;
221     //                            year     -    m - d - h : m : s . someRest  Z (if UTC)
222     return s.match(new RegExp('^\\d{4}' + sep + n + n + n + n + n + '\\d*' + 'Z?$'));
223   }
224 }