21225: Move progressWrapper to data explorer for conditional styles
[arvados.git] / services / workbench2 / src / components / multi-panel-view / multi-panel-view.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { MutableRefObject, ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
6 import {
7     Button,
8     Grid,
9     Paper,
10     StyleRulesCallback,
11     Tab,
12     Tabs,
13     Tooltip,
14     withStyles,
15     WithStyles
16 } from "@material-ui/core";
17 import { GridProps } from '@material-ui/core/Grid';
18 import { isArray } from 'lodash';
19 import { DefaultView } from 'components/default-view/default-view';
20 import { InfoIcon } from 'components/icon/icon';
21 import { ReactNodeArray } from 'prop-types';
22 import classNames from 'classnames';
23
24 type CssRules = 'root' | 'button' | 'buttonIcon' | 'content' | 'tabsWrapper' | 'tabsRoot' | 'tabs';
25
26 const styles: StyleRulesCallback<CssRules> = theme => ({
27     root: {
28         marginTop: '10px',
29     },
30     button: {
31         padding: '2px 5px',
32         marginRight: '5px',
33     },
34     buttonIcon: {
35         boxShadow: 'none',
36         padding: '2px 0px 2px 5px',
37         fontSize: '1rem'
38     },
39     content: {
40         overflow: 'auto',
41         maxWidth: 'initial',
42     },
43     tabsWrapper: {
44         width: '100%',
45     },
46     tabsRoot: {
47         flexGrow: 1,
48     },
49     tabs: {
50         flexGrow: 1,
51         flexShrink: 1,
52         maxWidth: 'initial',
53     },
54 });
55
56 interface MPVHideablePanelDataProps {
57     name: string;
58     visible: boolean;
59     maximized: boolean;
60     illuminated: boolean;
61     children: ReactNode;
62     panelRef?: MutableRefObject<any>;
63 }
64
65 interface MPVHideablePanelActionProps {
66     doHidePanel: () => void;
67     doMaximizePanel: () => void;
68     doUnMaximizePanel: () => void;
69 }
70
71 type MPVHideablePanelProps = MPVHideablePanelDataProps & MPVHideablePanelActionProps;
72
73 const MPVHideablePanel = ({ doHidePanel, doMaximizePanel, doUnMaximizePanel, name, visible, maximized, illuminated, ...props }: MPVHideablePanelProps) =>
74     visible
75         ? <>
76             {React.cloneElement((props.children as ReactElement), { doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName: name, panelMaximized: maximized, panelIlluminated: illuminated, panelRef: props.panelRef })}
77         </>
78         : null;
79
80 interface MPVPanelDataProps {
81     panelName?: string;
82     panelMaximized?: boolean;
83     panelIlluminated?: boolean;
84     panelRef?: MutableRefObject<any>;
85     forwardProps?: boolean;
86     maxHeight?: string;
87     minHeight?: string;
88 }
89
90 interface MPVPanelActionProps {
91     doHidePanel?: () => void;
92     doMaximizePanel?: () => void;
93     doUnMaximizePanel?: () => void;
94 }
95
96 // Props received by panel implementors
97 export type MPVPanelProps = MPVPanelDataProps & MPVPanelActionProps;
98
99 type MPVPanelContentProps = { children: ReactElement } & MPVPanelProps & GridProps;
100
101 // Grid item compatible component for layout and MPV props passing
102 export const MPVPanelContent = ({ doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName,
103     panelMaximized, panelIlluminated, panelRef, forwardProps, maxHeight, minHeight,
104     ...props }: MPVPanelContentProps) => {
105     useEffect(() => {
106         if (panelRef && panelRef.current) {
107             panelRef.current.scrollIntoView({ alignToTop: true });
108         }
109     }, [panelRef]);
110
111     const maxH = panelMaximized
112         ? '100%'
113         : maxHeight;
114
115     return <Grid item style={{ maxHeight: maxH, minHeight }} {...props}>
116         <span ref={panelRef} /> {/* Element to scroll to when the panel is selected */}
117         <Paper style={{ height: '100%' }} elevation={panelIlluminated ? 8 : 0}>
118             {forwardProps
119                 ? React.cloneElement(props.children, { doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName, panelMaximized })
120                 : props.children}
121         </Paper>
122     </Grid>;
123 }
124
125 export interface MPVPanelState {
126     name: string;
127     visible?: boolean;
128 }
129 interface MPVContainerDataProps {
130     panelStates?: MPVPanelState[];
131     mutuallyExclusive?: boolean;
132 }
133 type MPVContainerProps = MPVContainerDataProps & GridProps;
134
135 // Grid container compatible component that also handles panel toggling.
136 const MPVContainerComponent = ({ children, panelStates, classes, ...props }: MPVContainerProps & WithStyles<CssRules>) => {
137     if (children === undefined || children === null || children === {}) {
138         children = [];
139     } else if (!isArray(children)) {
140         children = [children];
141     }
142     const initialVisibility = (children as ReactNodeArray).map((_, idx) =>
143         !panelStates || // if panelStates wasn't passed, default to all visible panels
144         (panelStates[idx] &&
145             (panelStates[idx].visible || panelStates[idx].visible === undefined)));
146     const [panelVisibility, setPanelVisibility] = useState<boolean[]>(initialVisibility);
147     const [previousPanelVisibility, setPreviousPanelVisibility] = useState<boolean[]>(initialVisibility);
148     const [highlightedPanel, setHighlightedPanel] = useState<number>(-1);
149     const currentSelectedPanel = panelVisibility.findIndex(Boolean);
150     const [selectedPanel, setSelectedPanel] = useState<number>(-1);
151     const panelRef = useRef<any>(null);
152
153     let panels: JSX.Element[] = [];
154     let buttons: JSX.Element[] = [];
155     let tabs: JSX.Element[] = [];
156     let buttonBar: JSX.Element = <></>;
157
158     if (isArray(children)) {
159         const showFn = (idx: number) => () => {
160             setPreviousPanelVisibility(initialVisibility);
161             if (props.mutuallyExclusive) {
162                 // Hide all other panels
163                 setPanelVisibility([
164                     ...(new Array(idx).fill(false)),
165                     true,
166                     ...(new Array(panelVisibility.length-(idx+1)).fill(false)),
167                 ]);
168             } else {
169                 setPanelVisibility([
170                     ...panelVisibility.slice(0, idx),
171                     true,
172                     ...panelVisibility.slice(idx + 1)
173                 ]);
174             }
175             setSelectedPanel(idx);
176         };
177         const hideFn = (idx: number) => () => {
178             setPreviousPanelVisibility(initialVisibility);
179             setPanelVisibility([
180                 ...panelVisibility.slice(0, idx),
181                 false,
182                 ...panelVisibility.slice(idx+1)
183             ])
184         };
185         const maximizeFn = (idx: number) => () => {
186             setPreviousPanelVisibility(panelVisibility);
187             // Maximize X == hide all but X
188             setPanelVisibility([
189                 ...panelVisibility.slice(0, idx).map(() => false),
190                 true,
191                 ...panelVisibility.slice(idx+1).map(() => false),
192             ]);
193         };
194         const unMaximizeFn = (idx: number) => () => {
195             setPanelVisibility(previousPanelVisibility);
196             setSelectedPanel(idx);
197         }
198         for (let idx = 0; idx < children.length; idx++) {
199             const panelName = panelStates === undefined
200                 ? `Panel ${idx + 1}`
201                 : (panelStates[idx] && panelStates[idx].name) || `Panel ${idx + 1}`;
202             const btnVariant = panelVisibility[idx]
203                 ? "contained"
204                 : "outlined";
205             const btnTooltip = panelVisibility[idx]
206                 ? ``
207                 : `Open ${panelName} panel`;
208             const panelIsMaximized = panelVisibility[idx] &&
209                 panelVisibility.filter(e => e).length === 1;
210
211             buttons = [
212                 ...buttons,
213                 <Tooltip title={btnTooltip} disableFocusListener>
214                     <Button variant={btnVariant} size="small" color="primary"
215                         className={classNames(classes.button)}
216                         onMouseEnter={() => {
217                             setHighlightedPanel(idx);
218                         }}
219                         onMouseLeave={() => {
220                             setHighlightedPanel(-1);
221                         }}
222                         onClick={showFn(idx)}>
223                         {panelName}
224                     </Button>
225                 </Tooltip>
226             ];
227
228             tabs = [
229                 ...tabs,
230                 <>{panelName}</>
231             ];
232
233             const aPanel =
234                 <MPVHideablePanel key={idx} visible={panelVisibility[idx]} name={panelName}
235                     panelRef={(idx === selectedPanel) ? panelRef : undefined}
236                     maximized={panelIsMaximized} illuminated={idx === highlightedPanel}
237                     doHidePanel={hideFn(idx)} doMaximizePanel={maximizeFn(idx)} doUnMaximizePanel={panelIsMaximized ? unMaximizeFn(idx) : () => null}>
238                     {children[idx]}
239                 </MPVHideablePanel>;
240             panels = [...panels, aPanel];
241         };
242
243         buttonBar = props.mutuallyExclusive ?
244             <Paper className={classes.tabsWrapper}>
245                 <Tabs className={classes.tabsRoot} value={currentSelectedPanel} onChange={(e, val) => showFn(val)()}>
246                     {tabs.map((tgl, idx) => <Tab className={classes.tabs} key={idx} label={tgl} />)}
247                 </Tabs>
248             </Paper> :
249             <>
250                 {buttons.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>)}
251             </>;
252     };
253
254     return <Grid container {...props} className={classNames(classes.root, props.className)}>
255         <Grid container item direction="row">
256             {buttonBar}
257         </Grid>
258         <Grid container item {...props} xs className={classes.content}
259             onScroll={() => setSelectedPanel(-1)}>
260             {panelVisibility.includes(true)
261                 ? panels
262                 : <Grid container item alignItems='center' justify='center'>
263                     <DefaultView messages={["All panels are hidden.", "Click on the buttons above to show them."]} icon={InfoIcon} />
264                 </Grid>}
265         </Grid>
266     </Grid>;
267 };
268
269 export const MPVContainer = withStyles(styles)(MPVContainerComponent);