Skip to content

Commit

Permalink
[FEATURE] Panels can be viewed in full screen
Browse files Browse the repository at this point in the history
Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
  • Loading branch information
Gladorme committed May 14, 2024
1 parent 5aa8de5 commit 2b64cbe
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 42 deletions.
6 changes: 5 additions & 1 deletion ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import Router from './Router';
import { SignInRoute, SignUpRoute } from './model/route';
import { AuthorizationProvider } from './context/Authorization';

function isDashboardViewRoute(pathname: string): boolean {
return /\/projects\/[a-zA-Z0-9_]+\/dashboards\/[a-zA-Z0-9_]+/.test(pathname);
}

function App() {
const location = useLocation();
return (
Expand All @@ -42,7 +46,7 @@ function App() {
>
<Router />
</Box>
<Footer />
{!isDashboardViewRoute(location.pathname) && <Footer />}
</Box>
</AuthorizationProvider>
);
Expand Down
15 changes: 13 additions & 2 deletions ui/dashboards/src/components/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import { Box, BoxProps } from '@mui/material';
import { ErrorBoundary, ErrorAlert } from '@perses-dev/components';
import { useRef } from 'react';
import { usePanelGroupIds } from '../../context';
import { GridLayout } from '../GridLayout';
import { EmptyDashboard, EmptyDashboardProps } from '../EmptyDashboard';
Expand All @@ -32,10 +33,14 @@ export type DashboardProps = BoxProps & {
*/
export function Dashboard({ emptyDashboardProps, panelOptions, ...boxProps }: DashboardProps) {
const panelGroupIds = usePanelGroupIds();
const boxRef = useRef<HTMLDivElement>(null);
// const { showPanelRefState } = useShowPanelRef(); // TODO: check if panelRef exists => ignore
const isEmpty = !panelGroupIds.length;
const dashboardTopPosition = boxRef.current?.getBoundingClientRect().top ?? 165;
const panelFullHeight = window.innerHeight - dashboardTopPosition - window.scrollY;

return (
<Box {...boxProps} sx={{ height: '100%' }}>
<Box {...boxProps} sx={{ height: '100%' }} ref={boxRef}>
<ErrorBoundary FallbackComponent={ErrorAlert}>
{isEmpty && (
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center' }}>
Expand All @@ -44,7 +49,13 @@ export function Dashboard({ emptyDashboardProps, panelOptions, ...boxProps }: Da
)}
{!isEmpty &&
panelGroupIds.map((panelGroupId) => (
<GridLayout key={panelGroupId} panelGroupId={panelGroupId} panelOptions={panelOptions} />
<GridLayout
key={panelGroupId}
panelGroupId={panelGroupId}
panelOptions={panelOptions}
panelFullHeight={panelFullHeight}
// showPanelRef={showPanelRefState.showPanelRef}
/>
))}
</ErrorBoundary>
</Box>
Expand Down
24 changes: 14 additions & 10 deletions ui/dashboards/src/components/GridLayout/GridContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { useEffect, useState } from 'react';
import { styled } from '@mui/material';
import { ReactNode, useEffect, useState } from 'react';
import { styled, SxProps, Theme } from '@mui/material';

export interface GridContainerProps {
children: React.ReactNode;
children: ReactNode;
sx?: SxProps<Theme>;
}

export function GridContainer(props: GridContainerProps) {
Expand All @@ -28,13 +29,16 @@ export function GridContainer(props: GridContainerProps) {

return (
<ReactGridLayoutContainer
sx={{
// This adds spcing between grids (rows) in the overall dashboard
'& + &': { marginTop: (theme) => theme.spacing(1) },
// This disables the animation of grid items when a grid is first rendered
// (see https://github.com/react-grid-layout/react-grid-layout/issues/103)
'& .react-grid-item.cssTransforms': { transitionProperty: isFirstRender ? 'none' : 'transform' },
}}
sx={[
{
// This adds spcing between grids (rows) in the overall dashboard
'& + &': { marginTop: (theme) => theme.spacing(1) },
// This disables the animation of grid items when a grid is first rendered
// (see https://github.com/react-grid-layout/react-grid-layout/issues/103)
'& .react-grid-item.cssTransforms': { transitionProperty: isFirstRender ? 'none' : 'transform' },
},
...(Array.isArray(props.sx) ? props.sx : [props.sx]),
]}
data-testid="panel-group"
>
{props.children}
Expand Down
20 changes: 15 additions & 5 deletions ui/dashboards/src/components/GridLayout/GridItemContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
import { Box } from '@mui/material';
import { useInView } from 'react-intersection-observer';
import { DataQueriesProvider, useSuggestedStepMs } from '@perses-dev/plugin-system';
import { PanelGroupItemId, useEditMode, usePanel, usePanelActions } from '../../context';
import { Panel, PanelProps } from '../Panel/Panel';
import { PanelOptions } from '../Panel';
import { PanelGroupItemId, useEditMode, usePanel, usePanelActions, useShowPanel } from '../../context';
import { Panel, PanelProps, PanelOptions } from '../Panel';

export interface GridItemContentProps {
panelGroupItemId: PanelGroupItemId;
Expand All @@ -34,14 +33,24 @@ export function GridItemContent(props: GridItemContentProps) {
spec: { queries },
} = panelDefinition;
const { isEditMode } = useEditMode();
const { openEditPanel, openDeletePanelDialog, duplicatePanel } = usePanelActions(panelGroupItemId);

const { openEditPanel, openDeletePanelDialog, duplicatePanel, showPanel } = usePanelActions(panelGroupItemId);
const showPanelGroupItemId = useShowPanel();
const { ref, inView } = useInView({
threshold: 0.2, // we have the flexibility to adjust this threshold to trigger queries slightly earlier or later based on performance
initialInView: false,
triggerOnce: true,
});

const readHandlers = {
onShowPanelClick: function () {
if (showPanelGroupItemId === undefined) {
showPanel(panelGroupItemId);
} else {
showPanel(undefined);
}
},
};

// Provide actions to the panel when in edit mode
let editHandlers: PanelProps['editHandlers'] = undefined;
if (isEditMode) {
Expand Down Expand Up @@ -74,6 +83,7 @@ export function GridItemContent(props: GridItemContentProps) {
{inView && (
<Panel
definition={panelDefinition}
readHandlers={readHandlers}
editHandlers={editHandlers}
panelOptions={props.panelOptions}
panelGroupItemId={panelGroupItemId}
Expand Down
81 changes: 67 additions & 14 deletions ui/dashboards/src/components/GridLayout/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,86 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { useState } from 'react';
import { useMemo, useRef, useState } from 'react';

Check failure on line 13 in ui/dashboards/src/components/GridLayout/GridLayout.tsx

View workflow job for this annotation

GitHub Actions / lint

'useRef' is defined but never used
import { Responsive, WidthProvider, Layouts, Layout } from 'react-grid-layout';
import { Collapse, useTheme } from '@mui/material';
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
import { useEditMode, usePanelGroup, usePanelGroupActions, PanelGroupId } from '../../context';
import {
useEditMode,
usePanelGroup,
usePanelGroupActions,
PanelGroupId,
PanelGroupItemLayout,
useShowPanel,
PanelGroupDefinition,
} from '../../context';
import { GRID_LAYOUT_COLS, GRID_LAYOUT_SMALL_BREAKPOINT } from '../../constants';
import { PanelOptions } from '../Panel';
import { GridTitle } from './GridTitle';
import { GridItemContent } from './GridItemContent';
import { GridContainer } from './GridContainer';
const DEFAULT_MARGIN = 10;
const ROW_HEIGHT = 30;
const ResponsiveGridLayout = WidthProvider(Responsive);

export interface GridLayoutProps {
panelGroupId: PanelGroupId;
panelOptions?: PanelOptions;
panelFullHeight?: number;
// showPanelRef?: string;
}

/**
* Layout component that arranges children in a Grid based on the definition.
*/
export function GridLayout(props: GridLayoutProps) {
const { panelGroupId /*...others */ } = props;
const { panelGroupId, panelOptions, panelFullHeight /*showPanelRef*/ } = props;
const theme = useTheme();
const groupDefinition = usePanelGroup(panelGroupId);
const groupDefinition: PanelGroupDefinition = usePanelGroup(panelGroupId);
const { updatePanelGroupLayouts } = usePanelGroupActions(panelGroupId);

const [isOpen, setIsOpen] = useState(!groupDefinition.isCollapsed ?? true);
const { isEditMode } = useEditMode();

const [gridColWidth, setGridColWidth] = useState(0);

const showPanelItemId = useShowPanel();
const hasShowPanel = showPanelItemId?.panelGroupId === panelGroupId;
const itemLayoutShowed = showPanelItemId?.panelGroupItemLayoutId;

const showGrid = useMemo(() => {
if (showPanelItemId === undefined) {
return true;
}
return hasShowPanel;
}, [hasShowPanel, showPanelItemId]);

const itemLayouts: PanelGroupItemLayout[] = useMemo(() => {
if (itemLayoutShowed) {
return groupDefinition.itemLayouts.map((itemLayout) => {
if (itemLayout.i === itemLayoutShowed) {
const rowTitleHeight = 40 + 8; // 8 is the margin
return {
h: Math.round(((panelFullHeight ?? window.innerHeight) - rowTitleHeight) / (ROW_HEIGHT + DEFAULT_MARGIN)),
i: itemLayoutShowed,
w: 48,
x: 0,
y: 0,
} as PanelGroupItemLayout;
}
return itemLayout;
});
}
return groupDefinition.itemLayouts;
}, [groupDefinition.itemLayouts, itemLayoutShowed, panelFullHeight]);

const handleLayoutChange = (currentLayout: Layout[], allLayouts: Layouts) => {
// Using the value from `allLayouts` instead of `currentLayout` because of
// a bug in react-layout-grid where `currentLayout` does not adjust properly
// when going to a smaller breakpoint and then back to a larger breakpoint.
// https://github.com/react-grid-layout/react-grid-layout/issues/1663
const smallLayout = allLayouts[GRID_LAYOUT_SMALL_BREAKPOINT];
if (smallLayout) {
if (smallLayout && !hasShowPanel) {
updatePanelGroupLayouts(smallLayout);
}
};
Expand All @@ -71,7 +112,7 @@ export function GridLayout(props: GridLayoutProps) {
};

return (
<GridContainer>
<GridContainer sx={{ display: showGrid ? 'unset' : 'none' }}>
{groupDefinition.title !== undefined && (
<GridTitle
panelGroupId={panelGroupId}
Expand All @@ -88,24 +129,36 @@ export function GridLayout(props: GridLayoutProps) {
className="layout"
breakpoints={{ sm: theme.breakpoints.values.sm, xxs: 0 }}
cols={GRID_LAYOUT_COLS}
rowHeight={30}
rowHeight={ROW_HEIGHT}
draggableHandle=".drag-handle"
resizeHandles={['se']}
isDraggable={isEditMode}
isResizable={isEditMode}
isDraggable={isEditMode && !hasShowPanel}
isResizable={isEditMode && !hasShowPanel}
margin={[DEFAULT_MARGIN, DEFAULT_MARGIN]}
containerPadding={[0, 10]}
layouts={{ [GRID_LAYOUT_SMALL_BREAKPOINT]: groupDefinition.itemLayouts }}
layouts={{ [GRID_LAYOUT_SMALL_BREAKPOINT]: itemLayouts }}
onLayoutChange={handleLayoutChange}
onWidthChange={handleWidthChange}
allowOverlap={hasShowPanel} // Enabling overlap when showing a specific panel because panel in front will add empty spaces (empty row height)
>
{groupDefinition.itemLayouts.map(({ i, w }) => (
<div key={i}>
{itemLayouts.map(({ i, w }) => (
<div
key={i}
style={{
display: itemLayoutShowed !== undefined ? (itemLayoutShowed === i ? 'unset' : 'none') : 'unset',
}}
>
<ErrorBoundary FallbackComponent={ErrorAlert}>
<GridItemContent
panelOptions={props.panelOptions}
panelOptions={panelOptions}
panelGroupItemId={{ panelGroupId, panelGroupItemLayoutId: i }}
width={calculateGridItemWidth(w, gridColWidth)}
width={
itemLayoutShowed !== undefined
? itemLayoutShowed === i
? calculateGridItemWidth(w, 1) // TODO
: 0
: calculateGridItemWidth(w, gridColWidth)
}
/>
</ErrorBoundary>
</div>
Expand Down
14 changes: 13 additions & 1 deletion ui/dashboards/src/components/Panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PanelContent } from './PanelContent';

export interface PanelProps extends CardProps<'section'> {
definition: PanelDefinition;
readHandlers?: PanelHeaderProps['readHandlers'];
editHandlers?: PanelHeaderProps['editHandlers'];
panelOptions?: PanelOptions;
panelGroupItemId?: PanelGroupItemId;
Expand Down Expand Up @@ -50,7 +51,17 @@ export type PanelExtraProps = {
* Renders a PanelDefinition's content inside of a Card.
*/
export const Panel = memo(function Panel(props: PanelProps) {
const { definition, editHandlers, onMouseEnter, onMouseLeave, sx, panelOptions, panelGroupItemId, ...others } = props;
const {
definition,
readHandlers,
editHandlers,
onMouseEnter,
onMouseLeave,
sx,
panelOptions,
panelGroupItemId,
...others
} = props;

// Make sure we have an ID we can use for aria attributes
const generatedPanelId = useId('Panel');
Expand Down Expand Up @@ -100,6 +111,7 @@ export const Panel = memo(function Panel(props: PanelProps) {
id={headerId}
title={definition.spec.display.name}
description={definition.spec.display.description}
readHandlers={readHandlers}
editHandlers={editHandlers}
links={definition.spec.links}
sx={{ paddingX: `${chartsTheme.container.padding.default}px` }}
Expand Down
25 changes: 22 additions & 3 deletions ui/dashboards/src/components/Panel/PanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import InformationOutlineIcon from 'mdi-material-ui/InformationOutline';
import PencilIcon from 'mdi-material-ui/PencilOutline';
import DeleteIcon from 'mdi-material-ui/DeleteOutline';
import DragIcon from 'mdi-material-ui/DragVertical';
import ArrowExpand from 'mdi-material-ui/ArrowExpand';
import ContentCopyIcon from 'mdi-material-ui/ContentCopy';
import { useReplaceVariablesInString } from '@perses-dev/plugin-system';
import { ReactNode } from 'react';
Expand All @@ -31,6 +32,9 @@ export interface PanelHeaderProps extends Omit<CardHeaderProps, OmittedProps> {
description?: string;
links?: Link[];
extra?: ReactNode;
readHandlers?: {
onShowPanelClick: () => void;
};
editHandlers?: {
onEditPanelClick: () => void;
onDuplicatePanelClick: () => void;
Expand All @@ -43,6 +47,7 @@ export function PanelHeader({
title: rawTitle,
description: rawDescription,
links,
readHandlers,
editHandlers,
sx,
extra,
Expand All @@ -54,10 +59,24 @@ export function PanelHeader({
const title = useReplaceVariablesInString(rawTitle) as string;
const description = useReplaceVariablesInString(rawDescription);

let actions: CardHeaderProps['action'] = undefined;
let readActions: CardHeaderProps['action'] = undefined;
if (readHandlers !== undefined) {
readActions = (
<InfoTooltip description={TOOLTIP_TEXT.showPanel}>
<HeaderIconButton
aria-label={ARIA_LABEL_TEXT.showPanel(title)}
size="small"
onClick={readHandlers.onShowPanelClick}
>
<ArrowExpand fontSize="inherit" />
</HeaderIconButton>
</InfoTooltip>
);
}
let editActions: CardHeaderProps['action'] = undefined;
if (editHandlers !== undefined) {
// If there are edit handlers, always just show the edit buttons
actions = (
editActions = (
<>
<InfoTooltip description={TOOLTIP_TEXT.editPanel}>
<HeaderIconButton
Expand Down Expand Up @@ -144,7 +163,7 @@ export function PanelHeader({
}
action={
<HeaderActionWrapper direction="row" spacing={0.25} alignItems="center">
{editHandlers === undefined && extra} {actions}
{editHandlers === undefined && extra} {readActions} {editActions}
</HeaderActionWrapper>
}
sx={combineSx(
Expand Down
Loading

0 comments on commit 2b64cbe

Please sign in to comment.