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:
Ryan McKinley 2023-04-18 13:39:30 -07:00 committed by GitHub
parent a384194e15
commit 9452c0d718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1042 additions and 235 deletions

View File

@ -858,7 +858,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"]
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"]
],
"packages/grafana-toolkit/src/cli/tasks/component.create.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -2231,7 +2233,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"]
],
"public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"]
],
"public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -0,0 +1,433 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
},
{
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"enable": true,
"filter": {
"exclude": false,
"ids": [
1
]
},
"iconColor": "red",
"name": "Red, only panel 1",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"enable": true,
"filter": {
"exclude": true,
"ids": [
1
]
},
"iconColor": "yellow",
"name": "Yellow - all except 1",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"enable": true,
"filter": {
"exclude": false,
"ids": [
3,
4
]
},
"iconColor": "dark-purple",
"name": "Purple only panel 3+4",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations"
}
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 119,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"title": "Panel one",
"type": "timeseries"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"title": "Panel two",
"type": "timeseries"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"title": "Panel three",
"type": "timeseries"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"title": "Panel four",
"type": "timeseries"
}
],
"refresh": "",
"schemaVersion": 38,
"style": "dark",
"tags": ["gdev", "annotations"],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Annotation filtering",
"uid": "ed155665",
"version": 17,
"weekStart": ""
}

View File

@ -868,4 +868,4 @@
"uid": "e7c29343-6d1e-4167-9c13-803fe5be8c46",
"version": 48,
"weekStart": ""
}
}

View File

@ -119,8 +119,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@ -582,4 +581,4 @@
"uid": "cdd412c4",
"version": 6,
"weekStart": ""
}
}

View File

@ -453,4 +453,4 @@
"uid": "ZqZnVvFZz",
"version": 8,
"weekStart": ""
}
}

View File

@ -1040,4 +1040,4 @@
"uid": "U_bZIMRMk",
"version": 7,
"weekStart": ""
}
}

View File

@ -79,6 +79,13 @@ local dashboard = grafana.dashboard;
id: 0,
}
},
dashboard.new('annotation-filtering', import '../dev-dashboards/annotations/annotation-filtering.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('auto_decimals', import '../dev-dashboards/panel-common/auto_decimals.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{

View File

@ -13,32 +13,84 @@ title: Dashboard kind
A Grafana dashboard.
| Property | Type | Required | Default | Description |
|------------------------|-----------------------------------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `editable` | boolean | **Yes** | `true` | Whether a dashboard is editable or not. |
| `graphTooltip` | integer | **Yes** | `0` | 0 for no shared crosshair or tooltip (default).<br/>1 for shared crosshair.<br/>2 for shared crosshair AND shared tooltip.<br/>Possible values are: `0`, `1`, `2`. |
| `schemaVersion` | uint16 | **Yes** | `36` | Version of the JSON schema, incremented each time a Grafana update brings<br/>changes to said schema.<br/>TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion |
| `style` | string | **Yes** | `dark` | Theme of dashboard.<br/>Possible values are: `dark`, `light`. |
| `annotations` | [object](#annotations) | No | | TODO docs |
| `description` | string | No | | Description of dashboard. |
| `fiscalYearStartMonth` | integer | No | `0` | The month that the fiscal year starts on. 0 = January, 11 = December<br/>Constraint: `>=0 & <12`. |
| `gnetId` | string | No | | For dashboards imported from the https://grafana.com/grafana/dashboards/ portal |
| `id` | integer | No | | Unique numeric identifier for the dashboard.<br/>TODO must isolate or remove identifiers local to a Grafana instance...? |
| `links` | [DashboardLink](#dashboardlink)[] | No | | TODO docs |
| `liveNow` | boolean | No | | When set to true, the dashboard will redraw panels at an interval matching the pixel width.<br/>This will keep data "moving left" regardless of the query refresh rate. This setting helps<br/>avoid dashboards presenting stale live data |
| `panels` | [object](#panels)[] | No | | |
| `refresh` | | No | | Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". |
| `revision` | integer | No | | This property should only be used in dashboards defined by plugins. It is a quick check<br/>to see if the version has changed since the last time. Unclear why using the version property<br/>is insufficient. |
| `snapshot` | [Snapshot](#snapshot) | No | | TODO docs |
| `tags` | string[] | No | | Tags associated with dashboard. |
| `templating` | [object](#templating) | No | | TODO docs |
| `time` | [object](#time) | No | | Time range for dashboard, e.g. last 6 hours, last 7 days, etc |
| `timepicker` | [object](#timepicker) | No | | TODO docs<br/>TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes |
| `timezone` | string | No | `browser` | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". |
| `title` | string | No | | Title of dashboard. |
| `uid` | string | No | | Unique dashboard identifier that can be generated by anyone. string (8-40) |
| `version` | uint32 | No | | Version of the dashboard, incremented each time the dashboard is updated. |
| `weekStart` | string | No | | TODO docs |
| Property | Type | Required | Default | Description |
|------------------------|---------------------------------------------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `editable` | boolean | **Yes** | `true` | Whether a dashboard is editable or not. |
| `graphTooltip` | integer | **Yes** | `0` | 0 for no shared crosshair or tooltip (default).<br/>1 for shared crosshair.<br/>2 for shared crosshair AND shared tooltip.<br/>Possible values are: `0`, `1`, `2`. |
| `schemaVersion` | uint16 | **Yes** | `36` | Version of the JSON schema, incremented each time a Grafana update brings<br/>changes to said schema.<br/>TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion |
| `style` | string | **Yes** | `dark` | Theme of dashboard.<br/>Possible values are: `dark`, `light`. |
| `annotations` | [AnnotationContainer](#annotationcontainer) | No | | TODO -- should not be a public interface on its own, but required for Veneer |
| `description` | string | No | | Description of dashboard. |
| `fiscalYearStartMonth` | integer | No | `0` | The month that the fiscal year starts on. 0 = January, 11 = December<br/>Constraint: `>=0 & <12`. |
| `gnetId` | string | No | | For dashboards imported from the https://grafana.com/grafana/dashboards/ portal |
| `id` | integer | No | | Unique numeric identifier for the dashboard.<br/>TODO must isolate or remove identifiers local to a Grafana instance...? |
| `links` | [DashboardLink](#dashboardlink)[] | No | | TODO docs |
| `liveNow` | boolean | No | | When set to true, the dashboard will redraw panels at an interval matching the pixel width.<br/>This will keep data "moving left" regardless of the query refresh rate. This setting helps<br/>avoid dashboards presenting stale live data |
| `panels` | [object](#panels)[] | No | | |
| `refresh` | | No | | Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". |
| `revision` | integer | No | | This property should only be used in dashboards defined by plugins. It is a quick check<br/>to see if the version has changed since the last time. Unclear why using the version property<br/>is insufficient. |
| `snapshot` | [Snapshot](#snapshot) | No | | TODO docs |
| `tags` | string[] | No | | Tags associated with dashboard. |
| `templating` | [object](#templating) | No | | TODO docs |
| `time` | [object](#time) | No | | Time range for dashboard, e.g. last 6 hours, last 7 days, etc |
| `timepicker` | [object](#timepicker) | No | | TODO docs<br/>TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes |
| `timezone` | string | No | `browser` | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". |
| `title` | string | No | | Title of dashboard. |
| `uid` | string | No | | Unique dashboard identifier that can be generated by anyone. string (8-40) |
| `version` | uint32 | No | | Version of the dashboard, incremented each time the dashboard is updated. |
| `weekStart` | string | No | | TODO docs |
### AnnotationContainer
TODO -- should not be a public interface on its own, but required for Veneer
| Property | Type | Required | Default | Description |
|----------|---------------------------------------|----------|---------|-------------|
| `list` | [AnnotationQuery](#annotationquery)[] | No | | |
### AnnotationQuery
TODO docs
FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
| Property | Type | Required | Default | Description |
|--------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `datasource` | [object](#datasource) | **Yes** | | TODO: Should be DataSourceRef |
| `enable` | boolean | **Yes** | `true` | When enabled the annotation query is issued with every dashboard refresh |
| `iconColor` | string | **Yes** | | Color to use for the annotation event markers |
| `name` | string | **Yes** | | Name of annotation. |
| `filter` | [AnnotationPanelFilter](#annotationpanelfilter) | No | | |
| `hide` | boolean | No | `false` | Annotation queries can be toggled on or off at the top of the dashboard.<br/>When hide is true, the toggle is not shown in the dashboard. |
| `target` | [AnnotationTarget](#annotationtarget) | No | | TODO: this should be a regular DataQuery that depends on the selected dashboard<br/>these match the properties of the "grafana" datasouce that is default in most dashboards |
| `type` | string | No | | TODO -- this should not exist here, it is based on the --grafana-- datasource |
### AnnotationPanelFilter
| Property | Type | Required | Default | Description |
|-----------|-----------|----------|---------|-----------------------------------------------------|
| `ids` | integer[] | **Yes** | | Panel IDs that should be included or excluded |
| `exclude` | boolean | No | `false` | Should the specified panels be included or excluded |
### AnnotationTarget
TODO: this should be a regular DataQuery that depends on the selected dashboard
these match the properties of the "grafana" datasouce that is default in most dashboards
| Property | Type | Required | Default | Description |
|------------|----------|----------|---------|-------------------------------------------------------------------------------------------------------------------|
| `limit` | integer | **Yes** | | Only required/valid for the grafana datasource...<br/>but code+tests is already depending on it so hard to change |
| `matchAny` | boolean | **Yes** | | Only required/valid for the grafana datasource...<br/>but code+tests is already depending on it so hard to change |
| `tags` | string[] | **Yes** | | Only required/valid for the grafana datasource...<br/>but code+tests is already depending on it so hard to change |
| `type` | string | **Yes** | | Only required/valid for the grafana datasource...<br/>but code+tests is already depending on it so hard to change |
### Datasource
TODO: Should be DataSourceRef
| Property | Type | Required | Default | Description |
|----------|--------|----------|---------|-------------|
| `type` | string | No | | |
| `uid` | string | No | | |
### DashboardLink
@ -76,52 +128,6 @@ TODO docs
| `userId` | uint32 | **Yes** | | TODO docs |
| `url` | string | No | | TODO docs |
### Annotations
TODO docs
| Property | Type | Required | Default | Description |
|----------|---------------------------------------|----------|---------|-------------|
| `list` | [AnnotationQuery](#annotationquery)[] | No | | |
### AnnotationQuery
TODO docs
FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
| Property | Type | Required | Default | Description |
|--------------|---------------------------------------|----------|-------------|-----------------------------------|
| `builtIn` | uint8 | **Yes** | `0` | |
| `datasource` | [object](#datasource) | **Yes** | | Datasource to use for annotation. |
| `enable` | boolean | **Yes** | `true` | Whether annotation is enabled. |
| `showIn` | uint8 | **Yes** | `0` | |
| `type` | string | **Yes** | `dashboard` | |
| `hide` | boolean | No | `false` | Whether to hide annotation. |
| `iconColor` | string | No | | Annotation icon color. |
| `name` | string | No | | Name of annotation. |
| `rawQuery` | string | No | | Query for annotation data. |
| `target` | [AnnotationTarget](#annotationtarget) | No | | TODO docs |
### AnnotationTarget
TODO docs
| Property | Type | Required | Default | Description |
|------------|----------|----------|---------|-------------|
| `limit` | integer | **Yes** | | |
| `matchAny` | boolean | **Yes** | | |
| `tags` | string[] | **Yes** | | |
| `type` | string | **Yes** | | |
### Datasource
Datasource to use for annotation.
| Property | Type | Required | Default | Description |
|----------|--------|----------|---------|-------------|
| `type` | string | No | | |
| `uid` | string | No | | |
### Panels
| Property | Type | Required | Default | Description |

View File

@ -0,0 +1,61 @@
import { e2e } from '@grafana/e2e';
const DASHBOARD_ID = 'ed155665';
e2e.scenario({
describeName: 'Annotations filtering',
itName: 'Tests switching filter type updates the UI accordingly',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID });
e2e.components.PageToolbar.item('Dashboard settings').click();
e2e.components.Tab.title('Annotations').click();
cy.contains('New query').click();
e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type('Red - Panel two');
e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel()
.should('be.visible')
.within(() => {
// All panels
e2e.components.Annotations.annotationsTypeInput().click({ force: true }).type('All panels{enter}');
e2e.components.Annotations.annotationsChoosePanelInput().should('not.exist');
// All panels except
e2e.components.Annotations.annotationsTypeInput().click({ force: true }).type('All panels except{enter}');
e2e.components.Annotations.annotationsChoosePanelInput().should('be.visible');
// Selected panels
e2e.components.Annotations.annotationsTypeInput().click({ force: true }).type('Selected panels{enter}');
e2e.components.Annotations.annotationsChoosePanelInput()
.should('be.visible')
.click({ force: true })
.type('Panel two{enter}');
});
e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click({ force: true });
e2e.pages.Dashboard.SubMenu.Annotations.annotationsWrapper()
.should('be.visible')
.within(() => {
e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red - Panel two').should('be.visible');
e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red - Panel two')
.should('be.checked')
.uncheck({ force: true })
.should('not.be.checked')
.check({ force: true });
e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red, only panel 1').should('be.visible');
e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red, only panel 1').should('be.checked');
});
e2e().wait(3000);
e2e.components.Panels.Panel.title('Panel one')
.should('exist')
.within(() => {
e2e.pages.SoloPanel.Annotations.marker().should('exist').should('have.length', 4);
});
},
});

View File

@ -85,10 +85,18 @@ lineage: seqs: [
templating?: {
list?: [...#VariableModel] @grafanamaturity(NeedsExpertReview)
}
// TODO docs
annotations?: {
// TODO -- should not be a public interface on its own, but required for Veneer
#AnnotationContainer: {
// annoying... but required so that the list is defined using the nested Veneer
@grafana(TSVeneer="type")
list?: [...#AnnotationQuery] @grafanamaturity(NeedsExpertReview)
}
} @cuetsy(kind="interface")
// TODO docs
annotations?: #AnnotationContainer
// TODO docs
links?: [...#DashboardLink] @grafanamaturity(NeedsExpertReview)
@ -97,39 +105,72 @@ lineage: seqs: [
///////////////////////////////////////
// Definitions (referenced above) are declared below
// TODO docs
// TODO: this should be a regular DataQuery that depends on the selected dashboard
// these match the properties of the "grafana" datasouce that is default in most dashboards
#AnnotationTarget: {
limit: int64
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
limit: int64
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
matchAny: bool
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
tags: [...string]
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
type: string
... // datasource will stick their raw DataQuery here
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
#AnnotationPanelFilter: {
// Should the specified panels be included or excluded
exclude?: bool | *false
// Panel IDs that should be included or excluded
ids: [...uint8]
} @cuetsy(kind="interface")
// TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
#AnnotationQuery: {
// Datasource to use for annotation.
@grafana(TSVeneer="type")
// Name of annotation.
name: string
// TODO: Should be DataSourceRef
datasource: {
type?: string
uid?: string
} @grafanamaturity(NeedsExpertReview)
// Whether annotation is enabled.
enable: bool | *true @grafanamaturity(NeedsExpertReview)
// Name of annotation.
name?: string @grafanamaturity(NeedsExpertReview)
builtIn: uint8 | *0 @grafanamaturity(NeedsExpertReview) // TODO should this be persisted at all?
// Whether to hide annotation.
hide?: bool | *false @grafanamaturity(NeedsExpertReview)
// Annotation icon color.
iconColor?: string @grafanamaturity(NeedsExpertReview)
type: string | *"dashboard" @grafanamaturity(NeedsExpertReview)
// Query for annotation data.
rawQuery?: string @grafanamaturity(NeedsExpertReview)
showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview)
target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview)
// When enabled the annotation query is issued with every dashboard refresh
enable: bool | *true
// Annotation queries can be toggled on or off at the top of the dashboard.
// When hide is true, the toggle is not shown in the dashboard.
hide?: bool | *false
// Color to use for the annotation event markers
iconColor: string
// Optionally
filter?: #AnnotationPanelFilter
// TODO.. this should just be a normal query target
target?: #AnnotationTarget
// TODO -- this should not exist here, it is based on the --grafana-- datasource
type?: string @grafanamaturity(NeedsExpertReview)
// unless datasources have migrated to the target+mapping,
// they just spread their query into the base object :(
...
} @cuetsy(kind="interface")
#LoadingState: "NotStarted" | "Loading" | "Streaming" | "Done" | "Error" @cuetsy(kind="enum") @grafanamaturity(NeedsExpertReview)
// FROM: packages/grafana-data/src/types/templateVars.ts
// TODO docs
// TODO what about what's in public/app/features/types.ts?

View File

@ -1,30 +1,23 @@
import { ComponentType } from 'react';
import { Observable } from 'rxjs';
import { DataQuery, AnnotationQuery as SchemaAnnotationQuery } from '@grafana/schema';
import { DataFrame } from './dataFrame';
import { QueryEditorProps } from './datasource';
import { DataQuery, DataSourceRef } from './query';
/**
* This JSON object is stored in the dashboard json model.
*/
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> {
datasource?: DataSourceRef | null;
enable: boolean;
name: string;
iconColor: string;
hide?: boolean;
builtIn?: number;
type?: string;
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> extends SchemaAnnotationQuery<TQuery> {
snapshotData?: any;
// Standard datasource query
target?: TQuery;
// Convert a dataframe to an AnnotationEvent
mappings?: AnnotationEventMappings;
// When using the 'grafana' datasource, this may be dashboard
type?: string;
// Sadly plugins can set any property directly on the main object
[key: string]: any;
}
@ -51,7 +44,7 @@ export interface AnnotationEvent {
newState?: string;
// Currently used to merge annotations from alerts and dashboard
source?: any; // source.type === 'dashboard'
source?: any; // source.type === 'dashboard' -- should be AnnotationQuery
}
export interface AnnotationEventUIModel {

View File

@ -57,6 +57,7 @@ export class DataSourcePlugin<
return this;
}
/** @deprecated -- register the annotation support in the instance constructor */
setAnnotationQueryCtrl(AnnotationsQueryCtrl: any) {
this.components.AnnotationsQueryCtrl = AnnotationsQueryCtrl;
return this;

View File

@ -416,4 +416,8 @@ export const Components = {
Variables: {
variableOption: 'data-testid variable-option',
},
Annotations: {
annotationsTypeInput: 'annotations-type-input',
annotationsChoosePanelInput: 'choose-panels-input',
},
};

View File

@ -66,6 +66,11 @@ export const Pages = {
submenuItemValueDropDownDropDown: 'Variable options',
submenuItemValueDropDownOptionTexts: (item: string) =>
`data-testid Dashboard template variables Variable Value DropDown option text ${item}`,
Annotations: {
annotationsWrapper: 'data-testid annotation-wrapper',
annotationLabel: (label: string) => `data-testid Dashboard annotations submenu Label ${label}`,
annotationToggle: (label: string) => `data-testid Dashboard annotations submenu Toggle ${label}`,
},
},
Settings: {
Actions: {
@ -93,6 +98,11 @@ export const Pages = {
Settings: {
name: 'Annotations settings name input',
},
NewAnnotation: {
panelFilterSelect: 'data-testid annotations-panel-filter',
showInLabel: 'show-in-label',
previewInDashboard: 'data-testid annotations-preview',
},
},
Variables: {
List: {
@ -239,6 +249,9 @@ export const Pages = {
},
SoloPanel: {
url: (page: string) => `/d-solo/${page}`,
Annotations: {
marker: 'data-testid annotation-marker',
},
},
PluginsList: {
page: 'Plugins list page',

View File

@ -10,7 +10,7 @@
// Raw generated types from Dashboard kind.
export type {
AnnotationTarget,
AnnotationQuery,
AnnotationPanelFilter,
DashboardLink,
DashboardLinkType,
VariableType,
@ -34,7 +34,7 @@ export type {
// Raw generated enums and default consts from dashboard kind.
export {
defaultAnnotationTarget,
defaultAnnotationQuery,
defaultAnnotationPanelFilter,
LoadingState,
defaultDashboardLink,
FieldColorModeId,
@ -59,6 +59,8 @@ export {
// TODO generate code such that tsc enforces type compatibility between raw and veneer decls
export type {
Dashboard,
AnnotationContainer,
AnnotationQuery,
VariableModel,
DataSourceRef,
DataTransformerConfig,
@ -79,6 +81,8 @@ export type {
// TODO generate code such that tsc enforces type compatibility between raw and veneer decls
export {
defaultDashboard,
defaultAnnotationContainer,
defaultAnnotationQuery,
defaultVariableModel,
VariableHide,
defaultPanel,

View File

@ -9,12 +9,40 @@
// Run 'make gen-cue' from repository root to regenerate.
/**
* TODO docs
* TODO -- should not be a public interface on its own, but required for Veneer
*/
export interface AnnotationContainer {
list?: Array<AnnotationQuery>;
}
export const defaultAnnotationContainer: Partial<AnnotationContainer> = {
list: [],
};
/**
* TODO: this should be a regular DataQuery that depends on the selected dashboard
* these match the properties of the "grafana" datasouce that is default in most dashboards
*/
export interface AnnotationTarget {
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
limit: number;
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
matchAny: boolean;
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
tags: Array<string>;
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
type: string;
}
@ -22,52 +50,78 @@ export const defaultAnnotationTarget: Partial<AnnotationTarget> = {
tags: [],
};
export interface AnnotationPanelFilter {
/**
* Should the specified panels be included or excluded
*/
exclude?: boolean;
/**
* Panel IDs that should be included or excluded
*/
ids: Array<number>;
}
export const defaultAnnotationPanelFilter: Partial<AnnotationPanelFilter> = {
exclude: false,
ids: [],
};
/**
* TODO docs
* FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
*/
export interface AnnotationQuery {
builtIn: number; // TODO should this be persisted at all?
/**
* Datasource to use for annotation.
* TODO: Should be DataSourceRef
*/
datasource: {
type?: string;
uid?: string;
};
/**
* Whether annotation is enabled.
* When enabled the annotation query is issued with every dashboard refresh
*/
enable: boolean;
/**
* Whether to hide annotation.
* Optionally
*/
filter?: AnnotationPanelFilter;
/**
* Annotation queries can be toggled on or off at the top of the dashboard.
* When hide is true, the toggle is not shown in the dashboard.
*/
hide?: boolean;
/**
* Annotation icon color.
* Color to use for the annotation event markers
*/
iconColor?: string;
iconColor: string;
/**
* Name of annotation.
*/
name?: string;
name: string;
/**
* Query for annotation data.
* TODO.. this should just be a normal query target
*/
rawQuery?: string;
showIn: number;
target?: AnnotationTarget;
type: string;
/**
* TODO -- this should not exist here, it is based on the --grafana-- datasource
*/
type?: string;
}
export const defaultAnnotationQuery: Partial<AnnotationQuery> = {
builtIn: 0,
enable: true,
hide: false,
showIn: 0,
type: 'dashboard',
};
export enum LoadingState {
Done = 'Done',
Error = 'Error',
Loading = 'Loading',
NotStarted = 'NotStarted',
Streaming = 'Streaming',
}
/**
* FROM: packages/grafana-data/src/types/templateVars.ts
* TODO docs
@ -107,14 +161,6 @@ export enum VariableHide {
hideVariable = 2,
}
export enum LoadingState {
Done = 'Done',
Error = 'Error',
Loading = 'Loading',
NotStarted = 'NotStarted',
Streaming = 'Streaming',
}
/**
* Ref to a DataSource instance
*/
@ -662,9 +708,7 @@ export interface Dashboard {
/**
* TODO docs
*/
annotations?: {
list?: Array<AnnotationQuery>;
};
annotations?: AnnotationContainer;
/**
* Description of dashboard.
*/

View File

@ -1,6 +1,8 @@
import { DataSourceRef as CommonDataSourceRef } from '../common/common.gen';
import { DataSourceRef as CommonDataSourceRef, DataSourceRef } from '../common/common.gen';
import * as raw from '../raw/dashboard/x/dashboard_types.gen';
import { DataQuery } from './common.types';
export type { CommonDataSourceRef as DataSourceRef };
export interface Panel<TOptions = Record<string, unknown>, TCustomFieldConfig = Record<string, unknown>>
@ -28,13 +30,24 @@ export interface VariableModel
datasource: CommonDataSourceRef | null;
}
export interface Dashboard extends Omit<raw.Dashboard, 'templating'> {
export interface Dashboard extends Omit<raw.Dashboard, 'templating' | 'annotations'> {
panels?: Array<Panel | raw.RowPanel | raw.GraphPanel | raw.HeatmapPanel>;
annotations?: AnnotationContainer;
templating?: {
list?: VariableModel[];
};
}
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery>
extends Omit<raw.AnnotationQuery, 'target' | 'datasource'> {
datasource?: DataSourceRef | null;
target?: TQuery;
}
export interface AnnotationContainer extends Omit<raw.AnnotationContainer, 'list'> {
list?: AnnotationQuery[]; // use the version from this file
}
export interface FieldConfig<TOptions = Record<string, unknown>> extends raw.FieldConfig {
custom?: TOptions & Record<string, unknown>;
}
@ -69,3 +82,6 @@ export const defaultPanel: Partial<Panel> = raw.defaultPanel;
export const defaultFieldConfig: Partial<FieldConfig> = raw.defaultFieldConfig;
export const defaultFieldConfigSource: Partial<FieldConfigSource> = raw.defaultFieldConfigSource;
export const defaultMatcherConfig: Partial<MatcherConfig> = raw.defaultMatcherConfig;
export const defaultAnnotationQuery: Partial<AnnotationQuery> = raw.defaultAnnotationQuery as AnnotationQuery;
export const defaultAnnotationContainer: Partial<AnnotationContainer> =
raw.defaultAnnotationContainer as AnnotationContainer;

View File

@ -160,52 +160,75 @@ const (
VariableTypeTextbox VariableType = "textbox"
)
// TODO -- should not be a public interface on its own, but required for Veneer
type AnnotationContainer struct {
List []AnnotationQuery `json:"list,omitempty"`
}
// AnnotationPanelFilter defines model for AnnotationPanelFilter.
type AnnotationPanelFilter struct {
// Should the specified panels be included or excluded
Exclude *bool `json:"exclude,omitempty"`
// Panel IDs that should be included or excluded
Ids []int `json:"ids"`
}
// TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
type AnnotationQuery struct {
BuiltIn int `json:"builtIn"`
// Datasource to use for annotation.
// TODO: Should be DataSourceRef
Datasource struct {
Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"`
} `json:"datasource"`
// Whether annotation is enabled.
Enable bool `json:"enable"`
// When enabled the annotation query is issued with every dashboard refresh
Enable bool `json:"enable"`
Filter *AnnotationPanelFilter `json:"filter,omitempty"`
// Whether to hide annotation.
// Annotation queries can be toggled on or off at the top of the dashboard.
// When hide is true, the toggle is not shown in the dashboard.
Hide *bool `json:"hide,omitempty"`
// Annotation icon color.
IconColor *string `json:"iconColor,omitempty"`
// Color to use for the annotation event markers
IconColor string `json:"iconColor"`
// Name of annotation.
Name *string `json:"name,omitempty"`
Name string `json:"name"`
// Query for annotation data.
RawQuery *string `json:"rawQuery,omitempty"`
ShowIn int `json:"showIn"`
// TODO docs
// TODO: this should be a regular DataQuery that depends on the selected dashboard
// these match the properties of the "grafana" datasouce that is default in most dashboards
Target *AnnotationTarget `json:"target,omitempty"`
Type string `json:"type"`
// TODO -- this should not exist here, it is based on the --grafana-- datasource
Type *string `json:"type,omitempty"`
}
// TODO docs
// TODO: this should be a regular DataQuery that depends on the selected dashboard
// these match the properties of the "grafana" datasouce that is default in most dashboards
type AnnotationTarget struct {
Limit int64 `json:"limit"`
MatchAny bool `json:"matchAny"`
Tags []string `json:"tags"`
Type string `json:"type"`
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
Limit int64 `json:"limit"`
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
MatchAny bool `json:"matchAny"`
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
Tags []string `json:"tags"`
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
Type string `json:"type"`
}
// Dashboard defines model for Dashboard.
type Dashboard struct {
// TODO docs
Annotations *struct {
List []AnnotationQuery `json:"list,omitempty"`
} `json:"annotations,omitempty"`
// TODO -- should not be a public interface on its own, but required for Veneer
Annotations *AnnotationContainer `json:"annotations,omitempty"`
// Description of dashboard.
Description *string `json:"description,omitempty"`

View File

@ -70,7 +70,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
Tags: item.Tags,
IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd,
Text: item.Text,
Color: *anno.IconColor,
Color: anno.IconColor,
Time: item.Time,
TimeEnd: item.TimeEnd,
Source: anno,
@ -78,7 +78,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
// We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels
// which is only intended for tag and org annotations.
if anno.Type == "dashboard" {
if anno.Type != nil && *anno.Type == "dashboard" {
event.PanelId = item.PanelID
}

View File

@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -743,21 +744,21 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
Target: &dashboard2.AnnotationTarget{
Limit: 100,
MatchAny: false,
Tags: nil,
Type: "dashboard",
},
Type: "dashboard",
Type: util.Pointer("dashboard"),
}
grafanaTagAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
Target: &dashboard2.AnnotationTarget{
Limit: 100,
MatchAny: false,
@ -816,8 +817,8 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
Target: &dashboard2.AnnotationTarget{
Limit: 100,
MatchAny: false,
@ -876,26 +877,26 @@ func TestFindAnnotations(t *testing.T) {
disabledGrafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: false,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
}
grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
Target: &dashboard2.AnnotationTarget{
Limit: 100,
MatchAny: true,
Tags: nil,
Type: "dashboard",
},
Type: "dashboard",
Type: util.Pointer("dashboard"),
}
queryAnnotation := DashAnnotation{
Datasource: CreateDatasource("prometheus", "abc123"),
Enable: true,
Name: &name,
Name: name,
}
annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation}
dashboard := AddAnnotationsToDashboard(t, dash, annos)
@ -975,15 +976,15 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
Target: &dashboard2.AnnotationTarget{
Limit: 100,
MatchAny: false,
Tags: nil,
Type: "dashboard",
},
Type: "dashboard",
Type: util.Pointer("dashboard"),
}
annos := []DashAnnotation{grafanaAnnotation}
dashboard := AddAnnotationsToDashboard(t, dash, annos)
@ -1010,8 +1011,8 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Name: name,
IconColor: color,
Target: &dashboard2.AnnotationTarget{
Limit: 100,
MatchAny: false,
@ -1039,9 +1040,9 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"),
Enable: true,
Name: &name,
IconColor: &color,
Type: "dashboard",
Name: name,
IconColor: color,
Type: util.Pointer("dashboard"),
Target: nil,
}

View File

@ -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',
},
];

View File

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

View File

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

View File

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

View File

@ -2016,8 +2016,8 @@ describe('DashboardModel', () => {
},
annotations: {
list: [
// @ts-expect-error
{
// @ts-expect-error
datasource: null,
},
{

View File

@ -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,
},
],
},

View File

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

View File

@ -44,8 +44,31 @@ function notifyWithError(title: string, err: any) {
}
export function getAnnotationsByPanelId(annotations: AnnotationEvent[], panelId?: number) {
if (panelId == null) {
return annotations;
}
return annotations.filter((item) => {
if (panelId !== undefined && item.panelId && item.source?.type === 'dashboard') {
let source: AnnotationQuery;
source = item.source;
if (!source) {
return true; // should not happen
}
// generic panel filtering
if (source.filter) {
const includes = (source.filter.ids ?? []).includes(panelId);
if (source.filter.exclude) {
if (includes) {
return false;
}
} else if (!includes) {
return false;
}
}
// this is valid for the main 'grafana' datasource
if (item.panelId && item.source.type === 'dashboard') {
return item.panelId === panelId;
}
return true;

View File

@ -9,7 +9,6 @@ import {
DataQueryRequest,
DataQueryResponse,
DataSourceInstanceSettings,
DataSourceRef,
isValidLiveChannelAddress,
MutableDataFrame,
parseLiveChannelAddress,
@ -25,6 +24,7 @@ import {
getTemplateSrv,
StreamingFrameOptions,
} from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema';
import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator';
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
@ -61,7 +61,10 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
datasource = anno.datasource as DataSourceRef;
}
return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource };
// Filter from streaming query conflicts with filter from annotations
const { filter, ...rest } = anno;
return { ...rest, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource };
},
};
}

View File

@ -1,5 +1,6 @@
import { DataQuery, DataFrameJSON } from '@grafana/data';
import { DataFrameJSON } from '@grafana/data';
import { LiveDataFilter } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { SearchQuery } from 'app/features/search/service';
//----------------------------------------------

View File

@ -35,7 +35,7 @@ export class EventEditorCtrl {
canDelete(): boolean {
if (contextSrv.accessControlEnabled()) {
if (this.event.source.type === 'dashboard') {
if (this.event.source?.type === 'dashboard') {
return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.dashboard.canDelete;
}
return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.organization.canDelete;

View File

@ -3,6 +3,7 @@ import React, { HTMLAttributes, useCallback, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Portal, useStyles2, usePanelContext } from '@grafana/ui';
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
@ -127,6 +128,7 @@ export function AnnotationMarker({ annotation, timeZone, width }: Props) {
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={!isRegionAnnotation ? styles.markerWrapper : undefined}
data-testId={selectors.pages.SoloPanel.Annotations.marker}
>
{marker}
</div>