1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import { Tabs, Tab, List, ListItemButton } from '@mui/material';
7 import { CustomStyleRulesCallback } from 'common/custom-theme';
8 import { WithStyles, withStyles } from '@mui/styles';
9 import classNames from 'classnames';
10 import { ArvadosTheme } from 'common/custom-theme';
11 import { InlinePulser } from 'components/loading/inline-pulser';
13 type TabbedListClasses = 'root' | 'tabs' | 'listItem' | 'selected' | 'spinner' | 'notFoundLabel' | 'moreResults';
15 const tabbedListStyles: CustomStyleRulesCallback<TabbedListClasses> = (theme: ArvadosTheme) => ({
18 flexDirection: 'column',
22 backgroundColor: theme.palette.background.paper,
26 borderBottom: '1px solid lightgrey',
32 backgroundColor: theme.palette.grey[200],
36 backgroundColor: `${theme.palette.grey['300']} !important`
40 justifyContent: 'center',
46 padding: theme.spacing(1),
47 color: theme.palette.grey[700],
52 color: theme.palette.grey[700],
59 type TabPanelProps = {
60 children: React.ReactNode;
65 type TabbedListProps<T> = {
66 tabbedListContents: Record<string, T[]>;
67 injectedStyles?: string;
68 selectedIndex?: number;
70 includeContentsLength: boolean;
73 handleSelect?: (selection: T) => React.MouseEventHandler<HTMLElement> | undefined;
74 renderListItem?: (item: T) => React.ReactNode;
75 handleTabChange?: (event: React.SyntheticEvent, newValue: number) => void;
78 export const TabbedList = withStyles(tabbedListStyles)(
90 includeContentsLength,
91 }: TabbedListProps<T> & WithStyles<TabbedListClasses>) => {
92 const tabLabels = Object.keys(tabbedListContents);
93 const selectedTabLabel = tabLabels[selectedTab];
94 const listContents = tabbedListContents[selectedTabLabel] || [];
96 const getTabLabel = (label: string) => {
97 if (includeContentsLength) {
98 if (maxLength && tabbedListContents[label].length > maxLength) {
99 return `${label} (${maxLength}+)`;
101 return `${label} (${tabbedListContents[label].length})`;
107 const TabPanel = ({ children, value, index }: TabPanelProps) => {
108 return <div hidden={value !== index}>{value === index && children}</div>;
112 <div className={classNames(classes.root, injectedStyles)}>
114 className={classes.tabs}
116 onChange={handleTabChange}
119 {tabLabels.map((label) => (
120 <Tab key={label} data-cy={`${label}-tab-label`} label={getTabLabel(label)} />
127 {isWorking ? <div data-cy="loading-spinner" className={classes.spinner}><InlinePulser /></div> :
129 {listContents.length === 0 && <div className={classes.notFoundLabel}>no matching {tabLabels[selectedTab]} found</div>}
130 {listContents.slice(0, maxLength).map((item, i) => (
131 <div key={`${selectedTabLabel}-${i}`}>
133 className={classNames(classes.listItem, { [classes.selected]: i === selectedIndex })}
134 selected={i === selectedIndex}
135 onClick={handleSelect && handleSelect(item)}
137 {renderListItem ? renderListItem(item) : JSON.stringify(item)}
141 {maxLength && listContents.length > maxLength && <div className={classes.moreResults}>{'keep typing to refine search results'}</div>}