mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Annotations: Support filtering the target panels (#66325)
Co-authored-by: Adela Almasan <adela.almasan@grafana.com> Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
@@ -1,12 +1,31 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { AnnotationQuery, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
|
||||
import {
|
||||
AnnotationQuery,
|
||||
DataSourceInstanceSettings,
|
||||
getDataSourceRef,
|
||||
GrafanaTheme2,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { DataSourcePicker, getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||
import { Button, Checkbox, Field, FieldSet, HorizontalGroup, Input } from '@grafana/ui';
|
||||
import { AnnotationPanelFilter } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Field,
|
||||
FieldSet,
|
||||
HorizontalGroup,
|
||||
Input,
|
||||
MultiSelect,
|
||||
Select,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { ColorValueEditor } from 'app/core/components/OptionsUI/color';
|
||||
import config from 'app/core/config';
|
||||
import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor';
|
||||
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
@@ -21,8 +40,16 @@ type Props = {
|
||||
export const newAnnotationName = 'New annotation';
|
||||
|
||||
export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [annotation, setAnnotation] = useState(dashboard.annotations.list[editIdx]);
|
||||
|
||||
const panelFilter = useMemo(() => {
|
||||
if (!annotation.filter) {
|
||||
return PanelFilterType.AllPanels;
|
||||
}
|
||||
return annotation.filter.exclude ? PanelFilterType.ExcludePanels : PanelFilterType.IncludePanels;
|
||||
}, [annotation.filter]);
|
||||
|
||||
const { value: ds } = useAsync(() => {
|
||||
return getDataSourceSrv().get(annotation.datasource);
|
||||
}, [annotation.datasource]);
|
||||
@@ -65,6 +92,31 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onFilterTypeChange = (v: SelectableValue<PanelFilterType>) => {
|
||||
let filter =
|
||||
v.value === PanelFilterType.AllPanels
|
||||
? undefined
|
||||
: {
|
||||
exclude: v.value === PanelFilterType.ExcludePanels,
|
||||
ids: annotation.filter?.ids ?? [],
|
||||
};
|
||||
onUpdate({ ...annotation, filter });
|
||||
};
|
||||
|
||||
const onAddFilterPanelID = (selections: Array<SelectableValue<number>>) => {
|
||||
if (!Array.isArray(selections)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: AnnotationPanelFilter = {
|
||||
exclude: panelFilter === PanelFilterType.ExcludePanels,
|
||||
ids: [],
|
||||
};
|
||||
|
||||
selections.forEach((selection) => selection.value && filter.ids.push(selection.value));
|
||||
onUpdate({ ...annotation, filter });
|
||||
};
|
||||
|
||||
const onApply = goBackToList;
|
||||
|
||||
const onPreview = () => {
|
||||
@@ -79,9 +131,30 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
|
||||
const isNewAnnotation = annotation.name === newAnnotationName;
|
||||
|
||||
const sortFn = (a: SelectableValue<number>, b: SelectableValue<number>) => {
|
||||
if (a.label && b.label) {
|
||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const panels: Array<SelectableValue<number>> = useMemo(
|
||||
() =>
|
||||
dashboard?.panels
|
||||
.map((panel) => ({
|
||||
value: panel.id,
|
||||
label: panel.title ?? `Panel ${panel.id}`,
|
||||
description: panel.description,
|
||||
imgUrl: config.panels[panel.type].info.logos.small,
|
||||
}))
|
||||
.sort(sortFn) ?? [],
|
||||
[dashboard]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldSet>
|
||||
<FieldSet className={styles.settingsForm}>
|
||||
<Field label="Name">
|
||||
<Input
|
||||
aria-label={selectors.pages.Dashboard.Settings.Annotations.Settings.name}
|
||||
@@ -90,17 +163,10 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
autoFocus={isNewAnnotation}
|
||||
value={annotation.name}
|
||||
onChange={onNameChange}
|
||||
width={50}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Data source" htmlFor="data-source-picker">
|
||||
<DataSourcePicker
|
||||
width={50}
|
||||
annotations
|
||||
variables
|
||||
current={annotation.datasource}
|
||||
onChange={onDataSourceChange}
|
||||
/>
|
||||
<DataSourcePicker annotations variables current={annotation.datasource} onChange={onDataSourceChange} />
|
||||
</Field>
|
||||
<Field label="Enabled" description="When enabled the annotation query is issued every dashboard refresh">
|
||||
<Checkbox name="enable" id="enable" value={annotation.enable} onChange={onChange} />
|
||||
@@ -116,6 +182,31 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
<ColorValueEditor value={annotation?.iconColor} onChange={onColorChange} />
|
||||
</HorizontalGroup>
|
||||
</Field>
|
||||
<Field label="Show in" aria-label={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel}>
|
||||
<>
|
||||
<Select
|
||||
options={panelFilters}
|
||||
value={panelFilter}
|
||||
onChange={onFilterTypeChange}
|
||||
aria-label={selectors.components.Annotations.annotationsTypeInput}
|
||||
/>
|
||||
{panelFilter !== PanelFilterType.AllPanels && (
|
||||
<MultiSelect
|
||||
options={panels}
|
||||
value={panels.filter((panel) => annotation.filter?.ids.includes(panel.value!))}
|
||||
onChange={onAddFilterPanelID}
|
||||
isClearable={true}
|
||||
placeholder="Choose panels"
|
||||
width={100}
|
||||
closeMenuOnSelect={false}
|
||||
className={styles.select}
|
||||
aria-label={selectors.components.Annotations.annotationsChoosePanelInput}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<h3 className="page-heading">Query</h3>
|
||||
{ds?.annotations && dsi && (
|
||||
<StandardAnnotationQueryEditor
|
||||
@@ -133,7 +224,11 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" onClick={onPreview}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onPreview}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard}
|
||||
>
|
||||
Preview in dashboard
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onApply}>
|
||||
@@ -144,8 +239,43 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
AnnotationSettingsEdit.displayName = 'AnnotationSettingsEdit';
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
settingsForm: css({
|
||||
maxWidth: theme.spacing(60),
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
select: css`
|
||||
margin-top: 8px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
function goBackToList() {
|
||||
locationService.partial({ editIndex: null });
|
||||
}
|
||||
|
||||
// Synthetic type
|
||||
enum PanelFilterType {
|
||||
AllPanels,
|
||||
IncludePanels,
|
||||
ExcludePanels,
|
||||
}
|
||||
|
||||
const panelFilters = [
|
||||
{
|
||||
label: 'All panels',
|
||||
value: PanelFilterType.AllPanels,
|
||||
description: 'Send the annotation data to all panels that support annotations',
|
||||
},
|
||||
{
|
||||
label: 'Selected panels',
|
||||
value: PanelFilterType.IncludePanels,
|
||||
description: 'Send the annotations to the explicitly listed panels',
|
||||
},
|
||||
{
|
||||
label: 'All panels except',
|
||||
value: PanelFilterType.ExcludePanels,
|
||||
description: 'Do not send annotation data to the following panels',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,23 +74,11 @@ describe('AnnotationsSettings', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// we have a default build-in annotation
|
||||
dashboard = createDashboardModelFixture({
|
||||
id: 74,
|
||||
version: 7,
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: { uid: 'uid1', type: 'grafana' },
|
||||
enable: true,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
showIn: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
annotations: {},
|
||||
links: [],
|
||||
});
|
||||
});
|
||||
@@ -99,7 +87,8 @@ describe('AnnotationsSettings', () => {
|
||||
setup(dashboard);
|
||||
|
||||
expect(screen.queryByRole('grid')).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /annotations & alerts \(built-in\) -- grafana --/i })).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByTestId(selectors.components.CallToActionCard.buttonV2('Add annotation query'))
|
||||
).toBeInTheDocument();
|
||||
@@ -115,7 +104,7 @@ describe('AnnotationsSettings', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it renders the annotation names or uid if annotation doesnt exist', async () => {
|
||||
test('it renders the annotation names or uid if annotation does not exist', async () => {
|
||||
dashboard.annotations.list = [
|
||||
...dashboard.annotations.list,
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { AnnotationQuery, EventBus, GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { InlineField, InlineFieldRow, InlineSwitch, useStyles2 } from '@grafana/ui';
|
||||
import { LoadingIndicator } from '@grafana/ui/src/components/PanelChrome/LoadingIndicator';
|
||||
|
||||
@@ -44,8 +45,17 @@ export const AnnotationPicker = ({ annotation, events, onEnabledChanged }: Annot
|
||||
return (
|
||||
<div key={annotation.name} className={styles.annotation}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={annotation.name} disabled={loading}>
|
||||
<InlineSwitch value={annotation.enable} onChange={() => onEnabledChanged(annotation)} disabled={loading} />
|
||||
<InlineField
|
||||
label={annotation.name}
|
||||
disabled={loading}
|
||||
data-testid={selectors.pages.Dashboard.SubMenu.Annotations.annotationLabel(annotation.name)}
|
||||
>
|
||||
<InlineSwitch
|
||||
value={annotation.enable}
|
||||
onChange={() => onEnabledChanged(annotation)}
|
||||
disabled={loading}
|
||||
data-testid={selectors.pages.Dashboard.SubMenu.Annotations.annotationToggle(annotation.name)}
|
||||
/>
|
||||
</InlineField>
|
||||
<div className={styles.indicator}>
|
||||
<LoadingIndicator loading={loading} onCancel={onCancel} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { AnnotationQuery, DataQuery, EventBus } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { AnnotationPicker } from './AnnotationPicker';
|
||||
|
||||
@@ -21,7 +22,7 @@ export const Annotations = ({ annotations, onAnnotationChanged, events }: Props)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testId={selectors.pages.Dashboard.SubMenu.Annotations.annotationsWrapper}>
|
||||
{visibleAnnotations.map((annotation) => (
|
||||
<AnnotationPicker
|
||||
events={events}
|
||||
@@ -30,6 +31,6 @@ export const Annotations = ({ annotations, onAnnotationChanged, events }: Props)
|
||||
key={annotation.name}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2016,8 +2016,8 @@ describe('DashboardModel', () => {
|
||||
},
|
||||
annotations: {
|
||||
list: [
|
||||
// @ts-expect-error
|
||||
{
|
||||
// @ts-expect-error
|
||||
datasource: null,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -318,13 +318,11 @@ describe('DashboardModel', () => {
|
||||
list: [
|
||||
{
|
||||
datasource: { uid: 'fake-uid', type: 'prometheus' },
|
||||
showIn: 0,
|
||||
name: 'Fake annotation',
|
||||
type: 'dashboard',
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
enable: true,
|
||||
hide: false,
|
||||
builtIn: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -46,13 +46,12 @@ export function createPanelJSONFixture(panelInput: Partial<Panel | GraphPanel |
|
||||
}
|
||||
|
||||
export function createAnnotationJSONFixture(annotationInput: Partial<AnnotationQuery>): AnnotationQuery {
|
||||
// @ts-expect-error
|
||||
return {
|
||||
builtIn: 0, // ??
|
||||
datasource: {
|
||||
type: 'foo',
|
||||
uid: 'bar',
|
||||
},
|
||||
showIn: 2,
|
||||
enable: true,
|
||||
type: 'anno',
|
||||
...annotationInput,
|
||||
|
||||
Reference in New Issue
Block a user