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;
58 items?: Array<TreeItem<T>>;
61 interface TreeProps<T> {
62 items?: Array<TreeItem<T>>;
63 render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
64 toggleItemOpen: (id: string, status: TreeItemStatus) => void;
65 toggleItemActive: (id: string, status: TreeItemStatus) => void;
67 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
70 export const Tree = withStyles(styles)(
71 class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
72 render(): ReactElement<any> {
73 const level = this.props.level ? this.props.level : 0;
74 const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
75 const { list, loader, toggableIconContainer } = classes;
76 return <List component="div" className={list}>
77 {items && items.map((it: TreeItem<T>, idx: number) =>
78 <div key={`item/${level}/${idx}`}>
79 <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
80 onClick={() => toggleItemActive(it.id, it.status)}
81 onContextMenu={this.handleRowContextMenu(it)}>
82 {it.status === TreeItemStatus.PENDING ?
83 <CircularProgress size={10} className={loader} /> : null}
84 <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
85 className={toggableIconContainer}>
86 <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
87 {it.status !== TreeItemStatus.Initial && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
92 {it.items && it.items.length > 0 &&
93 <Collapse in={it.open} timeout="auto" unmountOnExit>
97 toggleItemOpen={toggleItemOpen}
98 toggleItemActive={toggleItemActive}
100 onContextMenu={onContextMenu} />
106 getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => {
107 const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
108 return classnames(toggableIcon, {
110 [iconClose]: !isOpen,
115 handleRowContextMenu = (item: TreeItem<T>) =>
116 (event: React.MouseEvent<HTMLElement>) =>
117 this.props.onContextMenu(event, item)