Skip to content

Commit

Permalink
feat(transitionvis): Collapse multiple null/undefined/empty string pa…
Browse files Browse the repository at this point in the history
…rameter values, add show/hide toggle. Improve styling and flexbox layouts. Refactor key/value components.
  • Loading branch information
christopherthielen committed Jan 3, 2018
1 parent 75fcbdb commit d0af65a
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 184 deletions.
3 changes: 2 additions & 1 deletion src/transition/BreadcrumbArrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export class BreadcrumbArrow extends Component<IProps, IState> {
{!this.props.message ? null : <span>: {this.props.message}</span>}
</div>
<div className="uirTranVis_transName">
<i className={this.iconClass()}/> {this.props.transition.to().name}
<i className={this.iconClass()}/>
<span>{this.props.transition.to().name}</span>
</div>
</div>
</div>
Expand Down
215 changes: 131 additions & 84 deletions src/transition/KeysAndValues.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,151 @@
import { h, render, Component } from "preact";
import {Modal} from "../util/modal";
import {ResolveData} from "./ResolveData";
import {maxLength} from "../util/strings";

const isObject = (val) => typeof val === 'object';

const displayValue = function (object) {
if (object === undefined) return "undefined";
if (object === null) return "null";
if (typeof object === 'string') return <span className="uirTranVis_code">"{maxLength(100, object)}"</span>;
if (Array.isArray(object)) return "[Array]";
if (isObject(object)) return "[Object]";
if (typeof object.toString === 'function') return maxLength(100, object.toString());
return object;
};

export interface IProps {
data: any;
labels: {
section?: string;
modalTitle?: string;
}
classes: {
outerdiv?: string;
section?: string;
keyvaldiv?: string;
_key?: string;
value?: string;
}
import { h, Component } from "preact";
import { Modal } from "../util/modal";
import { ResolveData } from "./ResolveData";
import { maxLength } from "../util/strings";

export interface KeyValueClasses {
div?: string;
key?: string;
val?: string;
}

export interface IState {

export interface IKeyValueRowProps {
classes: KeyValueClasses;
modalTitle: string;
tuple: { key: string, val: any };
}

let defaultClass = {
outerdiv: 'param',
keyvaldiv: 'uirTranVis_keyValue',
section: 'uirTranVis_paramsLabel uirTranVis_deemphasize',
_key: 'uirTranVis_paramId',
value: 'uirTranVis_paramValue'
};

export class KeysAndValues extends Component<IProps, IState> {
isEmpty = () =>
!this.props.data || Object.keys(this.props.data).length === 0;

classFor = (name) =>
this.props.classes && this.props.classes[name] !== undefined ?
this.props.classes[name] :
defaultClass[name];

renderValue = (key: string, val: any) => {
if (isObject(val)) return (
<span className="link" onClick={() => Modal.show(this.props.labels, key, val, ResolveData)}>[Object]</span>
);
export class KeyValueRow extends Component<IKeyValueRowProps, {}> {
render() {
const { tuple: { key, val }, classes, modalTitle } = this.props;
const showModal = () =>
Modal.show(modalTitle, key, val, ResolveData);

const renderValue = () => {
if (val === undefined) return <span className="uirTranVis_code">undefined</span>;
if (val === null) return <span className="uirTranVis_code">null</span>;
if (typeof val === 'string') return <span className="uirTranVis_code">"{maxLength(100, val)}"</span>;
if (typeof val === 'number') return <span className="uirTranVis_code">{val.toString()}</span>;
if (typeof val === 'boolean') return <span className="uirTranVis_code">{val.toString()}</span>;
if (Array.isArray(val)) return <span className="link" onClick={showModal}>[Array]</span>;
if (typeof val === 'object') return <span className="link" onClick={showModal}>[Object]</span>;
if (typeof val.toString === 'function') return <span>{maxLength(100, val.toString())}</span>;
};

return (
<div className={this.props.classes.value}>
{displayValue(val)}
<div className={classes.div}>
<div className={classes.key}>{key}:</div>
<div className={classes.val}>{renderValue()}</div>
</div>
);
};
)
}
}

render() {
if (this.isEmpty()) return null;
interface Bucket {
label: string;
is: (val) => boolean;
value?: any;
count: number;
data: { [key: string]: any };
}

const keys = Object.keys(this.props.data);
export interface IGroupDefinition {
value: any;
label: string;
}

const defineds = keys.filter(key => this.props.data[key] !== undefined);
const undefineds = keys.filter(key => this.props.data[key] === undefined);
export interface IKeysAndValuesProps {
data: any;
classes: KeyValueClasses;
modalTitle?: string;
groupedValues?: IGroupDefinition[];
enableGroupToggle?: boolean;
}

const renderKeyValues = (keys) =>
keys.map(key =>
<div key={key} className={this.classFor('keyvaldiv')}>
<div className={this.classFor('_key')}>
{key}:
</div>
export interface IKeysAndValuesState {
collapseFalsy: boolean;
}

<div className={this.classFor('value')}>
{this.renderValue(key, this.props.data[key])}
</div>
</div>
);
export class KeysAndValues extends Component<IKeysAndValuesProps, IKeysAndValuesState> {
public static falsyGroupDefinitions: IGroupDefinition[] = [
{ value: undefined, label: 'undefined' },
{ value: null, label: 'null' },
{ value: '', label: 'empty string' },
];
state = { collapseFalsy: true };

private makeBuckets(definitions: IGroupDefinition[], data: { [key: string]: any }): Bucket[] {
const makeBucket = (def: IGroupDefinition): Bucket => ({
label: def.label,
is: (val) => val === def.value,
value: def.value,
count: 0,
data: {},
});

let defaultBucket = {
label: 'default',
is: () => true,
count: 0,
data: {},
};

const buckets = definitions.map(makeBucket).concat(defaultBucket);

Object.keys(data).forEach(key => {
const bucket = buckets.find(bucket => bucket.is(data[key]));
bucket.data[key] = data[key];
bucket.value = data[key];
bucket.count++;
});

return buckets;
}

const renderUndefineds = (keys) => renderKeyValues([keys.join(', ')]);
render() {
const { data, classes, modalTitle } = this.props;

return (
<div className={this.classFor('outerdiv')}>
<div className={this.classFor('section')}>
{this.props.labels.section}
</div>
const groupedValues = this.props.groupedValues || KeysAndValues.falsyGroupDefinitions;
const enableGroupToggle = this.props.enableGroupToggle || false;

{renderKeyValues(defineds)}
const isCollapsed = this.state.collapseFalsy;

{/* { undefineds.length <= 2 && <KeyValues keys={undefineds} /> } */}
{ undefineds.length > 2 && renderUndefineds(undefineds) }
const buckets: Bucket[] = this.makeBuckets(groupedValues, data);
const defaultBucket = buckets.find(bucket => bucket.label === 'default');
const groupedBuckets = buckets.filter(bucket => !!bucket.count && bucket !== defaultBucket);
const groupedCount = groupedBuckets.reduce((total, bucket) => total += bucket.count, 0);

</div>
)
const tuples = Object.keys(defaultBucket.data).map(key => ({ key, val: defaultBucket.data[key] }));
const groupedTuples = groupedBuckets.map(bucket => {
const key = Object.keys(bucket.data).join(', ');
const val = bucket.value;
return { key, val };
});

const showGroupToggle = enableGroupToggle && groupedCount > 1;

return (
<div className="uirTranVis_keysAndValues">
{tuples.map(tuple => (
<KeyValueRow key={tuple.key} tuple={tuple} classes={classes} modalTitle={modalTitle} />
))}

{showGroupToggle && !!groupedTuples.length && (
<a
href="javascript:void(0)"
onClick={() => this.setState({ collapseFalsy: !isCollapsed })}
className="uirTranVis_keyValue"
>
{isCollapsed ? 'show' : 'hide'} {groupedCount} {groupedBuckets.map(bucket => bucket.label).join(' or ')} parameter values
</a>
)}

{(!showGroupToggle || !this.state.collapseFalsy) && (
groupedTuples.map(tuple => (
<KeyValueRow key={tuple.key} tuple={tuple} classes={classes} modalTitle={modalTitle}/>
))
)}
</div>
);
}
}
40 changes: 25 additions & 15 deletions src/transition/NodeDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { h, render, Component } from "preact";
import {stringify, maxLength} from "../util/strings";
import {KeysAndValues} from "./KeysAndValues";
import { stringify, maxLength } from "../util/strings";
import { KeysAndValues } from "./KeysAndValues";
import { Transition } from '@uirouter/core';

export interface IProps {
Expand All @@ -9,11 +9,7 @@ export interface IProps {
type: string;
}

export interface IState {

}

export class NodeDetail extends Component<IProps, IState> {
export class NodeDetail extends Component<IProps, {}> {
stateName() {
let node = this.props.node;
let name = node && node.state && node.state.name;
Expand Down Expand Up @@ -42,22 +38,36 @@ export class NodeDetail extends Component<IProps, IState> {
}

render() {
if (!this.props.node) return null;
const params = this.params();
const resolves = this.resolves();

return !this.props.node ? null : (
<div className="uirTranVis_nodeDetail">
<div className="uirTranVis_heading">
<div className="uirTranVis_nowrap uirTranVis_deemphasize">({ this.props.type } state)</div>
<div className="uirTranVis_stateName">{ this.stateName() }</div>
</div>

<KeysAndValues data={this.params()}
classes={{ outerdiv: 'params', section: 'uirTranVis_paramsLabel uirTranVis_deemphasize' }}
labels={{ section: 'Parameter values', modalTitle: 'Parameter value: ' }}
/>
{!!Object.keys(params).length && (
<div className="params">
<div className="uirTranVis_paramsLabel uirTranVis_deemphasize">
Parameter values
</div>

<KeysAndValues data={this.params()} classes={{ div: 'uirTranVis_keyValue' }} modalTitle="Parameter value"/>
</div>
)}

{!!Object.keys(resolves).length && (
<div className="params resolve">
<div className="uirTranVis_resolveLabel uirTranVis_deemphasize">
Resolved data
</div>

<KeysAndValues data={this.resolves()}
classes={{ outerdiv: 'params resolve', section: 'uirTranVis_resolveLabel uirTranVis_deemphasize' }}
labels={{ section: 'Resolved data', modalTitle: 'Resolved value: ' }}
/>
<KeysAndValues data={this.resolves()} classes={{ div: 'uirTranVis_keyValue' }} modalTitle="Resolved value"/>
</div>
)}
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions src/transition/ResolveData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {Pretty} from "../util/pretty";

export interface IProps {
toggles?: any;
labels?: any;
open?: boolean;
close?: Function;
modalTitle?: string;
id?: string;
value?: any;
}
Expand All @@ -19,7 +19,7 @@ export class ResolveData extends Component<IProps,any> {
<div>
<Modal>
<div className="uirTranVis_modal-header uir-resolve-header">
<div style={{"fontSize": "1.5em"}}>{this.props.labels.modalTitle}: {this.props.id}</div>
<div style={{"fontSize": "20px"}}>{this.props.modalTitle}: {this.props.id}</div>
<button className="uirTranVis_btn uirTranVis_btnXs uirTranVis_btnPrimary" onClick={this.close}>
<i className="uir-icon uir-iconw-close"/>
</button>
Expand Down
Loading

0 comments on commit d0af65a

Please sign in to comment.