Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions dashboards/src/components/GridLayout/GridItemContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,25 @@ export interface GridItemContentProps {
panelGroupItemId: PanelGroupItemId;
width: number; // necessary for determining the suggested step ms
panelOptions?: PanelOptions;
readonly?: boolean;
informationTooltip?: string;
}

/**
* Resolves the reference to panel content in a GridItemDefinition and renders the panel.
*/
export function GridItemContent(props: GridItemContentProps): ReactElement {
const { panelGroupItemId, width } = props;
const { readonly, panelGroupItemId, width, informationTooltip } = props;
const panelDefinition = usePanel(panelGroupItemId);

const {
spec: { queries },
} = panelDefinition;

const { isEditMode } = useEditMode();
const canModify = useMemo(() => {
return isEditMode && !readonly;
}, [isEditMode, readonly]);
const { openEditPanel, openDeletePanelDialog, duplicatePanel, viewPanel } = usePanelActions(panelGroupItemId);
const viewPanelGroupItemId = useViewPanelGroup();

Expand All @@ -64,14 +69,14 @@ export function GridItemContent(props: GridItemContentProps): ReactElement {
const [openQueryViewer, setOpenQueryViewer] = useState(false);

const viewQueriesHandler = useMemo(() => {
return isEditMode || !queries?.length
return canModify || !queries?.length
? undefined
: {
onClick: (): void => {
setOpenQueryViewer(true);
},
};
}, [isEditMode, queries]);
}, [canModify, queries]);

const readHandlers = {
isPanelViewed: isPanelGroupItemIdEqual(viewPanelGroupItemId, panelGroupItemId),
Expand All @@ -86,7 +91,7 @@ export function GridItemContent(props: GridItemContentProps): ReactElement {

// Provide actions to the panel when in edit mode
let editHandlers: PanelProps['editHandlers'] = undefined;
if (isEditMode) {
if (canModify && !readonly) {
editHandlers = {
onEditPanelClick: openEditPanel,
onDuplicatePanelClick: duplicatePanel,
Expand Down Expand Up @@ -137,6 +142,7 @@ export function GridItemContent(props: GridItemContentProps): ReactElement {
viewQueriesHandler={viewQueriesHandler}
panelOptions={props.panelOptions}
panelGroupItemId={panelGroupItemId}
informationTooltip={informationTooltip}
/>
)}
</DataQueriesProvider>
Expand Down
80 changes: 80 additions & 0 deletions dashboards/src/components/GridLayout/GridItemRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 { PanelGroupId } from '@perses-dev/spec';
import { PanelOptions, useViewPanelGroup } from '@perses-dev/dashboards';
import { ReactElement } from 'react';
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
import { PanelGroupItemId } from '../../model';
import { RepeatItemMeta } from '../../utils';
import { GridItemContent } from './GridItemContent';
import { RepeatGridItemContent } from './RepeatGridItemContent';

const DEFAULT_MARGIN = 10;

interface GridItemRendererProps {
panelGroupId: PanelGroupId;
panelGroupItemLayoutId: string;
width: number;
repeatItemMeta?: RepeatItemMeta;
groupRepeatVariable?: [string, string];
panelOptions?: PanelOptions;
isEditMode: boolean;
}

export function GridItemRenderer({
panelGroupId,
panelGroupItemLayoutId,
width,
repeatItemMeta,
groupRepeatVariable,
panelOptions,
isEditMode,
}: GridItemRendererProps): ReactElement {
const viewPanelItemId = useViewPanelGroup();

const panelRepeatVariable = repeatItemMeta?.itemRepeatVariable;
const panelVariableValues = repeatItemMeta?.values;
const effectiveValues = viewPanelItemId?.repeatVariable?.panel
? [viewPanelItemId.repeatVariable.panel[1]]
: panelVariableValues;

const panelGroupItemId: PanelGroupItemId = {
panelGroupId,
panelGroupItemLayoutId,
repeatVariable: { group: groupRepeatVariable },
};

return (
<ErrorBoundary FallbackComponent={ErrorAlert}>
{panelRepeatVariable && effectiveValues?.length ? (
<RepeatGridItemContent
panelGroupId={panelGroupId}
panelGroupItemLayoutId={panelGroupItemLayoutId}
panelRepeatVariable={{
name: panelRepeatVariable.value,
values: effectiveValues,
maxPer: panelRepeatVariable.alignment === 'vertical' ? 1 : panelRepeatVariable.maxPer,
}}
groupRepeatVariable={groupRepeatVariable}
width={width}
itemGap={DEFAULT_MARGIN}
panelOptions={panelOptions}
isEditMode={isEditMode}
/>
) : (
<GridItemContent panelOptions={panelOptions} panelGroupItemId={panelGroupItemId} width={width} />
)}
</ErrorBoundary>
);
}
11 changes: 10 additions & 1 deletion dashboards/src/components/GridLayout/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,16 @@ export function RepeatGridLayout({
{variable.value.map((value) => (
<VariableContext.Provider
key={`${repeatVariableName}-${value}`}
value={{ state: { ...variables, [repeatVariableName]: { value, loading: false } } }}
value={{
state: {
...variables,
[repeatVariableName]: {
...variables[repeatVariableName],
value: value,
loading: false,
},
},
}}
>
<Row
panelGroupId={panelGroupId}
Expand Down
120 changes: 120 additions & 0 deletions dashboards/src/components/GridLayout/RepeatGridItemContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 { ReactElement, useMemo } from 'react';
import { useVariableValues, VariableContext } from '@perses-dev/plugin-system';
import { GridItemContent, PanelOptions } from '@perses-dev/dashboards';
import { PanelGroupId } from '@perses-dev/spec';
import { Box } from '@mui/material';

interface RepeatPanelItemProps {
panelGroupId: PanelGroupId;
panelGroupItemLayoutId: string;
panelRepeatVariable: {
name: string;
values: string[];
maxPer?: number;
};
groupRepeatVariable?: [string, string];
width: number;
itemGap: number;
panelOptions?: PanelOptions;
isEditMode: boolean;
}

/**
* Renders a grid item that repeats based on a variable.
* It calculates the number of items per row and the width of each item,
* then renders the appropriate number of GridItemContent components with the correct variable context.
*/
export function RepeatGridItemContent({
panelGroupId,
panelGroupItemLayoutId,
panelRepeatVariable,
groupRepeatVariable,
width,
itemGap,
panelOptions,
isEditMode,
}: RepeatPanelItemProps): ReactElement {
const { name: repeatVariableName, values: variableValues, maxPer } = panelRepeatVariable;
const variables = useVariableValues();
const perRow = useMemo(() => {
const maxPerRow = maxPer ?? variableValues.length;
if (variableValues.length < maxPerRow) {
return variableValues.length;
}
return maxPerRow;
}, [maxPer, variableValues.length]);
const rows: string[][] = useMemo(() => {
const result: string[][] = [];
for (let i = 0; i < variableValues.length; i += perRow) {
result.push(variableValues.slice(i, i + perRow));
}
return result;
}, [variableValues, perRow]);
const perPanelWidth = Math.floor((width - itemGap * (perRow - 1)) / perRow);

return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
gap: `${itemGap}px`,
overflow: 'hidden',
}}
>
{rows.map((rowValues, rowIndex) => (
<Box key={rowIndex} sx={{ display: 'flex', flex: 1, gap: `${itemGap}px`, overflow: 'hidden' }}>
{rowValues.map((value, index) => {
const isNotFirst = index + rowIndex !== 0;
return (
<VariableContext.Provider
key={`${repeatVariableName}-${value}`}
value={{
state: {
...variables,
[repeatVariableName]: { ...variables[repeatVariableName], value, loading: false },
},
}}
>
<Box sx={{ width: perPanelWidth, overflow: 'hidden' }}>
<GridItemContent
panelOptions={panelOptions}
panelGroupItemId={{
panelGroupId,
panelGroupItemLayoutId,
repeatVariable: {
panel: [repeatVariableName, value],
group: groupRepeatVariable,
},
}}
width={perPanelWidth}
readonly={isNotFirst}
informationTooltip={
isNotFirst && isEditMode
? `This panel is generated from the variable "${repeatVariableName}" with the value "${value}". To change panel definition, please edit the first panel.`
: undefined
}
/>
</Box>
</VariableContext.Provider>
);
})}
</Box>
))}
</Box>
);
}
52 changes: 36 additions & 16 deletions dashboards/src/components/GridLayout/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import { PanelGroupId } from '@perses-dev/spec';
import { PanelOptions, useViewPanelGroup } from '@perses-dev/dashboards';
import { ReactElement, useEffect, useMemo, useState } from 'react';
import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
import { useVariableValues } from '@perses-dev/plugin-system';
import { GRID_LAYOUT_COLS, GRID_LAYOUT_SMALL_BREAKPOINT } from '../../constants';
import { PanelGroupDefinition, PanelGroupItemLayout } from '../../model';
import { buildRepeatMeta, restoreRepeatLayouts } from '../../utils';
import { GridContainer } from './GridContainer';
import { GridItemContent } from './GridItemContent';
import { GridItemRenderer } from './GridItemRenderer';
import { GridTitle } from './GridTitle';

const DEFAULT_MARGIN = 10;
Expand Down Expand Up @@ -57,14 +58,20 @@ export function Row({
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
const theme = useTheme();
const viewPanelItemId = useViewPanelGroup();
const variableValues = useVariableValues();

const [isOpen, setIsOpen] = useState(!groupDefinition.isCollapsed);

const { expandedItemLayouts, repeatMeta } = useMemo(
() => buildRepeatMeta(groupDefinition.itemLayouts, variableValues, repeatVariable),
[groupDefinition.itemLayouts, repeatVariable, variableValues]
);

const hasViewPanel =
viewPanelItemId?.panelGroupId === panelGroupId &&
// Check for repeatVariable panels
viewPanelItemId.repeatVariable?.[0] === repeatVariable?.[0] &&
viewPanelItemId.repeatVariable?.[1] === repeatVariable?.[1];
viewPanelItemId.repeatVariable?.group?.[0] === repeatVariable?.[0] &&
viewPanelItemId.repeatVariable?.group?.[1] === repeatVariable?.[1];
const itemLayoutViewed = viewPanelItemId?.panelGroupItemLayoutId;

// If there is a panel in view mode, we should hide the grid if the panel is not in the current group.
Expand All @@ -80,10 +87,11 @@ export function Row({
// Item layout is override if there is a panel in view mode
const itemLayouts: PanelGroupItemLayout[] = useMemo(() => {
if (itemLayoutViewed) {
return groupDefinition.itemLayouts.map((itemLayout) => {
return expandedItemLayouts.map((itemLayout) => {
if (itemLayout.i === itemLayoutViewed) {
const rowTitleHeight = 40 + 8; // 40 is the height of the row title and 8 is the margin height
return {
...itemLayout,
h: Math.round(((panelFullHeight ?? window.innerHeight) - rowTitleHeight) / (ROW_HEIGHT + DEFAULT_MARGIN)), // Viewed panel should take the full height remaining
i: itemLayoutViewed,
w: 48,
Expand All @@ -94,8 +102,18 @@ export function Row({
return itemLayout;
});
}
return groupDefinition.itemLayouts;
}, [groupDefinition.itemLayouts, itemLayoutViewed, panelFullHeight]);
return expandedItemLayouts;
}, [expandedItemLayouts, itemLayoutViewed, panelFullHeight]);

const handleLayoutChange = useMemo(() => {
if (!onLayoutChange) {
return undefined;
}
return (currentLayout: Layout[], allLayouts: Layouts): void => {
const restored = restoreRepeatLayouts(currentLayout, allLayouts, repeatMeta);
onLayoutChange(restored.currentLayout, restored.allLayouts);
};
}, [onLayoutChange, repeatMeta]);

return (
<GridContainer
Expand Down Expand Up @@ -129,8 +147,8 @@ export function Row({
margin={[DEFAULT_MARGIN, DEFAULT_MARGIN]}
containerPadding={[0, 10]}
layouts={{ sm: itemLayouts }}
onLayoutChange={onLayoutChange}
onWidthChange={onWidthChange}
onLayoutChange={handleLayoutChange}
onWidthChange={isGridDisplayed ? onWidthChange : undefined}
allowOverlap={hasViewPanel} // Enabling overlap when viewing a specific panel because panel in front of the viewed panel will add empty spaces (empty row height)
>
{itemLayouts.map(({ i, w }) => (
Expand All @@ -140,13 +158,15 @@ export function Row({
display: itemLayoutViewed ? (itemLayoutViewed === i ? 'unset' : 'none') : 'unset',
}}
>
<ErrorBoundary FallbackComponent={ErrorAlert}>
<GridItemContent
panelOptions={panelOptions}
panelGroupItemId={{ panelGroupId, panelGroupItemLayoutId: i, repeatVariable }}
width={calculateGridItemWidth(w, gridColWidth)}
/>
</ErrorBoundary>
<GridItemRenderer
panelGroupId={panelGroupId}
panelGroupItemLayoutId={i}
width={calculateGridItemWidth(w, gridColWidth)}
repeatItemMeta={repeatMeta.get(i)}
groupRepeatVariable={repeatVariable}
panelOptions={panelOptions}
isEditMode={isEditMode}
/>
</div>
))}
</ResponsiveGridLayout>
Expand Down
1 change: 1 addition & 0 deletions dashboards/src/components/GridLayout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './GridItemContent';
export * from './GridLayout';
export * from './GridTitle';
export * from './Row';
export * from './RepeatGridItemContent';
Loading
Loading