1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import * as React from 'react';
6 import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
7 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
8 import { ReactElement } from "react";
9 import CircularProgress from '@material-ui/core/CircularProgress';
10 import * as classnames from "classnames";
12 import { ArvadosTheme } from '../../common/custom-theme';
13 import { SidePanelRightArrowIcon } from '../icon/icon';
15 type CssRules = 'list' | 'active' | 'loader' | 'toggableIconContainer' | 'iconClose' | 'iconOpen' | 'toggableIcon';
17 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
23 transform: 'translate(0px)',
26 toggableIconContainer: {
27 color: theme.palette.grey["700"],
35 color: theme.palette.primary.main,
38 transition: 'all 0.1s ease',
41 transition: 'all 0.1s ease',
42 transform: 'rotate(90deg)',
46 export enum TreeItemStatus {
52 export interface TreeItem<T> {
57 status: TreeItemStatus;
59 items?: Array<TreeItem<T>>;
62 interface TreeProps<T> {
63 items?: Array<TreeItem<T>>;
64 render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
65 toggleItemOpen: (id: string, status: TreeItemStatus) => void;
66 toggleItemActive: (id: string, status: TreeItemStatus) => void;
68 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
71 export const Tree = withStyles(styles)(
72 class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
73 render(): ReactElement<any> {
74 const level = this.props.level ? this.props.level : 0;
75 const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
76 const { list, loader, toggableIconContainer } = classes;
77 return <List component="div" className={list}>
78 {items && items.map((it: TreeItem<T>, idx: number) =>
79 <div key={`item/${level}/${idx}`}>
80 <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
81 onClick={() => toggleItemActive(it.id, it.status)}
82 onContextMenu={this.handleRowContextMenu(it)}>
83 {it.status === TreeItemStatus.Pending ?
84 <CircularProgress size={10} className={loader} /> : null}
85 <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
86 className={toggableIconContainer}>
87 <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
88 {it.toggled && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
93 {it.items && it.items.length > 0 &&
94 <Collapse in={it.open} timeout="auto" unmountOnExit>
98 toggleItemOpen={toggleItemOpen}
99 toggleItemActive={toggleItemActive}
101 onContextMenu={onContextMenu} />
107 getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => {
108 const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
109 return classnames(toggableIcon, {
111 [iconClose]: !isOpen,
116 handleRowContextMenu = (item: TreeItem<T>) =>
117 (event: React.MouseEvent<HTMLElement>) =>
118 this.props.onContextMenu(event, item)