// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import React, { MutableRefObject, ReactElement, ReactNode, useEffect, useRef, useState } from 'react'; import { connect } from 'react-redux'; import { RouterState } from "react-router-redux"; import { RootState } from 'store/store'; import { CustomStyleRulesCallback } from 'common/custom-theme'; import { Grid, Paper, Tabs, Tab } from "@mui/material"; import { WithStyles } from '@mui/styles'; import withStyles from '@mui/styles/withStyles'; import { GridProps } from '@mui/material/Grid'; import { isArray, isEqual } from 'lodash'; import classNames from 'classnames'; type CssRules = | 'exclusiveGridContainerRoot' | 'symmetricTabs' | 'gridItemRoot' | 'paperRoot' | 'exclusiveContentPaper' | 'exclusiveContent' | 'tab' | 'selectedTab'; const styles: CustomStyleRulesCallback = theme => ({ exclusiveGridContainerRoot: { marginTop: 0, }, symmetricTabs: { "& button": { flexBasis: "0", }, }, gridItemRoot: { paddingTop: '0 !important', width: '100%', }, paperRoot: { height: '100%', width: '100%', display: 'flex', flexDirection: 'column', }, exclusiveContent: { overflow: 'auto', margin: 0, }, exclusiveContentPaper: { boxShadow: 'none', }, tab: { flexGrow: 1, flexShrink: 1, maxWidth: 'initial', minWidth: 'fit-content', padding: '0 5px', borderBottom: `1px solid ${theme.palette.grey[300]}`, }, selectedTab: { }, }); interface MPVHideablePanelDataProps { name: string; visible: boolean; children: ReactNode; panelRef?: MutableRefObject; paperClassName?: string; } const MPVHideablePanel = ({ name, visible, paperClassName, ...props }: MPVHideablePanelDataProps) => visible ? <> {React.cloneElement((props.children as ReactElement), { panelName: name, panelRef: props.panelRef, paperClassName, })} : null; interface MPVPanelDataProps { panelName?: string; panelRef?: MutableRefObject; forwardProps?: boolean; maxHeight?: string; minHeight?: string; paperClassName?: string; } // Props received by panel implementors export type MPVPanelProps = MPVPanelDataProps; type MPVPanelContentProps = { children: ReactElement } & MPVPanelProps & GridProps; // Grid item compatible component for layout and MPV props passing export const MPVPanelContent = React.memo(({ panelName, panelRef, forwardProps, maxHeight, minHeight, paperClassName, ...props }: MPVPanelContentProps) => { return {/* Element to scroll to when the panel is selected */} {forwardProps ? React.cloneElement(props.children, { panelName, paperClassName }) : React.cloneElement(props.children)} ; }, preventRerender); // return true to prevent re-render, false to allow re-render function preventRerender(prevProps: MPVPanelContentProps, nextProps: MPVPanelContentProps) { if (!isEqual(prevProps.children, nextProps.children)) { return false; } return true; } export interface MPVPanelState { name: string; visible?: boolean; } interface MPVContainerDataProps { panelStates?: MPVPanelState[]; router: RouterState; } type MPVContainerProps = MPVContainerDataProps & GridProps; const mapStateToProps = (state: RootState): Pick => ({ router: state.router, }); // Grid container compatible component that also handles panel toggling. const MPVContainerComponent = ({ children, panelStates, classes, router, ...props }: MPVContainerProps & WithStyles) => { if (children === undefined || children === null || Object.keys(children).length === 0) { children = []; } else if (!isArray(children)) { children = [children]; } else { children = children.filter(child => child !== null); } const [initialVisibility, setInitialVisibility] = useState(getInitialVisibility(panelStates, children as [])); useEffect(() => { setInitialVisibility(getInitialVisibility(panelStates, children as [])); // eslint-disable-next-line react-hooks/exhaustive-deps }, [(children as []).length]); const [panelVisibility, setPanelVisibility] = useState(initialVisibility); const currentSelectedPanel = panelVisibility.findIndex(Boolean); const [selectedPanel, setSelectedPanel] = useState(-1); const panelRef = useRef(null); // Reset MPV to initial state when route changes const currentRoute = router.location ? router.location.pathname : ""; useEffect(() => { setPanelVisibility(initialVisibility); setSelectedPanel(initialVisibility.indexOf(true)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentRoute, initialVisibility]); let panels: JSX.Element[] = []; let tabs: JSX.Element[] = []; let tabBar: JSX.Element = <>; if (isArray(children)) { const showFn = (idx: number) => () => { // Hide all other panels setPanelVisibility(Array.from({ length: (children as []).length }, (_, index) => index === idx)); setSelectedPanel(idx); }; for (let idx = 0; idx < children.length; idx++) { const panelName = panelStates === undefined ? `Panel ${idx + 1}` : (panelStates[idx] && panelStates[idx].name) || `Panel ${idx + 1}`; tabs = [ ...tabs, <>{panelName} ]; const aPanel = {children[idx]} ; panels = [...panels, aPanel]; }; tabBar = ( showFn(val)()} data-cy={"mpv-tabs"}> {tabs.map((tgl, idx) => )} ); }; const content = {panelVisibility.includes(true) && panels} ; return ( {tabBar} {content} ); }; const getInitialVisibility = (panelStates: MPVPanelState[] | undefined, children: ReactNode[]) => { if (panelStates && panelStates.some(state => state.visible)) { return panelStates.map((panelState) => panelState.visible || false); } // if panelStates wasn't passed or passed with none selected, default to first panel visible return new Array(children.length).fill(false).map((_, idx) => idx === 0); } export const MPVContainer = connect(mapStateToProps)(withStyles(styles)(MPVContainerComponent));