src/components/TypeTree.tsx (96 lines of code) (raw):
/* Copyright 2018 Spotify AB. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IconName, ITreeNode, Tree } from '@blueprintjs/core'
import {
Enum,
Field,
MapField,
Method,
Namespace,
OneOf,
ReflectionObject,
Service,
Type
} from 'protobufjs'
import * as React from 'react'
import { connect } from 'react-redux'
import { routerActions } from 'react-router-redux'
import actions from '../actions'
import * as reducers from '../reducers'
import './TypeTree.scss'
interface IProps {
// The roots of the tree; this corresponds to all root-level Protobuf packages
readonly roots: ReadonlyArray<ReflectionObject>
// The fully qualified names of types that are expanded (e.g. [".spotify", ".spotify.metadata"] to
// expand the `spotify.metadata` package)
readonly expanded: ReadonlyArray<string>
// The fully qualified name of the currently selected type, if any.
readonly selected: null | string
// Called when the user requests a type to be expanded
readonly onExpanded: (fullName: string) => void
// Called when the user requests a type to be collapsed
readonly onCollapse: (fullName: string) => void
// Called when the user requests a type to be selected
readonly onSelected: (fullName: string) => void
}
// A component that renders a Protobuf type hierarchy as a tree
class TypeTree extends React.PureComponent<IProps> {
public render () {
const roots =
this.props.roots.map((c) => buildTree(c, this.props.selected, this.props.expanded))
return (
<Tree
className='type-tree'
contents={roots}
onNodeExpand={this.onNodeExpand}
onNodeCollapse={this.onNodeCollapse}
onNodeClick={this.onNodeClick}
/>
)
}
private onNodeExpand = (node: ITreeNode<ReflectionObject>) =>
node.nodeData && this.props.onExpanded(node.nodeData.fullName)
private onNodeCollapse = (node: ITreeNode<ReflectionObject>) =>
node.nodeData && this.props.onCollapse(node.nodeData.fullName)
private onNodeClick = (node: ITreeNode<ReflectionObject>) =>
node.nodeData && this.props.onSelected(node.nodeData.fullName)
}
const buildTree = (node: ReflectionObject, selected: null | string,
expanded: ReadonlyArray<string>): ITreeNode<ReflectionObject> => {
let childNodes: Array<ITreeNode<ReflectionObject>>
if ('nestedArray' in node) {
const typedNode = node as ReflectionObject & { nestedArray: ReflectionObject[] }
childNodes = typedNode.nestedArray
.sort((a: ReflectionObject, b: ReflectionObject) => a.fullName.localeCompare(b.fullName))
.map((c: ReflectionObject) => buildTree(c, selected, expanded))
} else {
childNodes = []
}
let icon: IconName
if (node instanceof Type) {
icon = 'document'
} else if (node instanceof Enum) {
icon = 'properties'
} else if (node instanceof Field) {
icon = 'widget-button'
} else if (node instanceof MapField) {
icon = 'widget-button'
} else if (node instanceof Service) {
icon = 'graph'
} else if (node instanceof Method) {
icon = 'send-to-graph'
} else if (node instanceof OneOf) {
icon = 'group-objects'
} else if (node instanceof Namespace) {
icon = 'folder-close'
} else {
icon = 'blank'
}
return {
childNodes,
hasCaret: childNodes.length > 0,
icon,
id: node.fullName,
isExpanded: expanded.indexOf(node.fullName) !== -1,
isSelected: selected !== null && (node.fullName === selected),
label: node.name,
nodeData: node
}
}
export default connect((state: reducers.IState) => ({
expanded: state.nav.expandedTypes
}), (dispatch) => ({
onCollapse: (fullName: string) => dispatch(actions.nav.collapseType({ type: fullName })),
onExpanded: (fullName: string) => dispatch(actions.nav.expandType({ type: fullName })),
onSelected: (fullName: string) => {
dispatch(routerActions.push('/' + fullName.substr(1)))
dispatch(actions.nav.selectType({ type: fullName }))
}
}))(TypeTree)