blob: 46f78a8512cafc3a7e148f78dd8e453dddb30fbe [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m from 'mithril';
import {classNames} from '../../base/classnames';
import {raf} from '../../core/raf_scheduler';
interface ColumnDescriptor<T> {
name: string;
getData: (row: T) => string;
}
export interface TreeTableAttrs<T> {
columns: ColumnDescriptor<T>[];
getChildren: (row: T) => T[] | undefined;
rows: T[];
}
export class TreeTable<T> implements m.ClassComponent<TreeTableAttrs<T>> {
private collapsedPaths = new Set<string>();
view({attrs}: m.Vnode<TreeTableAttrs<T>, this>): void | m.Children {
const {columns, rows} = attrs;
const headers = columns.map(({name}) => m('th', name));
const renderedRows = this.renderRows(rows, 0, attrs, []);
return m(
'table.pf-treetable',
m('thead', m('tr', headers)),
m('tbody', renderedRows),
);
}
private renderRows(
rows: T[],
indentLevel: number,
attrs: TreeTableAttrs<T>,
path: string[],
): m.Children {
const {columns, getChildren} = attrs;
const renderedRows: m.Children = [];
for (const row of rows) {
const childRows = getChildren(row);
const key = this.keyForRow(row, attrs);
const thisPath = path.concat([key]);
const hasChildren = childRows && childRows.length > 0;
const cols = columns.map(({getData}, index) => {
const classes = classNames(
hasChildren && 'pf-treetable-node',
this.isCollapsed(thisPath) && 'pf-collapsed',
);
if (index === 0) {
const style = {
'--indentation-level': indentLevel,
};
return m(
'td',
{style, class: classNames(classes, 'pf-treetable-maincol')},
m('.pf-treetable-gutter', {
onclick: () => {
if (this.isCollapsed(thisPath)) {
this.expandPath(thisPath);
} else {
this.collapsePath(thisPath);
}
raf.scheduleFullRedraw();
},
}),
getData(row),
);
} else {
const style = {
'--indentation-level': 0,
};
return m('td', {style}, getData(row));
}
});
renderedRows.push(m('tr', cols));
if (childRows && !this.isCollapsed(thisPath)) {
renderedRows.push(
this.renderRows(childRows, indentLevel + 1, attrs, thisPath),
);
}
}
return renderedRows;
}
collapsePath(path: string[]) {
const pathStr = path.join('/');
this.collapsedPaths.add(pathStr);
}
expandPath(path: string[]) {
const pathStr = path.join('/');
this.collapsedPaths.delete(pathStr);
}
isCollapsed(path: string[]) {
const pathStr = path.join('/');
return this.collapsedPaths.has(pathStr);
}
keyForRow(row: T, attrs: TreeTableAttrs<T>): string {
const {columns} = attrs;
return columns[0].getData(row);
}
}