Dashboards: Show repeated row with Dashboard ds warning (#73787)

This commit is contained in:
Juan Cabanas 2023-08-28 10:00:11 -03:00 committed by GitHub
parent 5bd58cac57
commit 36d7cc9384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 115 additions and 8 deletions

View File

@ -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',

View File

@ -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();
});
}); });

View File

@ -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" />

View File

@ -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" />

View File

@ -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();
});
});

View File

@ -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

View File

@ -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>
); );
}; };