1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16 17import {classNames} from '../../base/classnames'; 18import {raf} from '../../core/raf_scheduler'; 19 20interface ColumnDescriptor<T> { 21 name: string; 22 getData: (row: T) => string; 23} 24 25export interface TreeTableAttrs<T> { 26 columns: ColumnDescriptor<T>[]; 27 getChildren: (row: T) => T[] | undefined; 28 rows: T[]; 29} 30 31export class TreeTable<T> implements m.ClassComponent<TreeTableAttrs<T>> { 32 private collapsedPaths = new Set<string>(); 33 34 view({attrs}: m.Vnode<TreeTableAttrs<T>, this>): void | m.Children { 35 const {columns, rows} = attrs; 36 const headers = columns.map(({name}) => m('th', name)); 37 const renderedRows = this.renderRows(rows, 0, attrs, []); 38 return m( 39 'table.pf-treetable', 40 m('thead', m('tr', headers)), 41 m('tbody', renderedRows), 42 ); 43 } 44 45 private renderRows( 46 rows: T[], 47 indentLevel: number, 48 attrs: TreeTableAttrs<T>, 49 path: string[], 50 ): m.Children { 51 const {columns, getChildren} = attrs; 52 const renderedRows: m.Children = []; 53 for (const row of rows) { 54 const childRows = getChildren(row); 55 const key = this.keyForRow(row, attrs); 56 const thisPath = path.concat([key]); 57 const hasChildren = childRows && childRows.length > 0; 58 const cols = columns.map(({getData}, index) => { 59 const classes = classNames( 60 hasChildren && 'pf-treetable-node', 61 this.isCollapsed(thisPath) && 'pf-collapsed', 62 ); 63 if (index === 0) { 64 const style = { 65 '--indentation-level': indentLevel, 66 }; 67 return m( 68 'td', 69 {style, class: classNames(classes, 'pf-treetable-maincol')}, 70 m('.pf-treetable-gutter', { 71 onclick: () => { 72 if (this.isCollapsed(thisPath)) { 73 this.expandPath(thisPath); 74 } else { 75 this.collapsePath(thisPath); 76 } 77 raf.scheduleFullRedraw(); 78 }, 79 }), 80 getData(row), 81 ); 82 } else { 83 const style = { 84 '--indentation-level': 0, 85 }; 86 return m('td', {style}, getData(row)); 87 } 88 }); 89 renderedRows.push(m('tr', cols)); 90 if (childRows && !this.isCollapsed(thisPath)) { 91 renderedRows.push( 92 this.renderRows(childRows, indentLevel + 1, attrs, thisPath), 93 ); 94 } 95 } 96 return renderedRows; 97 } 98 99 collapsePath(path: string[]) { 100 const pathStr = path.join('/'); 101 this.collapsedPaths.add(pathStr); 102 } 103 104 expandPath(path: string[]) { 105 const pathStr = path.join('/'); 106 this.collapsedPaths.delete(pathStr); 107 } 108 109 isCollapsed(path: string[]) { 110 const pathStr = path.join('/'); 111 return this.collapsedPaths.has(pathStr); 112 } 113 114 keyForRow(row: T, attrs: TreeTableAttrs<T>): string { 115 const {columns} = attrs; 116 return columns[0].getData(row); 117 } 118} 119