mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Show repeated row with Dashboard ds warning (#73787)
This commit is contained in:
parent
5bd58cac57
commit
36d7cc9384
@ -180,6 +180,13 @@ export const Pages = {
|
|||||||
Annotations: {
|
Annotations: {
|
||||||
marker: 'data-testid annotation-marker',
|
marker: 'data-testid annotation-marker',
|
||||||
},
|
},
|
||||||
|
Rows: {
|
||||||
|
Repeated: {
|
||||||
|
ConfigSection: {
|
||||||
|
warningMessage: 'data-testid Repeated rows warning message',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Dashboards: {
|
Dashboards: {
|
||||||
url: '/dashboards',
|
url: '/dashboards',
|
||||||
|
@ -2,6 +2,8 @@ import { screen, render } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/types';
|
||||||
|
|
||||||
import { PanelModel } from '../../state/PanelModel';
|
import { PanelModel } from '../../state/PanelModel';
|
||||||
|
|
||||||
import { DashboardRow } from './DashboardRow';
|
import { DashboardRow } from './DashboardRow';
|
||||||
@ -17,6 +19,7 @@ describe('DashboardRow', () => {
|
|||||||
canEdit: true,
|
canEdit: true,
|
||||||
},
|
},
|
||||||
events: { subscribe: jest.fn() },
|
events: { subscribe: jest.fn() },
|
||||||
|
getRowPanels: () => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
@ -67,4 +70,28 @@ describe('DashboardRow', () => {
|
|||||||
expect(screen.queryByRole('button', { name: 'Delete row' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Delete row' })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: 'Row options' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Row options' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should return warning message when row panel has a panel with dashboard ds set', async () => {
|
||||||
|
const panel = new PanelModel({
|
||||||
|
datasource: {
|
||||||
|
type: 'datasource',
|
||||||
|
uid: SHARED_DASHBOARD_QUERY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rowPanel = new PanelModel({ collapsed: true, panels: [panel] });
|
||||||
|
const dashboardRow = new DashboardRow({ panel: rowPanel, dashboard: dashboardMock });
|
||||||
|
expect(dashboardRow.getWarning()).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not return warning message when row panel does not have a panel with dashboard ds set', async () => {
|
||||||
|
const panel = new PanelModel({
|
||||||
|
datasource: {
|
||||||
|
type: 'datasource',
|
||||||
|
uid: 'ds-uid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rowPanel = new PanelModel({ collapsed: true, panels: [panel] });
|
||||||
|
const dashboardRow = new DashboardRow({ panel: rowPanel, dashboard: dashboardMock });
|
||||||
|
expect(dashboardRow.getWarning()).not.toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { indexOf } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
|
import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/types';
|
||||||
|
|
||||||
import { ShowConfirmModalEvent } from '../../../../types/events';
|
import { ShowConfirmModalEvent } from '../../../../types/events';
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
@ -38,6 +40,23 @@ export class DashboardRow extends React.Component<DashboardRowProps> {
|
|||||||
this.props.dashboard.toggleRow(this.props.panel);
|
this.props.dashboard.toggleRow(this.props.panel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getWarning = () => {
|
||||||
|
const panels = !!this.props.panel.panels?.length
|
||||||
|
? this.props.panel.panels
|
||||||
|
: this.props.dashboard.getRowPanels(indexOf(this.props.dashboard.panels, this.props.panel));
|
||||||
|
const isAnyPanelUsingDashboardDS = panels.some((p) => p.datasource?.uid === SHARED_DASHBOARD_QUERY);
|
||||||
|
if (isAnyPanelUsingDashboardDS) {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
onUpdate = (title: string, repeat?: string | null) => {
|
onUpdate = (title: string, repeat?: string | null) => {
|
||||||
this.props.panel.setProperty('title', title);
|
this.props.panel.setProperty('title', title);
|
||||||
this.props.panel.setProperty('repeat', repeat ?? undefined);
|
this.props.panel.setProperty('repeat', repeat ?? undefined);
|
||||||
@ -94,6 +113,7 @@ export class DashboardRow extends React.Component<DashboardRowProps> {
|
|||||||
title={this.props.panel.title}
|
title={this.props.panel.title}
|
||||||
repeat={this.props.panel.repeat}
|
repeat={this.props.panel.repeat}
|
||||||
onUpdate={this.onUpdate}
|
onUpdate={this.onUpdate}
|
||||||
|
warning={this.getWarning()}
|
||||||
/>
|
/>
|
||||||
<button type="button" className="pointer" onClick={this.onDelete} aria-label="Delete row">
|
<button type="button" className="pointer" onClick={this.onDelete} aria-label="Delete row">
|
||||||
<Icon name="trash-alt" />
|
<Icon name="trash-alt" />
|
||||||
|
@ -9,9 +9,10 @@ export interface RowOptionsButtonProps {
|
|||||||
title: string;
|
title: string;
|
||||||
repeat?: string | null;
|
repeat?: string | null;
|
||||||
onUpdate: OnRowOptionsUpdate;
|
onUpdate: OnRowOptionsUpdate;
|
||||||
|
warning?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RowOptionsButton = ({ repeat, title, onUpdate }: RowOptionsButtonProps) => {
|
export const RowOptionsButton = ({ repeat, title, onUpdate, warning }: RowOptionsButtonProps) => {
|
||||||
const onUpdateChange = (hideModal: () => void) => (title: string, repeat?: string | null) => {
|
const onUpdateChange = (hideModal: () => void) => (title: string, repeat?: string | null) => {
|
||||||
onUpdate(title, repeat);
|
onUpdate(title, repeat);
|
||||||
hideModal();
|
hideModal();
|
||||||
@ -26,7 +27,13 @@ export const RowOptionsButton = ({ repeat, title, onUpdate }: RowOptionsButtonPr
|
|||||||
className="pointer"
|
className="pointer"
|
||||||
aria-label="Row options"
|
aria-label="Row options"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showModal(RowOptionsModal, { title, repeat, onDismiss: hideModal, onUpdate: onUpdateChange(hideModal) });
|
showModal(RowOptionsModal, {
|
||||||
|
title,
|
||||||
|
repeat,
|
||||||
|
onDismiss: hideModal,
|
||||||
|
onUpdate: onUpdateChange(hideModal),
|
||||||
|
warning,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cog" />
|
<Icon name="cog" />
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
|
import { RowOptionsForm } from './RowOptionsForm';
|
||||||
|
|
||||||
|
jest.mock('../RepeatRowSelect/RepeatRowSelect', () => ({
|
||||||
|
RepeatRowSelect: () => <div />,
|
||||||
|
}));
|
||||||
|
describe('DashboardRow', () => {
|
||||||
|
it('Should show warning component when has warningMessage prop', () => {
|
||||||
|
render(
|
||||||
|
<TestProvider>
|
||||||
|
<RowOptionsForm repeat={'3'} title="" onCancel={jest.fn()} onUpdate={jest.fn()} warning="a warning message" />
|
||||||
|
</TestProvider>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not show warning component when does not have warningMessage prop', () => {
|
||||||
|
render(
|
||||||
|
<TestProvider>
|
||||||
|
<RowOptionsForm repeat={'3'} title="" onCancel={jest.fn()} onUpdate={jest.fn()} />
|
||||||
|
</TestProvider>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage)
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { Button, Field, Form, Modal, Input } from '@grafana/ui';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { Button, Field, Form, Modal, Input, Alert } from '@grafana/ui';
|
||||||
|
|
||||||
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
|
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
|
||||||
|
|
||||||
@ -11,9 +12,10 @@ export interface Props {
|
|||||||
repeat?: string | null;
|
repeat?: string | null;
|
||||||
onUpdate: OnRowOptionsUpdate;
|
onUpdate: OnRowOptionsUpdate;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
warning?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RowOptionsForm = ({ repeat, title, onUpdate, onCancel }: Props) => {
|
export const RowOptionsForm = ({ repeat, title, warning, onUpdate, onCancel }: Props) => {
|
||||||
const [newRepeat, setNewRepeat] = useState<string | null | undefined>(repeat);
|
const [newRepeat, setNewRepeat] = useState<string | null | undefined>(repeat);
|
||||||
const onChangeRepeat = useCallback((name?: string | null) => setNewRepeat(name), [setNewRepeat]);
|
const onChangeRepeat = useCallback((name?: string | null) => setNewRepeat(name), [setNewRepeat]);
|
||||||
|
|
||||||
@ -29,11 +31,20 @@ export const RowOptionsForm = ({ repeat, title, onUpdate, onCancel }: Props) =>
|
|||||||
<Field label="Title">
|
<Field label="Title">
|
||||||
<Input {...register('title')} type="text" />
|
<Input {...register('title')} type="text" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Repeat for">
|
<Field label="Repeat for">
|
||||||
<RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} />
|
<RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} />
|
||||||
</Field>
|
</Field>
|
||||||
|
{warning && (
|
||||||
|
<Alert
|
||||||
|
data-testid={selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage}
|
||||||
|
severity="warning"
|
||||||
|
title=""
|
||||||
|
topSpacing={3}
|
||||||
|
bottomSpacing={0}
|
||||||
|
>
|
||||||
|
{warning}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
|
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
|
||||||
Cancel
|
Cancel
|
||||||
|
@ -8,15 +8,16 @@ import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm';
|
|||||||
export interface RowOptionsModalProps {
|
export interface RowOptionsModalProps {
|
||||||
title: string;
|
title: string;
|
||||||
repeat?: string | null;
|
repeat?: string | null;
|
||||||
|
warning?: React.ReactNode;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
onUpdate: OnRowOptionsUpdate;
|
onUpdate: OnRowOptionsUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RowOptionsModal = ({ repeat, title, onDismiss, onUpdate }: RowOptionsModalProps) => {
|
export const RowOptionsModal = ({ repeat, title, onDismiss, onUpdate, warning }: RowOptionsModalProps) => {
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={true} title="Row options" icon="copy" onDismiss={onDismiss} className={styles.modal}>
|
<Modal isOpen={true} title="Row options" icon="copy" onDismiss={onDismiss} className={styles.modal}>
|
||||||
<RowOptionsForm repeat={repeat} title={title} onCancel={onDismiss} onUpdate={onUpdate} />
|
<RowOptionsForm repeat={repeat} title={title} onCancel={onDismiss} onUpdate={onUpdate} warning={warning} />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user