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.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"], [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.", "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": [ "packages/grafana-toolkit/src/cli/tasks/component.create.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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"] [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"]
], ],
"public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx:5381": [ "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": [ "public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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", "uid": "e7c29343-6d1e-4167-9c13-803fe5be8c46",
"version": 48, "version": 48,
"weekStart": "" "weekStart": ""
} }

View File

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

View File

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

View File

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

View File

@ -79,6 +79,13 @@ local dashboard = grafana.dashboard;
id: 0, 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') + dashboard.new('auto_decimals', import '../dev-dashboards/panel-common/auto_decimals.json') +
resource.addMetadata('folder', 'dev-dashboards') + resource.addMetadata('folder', 'dev-dashboards') +
{ {

View File

@ -13,32 +13,84 @@ title: Dashboard kind
A Grafana dashboard. A Grafana dashboard.
| Property | Type | Required | Default | Description | | Property | Type | Required | Default | Description |
|------------------------|-----------------------------------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |------------------------|---------------------------------------------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `editable` | boolean | **Yes** | `true` | Whether a dashboard is editable or not. | | `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`. | | `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 | | `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`. | | `style` | string | **Yes** | `dark` | Theme of dashboard.<br/>Possible values are: `dark`, `light`. |
| `annotations` | [object](#annotations) | No | | TODO docs | | `annotations` | [AnnotationContainer](#annotationcontainer) | No | | TODO -- should not be a public interface on its own, but required for Veneer |
| `description` | string | No | | Description of dashboard. | | `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`. | | `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 | | `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...? | | `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 | | `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 | | `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 | | | | `panels` | [object](#panels)[] | No | | |
| `refresh` | | No | | Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". | | `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. | | `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 | | `snapshot` | [Snapshot](#snapshot) | No | | TODO docs |
| `tags` | string[] | No | | Tags associated with dashboard. | | `tags` | string[] | No | | Tags associated with dashboard. |
| `templating` | [object](#templating) | No | | TODO docs | | `templating` | [object](#templating) | No | | TODO docs |
| `time` | [object](#time) | No | | Time range for dashboard, e.g. last 6 hours, last 7 days, etc | | `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 | | `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". | | `timezone` | string | No | `browser` | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". |
| `title` | string | No | | Title of dashboard. | | `title` | string | No | | Title of dashboard. |
| `uid` | string | No | | Unique dashboard identifier that can be generated by anyone. string (8-40) | | `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. | | `version` | uint32 | No | | Version of the dashboard, incremented each time the dashboard is updated. |
| `weekStart` | string | No | | TODO docs | | `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 ### DashboardLink
@ -76,52 +128,6 @@ TODO docs
| `userId` | uint32 | **Yes** | | TODO docs | | `userId` | uint32 | **Yes** | | TODO docs |
| `url` | string | No | | 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 ### Panels
| Property | Type | Required | Default | Description | | 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?: { templating?: {
list?: [...#VariableModel] @grafanamaturity(NeedsExpertReview) 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) list?: [...#AnnotationQuery] @grafanamaturity(NeedsExpertReview)
} } @cuetsy(kind="interface")
// TODO docs
annotations?: #AnnotationContainer
// TODO docs // TODO docs
links?: [...#DashboardLink] @grafanamaturity(NeedsExpertReview) links?: [...#DashboardLink] @grafanamaturity(NeedsExpertReview)
@ -97,39 +105,72 @@ lineage: seqs: [
/////////////////////////////////////// ///////////////////////////////////////
// Definitions (referenced above) are declared below // 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: { #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 matchAny: bool
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
tags: [...string] tags: [...string]
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
type: string type: string
... // datasource will stick their raw DataQuery here
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) } @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 // TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts // FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
#AnnotationQuery: { #AnnotationQuery: {
// Datasource to use for annotation. @grafana(TSVeneer="type")
// Name of annotation.
name: string
// TODO: Should be DataSourceRef
datasource: { datasource: {
type?: string type?: string
uid?: string uid?: string
} @grafanamaturity(NeedsExpertReview) } @grafanamaturity(NeedsExpertReview)
// Whether annotation is enabled. // When enabled the annotation query is issued with every dashboard refresh
enable: bool | *true @grafanamaturity(NeedsExpertReview) enable: bool | *true
// Name of annotation.
name?: string @grafanamaturity(NeedsExpertReview) // Annotation queries can be toggled on or off at the top of the dashboard.
builtIn: uint8 | *0 @grafanamaturity(NeedsExpertReview) // TODO should this be persisted at all? // When hide is true, the toggle is not shown in the dashboard.
// Whether to hide annotation. hide?: bool | *false
hide?: bool | *false @grafanamaturity(NeedsExpertReview)
// Annotation icon color. // Color to use for the annotation event markers
iconColor?: string @grafanamaturity(NeedsExpertReview) iconColor: string
type: string | *"dashboard" @grafanamaturity(NeedsExpertReview)
// Query for annotation data. // Optionally
rawQuery?: string @grafanamaturity(NeedsExpertReview) filter?: #AnnotationPanelFilter
showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview)
target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview) // 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") } @cuetsy(kind="interface")
#LoadingState: "NotStarted" | "Loading" | "Streaming" | "Done" | "Error" @cuetsy(kind="enum") @grafanamaturity(NeedsExpertReview)
// FROM: packages/grafana-data/src/types/templateVars.ts // FROM: packages/grafana-data/src/types/templateVars.ts
// TODO docs // TODO docs
// TODO what about what's in public/app/features/types.ts? // TODO what about what's in public/app/features/types.ts?

View File

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

View File

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

View File

@ -416,4 +416,8 @@ export const Components = {
Variables: { Variables: {
variableOption: 'data-testid variable-option', 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', submenuItemValueDropDownDropDown: 'Variable options',
submenuItemValueDropDownOptionTexts: (item: string) => submenuItemValueDropDownOptionTexts: (item: string) =>
`data-testid Dashboard template variables Variable Value DropDown option text ${item}`, `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: { Settings: {
Actions: { Actions: {
@ -93,6 +98,11 @@ export const Pages = {
Settings: { Settings: {
name: 'Annotations settings name input', name: 'Annotations settings name input',
}, },
NewAnnotation: {
panelFilterSelect: 'data-testid annotations-panel-filter',
showInLabel: 'show-in-label',
previewInDashboard: 'data-testid annotations-preview',
},
}, },
Variables: { Variables: {
List: { List: {
@ -239,6 +249,9 @@ export const Pages = {
}, },
SoloPanel: { SoloPanel: {
url: (page: string) => `/d-solo/${page}`, url: (page: string) => `/d-solo/${page}`,
Annotations: {
marker: 'data-testid annotation-marker',
},
}, },
PluginsList: { PluginsList: {
page: 'Plugins list page', page: 'Plugins list page',

View File

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

View File

@ -9,12 +9,40 @@
// Run 'make gen-cue' from repository root to regenerate. // 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 { export interface AnnotationTarget {
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
limit: number; limit: number;
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
matchAny: boolean; matchAny: boolean;
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
tags: Array<string>; tags: Array<string>;
/**
* Only required/valid for the grafana datasource...
* but code+tests is already depending on it so hard to change
*/
type: string; type: string;
} }
@ -22,52 +50,78 @@ export const defaultAnnotationTarget: Partial<AnnotationTarget> = {
tags: [], 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 * TODO docs
* FROM: AnnotationQuery in grafana-data/src/types/annotations.ts * FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
*/ */
export interface AnnotationQuery { export interface AnnotationQuery {
builtIn: number; // TODO should this be persisted at all?
/** /**
* Datasource to use for annotation. * TODO: Should be DataSourceRef
*/ */
datasource: { datasource: {
type?: string; type?: string;
uid?: string; uid?: string;
}; };
/** /**
* Whether annotation is enabled. * When enabled the annotation query is issued with every dashboard refresh
*/ */
enable: boolean; 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; hide?: boolean;
/** /**
* Annotation icon color. * Color to use for the annotation event markers
*/ */
iconColor?: string; iconColor: string;
/** /**
* Name of annotation. * 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; target?: AnnotationTarget;
type: string; /**
* TODO -- this should not exist here, it is based on the --grafana-- datasource
*/
type?: string;
} }
export const defaultAnnotationQuery: Partial<AnnotationQuery> = { export const defaultAnnotationQuery: Partial<AnnotationQuery> = {
builtIn: 0,
enable: true, enable: true,
hide: false, 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 * FROM: packages/grafana-data/src/types/templateVars.ts
* TODO docs * TODO docs
@ -107,14 +161,6 @@ export enum VariableHide {
hideVariable = 2, hideVariable = 2,
} }
export enum LoadingState {
Done = 'Done',
Error = 'Error',
Loading = 'Loading',
NotStarted = 'NotStarted',
Streaming = 'Streaming',
}
/** /**
* Ref to a DataSource instance * Ref to a DataSource instance
*/ */
@ -662,9 +708,7 @@ export interface Dashboard {
/** /**
* TODO docs * TODO docs
*/ */
annotations?: { annotations?: AnnotationContainer;
list?: Array<AnnotationQuery>;
};
/** /**
* Description of dashboard. * 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 * as raw from '../raw/dashboard/x/dashboard_types.gen';
import { DataQuery } from './common.types';
export type { CommonDataSourceRef as DataSourceRef }; export type { CommonDataSourceRef as DataSourceRef };
export interface Panel<TOptions = Record<string, unknown>, TCustomFieldConfig = Record<string, unknown>> export interface Panel<TOptions = Record<string, unknown>, TCustomFieldConfig = Record<string, unknown>>
@ -28,13 +30,24 @@ export interface VariableModel
datasource: CommonDataSourceRef | null; 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>; panels?: Array<Panel | raw.RowPanel | raw.GraphPanel | raw.HeatmapPanel>;
annotations?: AnnotationContainer;
templating?: { templating?: {
list?: VariableModel[]; 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 { export interface FieldConfig<TOptions = Record<string, unknown>> extends raw.FieldConfig {
custom?: TOptions & Record<string, unknown>; 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 defaultFieldConfig: Partial<FieldConfig> = raw.defaultFieldConfig;
export const defaultFieldConfigSource: Partial<FieldConfigSource> = raw.defaultFieldConfigSource; export const defaultFieldConfigSource: Partial<FieldConfigSource> = raw.defaultFieldConfigSource;
export const defaultMatcherConfig: Partial<MatcherConfig> = raw.defaultMatcherConfig; 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" 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 // TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts // FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
type AnnotationQuery struct { type AnnotationQuery struct {
BuiltIn int `json:"builtIn"` // TODO: Should be DataSourceRef
// Datasource to use for annotation.
Datasource struct { Datasource struct {
Type *string `json:"type,omitempty"` Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"` Uid *string `json:"uid,omitempty"`
} `json:"datasource"` } `json:"datasource"`
// Whether annotation is enabled. // When enabled the annotation query is issued with every dashboard refresh
Enable bool `json:"enable"` 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"` Hide *bool `json:"hide,omitempty"`
// Annotation icon color. // Color to use for the annotation event markers
IconColor *string `json:"iconColor,omitempty"` IconColor string `json:"iconColor"`
// Name of annotation. // Name of annotation.
Name *string `json:"name,omitempty"` Name string `json:"name"`
// Query for annotation data. // TODO: this should be a regular DataQuery that depends on the selected dashboard
RawQuery *string `json:"rawQuery,omitempty"` // these match the properties of the "grafana" datasouce that is default in most dashboards
ShowIn int `json:"showIn"`
// TODO docs
Target *AnnotationTarget `json:"target,omitempty"` 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 { type AnnotationTarget struct {
Limit int64 `json:"limit"` // Only required/valid for the grafana datasource...
MatchAny bool `json:"matchAny"` // but code+tests is already depending on it so hard to change
Tags []string `json:"tags"` Limit int64 `json:"limit"`
Type string `json:"type"`
// 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. // Dashboard defines model for Dashboard.
type Dashboard struct { type Dashboard struct {
// TODO docs // TODO -- should not be a public interface on its own, but required for Veneer
Annotations *struct { Annotations *AnnotationContainer `json:"annotations,omitempty"`
List []AnnotationQuery `json:"list,omitempty"`
} `json:"annotations,omitempty"`
// Description of dashboard. // Description of dashboard.
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`

View File

@ -70,7 +70,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
Tags: item.Tags, Tags: item.Tags,
IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd, IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd,
Text: item.Text, Text: item.Text,
Color: *anno.IconColor, Color: anno.IconColor,
Time: item.Time, Time: item.Time,
TimeEnd: item.TimeEnd, TimeEnd: item.TimeEnd,
Source: anno, 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 // 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. // which is only intended for tag and org annotations.
if anno.Type == "dashboard" { if anno.Type != nil && *anno.Type == "dashboard" {
event.PanelId = item.PanelID event.PanelId = item.PanelID
} }

View File

@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/intervalv2"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -743,21 +744,21 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{ grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Target: &dashboard2.AnnotationTarget{ Target: &dashboard2.AnnotationTarget{
Limit: 100, Limit: 100,
MatchAny: false, MatchAny: false,
Tags: nil, Tags: nil,
Type: "dashboard", Type: "dashboard",
}, },
Type: "dashboard", Type: util.Pointer("dashboard"),
} }
grafanaTagAnnotation := DashAnnotation{ grafanaTagAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Target: &dashboard2.AnnotationTarget{ Target: &dashboard2.AnnotationTarget{
Limit: 100, Limit: 100,
MatchAny: false, MatchAny: false,
@ -816,8 +817,8 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{ grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Target: &dashboard2.AnnotationTarget{ Target: &dashboard2.AnnotationTarget{
Limit: 100, Limit: 100,
MatchAny: false, MatchAny: false,
@ -876,26 +877,26 @@ func TestFindAnnotations(t *testing.T) {
disabledGrafanaAnnotation := DashAnnotation{ disabledGrafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: false, Enable: false,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
} }
grafanaAnnotation := DashAnnotation{ grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Target: &dashboard2.AnnotationTarget{ Target: &dashboard2.AnnotationTarget{
Limit: 100, Limit: 100,
MatchAny: true, MatchAny: true,
Tags: nil, Tags: nil,
Type: "dashboard", Type: "dashboard",
}, },
Type: "dashboard", Type: util.Pointer("dashboard"),
} }
queryAnnotation := DashAnnotation{ queryAnnotation := DashAnnotation{
Datasource: CreateDatasource("prometheus", "abc123"), Datasource: CreateDatasource("prometheus", "abc123"),
Enable: true, Enable: true,
Name: &name, Name: name,
} }
annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation} annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation}
dashboard := AddAnnotationsToDashboard(t, dash, annos) dashboard := AddAnnotationsToDashboard(t, dash, annos)
@ -975,15 +976,15 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{ grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Target: &dashboard2.AnnotationTarget{ Target: &dashboard2.AnnotationTarget{
Limit: 100, Limit: 100,
MatchAny: false, MatchAny: false,
Tags: nil, Tags: nil,
Type: "dashboard", Type: "dashboard",
}, },
Type: "dashboard", Type: util.Pointer("dashboard"),
} }
annos := []DashAnnotation{grafanaAnnotation} annos := []DashAnnotation{grafanaAnnotation}
dashboard := AddAnnotationsToDashboard(t, dash, annos) dashboard := AddAnnotationsToDashboard(t, dash, annos)
@ -1010,8 +1011,8 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{ grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Target: &dashboard2.AnnotationTarget{ Target: &dashboard2.AnnotationTarget{
Limit: 100, Limit: 100,
MatchAny: false, MatchAny: false,
@ -1039,9 +1040,9 @@ func TestFindAnnotations(t *testing.T) {
grafanaAnnotation := DashAnnotation{ grafanaAnnotation := DashAnnotation{
Datasource: CreateDatasource("grafana", "grafana"), Datasource: CreateDatasource("grafana", "grafana"),
Enable: true, Enable: true,
Name: &name, Name: name,
IconColor: &color, IconColor: color,
Type: "dashboard", Type: util.Pointer("dashboard"),
Target: nil, 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 { 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 { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { DataSourcePicker, getDataSourceSrv, locationService } from '@grafana/runtime'; 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 { ColorValueEditor } from 'app/core/components/OptionsUI/color';
import config from 'app/core/config';
import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor'; import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
@ -21,8 +40,16 @@ type Props = {
export const newAnnotationName = 'New annotation'; export const newAnnotationName = 'New annotation';
export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => { export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
const styles = useStyles2(getStyles);
const [annotation, setAnnotation] = useState(dashboard.annotations.list[editIdx]); 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(() => { const { value: ds } = useAsync(() => {
return getDataSourceSrv().get(annotation.datasource); return getDataSourceSrv().get(annotation.datasource);
}, [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 onApply = goBackToList;
const onPreview = () => { const onPreview = () => {
@ -79,9 +131,30 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
const isNewAnnotation = annotation.name === newAnnotationName; 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 ( return (
<div> <div>
<FieldSet> <FieldSet className={styles.settingsForm}>
<Field label="Name"> <Field label="Name">
<Input <Input
aria-label={selectors.pages.Dashboard.Settings.Annotations.Settings.name} aria-label={selectors.pages.Dashboard.Settings.Annotations.Settings.name}
@ -90,17 +163,10 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
autoFocus={isNewAnnotation} autoFocus={isNewAnnotation}
value={annotation.name} value={annotation.name}
onChange={onNameChange} onChange={onNameChange}
width={50}
/> />
</Field> </Field>
<Field label="Data source" htmlFor="data-source-picker"> <Field label="Data source" htmlFor="data-source-picker">
<DataSourcePicker <DataSourcePicker annotations variables current={annotation.datasource} onChange={onDataSourceChange} />
width={50}
annotations
variables
current={annotation.datasource}
onChange={onDataSourceChange}
/>
</Field> </Field>
<Field label="Enabled" description="When enabled the annotation query is issued every dashboard refresh"> <Field label="Enabled" description="When enabled the annotation query is issued every dashboard refresh">
<Checkbox name="enable" id="enable" value={annotation.enable} onChange={onChange} /> <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} /> <ColorValueEditor value={annotation?.iconColor} onChange={onColorChange} />
</HorizontalGroup> </HorizontalGroup>
</Field> </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> <h3 className="page-heading">Query</h3>
{ds?.annotations && dsi && ( {ds?.annotations && dsi && (
<StandardAnnotationQueryEditor <StandardAnnotationQueryEditor
@ -133,7 +224,11 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
Delete Delete
</Button> </Button>
)} )}
<Button variant="secondary" onClick={onPreview}> <Button
variant="secondary"
onClick={onPreview}
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard}
>
Preview in dashboard Preview in dashboard
</Button> </Button>
<Button variant="primary" onClick={onApply}> <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() { function goBackToList() {
locationService.partial({ editIndex: null }); 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(() => { beforeEach(() => {
// we have a default build-in annotation
dashboard = createDashboardModelFixture({ dashboard = createDashboardModelFixture({
id: 74, id: 74,
version: 7, version: 7,
annotations: { 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,
},
],
},
links: [], links: [],
}); });
}); });
@ -99,7 +87,8 @@ describe('AnnotationsSettings', () => {
setup(dashboard); setup(dashboard);
expect(screen.queryByRole('grid')).toBeInTheDocument(); 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( expect(
screen.getByTestId(selectors.components.CallToActionCard.buttonV2('Add annotation query')) screen.getByTestId(selectors.components.CallToActionCard.buttonV2('Add annotation query'))
).toBeInTheDocument(); ).toBeInTheDocument();
@ -115,7 +104,7 @@ describe('AnnotationsSettings', () => {
).toBeInTheDocument(); ).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 = [
...dashboard.annotations.list, ...dashboard.annotations.list,
{ {

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { AnnotationQuery, EventBus, GrafanaTheme2 } from '@grafana/data'; import { AnnotationQuery, EventBus, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { InlineField, InlineFieldRow, InlineSwitch, useStyles2 } from '@grafana/ui'; import { InlineField, InlineFieldRow, InlineSwitch, useStyles2 } from '@grafana/ui';
import { LoadingIndicator } from '@grafana/ui/src/components/PanelChrome/LoadingIndicator'; import { LoadingIndicator } from '@grafana/ui/src/components/PanelChrome/LoadingIndicator';
@ -44,8 +45,17 @@ export const AnnotationPicker = ({ annotation, events, onEnabledChanged }: Annot
return ( return (
<div key={annotation.name} className={styles.annotation}> <div key={annotation.name} className={styles.annotation}>
<InlineFieldRow> <InlineFieldRow>
<InlineField label={annotation.name} disabled={loading}> <InlineField
<InlineSwitch value={annotation.enable} onChange={() => onEnabledChanged(annotation)} disabled={loading} /> 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> </InlineField>
<div className={styles.indicator}> <div className={styles.indicator}>
<LoadingIndicator loading={loading} onCancel={onCancel} /> <LoadingIndicator loading={loading} onCancel={onCancel} />

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { AnnotationQuery, DataQuery, EventBus } from '@grafana/data'; import { AnnotationQuery, DataQuery, EventBus } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { AnnotationPicker } from './AnnotationPicker'; import { AnnotationPicker } from './AnnotationPicker';
@ -21,7 +22,7 @@ export const Annotations = ({ annotations, onAnnotationChanged, events }: Props)
} }
return ( return (
<> <div data-testId={selectors.pages.Dashboard.SubMenu.Annotations.annotationsWrapper}>
{visibleAnnotations.map((annotation) => ( {visibleAnnotations.map((annotation) => (
<AnnotationPicker <AnnotationPicker
events={events} events={events}
@ -30,6 +31,6 @@ export const Annotations = ({ annotations, onAnnotationChanged, events }: Props)
key={annotation.name} key={annotation.name}
/> />
))} ))}
</> </div>
); );
}; };

View File

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

View File

@ -318,13 +318,11 @@ describe('DashboardModel', () => {
list: [ list: [
{ {
datasource: { uid: 'fake-uid', type: 'prometheus' }, datasource: { uid: 'fake-uid', type: 'prometheus' },
showIn: 0,
name: 'Fake annotation', name: 'Fake annotation',
type: 'dashboard', type: 'dashboard',
iconColor: 'rgba(0, 211, 255, 1)', iconColor: 'rgba(0, 211, 255, 1)',
enable: true, enable: true,
hide: false, hide: false,
builtIn: 0,
}, },
], ],
}, },

View File

@ -46,13 +46,12 @@ export function createPanelJSONFixture(panelInput: Partial<Panel | GraphPanel |
} }
export function createAnnotationJSONFixture(annotationInput: Partial<AnnotationQuery>): AnnotationQuery { export function createAnnotationJSONFixture(annotationInput: Partial<AnnotationQuery>): AnnotationQuery {
// @ts-expect-error
return { return {
builtIn: 0, // ??
datasource: { datasource: {
type: 'foo', type: 'foo',
uid: 'bar', uid: 'bar',
}, },
showIn: 2,
enable: true, enable: true,
type: 'anno', type: 'anno',
...annotationInput, ...annotationInput,

View File

@ -44,8 +44,31 @@ function notifyWithError(title: string, err: any) {
} }
export function getAnnotationsByPanelId(annotations: AnnotationEvent[], panelId?: number) { export function getAnnotationsByPanelId(annotations: AnnotationEvent[], panelId?: number) {
if (panelId == null) {
return annotations;
}
return annotations.filter((item) => { 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 item.panelId === panelId;
} }
return true; return true;

View File

@ -9,7 +9,6 @@ import {
DataQueryRequest, DataQueryRequest,
DataQueryResponse, DataQueryResponse,
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataSourceRef,
isValidLiveChannelAddress, isValidLiveChannelAddress,
MutableDataFrame, MutableDataFrame,
parseLiveChannelAddress, parseLiveChannelAddress,
@ -25,6 +24,7 @@ import {
getTemplateSrv, getTemplateSrv,
StreamingFrameOptions, StreamingFrameOptions,
} from '@grafana/runtime'; } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema';
import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator'; import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator';
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv'; import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
@ -61,7 +61,10 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
datasource = anno.datasource as DataSourceRef; 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 { LiveDataFilter } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { SearchQuery } from 'app/features/search/service'; import { SearchQuery } from 'app/features/search/service';
//---------------------------------------------- //----------------------------------------------

View File

@ -35,7 +35,7 @@ export class EventEditorCtrl {
canDelete(): boolean { canDelete(): boolean {
if (contextSrv.accessControlEnabled()) { 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?.dashboard.canDelete;
} }
return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.organization.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 { usePopper } from 'react-popper';
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data'; import { GrafanaTheme2, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Portal, useStyles2, usePanelContext } from '@grafana/ui'; import { Portal, useStyles2, usePanelContext } from '@grafana/ui';
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins'; import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
@ -127,6 +128,7 @@ export function AnnotationMarker({ annotation, timeZone, width }: Props) {
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
className={!isRegionAnnotation ? styles.markerWrapper : undefined} className={!isRegionAnnotation ? styles.markerWrapper : undefined}
data-testId={selectors.pages.SoloPanel.Annotations.marker}
> >
{marker} {marker}
</div> </div>