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'; 16import {classNames} from '../../base/classnames'; 17import {raf} from '../../core/raf_scheduler'; 18 19interface ColumnDescriptor<T> { 20 name: string; 21 getData: (row: T) => string; 22} 23 24export interface TreeTableAttrs<T> { 25 columns: ColumnDescriptor<T>[]; 26 getChildren: (row: T) => T[] | undefined; 27 rows: T[]; 28} 29 30export class TreeTable<T> implements m.ClassComponent<TreeTableAttrs<T>> { 31 private collapsedPaths = new Set<string>(); 32 33 view({attrs}: m.Vnode<TreeTableAttrs<T>, this>): void | m.Children { 34 const {columns, rows} = attrs; 35 const headers = columns.map(({name}) => m('th', name)); 36 const renderedRows = this.renderRows(rows, 0, attrs, []); 37 return m( 38 'table.pf-treetable', 39 m('thead', m('tr', headers)), 40 m('tbody', renderedRows), 41 ); 42 } 43 44 private renderRows( 45 rows: T[], 46 indentLevel: number, 47 attrs: TreeTableAttrs<T>, 48 path: string[], 49 ): m.Children { 50 const {columns, getChildren} = attrs; 51 const renderedRows: m.Children = []; 52 for (const row of rows) { 53 const childRows = getChildren(row); 54 const key = this.keyForRow(row, attrs); 55 const thisPath = path.concat([key]); 56 const hasChildren = childRows && childRows.length > 0; 57 const cols = columns.map(({getData}, index) => { 58 const classes = classNames( 59 hasChildren && 'pf-treetable-node', 60 this.isCollapsed(thisPath) && 'pf-collapsed', 61 ); 62 if (index === 0) { 63 const style = { 64 '--indentation-level': indentLevel, 65 }; 66 return m( 67 'td', 68 {style, class: classNames(classes, 'pf-treetable-maincol')}, 69 m('.pf-treetable-gutter', { 70 onclick: () => { 71 if (this.isCollapsed(thisPath)) { 72 this.expandPath(thisPath); 73 } else { 74 this.collapsePath(thisPath); 75 } 76 raf.scheduleFullRedraw(); 77 }, 78 }), 79 getData(row), 80 ); 81 } else { 82 const style = { 83 '--indentation-level': 0, 84 }; 85 return m('td', {style}, getData(row)); 86 } 87 }); 88 renderedRows.push(m('tr', cols)); 89 if (childRows && !this.isCollapsed(thisPath)) { 90 renderedRows.push( 91 this.renderRows(childRows, indentLevel + 1, attrs, thisPath), 92 ); 93 } 94 } 95 return renderedRows; 96 } 97 98 collapsePath(path: string[]) { 99 const pathStr = path.join('/'); 100 this.collapsedPaths.add(pathStr); 101 } 102 103 expandPath(path: string[]) { 104 const pathStr = path.join('/'); 105 this.collapsedPaths.delete(pathStr); 106 } 107 108 isCollapsed(path: string[]) { 109 const pathStr = path.join('/'); 110 return this.collapsedPaths.has(pathStr); 111 } 112 113 keyForRow(row: T, attrs: TreeTableAttrs<T>): string { 114 const {columns} = attrs; 115 return columns[0].getData(row); 116 } 117} 118