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
40 changes: 40 additions & 0 deletions components/src/RepeatGrid/RepeatGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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 { ReactNode } from 'react';
import { Box, SxProps, Theme } from '@mui/material';

export interface RepeatGridProps<T> {
rows: T[][];
gap: number;
renderItem: (item: T, rowIndex: number, colIndex: number) => ReactNode;
containerSx?: SxProps<Theme>;
rowSx?: SxProps<Theme>;
}

export function RepeatGrid<T>({ rows, gap, renderItem, containerSx, rowSx }: RepeatGridProps<T>): ReactNode {
return (
<Box
sx={[
{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', gap: `${gap}px` },
...(Array.isArray(containerSx) ? containerSx : [containerSx]),
]}
>
{rows.map((rowItems, rowIndex) => (
<Box key={rowIndex} sx={[{ display: 'flex', gap: `${gap}px` }, ...(Array.isArray(rowSx) ? rowSx : [rowSx])]}>
{rowItems.map((item, colIndex) => renderItem(item, rowIndex, colIndex))}
</Box>
))}
</Box>
);
}
14 changes: 14 additions & 0 deletions components/src/RepeatGrid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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.

export * from './RepeatGrid';
1 change: 1 addition & 0 deletions components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ export * from './test-utils';
export * from './theme';
export * from './TransformsEditor';
export * from './RefreshIntervalPicker';
export * from './RepeatGrid';
export * from './ValueMappingEditor';
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 @@ -129,6 +134,7 @@ export function GridItemContent(props: GridItemContentProps): ReactElement {
viewQueriesHandler={viewQueriesHandler}
panelOptions={props.panelOptions}
panelGroupItemId={panelGroupItemId}
informationTooltip={informationTooltip}
/>
)}
</DataQueriesProvider>
Expand Down
81 changes: 81 additions & 0 deletions dashboards/src/components/GridLayout/GridItemRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 { ReactElement } from 'react';
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
import { PanelOptions } from '../Panel/Panel';
import { useViewPanelGroup } from '../../context';
import { PanelGroupItemId } from '../../model';
import { getPerRowCount, 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: getPerRowCount(panelRepeatVariable),
}}
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 @@ -140,7 +140,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
107 changes: 107 additions & 0 deletions dashboards/src/components/GridLayout/RepeatGridItemContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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 { ReactNode, useMemo } from 'react';
import { useVariableValues, VariableContext } from '@perses-dev/plugin-system';
import { PanelGroupId } from '@perses-dev/spec';
import { Box } from '@mui/material';
import { RepeatGrid } from '@perses-dev/components';
import { PanelOptions } from '../Panel/Panel';
import { GridItemContent } from './GridItemContent';

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): ReactNode {
const { name: repeatVariableName, values: variableValues, maxPer: perRow } = panelRepeatVariable;
const variables = useVariableValues();

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 = useMemo(() => Math.floor((width - itemGap * (perRow - 1)) / perRow), [itemGap, perRow, width]);

return (
<RepeatGrid
rows={rows}
gap={itemGap}
containerSx={{ overflow: 'hidden' }}
rowSx={{ flex: 1, overflow: 'hidden' }}
renderItem={(value, rowIndex, colIndex) => {
const isNotFirst = colIndex + 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>
);
}}
/>
);
}
Loading