]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
7b832640f3972e7909a7e9068cfcc48792b1d5d4
[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
52   // If set, the classAddingTpl is used to enable different css for different values
53   @Input()
54   customCss?: { [css: string]: number | string | ((any) => boolean) };
55
56   columns: Array<CdTableColumn> = [];
57   tableData: KeyValueItem[];
58
59   /**
60    * The function that will be called to update the input data.
61    */
62   @Output()
63   fetchData = new EventEmitter();
64
65   constructor(private datePipe: CdDatePipe) {}
66
67   ngOnInit() {
68     this.columns = [
69       {
70         prop: 'key',
71         flexGrow: 1,
72         cellTransformation: CellTemplate.bold
73       },
74       {
75         prop: 'value',
76         flexGrow: 3
77       }
78     ];
79     if (this.customCss) {
80       this.columns[1].cellTransformation = CellTemplate.classAdding;
81     }
82     // We need to subscribe the 'fetchData' event here and not in the
83     // HTML template, otherwise the data table will display the loading
84     // indicator infinitely if data is only bound via '[data]="xyz"'.
85     // See for 'loadingIndicator' in 'TableComponent::ngOnInit()'.
86     if (this.fetchData.observers.length > 0) {
87       this.table.fetchData.subscribe(() => {
88         // Forward event triggered by the 'cd-table' data table.
89         this.fetchData.emit();
90       });
91     }
92     this.useData();
93   }
94
95   ngOnChanges() {
96     this.useData();
97   }
98
99   useData() {
100     if (!this.data) {
101       return; // Wait for data
102     }
103     this.tableData = this.makePairs(this.data);
104   }
105
106   private makePairs(data: any): KeyValueItem[] {
107     let result: KeyValueItem[] = [];
108     if (!data) {
109       return; // Wait for data
110     } else if (_.isArray(data)) {
111       result = this.makePairsFromArray(data);
112     } else if (_.isObject(data)) {
113       result = this.makePairsFromObject(data);
114     } else {
115       throw new Error('Wrong data format');
116     }
117     result = result
118       .map((item) => {
119         item.value = this.convertValue(item.value);
120         return item;
121       })
122       .filter((i) => i.value !== null);
123     return _.sortBy(this.renderObjects ? this.insertFlattenObjects(result) : result, 'key');
124   }
125
126   private makePairsFromArray(data: any[]): KeyValueItem[] {
127     let temp = [];
128     const first = data[0];
129     if (_.isArray(first)) {
130       if (first.length === 2) {
131         temp = data.map((a) => ({
132           key: a[0],
133           value: a[1]
134         }));
135       } else {
136         throw new Error(
137           `Array contains too many elements (${first.length}). ` +
138             `Needs to be of type [string, any][]`
139         );
140       }
141     } else if (_.isObject(first)) {
142       if (_.has(first, 'key') && _.has(first, 'value')) {
143         temp = [...data];
144       } else {
145         temp = data.reduce(
146           (previous: any[], item) => previous.concat(this.makePairsFromObject(item)),
147           temp
148         );
149       }
150     }
151     return temp;
152   }
153
154   private makePairsFromObject(data: object): KeyValueItem[] {
155     return Object.keys(data).map((k) => ({
156       key: k,
157       value: data[k]
158     }));
159   }
160
161   private insertFlattenObjects(data: KeyValueItem[]): any[] {
162     return _.flattenDeep(
163       data.map((item) => {
164         const value = item.value;
165         const isObject = _.isObject(value);
166         if (!isObject || _.isEmpty(value)) {
167           if (isObject) {
168             item.value = '';
169           }
170           return item;
171         }
172         return this.splitItemIntoItems(item);
173       })
174     );
175   }
176
177   /**
178    * Split item into items will call _makePairs inside _makePairs (recursion), in oder to split
179    * the object item up into items as planned.
180    */
181   private splitItemIntoItems(data: { key: string; value: object }): KeyValueItem[] {
182     return this.makePairs(data.value).map((item) => {
183       if (this.appendParentKey) {
184         item.key = data.key + ' ' + item.key;
185       }
186       return item;
187     });
188   }
189
190   private convertValue(value: any): KeyValueItem {
191     if (_.isArray(value)) {
192       if (_.isEmpty(value) && this.hideEmpty) {
193         return null;
194       }
195       value = value.map((item) => (_.isObject(item) ? JSON.stringify(item) : item)).join(', ');
196     } else if (_.isObject(value)) {
197       if ((this.hideEmpty && _.isEmpty(value)) || !this.renderObjects) {
198         return null;
199       }
200     } else if (_.isString(value)) {
201       if (value === '' && this.hideEmpty) {
202         return null;
203       }
204       if (this.isDate(value)) {
205         value = this.datePipe.transform(value) || value;
206       }
207     }
208
209     return value;
210   }
211
212   private isDate(s) {
213     const sep = '[ -:.TZ]';
214     const n = '\\d{2}' + sep;
215     //                            year     -    m - d - h : m : s . someRest  Z (if UTC)
216     return s.match(new RegExp('^\\d{4}' + sep + n + n + n + n + n + '\\d*' + 'Z?$'));
217   }
218 }