Skip to content

Commit d0af65a

Browse files
feat(transitionvis): Collapse multiple null/undefined/empty string parameter values, add show/hide toggle. Improve styling and flexbox layouts. Refactor key/value components.
1 parent 75fcbdb commit d0af65a

File tree

8 files changed

+195
-184
lines changed

8 files changed

+195
-184
lines changed

src/transition/BreadcrumbArrow.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export class BreadcrumbArrow extends Component<IProps, IState> {
3636
{!this.props.message ? null : <span>: {this.props.message}</span>}
3737
</div>
3838
<div className="uirTranVis_transName">
39-
<i className={this.iconClass()}/> {this.props.transition.to().name}
39+
<i className={this.iconClass()}/>
40+
<span>{this.props.transition.to().name}</span>
4041
</div>
4142
</div>
4243
</div>

src/transition/KeysAndValues.tsx

Lines changed: 131 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,151 @@
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;
3110
}
3211

33-
export interface IState {
34-
12+
export interface IKeyValueRowProps {
13+
classes: KeyValueClasses;
14+
modalTitle: string;
15+
tuple: { key: string, val: any };
3516
}
3617

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+
};
5834

5935
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>
6239
</div>
63-
);
64-
};
40+
)
41+
}
42+
}
6543

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+
}
6851

69-
const keys = Object.keys(this.props.data);
52+
export interface IGroupDefinition {
53+
value: any;
54+
label: string;
55+
}
7056

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+
}
7364

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+
}
8068

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+
}
86104

87-
const renderUndefineds = (keys) => renderKeyValues([keys.join(', ')]);
105+
render() {
106+
const { data, classes, modalTitle } = this.props;
88107

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;
94110

95-
{renderKeyValues(defineds)}
111+
const isCollapsed = this.state.collapseFalsy;
96112

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);
99117

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;
102126

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+
);
103150
}
104151
}

src/transition/NodeDetail.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { h, render, Component } from "preact";
2-
import {stringify, maxLength} from "../util/strings";
3-
import {KeysAndValues} from "./KeysAndValues";
2+
import { stringify, maxLength } from "../util/strings";
3+
import { KeysAndValues } from "./KeysAndValues";
44
import { Transition } from '@uirouter/core';
55

66
export interface IProps {
@@ -9,11 +9,7 @@ export interface IProps {
99
type: string;
1010
}
1111

12-
export interface IState {
13-
14-
}
15-
16-
export class NodeDetail extends Component<IProps, IState> {
12+
export class NodeDetail extends Component<IProps, {}> {
1713
stateName() {
1814
let node = this.props.node;
1915
let name = node && node.state && node.state.name;
@@ -42,22 +38,36 @@ export class NodeDetail extends Component<IProps, IState> {
4238
}
4339

4440
render() {
41+
if (!this.props.node) return null;
42+
const params = this.params();
43+
const resolves = this.resolves();
44+
4545
return !this.props.node ? null : (
4646
<div className="uirTranVis_nodeDetail">
4747
<div className="uirTranVis_heading">
4848
<div className="uirTranVis_nowrap uirTranVis_deemphasize">({ this.props.type } state)</div>
4949
<div className="uirTranVis_stateName">{ this.stateName() }</div>
5050
</div>
5151

52-
<KeysAndValues data={this.params()}
53-
classes={{ outerdiv: 'params', section: 'uirTranVis_paramsLabel uirTranVis_deemphasize' }}
54-
labels={{ section: 'Parameter values', modalTitle: 'Parameter value: ' }}
55-
/>
52+
{!!Object.keys(params).length && (
53+
<div className="params">
54+
<div className="uirTranVis_paramsLabel uirTranVis_deemphasize">
55+
Parameter values
56+
</div>
57+
58+
<KeysAndValues data={this.params()} classes={{ div: 'uirTranVis_keyValue' }} modalTitle="Parameter value"/>
59+
</div>
60+
)}
61+
62+
{!!Object.keys(resolves).length && (
63+
<div className="params resolve">
64+
<div className="uirTranVis_resolveLabel uirTranVis_deemphasize">
65+
Resolved data
66+
</div>
5667

57-
<KeysAndValues data={this.resolves()}
58-
classes={{ outerdiv: 'params resolve', section: 'uirTranVis_resolveLabel uirTranVis_deemphasize' }}
59-
labels={{ section: 'Resolved data', modalTitle: 'Resolved value: ' }}
60-
/>
68+
<KeysAndValues data={this.resolves()} classes={{ div: 'uirTranVis_keyValue' }} modalTitle="Resolved value"/>
69+
</div>
70+
)}
6171
</div>
6272
)
6373
}

src/transition/ResolveData.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {Pretty} from "../util/pretty";
44

55
export interface IProps {
66
toggles?: any;
7-
labels?: any;
87
open?: boolean;
98
close?: Function;
9+
modalTitle?: string;
1010
id?: string;
1111
value?: any;
1212
}
@@ -19,7 +19,7 @@ export class ResolveData extends Component<IProps,any> {
1919
<div>
2020
<Modal>
2121
<div className="uirTranVis_modal-header uir-resolve-header">
22-
<div style={{"fontSize": "1.5em"}}>{this.props.labels.modalTitle}: {this.props.id}</div>
22+
<div style={{"fontSize": "20px"}}>{this.props.modalTitle}: {this.props.id}</div>
2323
<button className="uirTranVis_btn uirTranVis_btnXs uirTranVis_btnPrimary" onClick={this.close}>
2424
<i className="uir-icon uir-iconw-close"/>
2525
</button>

0 commit comments

Comments
 (0)