22083: Store "failedToLoadOutputCollection" state
[arvados.git] / services / workbench2 / src / components / tabbedList / tabbed-list.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
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';
12
13 type TabbedListClasses = 'root' | 'tabs' | 'listItem' | 'selected' | 'spinner' | 'notFoundLabel' | 'moreResults';
14
15 const tabbedListStyles: CustomStyleRulesCallback<TabbedListClasses> = (theme: ArvadosTheme) => ({
16     root: {
17         display: 'flex',
18         flexDirection: 'column',
19         height: '100%',
20     },
21     tabs: {
22         backgroundColor: theme.palette.background.paper,
23         position: 'sticky',
24         top: 0,
25         zIndex: 1,
26         borderBottom: '1px solid lightgrey',
27     },
28     listItem: {
29         height: '2rem',
30         cursor: 'pointer',
31         '&:hover': {
32             backgroundColor: theme.palette.grey[200],
33         }
34     },
35     selected: {
36         backgroundColor: `${theme.palette.grey['300']} !important`
37     },
38     spinner: {
39         display: 'flex',
40         justifyContent: 'center',
41         alignItems: 'center',
42         height: '4rem',
43     },
44     notFoundLabel: {
45         cursor: 'default',
46         padding: theme.spacing(1),
47         color: theme.palette.grey[700],
48         textAlign: 'center',
49     },
50     moreResults: {
51         padding: 0,
52         color: theme.palette.grey[700],
53         textAlign: 'center',
54         fontStyle: 'italic',
55         fontSize: '0.8rem',
56     },
57 });
58
59 type TabPanelProps = {
60   children: React.ReactNode;
61   value: number;
62   index: number;
63 };
64
65 type TabbedListProps<T> = {
66     tabbedListContents: Record<string, T[]>;
67     injectedStyles?: string;
68     selectedIndex?: number;
69     selectedTab?: number;
70     includeContentsLength: boolean;
71     isWorking?: boolean;
72     maxLength?: number;
73     handleSelect?: (selection: T) => React.MouseEventHandler<HTMLElement> | undefined;
74     renderListItem?: (item: T) => React.ReactNode;
75     handleTabChange?: (event: React.SyntheticEvent, newValue: number) => void;
76 };
77
78 export const TabbedList = withStyles(tabbedListStyles)(
79     <T,>({
80         tabbedListContents,
81         selectedIndex = 0,
82         selectedTab = 0,
83         isWorking,
84         maxLength,
85         injectedStyles,
86         classes,
87         handleSelect,
88         renderListItem,
89         handleTabChange,
90         includeContentsLength,
91     }: TabbedListProps<T> & WithStyles<TabbedListClasses>) => {
92     const tabLabels = Object.keys(tabbedListContents);
93     const selectedTabLabel = tabLabels[selectedTab];
94     const listContents = tabbedListContents[selectedTabLabel] || [];
95
96     const getTabLabel = (label: string) => {
97         if (includeContentsLength) { 
98             if (maxLength && tabbedListContents[label].length > maxLength) {
99                 return `${label} (${maxLength}+)`;
100             }
101             return `${label} (${tabbedListContents[label].length})`;
102         } else {
103             return label;
104         }
105     };
106
107     const TabPanel = ({ children, value, index }: TabPanelProps) => {
108         return <div hidden={value !== index}>{value === index && children}</div>;
109     };
110
111     return (
112         <div className={classNames(classes.root, injectedStyles)}>
113             <Tabs
114                 className={classes.tabs}
115                 value={selectedTab}
116                 onChange={handleTabChange}
117                 variant='fullWidth'
118             >
119                 {tabLabels.map((label) => (
120                     <Tab key={label} data-cy={`${label}-tab-label`} label={getTabLabel(label)} />
121                 ))}
122             </Tabs>
123             <TabPanel
124                 value={selectedTab}
125                 index={selectedTab}
126             >
127                 {isWorking ? <div data-cy="loading-spinner" className={classes.spinner}><InlinePulser /></div> :
128                     <List dense>
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}`}>
132                             <ListItemButton
133                                 className={classNames(classes.listItem, { [classes.selected]: i === selectedIndex })}
134                                 selected={i === selectedIndex}
135                                 onClick={handleSelect && handleSelect(item)}
136                                 >
137                                 {renderListItem ? renderListItem(item) : JSON.stringify(item)}
138                             </ListItemButton>
139                         </div>
140                         ))}
141                         {maxLength && listContents.length > maxLength && <div className={classes.moreResults}>{'keep typing to refine search results'}</div>}
142                     </List>
143                 }
144             </TabPanel>
145         </div>
146     );
147 });