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