|
1 |
| -import { h, render, Component } from "preact"; |
2 |
| -import {Modal} from "../util/modal"; |
3 |
| -import {ResolveData} from "./ResolveData"; |
4 |
| -import {maxLength} from "../util/strings"; |
5 |
| - |
6 |
| -const isObject = (val) => typeof val === 'object'; |
7 |
| - |
8 |
| -const displayValue = function (object) { |
9 |
| - if (object === undefined) return "undefined"; |
10 |
| - if (object === null) return "null"; |
11 |
| - if (typeof object === 'string') return <span className="uirTranVis_code">"{maxLength(100, object)}"</span>; |
12 |
| - if (Array.isArray(object)) return "[Array]"; |
13 |
| - if (isObject(object)) return "[Object]"; |
14 |
| - if (typeof object.toString === 'function') return maxLength(100, object.toString()); |
15 |
| - return object; |
16 |
| -}; |
17 |
| - |
18 |
| -export interface IProps { |
19 |
| - data: any; |
20 |
| - labels: { |
21 |
| - section?: string; |
22 |
| - modalTitle?: string; |
23 |
| - } |
24 |
| - classes: { |
25 |
| - outerdiv?: string; |
26 |
| - section?: string; |
27 |
| - keyvaldiv?: string; |
28 |
| - _key?: string; |
29 |
| - value?: string; |
30 |
| - } |
| 1 | +import { h, Component } from "preact"; |
| 2 | +import { Modal } from "../util/modal"; |
| 3 | +import { ResolveData } from "./ResolveData"; |
| 4 | +import { maxLength } from "../util/strings"; |
| 5 | + |
| 6 | +export interface KeyValueClasses { |
| 7 | + div?: string; |
| 8 | + key?: string; |
| 9 | + val?: string; |
31 | 10 | }
|
32 | 11 |
|
33 |
| -export interface IState { |
34 |
| - |
| 12 | +export interface IKeyValueRowProps { |
| 13 | + classes: KeyValueClasses; |
| 14 | + modalTitle: string; |
| 15 | + tuple: { key: string, val: any }; |
35 | 16 | }
|
36 | 17 |
|
37 |
| -let defaultClass = { |
38 |
| - outerdiv: 'param', |
39 |
| - keyvaldiv: 'uirTranVis_keyValue', |
40 |
| - section: 'uirTranVis_paramsLabel uirTranVis_deemphasize', |
41 |
| - _key: 'uirTranVis_paramId', |
42 |
| - value: 'uirTranVis_paramValue' |
43 |
| -}; |
44 |
| - |
45 |
| -export class KeysAndValues extends Component<IProps, IState> { |
46 |
| - isEmpty = () => |
47 |
| - !this.props.data || Object.keys(this.props.data).length === 0; |
48 |
| - |
49 |
| - classFor = (name) => |
50 |
| - this.props.classes && this.props.classes[name] !== undefined ? |
51 |
| - this.props.classes[name] : |
52 |
| - defaultClass[name]; |
53 |
| - |
54 |
| - renderValue = (key: string, val: any) => { |
55 |
| - if (isObject(val)) return ( |
56 |
| - <span className="link" onClick={() => Modal.show(this.props.labels, key, val, ResolveData)}>[Object]</span> |
57 |
| - ); |
| 18 | +export class KeyValueRow extends Component<IKeyValueRowProps, {}> { |
| 19 | + render() { |
| 20 | + const { tuple: { key, val }, classes, modalTitle } = this.props; |
| 21 | + const showModal = () => |
| 22 | + Modal.show(modalTitle, key, val, ResolveData); |
| 23 | + |
| 24 | + const renderValue = () => { |
| 25 | + if (val === undefined) return <span className="uirTranVis_code">undefined</span>; |
| 26 | + if (val === null) return <span className="uirTranVis_code">null</span>; |
| 27 | + if (typeof val === 'string') return <span className="uirTranVis_code">"{maxLength(100, val)}"</span>; |
| 28 | + if (typeof val === 'number') return <span className="uirTranVis_code">{val.toString()}</span>; |
| 29 | + if (typeof val === 'boolean') return <span className="uirTranVis_code">{val.toString()}</span>; |
| 30 | + if (Array.isArray(val)) return <span className="link" onClick={showModal}>[Array]</span>; |
| 31 | + if (typeof val === 'object') return <span className="link" onClick={showModal}>[Object]</span>; |
| 32 | + if (typeof val.toString === 'function') return <span>{maxLength(100, val.toString())}</span>; |
| 33 | + }; |
58 | 34 |
|
59 | 35 | return (
|
60 |
| - <div className={this.props.classes.value}> |
61 |
| - {displayValue(val)} |
| 36 | + <div className={classes.div}> |
| 37 | + <div className={classes.key}>{key}:</div> |
| 38 | + <div className={classes.val}>{renderValue()}</div> |
62 | 39 | </div>
|
63 |
| - ); |
64 |
| - }; |
| 40 | + ) |
| 41 | + } |
| 42 | +} |
65 | 43 |
|
66 |
| - render() { |
67 |
| - if (this.isEmpty()) return null; |
| 44 | +interface Bucket { |
| 45 | + label: string; |
| 46 | + is: (val) => boolean; |
| 47 | + value?: any; |
| 48 | + count: number; |
| 49 | + data: { [key: string]: any }; |
| 50 | +} |
68 | 51 |
|
69 |
| - const keys = Object.keys(this.props.data); |
| 52 | +export interface IGroupDefinition { |
| 53 | + value: any; |
| 54 | + label: string; |
| 55 | +} |
70 | 56 |
|
71 |
| - const defineds = keys.filter(key => this.props.data[key] !== undefined); |
72 |
| - const undefineds = keys.filter(key => this.props.data[key] === undefined); |
| 57 | +export interface IKeysAndValuesProps { |
| 58 | + data: any; |
| 59 | + classes: KeyValueClasses; |
| 60 | + modalTitle?: string; |
| 61 | + groupedValues?: IGroupDefinition[]; |
| 62 | + enableGroupToggle?: boolean; |
| 63 | +} |
73 | 64 |
|
74 |
| - const renderKeyValues = (keys) => |
75 |
| - keys.map(key => |
76 |
| - <div key={key} className={this.classFor('keyvaldiv')}> |
77 |
| - <div className={this.classFor('_key')}> |
78 |
| - {key}: |
79 |
| - </div> |
| 65 | +export interface IKeysAndValuesState { |
| 66 | + collapseFalsy: boolean; |
| 67 | +} |
80 | 68 |
|
81 |
| - <div className={this.classFor('value')}> |
82 |
| - {this.renderValue(key, this.props.data[key])} |
83 |
| - </div> |
84 |
| - </div> |
85 |
| - ); |
| 69 | +export class KeysAndValues extends Component<IKeysAndValuesProps, IKeysAndValuesState> { |
| 70 | + public static falsyGroupDefinitions: IGroupDefinition[] = [ |
| 71 | + { value: undefined, label: 'undefined' }, |
| 72 | + { value: null, label: 'null' }, |
| 73 | + { value: '', label: 'empty string' }, |
| 74 | + ]; |
| 75 | + state = { collapseFalsy: true }; |
| 76 | + |
| 77 | + private makeBuckets(definitions: IGroupDefinition[], data: { [key: string]: any }): Bucket[] { |
| 78 | + const makeBucket = (def: IGroupDefinition): Bucket => ({ |
| 79 | + label: def.label, |
| 80 | + is: (val) => val === def.value, |
| 81 | + value: def.value, |
| 82 | + count: 0, |
| 83 | + data: {}, |
| 84 | + }); |
| 85 | + |
| 86 | + let defaultBucket = { |
| 87 | + label: 'default', |
| 88 | + is: () => true, |
| 89 | + count: 0, |
| 90 | + data: {}, |
| 91 | + }; |
| 92 | + |
| 93 | + const buckets = definitions.map(makeBucket).concat(defaultBucket); |
| 94 | + |
| 95 | + Object.keys(data).forEach(key => { |
| 96 | + const bucket = buckets.find(bucket => bucket.is(data[key])); |
| 97 | + bucket.data[key] = data[key]; |
| 98 | + bucket.value = data[key]; |
| 99 | + bucket.count++; |
| 100 | + }); |
| 101 | + |
| 102 | + return buckets; |
| 103 | + } |
86 | 104 |
|
87 |
| - const renderUndefineds = (keys) => renderKeyValues([keys.join(', ')]); |
| 105 | + render() { |
| 106 | + const { data, classes, modalTitle } = this.props; |
88 | 107 |
|
89 |
| - return ( |
90 |
| - <div className={this.classFor('outerdiv')}> |
91 |
| - <div className={this.classFor('section')}> |
92 |
| - {this.props.labels.section} |
93 |
| - </div> |
| 108 | + const groupedValues = this.props.groupedValues || KeysAndValues.falsyGroupDefinitions; |
| 109 | + const enableGroupToggle = this.props.enableGroupToggle || false; |
94 | 110 |
|
95 |
| - {renderKeyValues(defineds)} |
| 111 | + const isCollapsed = this.state.collapseFalsy; |
96 | 112 |
|
97 |
| - {/* { undefineds.length <= 2 && <KeyValues keys={undefineds} /> } */} |
98 |
| - { undefineds.length > 2 && renderUndefineds(undefineds) } |
| 113 | + const buckets: Bucket[] = this.makeBuckets(groupedValues, data); |
| 114 | + const defaultBucket = buckets.find(bucket => bucket.label === 'default'); |
| 115 | + const groupedBuckets = buckets.filter(bucket => !!bucket.count && bucket !== defaultBucket); |
| 116 | + const groupedCount = groupedBuckets.reduce((total, bucket) => total += bucket.count, 0); |
99 | 117 |
|
100 |
| - </div> |
101 |
| - ) |
| 118 | + const tuples = Object.keys(defaultBucket.data).map(key => ({ key, val: defaultBucket.data[key] })); |
| 119 | + const groupedTuples = groupedBuckets.map(bucket => { |
| 120 | + const key = Object.keys(bucket.data).join(', '); |
| 121 | + const val = bucket.value; |
| 122 | + return { key, val }; |
| 123 | + }); |
| 124 | + |
| 125 | + const showGroupToggle = enableGroupToggle && groupedCount > 1; |
102 | 126 |
|
| 127 | + return ( |
| 128 | + <div className="uirTranVis_keysAndValues"> |
| 129 | + {tuples.map(tuple => ( |
| 130 | + <KeyValueRow key={tuple.key} tuple={tuple} classes={classes} modalTitle={modalTitle} /> |
| 131 | + ))} |
| 132 | + |
| 133 | + {showGroupToggle && !!groupedTuples.length && ( |
| 134 | + <a |
| 135 | + href="javascript:void(0)" |
| 136 | + onClick={() => this.setState({ collapseFalsy: !isCollapsed })} |
| 137 | + className="uirTranVis_keyValue" |
| 138 | + > |
| 139 | + {isCollapsed ? 'show' : 'hide'} {groupedCount} {groupedBuckets.map(bucket => bucket.label).join(' or ')} parameter values |
| 140 | + </a> |
| 141 | + )} |
| 142 | + |
| 143 | + {(!showGroupToggle || !this.state.collapseFalsy) && ( |
| 144 | + groupedTuples.map(tuple => ( |
| 145 | + <KeyValueRow key={tuple.key} tuple={tuple} classes={classes} modalTitle={modalTitle}/> |
| 146 | + )) |
| 147 | + )} |
| 148 | + </div> |
| 149 | + ); |
103 | 150 | }
|
104 | 151 | }
|
0 commit comments