summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts224
1 files changed, 224 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
new file mode 100644
index 000000000..0f450ce2a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
@@ -0,0 +1,224 @@
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import _ from 'lodash';
+
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { TableComponent } from '../table/table.component';
+
+interface KeyValueItem {
+ key: string;
+ value: any;
+}
+
+/**
+ * Display the given data in a 2 column data table. The left column
+ * shows the 'key' attribute, the right column the 'value' attribute.
+ * The data table has the following characteristics:
+ * - No header and footer is displayed
+ * - The relation of the width for the columns 'key' and 'value' is 1:3
+ * - The 'key' column is displayed in bold text
+ */
+@Component({
+ selector: 'cd-table-key-value',
+ templateUrl: './table-key-value.component.html',
+ styleUrls: ['./table-key-value.component.scss']
+})
+export class TableKeyValueComponent implements OnInit, OnChanges {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+
+ @Input()
+ data: any;
+ @Input()
+ autoReload: any = 5000;
+ @Input()
+ renderObjects = false;
+ // Only used if objects are rendered
+ @Input()
+ appendParentKey = true;
+ @Input()
+ hideEmpty = false;
+ @Input()
+ hideKeys: string[] = []; // Keys of pairs not to be displayed
+
+ // If set, the classAddingTpl is used to enable different css for different values
+ @Input()
+ customCss?: { [css: string]: number | string | ((any: any) => boolean) };
+
+ columns: Array<CdTableColumn> = [];
+ tableData: KeyValueItem[];
+
+ /**
+ * The function that will be called to update the input data.
+ */
+ @Output()
+ fetchData = new EventEmitter();
+
+ constructor(private datePipe: CdDatePipe) {}
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'key',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.bold
+ },
+ {
+ prop: 'value',
+ flexGrow: 3
+ }
+ ];
+ if (this.customCss) {
+ this.columns[1].cellTransformation = CellTemplate.classAdding;
+ }
+ // We need to subscribe the 'fetchData' event here and not in the
+ // HTML template, otherwise the data table will display the loading
+ // indicator infinitely if data is only bound via '[data]="xyz"'.
+ // See for 'loadingIndicator' in 'TableComponent::ngOnInit()'.
+ if (this.fetchData.observers.length > 0) {
+ this.table.fetchData.subscribe(() => {
+ // Forward event triggered by the 'cd-table' data table.
+ this.fetchData.emit();
+ });
+ }
+ this.useData();
+ }
+
+ ngOnChanges() {
+ this.useData();
+ }
+
+ useData() {
+ if (!this.data) {
+ return; // Wait for data
+ }
+ let pairs = this.makePairs(this.data);
+ if (this.hideKeys) {
+ pairs = pairs.filter((pair) => !this.hideKeys.includes(pair.key));
+ }
+ this.tableData = pairs;
+ }
+
+ private makePairs(data: any): KeyValueItem[] {
+ let result: KeyValueItem[] = [];
+ if (!data) {
+ return undefined; // Wait for data
+ } else if (_.isArray(data)) {
+ result = this.makePairsFromArray(data);
+ } else if (_.isObject(data)) {
+ result = this.makePairsFromObject(data);
+ } else {
+ throw new Error('Wrong data format');
+ }
+ result = result
+ .map((item) => {
+ item.value = this.convertValue(item.value);
+ return item;
+ })
+ .filter((i) => i.value !== null);
+ return _.sortBy(this.renderObjects ? this.insertFlattenObjects(result) : result, 'key');
+ }
+
+ private makePairsFromArray(data: any[]): KeyValueItem[] {
+ let temp: any[] = [];
+ const first = data[0];
+ if (_.isArray(first)) {
+ if (first.length === 2) {
+ temp = data.map((a) => ({
+ key: a[0],
+ value: a[1]
+ }));
+ } else {
+ throw new Error(
+ `Array contains too many elements (${first.length}). ` +
+ `Needs to be of type [string, any][]`
+ );
+ }
+ } else if (_.isObject(first)) {
+ if (_.has(first, 'key') && _.has(first, 'value')) {
+ temp = [...data];
+ } else {
+ temp = data.reduce(
+ (previous: any[], item) => previous.concat(this.makePairsFromObject(item)),
+ temp
+ );
+ }
+ }
+ return temp;
+ }
+
+ private makePairsFromObject(data: any): KeyValueItem[] {
+ return Object.keys(data).map((k) => ({
+ key: k,
+ value: data[k]
+ }));
+ }
+
+ private insertFlattenObjects(data: KeyValueItem[]): any[] {
+ return _.flattenDeep(
+ data.map((item) => {
+ const value = item.value;
+ const isObject = _.isObject(value);
+ if (!isObject || _.isEmpty(value)) {
+ if (isObject) {
+ item.value = '';
+ }
+ return item;
+ }
+ return this.splitItemIntoItems(item);
+ })
+ );
+ }
+
+ /**
+ * Split item into items will call _makePairs inside _makePairs (recursion), in oder to split
+ * the object item up into items as planned.
+ */
+ private splitItemIntoItems(data: { key: string; value: object }): KeyValueItem[] {
+ return this.makePairs(data.value).map((item) => {
+ if (this.appendParentKey) {
+ item.key = data.key + ' ' + item.key;
+ }
+ return item;
+ });
+ }
+
+ private convertValue(value: any): KeyValueItem {
+ if (_.isArray(value)) {
+ if (_.isEmpty(value) && this.hideEmpty) {
+ return null;
+ }
+ value = value.map((item) => (_.isObject(item) ? JSON.stringify(item) : item)).join(', ');
+ } else if (_.isObject(value)) {
+ if ((this.hideEmpty && _.isEmpty(value)) || !this.renderObjects) {
+ return null;
+ }
+ } else if (_.isString(value)) {
+ if (value === '' && this.hideEmpty) {
+ return null;
+ }
+ if (this.isDate(value)) {
+ value = this.datePipe.transform(value) || value;
+ }
+ }
+
+ return value;
+ }
+
+ private isDate(s: string) {
+ const sep = '[ -:.TZ]';
+ const n = '\\d{2}' + sep;
+ // year - m - d - h : m : s . someRest Z (if UTC)
+ return s.match(new RegExp('^\\d{4}' + sep + n + n + n + n + n + '\\d*' + 'Z?$'));
+ }
+}