{
}
}
+ // render the acual component
public render (): React.ReactNode {
const { className } = this.props;
- const { all } = this.state;
+ const { tree } = this.state;
+
+ if (!tree) {
+ return null;
+ }
return (
- {all.map(this.renderEntry)}
+ {this.renderRows(this.createRows(tree.arr))}
);
}
- private renderEntry = ([bn, curr]: [string, ForkHeader[]], index: number): React.ReactNode => {
- const { all } = this.state;
- const lastIndex = all.length - 1;
+ // create a simplified structure that allows for easy rendering
+ private createRows (arr: LinkArray): Row[] {
+ if (!arr.length) {
+ return [];
+ }
- // when we are not in the first of last spot and we have the same lengths, just ellipsis the thing
- if (index !== lastIndex && index !== 0) {
- // are the lengths matching here, if not we just want to skip
- if (curr.length === all[index + 1][1].length && curr.length === all[index - 1][1].length) {
- // now check to ensure that each entry has the same parent
- const sameParent = curr.reduce((same, hdr, index): boolean => {
- return same && ((index === 0) || (hdr.hash === curr[index - 1].hash));
- }, true);
+ return this
+ .createRows(
+ arr.reduce((children: LinkArray, { arr }: Link): LinkArray =>
+ children.concat(...arr), [])
+ )
+ .concat({
+ bn: arr.reduce((result, { hdr: { bn } }): string =>
+ result || bn, ''),
+ cols: arr.map(this.createCol)
+ });
+ }
- // cool, we are ok to shortcut
- if (sameParent) {
- // if the previous result was an ellipsis, we just don't do anything, one ellipsis only
- if (this._isPrevShort) {
- return null;
- }
+ // a single column in a row, it just has the details for the entry
+ private createCol = ({ hdr: { hash, isEmpty, isFinalized, parent, width } }: Link): Col => {
+ return { hash, isEmpty, isFinalized, parent, width };
+ }
- this._isPrevShort = true;
+ // checks to see if a row has a single non-empty entry, i.e. it is a candidate for collapsing
+ private isSingleRow (cols: Col[]): boolean {
+ if (!cols[0] || cols[0].isEmpty) {
+ return false;
+ }
+
+ return cols.reduce((result: boolean, col, index): boolean => {
+ return index === 0
+ ? result
+ : (!col.isEmpty ? false : result);
+ }, true);
+ }
+
+ // render the rows created by createRows to React nodes
+ private renderRows (rows: Row[]): React.ReactNode[] {
+ const lastIndex = rows.length - 1;
+ let isPrevShort = false;
+
+ return rows.map(({ bn, cols }, index): React.ReactNode => {
+ // if not first, not last and single only, see if we can collapse
+ if (index !== 0 && index !== lastIndex && this.isSingleRow(cols)) {
+ if (isPrevShort) {
+ // previous one was already a link, this one as well - skip it
+ return null;
+ } else if (this.isSingleRow(rows[index - 1].cols)) {
+ isPrevShort = true;
return (
|
- ⋮ |
+
+ ⋮
+ |
);
}
}
- }
- this._isPrevShort = false;
+ isPrevShort = false;
- return (
-
- | #{bn} |
- {curr.map(this.renderCol)}
-
- );
+ return (
+
+ | {`#${bn}`} |
+ {cols.map(this.renderCol)}
+
+ );
+ });
}
- private renderCol = (column: ForkHeader, index: number): React.ReactNode => {
- const extraClassName = column.hash
- ? (
- this._allFin.has(column.hash)
- ? 'isFinalized'
- : ''
- )
- : 'isMissing';
-
+ private renderCol = ({ hash, isEmpty, isFinalized, parent, width }: Col, index: number): React.ReactNode => {
return (
{
- column.hash
- ? (
+ className={`'header ${isEmpty && 'isEmpty'} ${isFinalized && 'isFinalized'}`}
+ colSpan={width}
+ key={`${hash}:${index}:${width}`}
+ >
+ {
+ isEmpty
+ ?
+ : (
<>
- {column.hash}
- {column.parent}
+ {hash}
+ {parent}
>
)
- :
}
|
);
}
+
+ // adjust the number of columns in a cell based on the children and tree depth
+ private countCols (children: LinkArray): number {
+ return Math.max(1, children.reduce((total, { hdr: { width } }): number => {
+ return total + width;
+ }, 0));
+ }
+
+ // empty link helper
+ private emptyLink (): Link {
+ return {
+ arr: [],
+ hdr: { bn: '', hash: ' ', isEmpty: true, isFinalized: false, parent: ' ', width: 0 }
+ };
+ }
+
+ // adds children for a specific header, retrieving based on matching parent
+ private addChildren (base: LinkHeader, children: LinkArray): LinkArray {
+ const hdrs = (this._children.get(base.hash) || [])
+ .map((hash): LinkHeader | null => this._headers.get(hash) || null)
+ .filter((hdr): boolean => !!hdr) as LinkHeader[];
+
+ hdrs.forEach((hdr): void => {
+ children.push({ arr: this.addChildren(hdr, []), hdr });
+ });
+
+ // place the active (larger, finalized) columns first for the pyramid display
+ children.sort((a, b): number => {
+ if (a.hdr.width > b.hdr.width || a.hdr.isFinalized) {
+ return -1;
+ } else if (a.hdr.width < b.hdr.width || b.hdr.isFinalized) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ base.width = this.countCols(children);
+
+ return children;
+ }
+
+ // even out the columns, i.e. add empty spacers as applicable to get tree rendering right
+ private addColumnSpacers (arr: LinkArray): void {
+ // check is any of the children has a non-empty set
+ const hasChildren = arr.some(({ arr }): boolean => arr.length !== 0);
+
+ if (hasChildren) {
+ // ok, non-empty found - iterate through an add at least an empty cell to all
+ arr
+ .filter(({ arr }): boolean => arr.length === 0)
+ .forEach(({ arr }): number => arr.push(this.emptyLink()));
+
+ const newArr = arr.reduce((flat: LinkArray, { arr }): LinkArray => flat.concat(...arr), []);
+
+ // go one level deeper, ensure that the full tree has empty spacers
+ this.addColumnSpacers(newArr);
+ }
+ }
+
+ // create a tree list from the available headers
+ private generateTree (): Link {
+ const root = this.emptyLink();
+
+ // add all the root entries first, we iterate from these
+ // @ts-ignore We add the root entry explicitly, it exists as per init
+ this._children.get('root').forEach((hash): void => {
+ const hdr = this._headers.get(hash);
+
+ // if this fails, well, we have a bigger issue :(
+ if (hdr) {
+ root.arr.push({ arr: [], hdr: { ...hdr } });
+ }
+ });
+
+ // iterate through, adding the children for each of the root nodes
+ root.arr.forEach(({ arr, hdr }): void => {
+ this.addChildren(hdr, arr);
+ });
+
+ // align the columns with empty spacers - this aids in display
+ this.addColumnSpacers(root.arr);
+
+ root.hdr.width = this.countCols(root.arr);
+
+ return root;
+ }
}
export default withMulti(
styled(Forks as React.ComponentClass)`
+ margin-bottom: 1.5rem;
+
table {
+ border-collapse: separate;
+ border-spacing: 0.25rem;
font-family: monospace;
td {
@@ -302,7 +371,7 @@ export default withMulti(
div {
margin: 0 auto;
- max-width: 7rem;
+ max-width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -310,7 +379,7 @@ export default withMulti(
&.parent {
font-size: 0.75rem;
line-height: 0.75rem;
- max-width: 5.25rem; /* 0.75 * 7rem */
+ max-width: 4.5rem;
}
}
@@ -322,10 +391,20 @@ export default withMulti(
background: #f2f2f2;
border-radius: 0.25rem;
+ &.isEmpty {
+ background: transparent;
+ }
+
&.isFinalized {
background: rgba(0, 255, 0, 0.15);
}
+ &.isLink {
+ background: transparent;
+ line-height: 1rem;
+ padding: 0;
+ }
+
&.isMissing {
background: rgba(255, 0, 0, 0.05);
}