Skip to content

Commit

Permalink
filter-tree: Use a table layout in filter tree
Browse files Browse the repository at this point in the history
This allows to display many columns in the tree. Columns are sortable.

Signed-off-by: Lea <lea.carlier@polymtl.ca>
Signed-off-by: Geneviève Bastien <gbastien+lttng@versatic.net>
  • Loading branch information
chertyb authored and tahini committed Oct 2, 2020
1 parent 593c794 commit 2d38260
Show file tree
Hide file tree
Showing 15 changed files with 477 additions and 138 deletions.
24 changes: 24 additions & 0 deletions viewer-prototype/src/browser/style/output-components-style.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ canvas {
width: 100%;
}

.table-tree>tbody>tr:nth-child(1)>th {
background-color: var(--theia-editor-background);
position: sticky;
top: 0;
}

.table-tree th, .table-tree td {
padding: 3px 5px;
text-align: left;
border-bottom: 1px solid #333;
border-right: 1px solid #333;
white-space: nowrap;
min-width: 50px;
}

.timegraph-tree tr {
/* TODO: Fix row alignment, this number is arbitrary, it works [on my machine], but it should match line height in timeline-chart */
line-height: 18px;
position: relative;
white-space: nowrap;
top: 50%;
padding: 0 0;
}

#input-filter-tree {
background-color: var(--theia-input-background);
border: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,15 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
}

renderTree(): React.ReactNode {
// TODO Show header, when we can have entries in-line with timeline-chart
return <EntryTree
collapsedNodes={this.state.collapsedNodes}
showFilter={false}
entries={this.state.timegraphTree}
showCheckboxes={false}
onToggleCollapse={this.onToggleCollapse}
showHeader={false}
className="timegraph-tree"
/>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import icons from './icons';

interface CheckboxProps {
id: number;
name: string;
checkedStatus: number;
onToggleCheck: (id: number) => void;
}
Expand Down Expand Up @@ -31,11 +30,8 @@ export class CheckboxComponent extends React.Component<CheckboxProps> {
};

render(): JSX.Element {
return <div onClick={this.handleClick} >
<span style={{padding: 5}}>
return <span style={{padding: 5}} onClick={this.handleClick}>
{this.renderCheckbox(this.props.checkedStatus)}
</span>
{this.props.name}
</div>;
</span>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { Entry } from 'tsp-typescript-client/lib/models/entry';
import { listToTree } from './utils';
import { FilterTree } from './tree';
import { TreeNode } from './tree-node';

interface EntryTreeProps {
entries: Entry[];
Expand All @@ -10,12 +11,18 @@ interface EntryTreeProps {
collapsedNodes: number[];
showFilter: boolean;
onToggleCheck: (ids: number[]) => void;
onToggleCollapse: (id: number) => void;
onToggleCollapse: (id: number, nodes: TreeNode[]) => void;
onOrderChange: (ids: number[]) => void;
showHeader: boolean;
className: string;
}

export class EntryTree extends React.Component<EntryTreeProps> {
static defaultProps: Partial<EntryTreeProps> = {
showFilter: true
showFilter: true,
onOrderChange: () => { /* Nothing to do */ },
showHeader: true,
className: 'table-tree'
};

constructor(props: EntryTreeProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import * as React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronDown, faChevronRight, faCheckSquare, faSquare, faMinusSquare } from '@fortawesome/free-solid-svg-icons';
import { faChevronDown, faChevronRight, faCheckSquare, faSquare, faMinusSquare, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons';

interface iconsShape {
expand: React.ReactNode,
collapse: React.ReactNode,
unchecked: React.ReactNode,
checked: React.ReactNode,
halfChecked: React.ReactNode
halfChecked: React.ReactNode,
sort: React.ReactNode,
sortDown: React.ReactNode,
sortUp: React.ReactNode
}

const icons: iconsShape = {
expand: <FontAwesomeIcon icon={faChevronRight}/>,
collapse: <FontAwesomeIcon icon={faChevronDown}/>,
unchecked: <FontAwesomeIcon icon={faSquare}/>,
checked: <FontAwesomeIcon icon={faCheckSquare}/>,
halfChecked: <FontAwesomeIcon icon={faMinusSquare}/>
halfChecked: <FontAwesomeIcon icon={faMinusSquare}/>,
sort: <FontAwesomeIcon icon={faSort}/>,
sortDown: <FontAwesomeIcon icon={faSortDown}/>,
sortUp: <FontAwesomeIcon icon={faSortUp}/>
};

export default icons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import icons from './icons';
import { TreeNode } from './tree-node';

interface SortState {
asc: React.ReactNode,
desc: React.ReactNode,
default: React.ReactNode;
}

export const sortState: SortState = {
asc: icons.sortUp,
desc: icons.sortDown,
default: icons.sort
};

export interface SortConfig {
column: string;
sortState: React.ReactNode;
}

export const nextSortState = (currentState: React.ReactNode): React.ReactNode => {
if (currentState === sortState.default || currentState === sortState.asc) {
return sortState.desc;
} else if (currentState === sortState.desc) {
return sortState.asc;
} else {
return sortState.default;
}
};

export const sortNodes = (nodes: TreeNode[], sortConfig: SortConfig[]): TreeNode[] => {
const sortedNodes = [...nodes];
const orderToSort = sortConfig.find((config: SortConfig) => config.sortState !== sortState.default);
if (orderToSort) {
sortedNodes.sort((node1: TreeNode, node2: TreeNode) => {
const key = orderToSort.column;
const order = (orderToSort.sortState === sortState.asc) ? 'asc' : 'desc';
const value1 = node1[key as keyof TreeNode];
const value2 = node2[key as keyof TreeNode];
let result = 0;
if (!value1 && value2) {
result = -1;
} else if (value1 && !value2) {
result = 1;
} else if (!value1 && !value2) {
result = 0;
} else {
if (typeof value1 === 'string' && typeof value2 === 'string') {
const comp = value1.localeCompare(value2);
result = (order === 'asc') ? -comp : comp;
} else {
if (value1 < value2) {
result = (order === 'asc') ? -1 : 1;
} else if (value1 > value2) {
result = (order === 'asc') ? 1 : -1;
} else {
result = 0;
}
}
}
return result;
});
sortedNodes.forEach((node: TreeNode) => {
if (node.children.length) {
node.children = sortNodes(node.children, sortConfig);
}
});
}
return sortedNodes;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import { TreeNode } from './tree-node';
import { TableRow } from './table-row';

interface TableBodyProps {
nodes: TreeNode[];
keys: string[];
collapsedNodes: number[];
isCheckable: boolean;
getCheckedStatus: (id: number) => number;
onToggleCollapse: (id: number) => void;
onToggleCheck: (id: number) => void;
}

export class TableBody extends React.Component<TableBodyProps> {
constructor(props: TableBodyProps) {
super(props);
}

createRow = (node: TreeNode): React.ReactNode =>
<TableRow
{...this.props}
key={'row-'+node.id}
node={node}
level={0}
/>;

renderRows = (): React.ReactNode => this.props.nodes.map((node: TreeNode) => this.createRow(node));

render(): React.ReactNode | undefined {
if (!this.props.nodes) {return undefined;}

return (
<tbody>
{this.renderRows()}
</tbody>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import { TreeNode } from './tree-node';

interface TableCellProps {
nodeKey: string;
node: TreeNode;
}

export class TableCell extends React.Component<TableCellProps> {
constructor(props: TableCellProps) {
super(props);
}

render(): React.ReactNode {
const content: React.ReactNode = (this.props.nodeKey !== 'Legend')
? this.props.node[this.props.nodeKey as keyof TreeNode]
: undefined;
return (
<td key={this.props.nodeKey+'-'+this.props.node.id}>
{this.props.children}
{content}
</td>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import { SortConfig } from './sort';

interface TableHeaderProps {
columns: string[];
sortableColumns: string[];
sortConfig: SortConfig[];
onSort: (sortColumn: string) => void;
}

export class TableHeader extends React.Component<TableHeaderProps> {
constructor(props: TableHeaderProps) {
super(props);
}

handleSortChange = (sortColumn: string): void => {
this.props.onSort(sortColumn);
};

toCapitalCase = (name: string): string => (name.charAt(0).toUpperCase() + name.slice(1));

renderSortIcon = (column: string): React.ReactNode | undefined => {
if (this.props.sortableColumns.includes(column)) {
const state = this.props.sortConfig.find((config: SortConfig) => config.column === column);
return state
? <span style={{float: 'right'}}>{state.sortState}</span>
: undefined;
}
return undefined;
};

renderHeader = (): React.ReactNode => this.props.columns.map((column: string, index) =>
<th key={'th-'+index} onClick={() => this.handleSortChange(column)}>
{this.toCapitalCase(column)}
{this.renderSortIcon(column)}
</th>
);

render(): React.ReactNode {
return <thead>
<tr>
{this.renderHeader()}
</tr>
</thead>;
}
}
Loading

0 comments on commit 2d38260

Please sign in to comment.