Skip to content

Commit 74aa84c

Browse files
committed
[FEATURE] Add panel-level repeat variable support
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
1 parent 9bd2cb0 commit 74aa84c

36 files changed

Lines changed: 1429 additions & 74 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { ReactNode } from 'react';
15+
import { Box, SxProps, Theme } from '@mui/material';
16+
17+
export interface RepeatGridProps<T> {
18+
rows: T[][];
19+
gap: number;
20+
renderItem: (item: T, rowIndex: number, colIndex: number) => ReactNode;
21+
containerSx?: SxProps<Theme>;
22+
rowSx?: SxProps<Theme>;
23+
}
24+
25+
export function RepeatGrid<T>({ rows, gap, renderItem, containerSx, rowSx }: RepeatGridProps<T>): ReactNode {
26+
return (
27+
<Box
28+
sx={[
29+
{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', gap: `${gap}px` },
30+
...(Array.isArray(containerSx) ? containerSx : [containerSx]),
31+
]}
32+
>
33+
{rows.map((rowItems, rowIndex) => (
34+
<Box key={rowIndex} sx={[{ display: 'flex', gap: `${gap}px` }, ...(Array.isArray(rowSx) ? rowSx : [rowSx])]}>
35+
{rowItems.map((item, colIndex) => renderItem(item, rowIndex, colIndex))}
36+
</Box>
37+
))}
38+
</Box>
39+
);
40+
}

components/src/RepeatGrid/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
export * from './RepeatGrid';

components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ export * from './test-utils';
4848
export * from './theme';
4949
export * from './TransformsEditor';
5050
export * from './RefreshIntervalPicker';
51+
export * from './RepeatGrid';
5152
export * from './ValueMappingEditor';

dashboards/src/components/GridLayout/GridItemContent.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,25 @@ export interface GridItemContentProps {
2525
panelGroupItemId: PanelGroupItemId;
2626
width: number; // necessary for determining the suggested step ms
2727
panelOptions?: PanelOptions;
28+
readonly?: boolean;
29+
informationTooltip?: string;
2830
}
2931

3032
/**
3133
* Resolves the reference to panel content in a GridItemDefinition and renders the panel.
3234
*/
3335
export function GridItemContent(props: GridItemContentProps): ReactElement {
34-
const { panelGroupItemId, width } = props;
36+
const { readonly, panelGroupItemId, width, informationTooltip } = props;
3537
const panelDefinition = usePanel(panelGroupItemId);
3638

3739
const {
3840
spec: { queries },
3941
} = panelDefinition;
4042

4143
const { isEditMode } = useEditMode();
44+
const canModify = useMemo(() => {
45+
return isEditMode && !readonly;
46+
}, [isEditMode, readonly]);
4247
const { openEditPanel, openDeletePanelDialog, duplicatePanel, viewPanel } = usePanelActions(panelGroupItemId);
4348
const viewPanelGroupItemId = useViewPanelGroup();
4449

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

6671
const viewQueriesHandler = useMemo(() => {
67-
return isEditMode || !queries?.length
72+
return canModify || !queries?.length
6873
? undefined
6974
: {
7075
onClick: (): void => {
7176
setOpenQueryViewer(true);
7277
},
7378
};
74-
}, [isEditMode, queries]);
79+
}, [canModify, queries]);
7580

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

8792
// Provide actions to the panel when in edit mode
8893
let editHandlers: PanelProps['editHandlers'] = undefined;
89-
if (isEditMode) {
94+
if (canModify && !readonly) {
9095
editHandlers = {
9196
onEditPanelClick: openEditPanel,
9297
onDuplicatePanelClick: duplicatePanel,
@@ -129,6 +134,7 @@ export function GridItemContent(props: GridItemContentProps): ReactElement {
129134
viewQueriesHandler={viewQueriesHandler}
130135
panelOptions={props.panelOptions}
131136
panelGroupItemId={panelGroupItemId}
137+
informationTooltip={informationTooltip}
132138
/>
133139
)}
134140
</DataQueriesProvider>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { PanelGroupId } from '@perses-dev/spec';
15+
import { PanelOptions, useViewPanelGroup } from '@perses-dev/dashboards';
16+
import { ReactElement } from 'react';
17+
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
18+
import { PanelGroupItemId } from '../../model';
19+
import { RepeatItemMeta } from '../../utils';
20+
import { GridItemContent } from './GridItemContent';
21+
import { RepeatGridItemContent } from './RepeatGridItemContent';
22+
23+
const DEFAULT_MARGIN = 10;
24+
25+
interface GridItemRendererProps {
26+
panelGroupId: PanelGroupId;
27+
panelGroupItemLayoutId: string;
28+
width: number;
29+
repeatItemMeta?: RepeatItemMeta;
30+
groupRepeatVariable?: [string, string];
31+
panelOptions?: PanelOptions;
32+
isEditMode: boolean;
33+
}
34+
35+
export function GridItemRenderer({
36+
panelGroupId,
37+
panelGroupItemLayoutId,
38+
width,
39+
repeatItemMeta,
40+
groupRepeatVariable,
41+
panelOptions,
42+
isEditMode,
43+
}: GridItemRendererProps): ReactElement {
44+
const viewPanelItemId = useViewPanelGroup();
45+
46+
const panelRepeatVariable = repeatItemMeta?.itemRepeatVariable;
47+
const panelVariableValues = repeatItemMeta?.values;
48+
const effectiveValues = viewPanelItemId?.repeatVariable?.panel
49+
? [viewPanelItemId.repeatVariable.panel[1]]
50+
: panelVariableValues;
51+
52+
const panelGroupItemId: PanelGroupItemId = {
53+
panelGroupId,
54+
panelGroupItemLayoutId,
55+
repeatVariable: { group: groupRepeatVariable },
56+
};
57+
58+
return (
59+
<ErrorBoundary FallbackComponent={ErrorAlert}>
60+
{panelRepeatVariable && effectiveValues?.length ? (
61+
<RepeatGridItemContent
62+
panelGroupId={panelGroupId}
63+
panelGroupItemLayoutId={panelGroupItemLayoutId}
64+
panelRepeatVariable={{
65+
name: panelRepeatVariable.value,
66+
values: effectiveValues,
67+
maxPer: panelRepeatVariable.alignment === 'vertical' ? 1 : panelRepeatVariable.maxPer,
68+
}}
69+
groupRepeatVariable={groupRepeatVariable}
70+
width={width}
71+
itemGap={DEFAULT_MARGIN}
72+
panelOptions={panelOptions}
73+
isEditMode={isEditMode}
74+
/>
75+
) : (
76+
<GridItemContent panelOptions={panelOptions} panelGroupItemId={panelGroupItemId} width={width} />
77+
)}
78+
</ErrorBoundary>
79+
);
80+
}

dashboards/src/components/GridLayout/GridLayout.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,16 @@ export function RepeatGridLayout({
140140
{variable.value.map((value) => (
141141
<VariableContext.Provider
142142
key={`${repeatVariableName}-${value}`}
143-
value={{ state: { ...variables, [repeatVariableName]: { value, loading: false } } }}
143+
value={{
144+
state: {
145+
...variables,
146+
[repeatVariableName]: {
147+
...variables[repeatVariableName],
148+
value: value,
149+
loading: false,
150+
},
151+
},
152+
}}
144153
>
145154
<Row
146155
panelGroupId={panelGroupId}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { ReactNode, useMemo } from 'react';
15+
import { useVariableValues, VariableContext } from '@perses-dev/plugin-system';
16+
import { GridItemContent, PanelOptions } from '@perses-dev/dashboards';
17+
import { PanelGroupId } from '@perses-dev/spec';
18+
import { Box } from '@mui/material';
19+
import { RepeatGrid } from '@perses-dev/components';
20+
21+
interface RepeatPanelItemProps {
22+
panelGroupId: PanelGroupId;
23+
panelGroupItemLayoutId: string;
24+
panelRepeatVariable: {
25+
name: string;
26+
values: string[];
27+
maxPer?: number;
28+
};
29+
groupRepeatVariable?: [string, string];
30+
width: number;
31+
itemGap: number;
32+
panelOptions?: PanelOptions;
33+
isEditMode: boolean;
34+
}
35+
36+
/**
37+
* Renders a grid item that repeats based on a variable.
38+
* It calculates the number of items per row and the width of each item,
39+
* then renders the appropriate number of GridItemContent components with the correct variable context.
40+
*/
41+
export function RepeatGridItemContent({
42+
panelGroupId,
43+
panelGroupItemLayoutId,
44+
panelRepeatVariable,
45+
groupRepeatVariable,
46+
width,
47+
itemGap,
48+
panelOptions,
49+
isEditMode,
50+
}: RepeatPanelItemProps): ReactNode {
51+
const { name: repeatVariableName, values: variableValues, maxPer } = panelRepeatVariable;
52+
const variables = useVariableValues();
53+
const perRow = useMemo(() => {
54+
const maxPerRow = maxPer ?? variableValues.length;
55+
if (variableValues.length < maxPerRow) {
56+
return variableValues.length;
57+
}
58+
return maxPerRow;
59+
}, [maxPer, variableValues.length]);
60+
const rows: string[][] = useMemo(() => {
61+
const result: string[][] = [];
62+
for (let i = 0; i < variableValues.length; i += perRow) {
63+
result.push(variableValues.slice(i, i + perRow));
64+
}
65+
return result;
66+
}, [variableValues, perRow]);
67+
const perPanelWidth = Math.floor((width - itemGap * (perRow - 1)) / perRow);
68+
69+
return (
70+
<RepeatGrid
71+
rows={rows}
72+
gap={itemGap}
73+
containerSx={{ overflow: 'hidden' }}
74+
rowSx={{ flex: 1, overflow: 'hidden' }}
75+
renderItem={(value, rowIndex, colIndex) => {
76+
const isNotFirst = colIndex + rowIndex !== 0;
77+
return (
78+
<VariableContext.Provider
79+
key={`${repeatVariableName}-${value}`}
80+
value={{
81+
state: {
82+
...variables,
83+
[repeatVariableName]: { ...variables[repeatVariableName], value, loading: false },
84+
},
85+
}}
86+
>
87+
<Box sx={{ width: perPanelWidth, overflow: 'hidden' }}>
88+
<GridItemContent
89+
panelOptions={panelOptions}
90+
panelGroupItemId={{
91+
panelGroupId,
92+
panelGroupItemLayoutId,
93+
repeatVariable: {
94+
panel: [repeatVariableName, value],
95+
group: groupRepeatVariable,
96+
},
97+
}}
98+
width={perPanelWidth}
99+
readonly={isNotFirst}
100+
informationTooltip={
101+
isNotFirst && isEditMode
102+
? `This panel is generated from the variable "${repeatVariableName}" with the value "${value}". To change panel definition, please edit the first panel.`
103+
: undefined
104+
}
105+
/>
106+
</Box>
107+
</VariableContext.Provider>
108+
);
109+
}}
110+
/>
111+
);
112+
}

0 commit comments

Comments
 (0)