2024-12-10 14:21:30 +08:00
|
|
|
import { css, cx } from '@emotion/css';
|
2025-02-03 17:46:47 +08:00
|
|
|
import { ReactNode, useMemo, useRef } from 'react';
|
2024-12-10 14:21:30 +08:00
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
|
|
import { selectors } from '@grafana/e2e-selectors';
|
2025-02-03 17:46:47 +08:00
|
|
|
import {
|
|
|
|
SceneObjectState,
|
|
|
|
SceneObjectBase,
|
|
|
|
SceneComponentProps,
|
|
|
|
sceneGraph,
|
|
|
|
VariableDependencyConfig,
|
2025-02-03 23:21:38 +08:00
|
|
|
SceneObject,
|
2025-02-03 17:46:47 +08:00
|
|
|
} from '@grafana/scenes';
|
|
|
|
import {
|
|
|
|
Alert,
|
|
|
|
Button,
|
|
|
|
Icon,
|
|
|
|
Input,
|
|
|
|
RadioButtonGroup,
|
|
|
|
Switch,
|
|
|
|
TextLink,
|
|
|
|
useElementSelection,
|
|
|
|
useStyles2,
|
|
|
|
} from '@grafana/ui';
|
|
|
|
import { Trans } from 'app/core/internationalization';
|
2024-12-10 14:21:30 +08:00
|
|
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
|
|
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
2025-02-03 17:46:47 +08:00
|
|
|
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
|
|
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
|
|
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
2024-12-10 14:21:30 +08:00
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
import { isClonedKey } from '../../utils/clone';
|
|
|
|
import { getDashboardSceneFor, getDefaultVizPanel, getQueryRunnerFor } from '../../utils/utils';
|
|
|
|
import { DashboardScene } from '../DashboardScene';
|
2024-12-10 14:21:30 +08:00
|
|
|
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
2025-02-03 23:21:38 +08:00
|
|
|
import { BulkActionElement, DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types';
|
2024-12-10 14:21:30 +08:00
|
|
|
|
2025-02-03 23:21:38 +08:00
|
|
|
import { MultiSelectedRowItemsElement } from './MultiSelectedRowItemsElement';
|
2025-02-03 17:46:47 +08:00
|
|
|
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
|
2024-12-10 14:21:30 +08:00
|
|
|
import { RowsLayoutManager } from './RowsLayoutManager';
|
|
|
|
|
|
|
|
export interface RowItemState extends SceneObjectState {
|
|
|
|
layout: DashboardLayoutManager;
|
|
|
|
title?: string;
|
|
|
|
isCollapsed?: boolean;
|
|
|
|
isHeaderHidden?: boolean;
|
|
|
|
height?: 'expand' | 'min';
|
|
|
|
}
|
|
|
|
|
2025-02-03 23:21:38 +08:00
|
|
|
export class RowItem
|
|
|
|
extends SceneObjectBase<RowItemState>
|
|
|
|
implements LayoutParent, BulkActionElement, EditableDashboardElement
|
|
|
|
{
|
2025-02-03 17:46:47 +08:00
|
|
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
|
|
|
statePaths: ['title'],
|
|
|
|
});
|
|
|
|
|
2024-12-10 14:21:30 +08:00
|
|
|
public isEditableDashboardElement: true = true;
|
|
|
|
|
|
|
|
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
|
|
|
const row = this;
|
|
|
|
|
|
|
|
const rowOptions = useMemo(() => {
|
|
|
|
return new OptionsPaneCategoryDescriptor({
|
|
|
|
title: 'Row options',
|
|
|
|
id: 'row-options',
|
|
|
|
isOpenDefault: true,
|
|
|
|
})
|
|
|
|
.addItem(
|
|
|
|
new OptionsPaneItemDescriptor({
|
|
|
|
title: 'Title',
|
|
|
|
render: () => <RowTitleInput row={row} />,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.addItem(
|
|
|
|
new OptionsPaneItemDescriptor({
|
|
|
|
title: 'Height',
|
|
|
|
render: () => <RowHeightSelect row={row} />,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.addItem(
|
|
|
|
new OptionsPaneItemDescriptor({
|
|
|
|
title: 'Hide row header',
|
|
|
|
render: () => <RowHeaderSwitch row={row} />,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}, [row]);
|
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
const rowRepeatOptions = useMemo(() => {
|
|
|
|
const dashboard = getDashboardSceneFor(row);
|
|
|
|
|
|
|
|
return new OptionsPaneCategoryDescriptor({
|
|
|
|
title: 'Repeat options',
|
|
|
|
id: 'row-repeat-options',
|
|
|
|
isOpenDefault: true,
|
|
|
|
}).addItem(
|
|
|
|
new OptionsPaneItemDescriptor({
|
|
|
|
title: 'Variable',
|
|
|
|
render: () => <RowRepeatSelect row={row} dashboard={dashboard} />,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}, [row]);
|
|
|
|
|
2024-12-10 14:21:30 +08:00
|
|
|
const { layout } = this.useState();
|
|
|
|
const layoutOptions = useLayoutCategory(layout);
|
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
return [rowOptions, rowRepeatOptions, layoutOptions];
|
2024-12-10 14:21:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
public getTypeName(): string {
|
|
|
|
return 'Row';
|
|
|
|
}
|
|
|
|
|
2025-02-03 23:21:38 +08:00
|
|
|
public createMultiSelectedElement(items: SceneObject[]) {
|
|
|
|
return new MultiSelectedRowItemsElement(items);
|
|
|
|
}
|
|
|
|
|
2024-12-10 14:21:30 +08:00
|
|
|
public onDelete = () => {
|
|
|
|
const layout = sceneGraph.getAncestor(this, RowsLayoutManager);
|
|
|
|
layout.removeRow(this);
|
|
|
|
};
|
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
public renderActions(): ReactNode {
|
2024-12-10 14:21:30 +08:00
|
|
|
return (
|
|
|
|
<>
|
2025-01-13 19:15:16 +08:00
|
|
|
<Button size="sm" variant="secondary" icon="copy" />
|
|
|
|
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete} icon="trash-alt" />
|
2024-12-10 14:21:30 +08:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public getLayout(): DashboardLayoutManager {
|
|
|
|
return this.state.layout;
|
|
|
|
}
|
|
|
|
|
|
|
|
public switchLayout(layout: DashboardLayoutManager): void {
|
|
|
|
this.setState({ layout });
|
|
|
|
}
|
|
|
|
|
|
|
|
public onCollapseToggle = () => {
|
|
|
|
this.setState({ isCollapsed: !this.state.isCollapsed });
|
|
|
|
};
|
|
|
|
|
2025-01-13 19:15:16 +08:00
|
|
|
public onAddPanel = (vizPanel = getDefaultVizPanel()) => {
|
2025-02-03 17:46:47 +08:00
|
|
|
this.getLayout().addPanel(vizPanel);
|
2024-12-10 14:21:30 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
public static Component = ({ model }: SceneComponentProps<RowItem>) => {
|
2025-01-22 21:57:45 +08:00
|
|
|
const { layout, title, isCollapsed, height = 'expand', isHeaderHidden, key } = model.useState();
|
2025-02-03 17:46:47 +08:00
|
|
|
const isClone = useMemo(() => isClonedKey(key!), [key]);
|
|
|
|
const dashboard = getDashboardSceneFor(model);
|
|
|
|
const { isEditing, showHiddenElements } = dashboard.useState();
|
2024-12-10 14:21:30 +08:00
|
|
|
const styles = useStyles2(getStyles);
|
|
|
|
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text');
|
|
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const shouldGrow = !isCollapsed && height === 'expand';
|
2025-01-22 21:57:45 +08:00
|
|
|
const { isSelected, onSelect } = useElementSelection(key);
|
2024-12-10 14:21:30 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2025-01-13 19:15:16 +08:00
|
|
|
className={cx(
|
|
|
|
styles.wrapper,
|
|
|
|
isCollapsed && styles.wrapperCollapsed,
|
|
|
|
shouldGrow && styles.wrapperGrow,
|
2025-02-03 17:46:47 +08:00
|
|
|
!isClone && isSelected && 'dashboard-selected-element'
|
2025-01-13 19:15:16 +08:00
|
|
|
)}
|
2024-12-10 14:21:30 +08:00
|
|
|
ref={ref}
|
|
|
|
>
|
2025-01-17 21:40:18 +08:00
|
|
|
{(!isHeaderHidden || (isEditing && showHiddenElements)) && (
|
|
|
|
<div className={styles.rowHeader}>
|
|
|
|
<button
|
|
|
|
onClick={model.onCollapseToggle}
|
|
|
|
className={styles.rowTitleButton}
|
|
|
|
aria-label={isCollapsed ? 'Expand row' : 'Collapse row'}
|
2025-02-03 17:46:47 +08:00
|
|
|
data-testid={selectors.components.DashboardRow.title(titleInterpolated!)}
|
2025-01-17 21:40:18 +08:00
|
|
|
>
|
|
|
|
<Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />
|
|
|
|
<span className={styles.rowTitle} role="heading">
|
|
|
|
{titleInterpolated}
|
|
|
|
</span>
|
|
|
|
</button>
|
2025-02-03 17:46:47 +08:00
|
|
|
{!isClone && isEditing && (
|
2025-01-17 21:40:18 +08:00
|
|
|
<Button icon="pen" variant="secondary" size="sm" fill="text" onPointerDown={(evt) => onSelect?.(evt)} />
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
2024-12-10 14:21:30 +08:00
|
|
|
{!isCollapsed && <layout.Component model={layout} />}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function getStyles(theme: GrafanaTheme2) {
|
|
|
|
return {
|
|
|
|
rowHeader: css({
|
|
|
|
width: '100%',
|
|
|
|
display: 'flex',
|
|
|
|
gap: theme.spacing(1),
|
|
|
|
padding: theme.spacing(0, 0, 0.5, 0),
|
|
|
|
margin: theme.spacing(0, 0, 1, 0),
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
|
|
'&:hover, &:focus-within': {
|
|
|
|
'& > div': {
|
|
|
|
opacity: 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
'& > div': {
|
|
|
|
marginBottom: 0,
|
|
|
|
marginRight: theme.spacing(1),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
rowTitleButton: css({
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'center',
|
|
|
|
cursor: 'pointer',
|
|
|
|
background: 'transparent',
|
|
|
|
border: 'none',
|
|
|
|
minWidth: 0,
|
|
|
|
gap: theme.spacing(1),
|
|
|
|
}),
|
|
|
|
rowTitle: css({
|
|
|
|
fontSize: theme.typography.h5.fontSize,
|
|
|
|
fontWeight: theme.typography.fontWeightMedium,
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
overflow: 'hidden',
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
maxWidth: '100%',
|
|
|
|
flexGrow: 1,
|
|
|
|
minWidth: 0,
|
|
|
|
}),
|
|
|
|
wrapper: css({
|
|
|
|
display: 'flex',
|
|
|
|
flexDirection: 'column',
|
|
|
|
width: '100%',
|
2025-02-03 17:46:47 +08:00
|
|
|
minHeight: '100px',
|
2024-12-10 14:21:30 +08:00
|
|
|
}),
|
|
|
|
wrapperGrow: css({
|
|
|
|
flexGrow: 1,
|
|
|
|
}),
|
|
|
|
wrapperCollapsed: css({
|
|
|
|
flexGrow: 0,
|
|
|
|
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
2025-02-03 17:46:47 +08:00
|
|
|
minHeight: 'unset',
|
2024-12-10 14:21:30 +08:00
|
|
|
}),
|
|
|
|
rowActions: css({
|
|
|
|
display: 'flex',
|
|
|
|
opacity: 0,
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function RowTitleInput({ row }: { row: RowItem }) {
|
|
|
|
const { title } = row.useState();
|
|
|
|
|
|
|
|
return <Input value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function RowHeaderSwitch({ row }: { row: RowItem }) {
|
2025-01-17 21:40:18 +08:00
|
|
|
const { isHeaderHidden = false } = row.useState();
|
2024-12-10 14:21:30 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Switch
|
|
|
|
value={isHeaderHidden}
|
|
|
|
onChange={() => {
|
|
|
|
row.setState({
|
|
|
|
isHeaderHidden: !row.state.isHeaderHidden,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function RowHeightSelect({ row }: { row: RowItem }) {
|
|
|
|
const { height = 'expand' } = row.useState();
|
|
|
|
|
|
|
|
const options = [
|
|
|
|
{ label: 'Expand', value: 'expand' as const },
|
|
|
|
{ label: 'Min', value: 'min' as const },
|
|
|
|
];
|
|
|
|
|
|
|
|
return (
|
|
|
|
<RadioButtonGroup
|
|
|
|
options={options}
|
|
|
|
value={height}
|
|
|
|
onChange={(option) =>
|
|
|
|
row.setState({
|
|
|
|
height: option,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2025-02-03 17:46:47 +08:00
|
|
|
|
|
|
|
export function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) {
|
|
|
|
const { layout, $behaviors } = row.useState();
|
|
|
|
|
|
|
|
let repeatBehavior: RowItemRepeaterBehavior | undefined = $behaviors?.find(
|
|
|
|
(b) => b instanceof RowItemRepeaterBehavior
|
|
|
|
);
|
|
|
|
const { variableName } = repeatBehavior?.state ?? {};
|
|
|
|
|
|
|
|
const isAnyPanelUsingDashboardDS = layout.getVizPanels().some((vizPanel) => {
|
|
|
|
const runner = getQueryRunnerFor(vizPanel);
|
|
|
|
return (
|
|
|
|
runner?.state.datasource?.uid === SHARED_DASHBOARD_QUERY ||
|
|
|
|
(runner?.state.datasource?.uid === MIXED_DATASOURCE_NAME &&
|
|
|
|
runner?.state.queries.some((query) => query.datasource?.uid === SHARED_DASHBOARD_QUERY))
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<RepeatRowSelect2
|
|
|
|
sceneContext={dashboard}
|
|
|
|
repeat={variableName}
|
|
|
|
onChange={(repeat) => {
|
|
|
|
if (repeat) {
|
|
|
|
// Remove repeat behavior if it exists to trigger repeat when adding new one
|
|
|
|
if (repeatBehavior) {
|
|
|
|
repeatBehavior.removeBehavior();
|
|
|
|
}
|
|
|
|
|
|
|
|
repeatBehavior = new RowItemRepeaterBehavior({ variableName: repeat });
|
|
|
|
row.setState({ $behaviors: [...(row.state.$behaviors ?? []), repeatBehavior] });
|
|
|
|
repeatBehavior.activate();
|
|
|
|
} else {
|
|
|
|
repeatBehavior?.removeBehavior();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{isAnyPanelUsingDashboardDS ? (
|
|
|
|
<Alert
|
|
|
|
data-testid={selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage}
|
|
|
|
severity="warning"
|
|
|
|
title=""
|
|
|
|
topSpacing={3}
|
|
|
|
bottomSpacing={0}
|
|
|
|
>
|
|
|
|
<p>
|
|
|
|
<Trans i18nKey="dashboard.rows-layout.row.repeat.warning">
|
|
|
|
Panels in this row use the {{ SHARED_DASHBOARD_QUERY }} data source. These panels will reference the panel
|
|
|
|
in the original row, not the ones in the repeated rows.
|
|
|
|
</Trans>
|
|
|
|
</p>
|
|
|
|
<TextLink
|
|
|
|
external
|
|
|
|
href={
|
|
|
|
'https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows'
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<Trans i18nKey="dashboard.rows-layout.row.repeat.learn-more">Learn more</Trans>
|
|
|
|
</TextLink>
|
|
|
|
</Alert>
|
|
|
|
) : undefined}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|