Merge remote-tracking branch 'origin/main' into resource-store
This commit is contained in:
+3
-4
@@ -3891,9 +3891,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/explore/Logs/Logs.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/explore/Logs/LogsFeedback.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
@@ -4653,7 +4651,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "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"]
|
||||
],
|
||||
"public/app/features/logs/components/InfiniteScroll.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
|
||||
@@ -1265,6 +1265,10 @@ ha_label =
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
ha_gossip_interval = 200ms
|
||||
|
||||
# Length of time to attempt to reconnect to a lost peer. Recommended to be short (<15m) when Grafana is running in a Kubernetes cluster.
|
||||
# The string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
ha_reconnect_timeout = 6h
|
||||
|
||||
# The interval between gossip full state syncs. Setting this interval lower (more frequent) will increase convergence speeds
|
||||
# across larger clusters at the expense of increased bandwidth usage.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
@@ -1251,6 +1251,10 @@
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;ha_gossip_interval = "200ms"
|
||||
|
||||
# Length of time to attempt to reconnect to a lost peer. Recommended to be short (<15m) when Grafana is running in a Kubernetes cluster.
|
||||
# The string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;ha_reconnect_timeout = 6h
|
||||
|
||||
# The interval between gossip full state syncs. Setting this interval lower (more frequent) will increase convergence speeds
|
||||
# across larger clusters at the expense of increased bandwidth usage.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 271,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"axisSoftMin": 0,
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 1,
|
||||
"fullHighlight": false,
|
||||
"groupWidth": 0.82,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1\nStockholm, 10\nNew York, 19\nLondon, 10\nNegative, 15\nLong value, 15",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "One series",
|
||||
"type": "barchart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"axisSoftMin": 0,
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.97,
|
||||
"fullHighlight": false,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, 5\nLong value, 15,10",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Two series",
|
||||
"type": "barchart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"axisSoftMin": 0,
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 11
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 1,
|
||||
"fullHighlight": false,
|
||||
"groupWidth": 0.82,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "normal",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, 5\nLong value, 15,10",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Two series",
|
||||
"type": "barchart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"axisSoftMin": 0,
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 11
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.7,
|
||||
"fullHighlight": false,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "normal",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, 5\nLong value, 15,10",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Two series",
|
||||
"type": "barchart"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"barchart",
|
||||
"graph-ng"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "BarChart - Panel Tests - Series toggle / bar widths",
|
||||
"uid": "adoero0hbka9sf",
|
||||
"version": 4,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"bar_gauge_demo": (import '../dev-dashboards/panel-bargauge/bar_gauge_demo.json'),
|
||||
"barchart-autosizing": (import '../dev-dashboards/panel-barchart/barchart-autosizing.json'),
|
||||
"barchart-label-rotation-skipping": (import '../dev-dashboards/panel-barchart/barchart-label-rotation-skipping.json'),
|
||||
"barchart-series-toggle": (import '../dev-dashboards/panel-barchart/barchart-series-toggle.json'),
|
||||
"barchart-thresholds-mappings": (import '../dev-dashboards/panel-barchart/barchart-thresholds-mappings.json'),
|
||||
"barchart-tooltips-legends": (import '../dev-dashboards/panel-barchart/barchart-tooltips-legends.json'),
|
||||
"candlestick": (import '../dev-dashboards/panel-candlestick/candlestick.json'),
|
||||
|
||||
@@ -16,13 +16,13 @@ labels:
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Grafana documentation
|
||||
title: Grafana OSS and Enterprise documentation
|
||||
title: Grafana OSS and Enterprise
|
||||
hero:
|
||||
title: Grafana OSS and Enterprise documentation
|
||||
title: Grafana OSS and Enterprise
|
||||
level: 1
|
||||
width: 110
|
||||
width: 100
|
||||
image: /media/docs/grafana-cloud/infrastructure/grafanalogo.svg
|
||||
height: 110
|
||||
height: 100
|
||||
description: Query, visualize, alert on, and explore your metrics, logs, and traces wherever they are stored.
|
||||
cards:
|
||||
title_class: pt-0 lh-1
|
||||
|
||||
@@ -147,4 +147,5 @@ The following metrics can be used for meta monitoring, exposed by the `/metrics`
|
||||
ha_peers = "grafana-alerting.grafana:9094"
|
||||
ha_advertise_address = "${POD_IP}:9094"
|
||||
ha_peer_timeout = 15s
|
||||
ha_reconnect_timeout = 2m
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ To learn more about Grafana Enterprise, refer to [our product page](/enterprise)
|
||||
|
||||
## Enterprise features in Grafana Cloud
|
||||
|
||||
Many Grafana Enterprise features are also available in [Grafana Cloud](/docs/grafana-cloud) Free, Pro, and Advanced accounts. For details, refer to [the Grafana Cloud features table](/pricing/#featuresTable).
|
||||
Many Grafana Enterprise features are also available in [Grafana Cloud](/docs/grafana-cloud) Free, Pro, and Advanced accounts. For details, refer to [Grafana Cloud pricing](/pricing/#featuresTable).
|
||||
|
||||
To migrate to Grafana Cloud, refer to [Migrate from Grafana Enterprise to Grafana Cloud](/docs/grafana-cloud/account-management/e2c-guide/).
|
||||
|
||||
@@ -79,12 +79,10 @@ With a Grafana Enterprise license, you also get access to premium data sources,
|
||||
- [Azure Devops](/grafana/plugins/grafana-azuredevops-datasource)
|
||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
||||
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource/)
|
||||
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)
|
||||
- [Gitlab](/grafana/plugins/grafana-gitlab-datasource)
|
||||
- [Honeycomb](/grafana/plugins/grafana-honeycomb-datasource)
|
||||
- [Jira](/grafana/plugins/grafana-jira-datasource)
|
||||
- [k6 Cloud App](/grafana/plugins/grafana-k6-app)
|
||||
- [MongoDB](/grafana/plugins/grafana-mongodb-datasource)
|
||||
- [New Relic](/grafana/plugins/grafana-newrelic-datasource)
|
||||
- [Oracle Database](/grafana/plugins/grafana-oracle-datasource)
|
||||
|
||||
@@ -28,12 +28,14 @@ refs:
|
||||
|
||||
# Alert list
|
||||
|
||||
Alert lists allow you to display a list of important alerts that you want to track. You can configure the alert list to show the current state of your alert, such as firing, pending, or normal. Learn more about alerts in [Grafana Alerting overview][].
|
||||
Alert lists allow you to display a list of important alerts that you want to track. You can configure the alert list to show the current state of your alert, such as firing, pending, or normal. Learn more about alerts in [Grafana Alerting overview](ref:grafana-alerting-overview).
|
||||
|
||||
{{< figure src="/static/img/docs/alert-list-panel/alert-list-panel.png" max-width="850px" alt="An alert list visualization" >}}
|
||||
|
||||
On each dashboard load, this visualization queries the alert list, always providing the most up-to-date results.
|
||||
|
||||
{{< docs/play title="Alert List" url="https://play.grafana.org/d/bdodlcyou483ke/" >}}
|
||||
|
||||
## Configure an alert list
|
||||
|
||||
Once you’ve created a [dashboard](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/), the following video shows you how to configure an alert list visualization:
|
||||
|
||||
@@ -30,6 +30,8 @@ In version 8.5, we discontinued the "Use Proxy" option for Grafana news visualiz
|
||||
|
||||
You can use the news visualization to provide regular news and updates to your users.
|
||||
|
||||
{{< docs/play title="News Panel" url="https://play.grafana.org/d/cdodkwspaaa68b/" >}}
|
||||
|
||||
## Configure a news visualization
|
||||
|
||||
Once you’ve created a [dashboard](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/), enter the URL of an RSS in the [URL](#url) field in the **News** section. This visualization type doesn't accept any other queries, and you shouldn't expect to be able to filter or query the RSS feed data in any way using this visualization.
|
||||
|
||||
@@ -29,6 +29,8 @@ The following video provides beginner steps for creating node panel visualizatio
|
||||
|
||||
{{< youtube id="VrvsMkRwoKw" >}}
|
||||
|
||||
{{< docs/play title="Node graph panel" url="https://play.grafana.org/d/bdodfbi3d57uoe/" >}}
|
||||
|
||||
## Data requirements
|
||||
|
||||
A node graph requires a specific shape of the data to be able to display its nodes and edges. This means not every data source or query can be visualized with this graph. If you want to use this as a data source developer see the section about data API.
|
||||
|
||||
@@ -65,6 +65,8 @@ For more information about traces and how to use them, refer to the following do
|
||||
|
||||
{{< figure src="/static/img/docs/explore/trace-view-9-4.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view" >}}
|
||||
|
||||
{{< docs/play title="Traces Panel" url="https://play.grafana.org/d/edodkfmj0tce8f/" >}}
|
||||
|
||||
## Add a panel with tracing visualizations
|
||||
|
||||
Once you have tracing data available in your Grafana stack, you can add tracing panels to your Grafana dashboards.
|
||||
|
||||
@@ -1635,6 +1635,12 @@ across cluster more quickly at the expense of increased bandwidth usage. The def
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### ha_reconnect_timeout
|
||||
|
||||
Length of time to attempt to reconnect to a lost peer. When running Grafana in a Kubernetes cluster, set this duration to less than `15m`.
|
||||
|
||||
The string is a possibly signed sequence of decimal numbers followed by a unit suffix (ms, s, m, h, d), such as `30s` or `1m`.
|
||||
|
||||
### ha_push_pull_interval
|
||||
|
||||
The interval between gossip full state syncs. Setting this interval lower (more frequent) will increase convergence speeds
|
||||
|
||||
@@ -23,6 +23,8 @@ weight: -42
|
||||
|
||||
Welcome to Grafana 11.0! This release contains some major improvements: most notably, the ability to explore your Prometheus metrics and Loki logs without writing any PromQL or LogQL, using Explore Metrics and Explore Logs. The dashboard experience is better than ever with edit mode for dashboards, AI-generated dashboard names and descriptions, and general availability for subfolders. You can also take advantage of improvements to the canvas and table visualizations, new transformations, a revamp of the Alert Rule page, and more.
|
||||
|
||||
{{< youtube id="96ZXShRyMB8" >}}
|
||||
|
||||
For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v11.0, check out our [Upgrade Guide](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/upgrade-guide/upgrade-v11.0/).
|
||||
|
||||
## Breaking changes
|
||||
|
||||
@@ -33,10 +33,12 @@ describe('Keyboard shortcuts', () => {
|
||||
zone: 'Browser',
|
||||
});
|
||||
e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query');
|
||||
let expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`;
|
||||
e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange);
|
||||
|
||||
cy.get('body').type('{ctrl}z');
|
||||
e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query');
|
||||
let expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`;
|
||||
expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`;
|
||||
e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange);
|
||||
});
|
||||
|
||||
@@ -51,11 +53,13 @@ describe('Keyboard shortcuts', () => {
|
||||
zone: 'Browser',
|
||||
});
|
||||
e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query');
|
||||
let expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`;
|
||||
e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange);
|
||||
|
||||
cy.log('Trying one shift-left');
|
||||
cy.get('body').type('t{leftarrow}');
|
||||
e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query');
|
||||
let expectedRange = `Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00`; // 1 min back
|
||||
expectedRange = `Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00`; // 1 min back
|
||||
e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange);
|
||||
|
||||
cy.log('Trying two shift-lefts');
|
||||
|
||||
@@ -15,6 +15,14 @@ export const pluginVersion = "11.1.0-pre";
|
||||
export interface Options {
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
/**
|
||||
* TODO: figure out how to define callbacks
|
||||
*/
|
||||
onClickFilterLabel?: unknown;
|
||||
onClickFilterOutLabel?: unknown;
|
||||
onClickFilterOutString?: unknown;
|
||||
onClickFilterString?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showLabels: boolean;
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ var DependencyError = errutil.NewBase(
|
||||
depErrStr,
|
||||
errutil.WithPublic(depErrStr))
|
||||
|
||||
func makeDependencyError(refID, depRefID string) error {
|
||||
func MakeDependencyError(refID, depRefID string) error {
|
||||
data := errutil.TemplateData{
|
||||
Public: map[string]interface{}{
|
||||
"refId": refID,
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (m
|
||||
if res, ok := vars[neededVar]; ok {
|
||||
if res.Error != nil {
|
||||
errResult := mathexp.Results{
|
||||
Error: makeDependencyError(node.RefID(), neededVar),
|
||||
Error: MakeDependencyError(node.RefID(), neededVar),
|
||||
}
|
||||
vars[node.RefID()] = errResult
|
||||
hasDepError = true
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
#name,created,deleted,hash,author
|
||||
newNavigation,2022-01-26T17:44:20Z,2022-06-16T09:48:38Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
trimDefaults,2022-01-26T17:44:20Z,2023-11-02T15:35:14Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
dashboardPreviews,2022-01-26T17:44:20Z,2023-04-13T17:42:24Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
live-config,2022-01-26T17:44:20Z,2023-02-03T21:21:48Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
accesscontrol,2022-01-26T17:44:20Z,2022-05-16T10:45:41Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
tempoServiceGraph,2022-01-26T17:44:20Z,2022-07-19T07:00:58Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
httpclientprovider_azure_auth,2022-01-26T17:44:20Z,2022-05-30T15:43:32Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
tempoBackendSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
showFeatureFlagsInUI,2022-01-26T17:44:20Z,2023-02-09T00:01:34Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
queryOverLive,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
validatedQueries,2022-01-26T17:44:20Z,2022-05-16T21:17:05Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
fullRangeLogsVolume,2022-01-26T17:44:20Z,2022-02-15T08:05:03Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
prometheus_azure_auth,2022-01-26T17:44:20Z,2022-08-11T14:12:57Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
live-service-web-worker,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
envelopeEncryption,2022-01-26T17:44:20Z,2022-05-24T08:34:47Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
database_metrics,2022-01-26T17:44:20Z,2023-04-28T13:19:06Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
live-pipeline,2022-01-26T17:44:20Z,2023-03-22T18:09:44Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
service-accounts,2022-01-26T17:44:20Z,2022-04-21T09:41:37Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
tempoSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
disable_http_request_histogram,2022-01-26T17:44:20Z,2022-06-01T12:33:59Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
|
||||
featureHighlights,2022-02-03T11:53:23Z,,a79c048344bddff7a868b040d9a08953917480f9,Alex Khomenko
|
||||
lokiBackendMode,2022-02-07T07:43:48Z,2022-06-08T06:14:34Z,560c77390550e12e8c0be507c00f27cde0aa31e5,Gábor Farkas
|
||||
swaggerUi,2022-02-08T12:38:43Z,2023-03-01T14:36:37Z,35fe58de374003bb4b077a878cc47ffc0a9d27b5,Sofia Papagiannaki
|
||||
influxdbBackendMigration,2022-02-09T18:26:16Z,2023-01-17T14:11:26Z,10232c78577520ce7b3fc6bba86d2e48ee5bab88,Joey Tawadrous
|
||||
dashboardPreviewsScheduler,2022-02-10T18:45:00Z,2022-04-24T21:55:10Z,0276b029fc656e61763f6ae045ad7826380a43fc,Artur Wierzbicki
|
||||
migrationLocking,2022-02-15T16:54:27Z,2024-03-22T19:22:29Z,d718ee191834b3084b2e8c8c6cac473bd9b97681,Sofia Papagiannaki
|
||||
panelTitleSearch,2022-02-15T18:26:03Z,,d665306ad1c2abe515c7ba48350904f749eafc18,Ryan McKinley
|
||||
dashboardPreviewsAdmin,2022-02-17T08:34:07Z,2023-02-03T21:45:10Z,3e53a08090ad8ac61d7d316b97a93a7a5ea63a57,Artur Wierzbicki
|
||||
annotationComments,2022-02-22T07:47:42Z,2023-03-11T12:28:12Z,28c30a34adbe94d0f08acdba3c1605143dce6c6c,Alexander Emelin
|
||||
dashboardComments,2022-02-22T07:47:42Z,2023-03-11T12:28:12Z,28c30a34adbe94d0f08acdba3c1605143dce6c6c,Alexander Emelin
|
||||
lokiLive,2022-03-01T22:46:52Z,2023-06-19T10:03:51Z,796bc27f75d52148d5b15cc8c4901276c344df2d,Ryan McKinley
|
||||
fileStoreApi,2022-03-03T06:53:26Z,2022-03-11T18:08:19Z,a8b90d9a2524765c49923c48a7fcf0025c85b733,Artur Wierzbicki
|
||||
azureMonitorResourcePickerForMetrics,2022-03-14T19:07:45Z,2023-01-30T16:19:03Z,275f33cf37bb4221ef120310a87c17d2e0ff5f35,Sarah Zinger
|
||||
storageLocalUpload,2022-03-17T17:19:23Z,2022-07-18T17:44:42Z,1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f,Ryan McKinley
|
||||
storage,2022-03-17T17:19:23Z,,1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f,Ryan McKinley
|
||||
saveDashboardDrawer,2022-03-30T17:07:41Z,2022-05-02T16:29:22Z,edf384c730a448a5e90b21bbbce49bc0e70e8197,Ryan McKinley
|
||||
accesscontrol-builtins,2022-03-31T09:40:57Z,2022-05-19T07:29:36Z,0d87de153a2b8406d03efe0cf43b5a926ce7acab,Gabriel MABILLE
|
||||
alertProvisioning,2022-04-01T06:32:00Z,2022-06-05T05:45:36Z,b8e277ee4c070b64ba4cf024b18c460626d1a2d3,Alexander Weaver
|
||||
explore2Dashboard,2022-04-07T08:26:01Z,2022-11-07T16:06:40Z,ca286a238d1e9ce1e5357fde8148476a9d618cee,Giordano Ricci
|
||||
publicDashboards,2022-04-07T18:30:19Z,,4a4d87dbdcaf4dca753bcf29205ac036b6c59862,Jeff Levin
|
||||
persistNotifications,2022-04-20T09:42:32Z,2022-05-26T11:03:04Z,c48d8d1d488e0ce5870c54eb5828660081743108,kay delaney
|
||||
serviceAccounts,2022-04-21T09:41:37Z,2022-06-27T08:22:49Z,8677552dda7b63f5d44df6fe51ca8a28726c5a5c,Eric Leijonmarck
|
||||
commandPalette,2022-04-21T21:50:34Z,2023-02-02T14:44:21Z,3e7db088acd1da32ea489b0756cdf4e15ec2da69,Kristina
|
||||
export,2022-04-25T23:59:18Z,2023-03-01T17:42:53Z,e0aeb83786731769e870d79c0d8a21ef2c33b073,Ryan McKinley
|
||||
savedItems,2022-04-26T10:27:01Z,2022-06-27T14:41:00Z,e420252d45cd48fdcf5cc68ad84f89e36d5c936c,Ashley Harrison
|
||||
tracing,2022-04-26T13:31:27Z,2023-03-21T09:35:21Z,3b4d237ade03e67a971ca8899e8e0371e1d83f2a,kay delaney
|
||||
cloudWatchDynamicLabels,2022-04-29T09:43:04Z,2023-05-02T08:48:17Z,7bb4f5cd9baff81b354e4bb65fd4b6141e3488f6,Shirley
|
||||
datasourceQueryMultiStatus,2022-05-03T16:02:20Z,,4ecd57f49c75d72927be62aba23a15dca02f8eb6,Will Browne
|
||||
azureMonitorExperimentalUI,2022-05-04T13:54:09Z,2022-07-14T13:07:31Z,b2644de6c8a08181cbe0a9f545393bd42753477c,Adam Simpson
|
||||
traceToMetrics,2022-05-05T20:46:18Z,2024-02-22T13:30:41Z,c1b5ea3e54169c7a4bbd58bd8be2c4c192a093c0,Connor Lindsey
|
||||
prometheusStreamingJSONParser,2022-05-13T18:28:54Z,2022-11-11T16:53:12Z,87e8521591534bcfd8a7c78fbd39b226c13a99b2,Todd Treece
|
||||
validateDashboardsOnSave,2022-05-22T00:44:12Z,2023-09-06T15:25:44Z,a3402641d6cac01208179126dcbd2256ad500cd8,sam boyer
|
||||
disableEnvelopeEncryption,2022-05-24T08:34:47Z,,3e4b4dba46e5310015c7de5c3de1f3fae5332cb8,Joan López de la Franca Beltran
|
||||
prometheusWideSeries,2022-05-24T20:17:11Z,2023-08-24T14:47:19Z,94b9c524a81df2085ad769229ca09cfd1923a8e9,Todd Treece
|
||||
prometheusAzureOverrideAudience,2022-05-30T15:43:32Z,2023-07-16T21:30:14Z,2b83cf46186f745e356a24e4c78a1fbc24b3051e,Sergey Kostrukov
|
||||
canvasPanelNesting,2022-05-31T19:03:34Z,,e18e8002c4dda17e95cb4f04977a6aefcc5984a4,Adela Almasan
|
||||
cloudMonitoringExperimentalUI,2022-06-09T13:56:32Z,2022-09-15T14:12:26Z,615132002044e56074be63c5b597b9982e0a2a21,Kevin Yu
|
||||
tempoApmTable,2022-06-09T16:56:15Z,2023-02-01T15:56:34Z,4ed7ff2ed13f2b5ec79f0946670c945edc902d7b,Joey Tawadrous
|
||||
logRequestsInstrumentedAsUnknown,2022-06-10T08:56:55Z,,2d6e69226be20a7f001c7792bda8df68d90287c6,Carl Bergquist
|
||||
dataConnectionsConsole,2022-06-10T10:13:31Z,2024-02-29T09:29:41Z,9a85a2e4410266270a1cd59db1937c7ba248966e,Levente Balogh
|
||||
internationalization,2022-06-10T11:02:52Z,2023-06-02T08:24:40Z,9a62849dc39e960640b5762cc5eace1baf29c0ee,Josh Hunt
|
||||
autoMigrateGraphPanels,2022-06-11T00:12:56Z,2023-03-23T04:02:36Z,ae8dd737704bb6232c52e19090e593d9165f5e77,Ryan McKinley
|
||||
lokiDataframeApi,2022-06-13T06:33:46Z,2023-04-13T13:22:09Z,8fd9cb48548313cf43ebcf5dcfa0daad0bfdfe0c,Gábor Farkas
|
||||
topnav,2022-06-20T14:25:43Z,,3c3293df78344a782fb65e5f977fd148daa597ce,Torkel Ödegaard
|
||||
customBranding,2022-06-22T15:05:52Z,2022-10-05T12:07:35Z,405df77e3e6abcf5264ba192abe33630bd64da20,Tania
|
||||
useLegacyHeatmapPanel,2022-06-23T18:48:28Z,2022-11-23T18:46:21Z,dd5a3b77472884035de13ddd441f41d0dd007e4b,Ryan McKinley
|
||||
scenes,2022-07-07T06:53:02Z,,935334cbdabef8b0516bfe6c8ed45dd063cce7e9,Torkel Ödegaard
|
||||
disableSecretsCompatibility,2022-07-12T20:27:37Z,,2d8a91a8461098f83aa512c867aff5af17b7893e,Guilherme Caulada
|
||||
dashboardsFromStorage,2022-07-14T22:36:17Z,2023-03-20T16:36:49Z,da1701ce576ab26506d6256567b73a597043623a,Ryan McKinley
|
||||
exploreMixedDatasource,2022-07-27T14:40:59Z,2022-07-27T15:17:31Z,e2258120e742b31ecde50e8de93544220a0762a3,Kristina
|
||||
prometheusStreamingJSONParserTest,2022-07-28T12:26:51Z,2022-11-07T18:20:00Z,d0e548c3e53ffdbc29f35e055d52aa63cca310db,Andrej Ocenas
|
||||
fullstoryUserTracking,2022-08-16T08:32:00Z,2022-08-16T10:45:14Z,961479b1115d39f3918b60c49a48841916939f03,Ivan Ortega Alba
|
||||
traceqlEditor,2022-08-24T16:57:59Z,2022-12-13T13:27:45Z,c8f2148f7504b393b8f9b32fe19e7f519fa66793,Andre Pereira
|
||||
athenaAsyncQueryDataSupport,2022-09-05T15:39:45Z,2024-03-20T14:14:21Z,34fe7a1119c0f649d3e9df866353438ff2177fdc,Kevin Yu
|
||||
redshiftAsyncQueryDataSupport,2022-09-05T15:39:45Z,2024-03-20T14:14:21Z,34fe7a1119c0f649d3e9df866353438ff2177fdc,Kevin Yu
|
||||
increaseInMemDatabaseQueryCache,2022-09-12T07:50:54Z,2023-02-06T21:01:04Z,72ae4a5aa368d18793f778683e92ae4a3ac7a1db,Carl Bergquist
|
||||
correlations,2022-09-16T13:14:27Z,,9b4cdfe6526a290e5708a8c0760d50efda607d5c,Piotr Jamróz
|
||||
grpcServer,2022-09-26T20:25:34Z,,55aae797441d2beadc911e390f14c04bb3f101da,Alexander Emelin
|
||||
alertingBigTransactions,2022-10-06T06:22:58Z,2023-04-06T16:06:25Z,b476ae62fba9b94dd272409c521812108750bf50,Joe Blubaugh
|
||||
lokiMonacoEditor,2022-10-06T14:35:30Z,2023-02-06T10:18:01Z,729ce8bb72a5db53fbeb1b5856557652bb175c7a,Matias Chomicki
|
||||
objectStore,2022-10-06T19:48:53Z,2022-11-30T22:52:15Z,609abf00d1c6bd065f163e06882aa2f09dc0fcb5,Ryan McKinley
|
||||
flameGraph,2022-10-07T10:39:14Z,2023-03-01T11:32:39Z,74c809f54459d235f4d3e9b1c117f291cbf16339,Joey Tawadrous
|
||||
queryLibrary,2022-10-07T18:31:45Z,2023-03-20T16:00:14Z,bf264d2f76a1efb902b41791e110077d5c3a5557,Artur Wierzbicki
|
||||
newPanelChromeUI,2022-10-10T15:03:16Z,2023-09-12T08:54:41Z,129a5a29e78853bb1427c8dc7473125ba2a35981,Polina Boneva
|
||||
mysqlAnsiQuotes,2022-10-12T11:43:35Z,,80ede174dd6e343722cc708ebc4c5f21fb9e90af,ying-jeanne
|
||||
showDashboardValidationWarnings,2022-10-14T13:51:05Z,,2e16d5499e1cf29e67db5031feda09407f69550c,Josh Hunt
|
||||
interFont,2022-10-15T14:22:33Z,2022-12-01T11:59:37Z,9f5e691994c9db15f5984b196ab55fd7213b7f72,Torkel Ödegaard
|
||||
accessControlOnCall,2022-10-19T16:10:09Z,,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE
|
||||
newDBLibrary,2022-10-26T01:20:41Z,2023-11-14T14:51:35Z,a3acfb1a48126119fd4913efbb572de162264e92,Ryan McKinley
|
||||
nestedFolders,2022-10-26T14:15:14Z,,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert
|
||||
datasourceLogger,2022-11-02T13:51:51Z,2023-02-07T11:49:16Z,06705a49e236dc3ab3b35db51b7bad8625e4866f,Carl Bergquist
|
||||
promQueryBuilder,2022-11-03T17:34:01Z,2022-12-19T13:52:06Z,857e545c5ac71722c8b2d3d27621dfb453dbb8ff,Ryan McKinley
|
||||
elasticsearchBackendMigration,2022-11-10T15:35:15Z,2023-04-12T12:20:43Z,261d620f1c46eb43282cc444ffb4633b77af4283,Ivana Huckova
|
||||
prometheusBufferedClient,2022-11-11T16:53:12Z,2022-12-12T17:05:55Z,1c5039085bdf9252d0aa1666675f5313589c1703,ismail simsek
|
||||
accessTokenExpirationCheck,2022-11-14T15:47:46Z,2023-12-15T12:20:17Z,4915d21c25891dae2ee7a4eec4fe0dd2659d614d,Misi
|
||||
cloudWatchCrossAccountQuerying,2022-11-28T11:39:12Z,,254577ba568d8010dcc96446e788866d618c96c8,Erik Sundell
|
||||
authnService,2022-11-29T09:57:47Z,2023-06-07T06:57:41Z,17ec4089dc75da0e54edf60a754a0fc9291275e3,Karl Persson
|
||||
secureSocksDatasourceProxy,2022-11-30T05:50:59Z,2023-04-18T17:11:23Z,6805c951e94c626401770ed468c6afb462093c0d,Stephanie Hingtgen
|
||||
entityStore,2022-11-30T22:52:15Z,2023-12-06T20:21:21Z,14a080ec122f8ff3f656f44a46683f5161c15e14,Ryan McKinley
|
||||
sessionRemoteCache,2022-12-07T09:55:48Z,2023-02-06T12:51:12Z,df4f0343e587695f37fa7a6de04cc884af4df284,Jo
|
||||
returnUnameHeader,2022-12-07T12:14:53Z,2023-02-07T09:04:20Z,1b676d0d49460572352af4639274a4e7fe03fc9b,Carl Bergquist
|
||||
datasourceOnboarding,2022-12-07T23:29:38Z,2023-05-17T12:22:40Z,955bf55c6a6a710a2fd7ebdbdac2d9a826858eaf,kay delaney
|
||||
k8s,2022-12-13T15:41:16Z,2023-04-26T13:52:13Z,cf055ab4ecda9db9427ae09560aa26553433a5c0,Ryan McKinley
|
||||
alertingBacktesting,2022-12-14T14:44:14Z,,ad09feed837737c188e37fc6adfead7b8a45715e,Yuri Tseretyan
|
||||
disablePrometheusExemplarSampling,2022-12-19T15:00:15Z,2024-01-30T16:00:04Z,f67b8fe0dc2a2d860e71887a4d2e6e25da80bd8f,Ludovic Viaud
|
||||
supportBundles,2022-12-20T10:13:37Z,2023-02-10T09:12:04Z,2c7410c87d81b8a644e738fee2bb6f34355111c6,Jo
|
||||
publicDashboardsEmailSharing,2023-01-03T19:45:15Z,,f0ee3ac80ae2af695c56e709af435c155a2fb33d,owensmallwood
|
||||
alertingNoNormalState,2023-01-13T23:29:29Z,,9d57b1c72eb817c5fe1b43ad7bc9706a264b2da0,Yuri Tseretyan
|
||||
azureMultipleResourcePicker,2023-01-20T15:29:23Z,2023-01-30T16:19:03Z,b1efd911c1f7bfabe1b8231b059470c72dce4712,Andres Martinez Gotor
|
||||
editPanelCSVDragAndDrop,2023-01-24T09:43:44Z,,4167214e3535f41c85004c6aca86fcb9ae8956dd,Zoltán Bedi
|
||||
topNavCommandPalette,2023-01-24T12:41:09Z,2023-02-24T12:14:53Z,88347caf5fcc99c21c8a07367b9e846947367636,Josh Hunt
|
||||
k8sDashboards,2023-01-25T19:10:16Z,2023-02-09T17:54:00Z,4965cf2edae6eae8e341e6c93c3a1aa56417efb3,Ryan McKinley
|
||||
logsContextDatasourceUi,2023-01-27T14:12:01Z,,7c02d9bb8a864aab35c676f1a44c6c5be9eb6c09,Sven Grossmann
|
||||
logsSampleInExplore,2023-01-27T16:30:25Z,2023-07-24T12:24:43Z,d5294eb8fa71ba62546d00ebce7b9376ab6f2b1a,Ivana Huckova
|
||||
apiserver,2023-02-01T20:28:19Z,2023-02-09T17:54:00Z,d4f4a83574dd03a5419fa890b916d09d03cb9acd,Ryan McKinley
|
||||
lokiQuerySplitting,2023-02-09T17:27:02Z,,94241f6676cf36824a180c11c2824ad41c81a470,Matias Chomicki
|
||||
individualCookiePreferences,2023-02-21T10:19:07Z,,0caacb3333152015bf0369f54315a26ff6f60f88,Emil Tullstedt
|
||||
newTraceView,2023-02-28T15:41:40Z,2023-04-21T10:31:24Z,82bcfb492867138bcc4744f88d36eb081c1b6811,Joey
|
||||
drawerDataSourcePicker,2023-03-01T10:26:19Z,2023-04-14T11:01:10Z,dc1600ff14acc3deecc5de02f6584dcf20d3c8b1,Oscar Kilhed
|
||||
traceqlSearch,2023-03-06T16:31:08Z,2023-07-24T15:26:10Z,fd37ff29b57520036fdc02f8a93a7d74770fd153,Andre Pereira
|
||||
prometheusMetricEncyclopedia,2023-03-07T18:41:05Z,,9b6e531549e145d1a1a0ede870a6163c69b0bfd5,Brendan O'Handley
|
||||
timeSeriesTable,2023-03-10T12:41:06Z,2023-10-13T08:00:42Z,548a5054ad8fa5c199bd01e20a02fbbe558d52fe,Domas
|
||||
lokiQuerySplittingConfig,2023-03-20T15:51:36Z,,68551ac9ca5b92621e6ab895c759822797bbf418,Sven Grossmann
|
||||
onlyExternalOrgRoleSync,2023-03-22T17:41:59Z,2023-07-25T09:51:47Z,3cd952b8bad8d23daee10fcc303a6d19c8b6d0a0,Eric Leijonmarck
|
||||
autoMigrateOldPanels,2023-03-23T04:02:36Z,,baf5a1d1411ee7e5f297aba099e735ebeee3ab83,Ryan McKinley
|
||||
clientTokenRotation,2023-03-23T13:39:04Z,2024-02-16T14:03:37Z,382b24742ab8b124af072533f74027a1a103b037,Karl Persson
|
||||
disableAngular,2023-03-23T15:43:45Z,,58704390269c8eeedd32c571245d5ef08f0ffb82,Ryan McKinley
|
||||
disableElasticsearchBackendExploreQuery,2023-03-27T13:52:27Z,2023-04-12T12:20:43Z,f3da91f53fa32a8e870124e9514a010b97359529,Ivana Huckova
|
||||
emptyDashboardPage,2023-03-28T09:42:23Z,2024-01-25T14:04:29Z,221c5efedc593fb42d87c849b3d0827e325fc6cd,Polina Boneva
|
||||
prometheusDataplane,2023-03-29T15:26:32Z,,674144c8e84cb4aca3bfc448f08b88bb516d3b77,Kyle Brandt
|
||||
alertStateHistoryLokiOnly,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver
|
||||
alertStateHistoryLokiPrimary,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver
|
||||
alertStateHistoryLokiSecondary,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver
|
||||
unifiedRequestLog,2023-03-31T13:38:09Z,,be9361cb9ec2f1ad0bc0cda139c56e3cd46deb16,Emil Tullstedt
|
||||
prometheusResourceBrowserCache,2023-04-03T14:07:17Z,2023-10-11T13:18:56Z,96e9e807396b600c3325f4d3580eeef0710f6050,Galen Kistler
|
||||
renderAuthJWT,2023-04-03T16:53:38Z,,87a0c95164074dce6a877ea3faa3c7ac0e705ad3,Joan López de la Franca Beltran
|
||||
showTraceId,2023-04-05T07:13:24Z,2023-07-20T05:59:38Z,4ded937c79a38087576b3224bf6f2ba59345571e,Gábor Farkas
|
||||
pyroscopeFlameGraph,2023-04-05T12:57:24Z,2023-08-02T15:01:12Z,37eaf5019751aa76568574757830d63b764bbb1f,Andrej Ocenas
|
||||
dataplaneFrontendFallback,2023-04-07T21:13:19Z,,af31c77331c89c6a1f8e00747c44c3f39bb737bb,Ryan McKinley
|
||||
externalServiceAuth,2023-04-11T13:21:19Z,2024-02-26T10:29:09Z,5df9e64986e8c0324b4da96de5b55e533c3990ed,Gabriel MABILLE
|
||||
disableElasticsearchBackendQuerying,2023-04-12T12:20:43Z,2023-04-14T09:24:35Z,303777e682758076d6a8e1df6b82f97b3b2393a7,Ivana Huckova
|
||||
disableSSEDataplane,2023-04-12T16:24:34Z,,e78be44e1a38b7afb85909b807b6a2e9e5a98830,Kyle Brandt
|
||||
useCachingService,2023-04-12T16:30:33Z,2024-01-17T21:53:25Z,5626461b3c358f5c5c5c690b82977c09744d09b4,Michael Mandrus
|
||||
lokiMetricDataplane,2023-04-13T13:07:08Z,,531caec60263738ed6b31878b1ec093de635e4e2,Gábor Farkas
|
||||
authenticationConfigUI,2023-04-13T14:07:43Z,2023-06-14T16:58:15Z,7476219b0c2876f60bc9c6e28b22190f239e1426,Alexander Zobnin
|
||||
enableElasticsearchBackendQuerying,2023-04-14T09:24:35Z,2024-06-05T15:03:29Z,f48c858ca22b3f246d1033670f14605adb13cdf3,Ivana Huckova
|
||||
advancedDataSourcePicker,2023-04-14T11:01:10Z,2024-02-06T09:20:07Z,7bc31ab04b5c87f06bdb383c7f4a6195e70d53d1,Ivan Ortega Alba
|
||||
opensearchDetectVersion,2023-04-14T17:59:31Z,2023-05-16T14:22:23Z,1b0cee491a06fa135936b883406a6b11e6221322,Isabella Siu
|
||||
pluginsAPIManifestKey,2023-04-18T14:12:05Z,2023-06-16T09:20:30Z,98c695c68fb3f0d276da0d98b1e78186e459db50,Andres Martinez Gotor
|
||||
newTraceViewHeader,2023-04-21T10:31:24Z,2023-07-19T13:31:58Z,6522bb377e1bc4b64fe0aebd7e34ec2111859e11,Joey
|
||||
enableDatagridEditing,2023-04-24T14:46:31Z,,efd0e9cbea4a6c4c51337f14004f2df2f876fdb1,Victor Marin
|
||||
faroDatasourceSelector,2023-05-05T00:35:10Z,,e7cbe0276e3a6f41c31dd8e1ac16ccaa90b184f3,Elliot Kirk
|
||||
extraThemes,2023-05-10T13:37:04Z,,f8cf67347f069310b8cc6a2d9ff83345a81e3247,Torkel Ödegaard
|
||||
dataSourcePageHeader,2023-05-23T13:18:00Z,2023-11-06T20:21:15Z,7f84e83ffee53446b4c458c29f7c1385dac09ce3,Taewoo K
|
||||
alertingNotificationsPoliciesMatchingInstances,2023-05-30T13:15:22Z,2023-11-09T17:35:03Z,2f0728ac677c26b291947354058253d2e8a50bb7,Konrad Lalik
|
||||
lokiPredefinedOperations,2023-06-02T10:52:36Z,,06003c98c855fcb7c1c718c3f0e5f190b644656a,Ivana Huckova
|
||||
pluginsFrontendSandbox,2023-06-05T08:51:36Z,,1ed4c0382b3812eecf9722596d8a3ac922f3543d,Esteban Beltran
|
||||
refactorVariablesTimeRange,2023-06-06T13:12:09Z,,07dd90b5a8866273813c6b4a5e7087cea529f908,Alexa V
|
||||
sqlDatasourceDatabaseSelection,2023-06-06T16:28:52Z,,c0a1fc2cbdc1ceab4fdcb43270fd3720efe4494a,Jev Forsberg
|
||||
cloudWatchLogsMonacoEditor,2023-06-12T13:49:52Z,2024-03-18T12:56:57Z,5a831d877ac3305e7dfdb54057069b41b36b40fa,Isabella Siu
|
||||
recordedQueriesMulti,2023-06-14T12:34:22Z,,db75f20e53e717503fe3bf27217fedc40f4a12e1,Kyle Brandt
|
||||
exploreScrollableLogsContainer,2023-06-15T11:25:34Z,2024-03-12T14:53:13Z,cda10fae525d5cf784368791e1aaa1de2ce88f4b,Gareth Dawson
|
||||
alertingLokiRangeToInstant,2023-06-16T17:55:49Z,2023-09-13T11:52:40Z,934ba1aaa1fa4c1b9f1dcc0d6753c67a23c4f466,Jean-Philippe Quéméner
|
||||
lokiExperimentalStreaming,2023-06-19T10:03:51Z,,271cdb4baa6d5b021e013b463350f227173c4e95,Gábor Farkas
|
||||
flameGraphV2,2023-06-19T14:34:06Z,2023-07-11T12:58:43Z,5ca03a82f042808efabae84b46991106c23de514,Andrej Ocenas
|
||||
pluginsDynamicAngularDetectionPatterns,2023-06-26T13:33:21Z,2024-04-15T08:37:28Z,cca9d897339ae2de087bc5b7177849a730d4b38d,Giuseppe Guerra
|
||||
elasticToggleableFilters,2023-06-27T08:38:20Z,2023-07-28T12:49:02Z,c1ce24c90f75daa133e2cb59288ecc14195e5194,Matias Chomicki
|
||||
vizAndWidgetSplit,2023-06-27T10:22:13Z,,2785ed80d999b9ee1770e1209284d4fccd985401,Alexa V
|
||||
nestedFolderPicker,2023-06-28T09:40:29Z,2024-06-04T09:16:12Z,f18a7f7d9696a6e80606dc49d5ac0064384f111d,Josh Hunt
|
||||
frontendSandboxMonitorOnly,2023-07-05T11:48:25Z,,72f6793344fb3a63f5a8a86570b786b518264366,Esteban Beltran
|
||||
prometheusIncrementalQueryInstrumentation,2023-07-05T19:39:49Z,,daf9f9cd199e0bbc110222a3f005dc05dab1681a,Galen Kistler
|
||||
dashboardEmbed,2023-07-06T14:43:20Z,2024-04-19T10:48:08Z,420b19e0e4bbdb97ae707cc1360bef7f79839d02,Alex Khomenko
|
||||
awsDatasourcesTempCredentials,2023-07-06T15:06:11Z,,d33508453f6f1f7aab262b54c36ef471ed3eab87,Ida Štambuk
|
||||
logsExploreTableVisualisation,2023-07-12T13:52:42Z,,7e4e743a42052183f549f7fa88cd0cc362844ad1,Sven Grossmann
|
||||
transformationsRedesign,2023-07-12T16:35:49Z,,5099e882276227f4b4c8e695f35156b7873d81b3,Ludovic Viaud
|
||||
lokiLogsDataplane,2023-07-13T07:58:00Z,,e045860fbf449feb101d187730a46bb1cdab787a,Gábor Farkas
|
||||
mlExpressions,2023-07-13T17:37:50Z,,541bfe636daa55705a130df4004aa417c99adc09,Yuri Tseretyan
|
||||
disableTraceQLStreaming,2023-07-14T14:10:46Z,2023-07-26T13:33:16Z,c1709c93013a3fa5797898e81c046a2a53ae7ed4,Andre Pereira
|
||||
grafanaAPIServer,2023-07-14T19:22:10Z,2024-01-23T16:27:28Z,52121b7165b6ffcdf10dd7a62fdf04043ef0c00a,Todd Treece
|
||||
featureToggleAdminPage,2023-07-18T20:43:32Z,,600f623610b00547400e00fd0b2ee7dc4bab5849,Ibrahim
|
||||
lokiFormatQuery,2023-07-21T12:03:56Z,2024-06-05T09:46:28Z,4e42f9b6198d39ffca82c3c3d32a16d54c66090b,Gareth Dawson
|
||||
splitScopes,2023-07-21T14:23:01Z,2024-02-23T15:53:37Z,cfa1a2c55feea58bd7f82e4e03df2ea712ef34a7,Ieva
|
||||
awsAsyncQueryCaching,2023-07-21T15:34:07Z,,56913fbd9578c4666599b2ab46423361d6c9261f,Isabella Siu
|
||||
toggleLabelsInLogsUI,2023-07-24T08:22:47Z,2023-10-27T11:00:49Z,84f94cdc24c3b911ae4f8eabfe875b3e4a9c1f1b,Matias Chomicki
|
||||
azureMonitorDataplane,2023-07-24T13:50:49Z,2023-11-08T15:02:46Z,ee60d8c82dbcc8496d52d673bfc000901d72f232,Kyle Brandt
|
||||
gcomOnlyExternalOrgRoleSync,2023-07-25T12:27:02Z,2023-11-13T09:56:02Z,f7c6491f7317f3fbaf4f21b8beec967d76778a87,Ieva
|
||||
traceQLStreaming,2023-07-26T13:33:16Z,,89092a1e697788428f76fead65371b7c904cd13f,Andre Pereira
|
||||
prometheusConfigOverhaulAuth,2023-07-26T16:09:53Z,,98cb3ce3b63ee4d1b82e6b04d11f8a6a065de7b4,Brendan O'Handley
|
||||
configurableSchedulerTick,2023-07-26T16:44:12Z,,c7598cc6fb492cb7cc34eec90fbd2a08d859cffd,Yuri Tseretyan
|
||||
permissionsFilterRemoveSubquery,2023-08-02T07:39:25Z,,2c26a02b82238e0f89ca08844914cff22a84255a,Sofia Papagiannaki
|
||||
influxdbSqlSupport,2023-08-02T16:27:43Z,2024-04-18T14:29:27Z,77e7ae2a1b253276ceae4ef5cda1dffc0e674b4d,ismail simsek
|
||||
noBasicRole,2023-08-04T09:08:14Z,2023-10-12T11:00:26Z,64ed77ddce79009ce05f43801cc578c135f05b6b,linoman
|
||||
alertingNoDataErrorExecution,2023-08-15T14:27:15Z,,0717ec11d6042bbc5b4cb8d908a396ecec78cdd5,Yuri Tseretyan
|
||||
metricsSummary,2023-08-28T14:02:12Z,,59e4c257bba00968741ffc235e2dbe9d4c5b90ac,Joey
|
||||
angularDeprecationUI,2023-08-29T14:05:47Z,,bf4d025012af42ce023b1166e95626f86e819855,Giuseppe Guerra
|
||||
dashgpt,2023-08-30T20:22:05Z,,2f4fbf89cac96b74c2375b9ec9710a2c890dcf8f,Nathan Marrs
|
||||
reportingRetries,2023-08-31T07:47:47Z,,f919c55bbb417f3b1023d72bbfce6617acfd1ab9,Agnès Toulet
|
||||
newBrowseDashboards,2023-09-07T10:41:00Z,2023-11-22T15:22:00Z,ebe13a53f729c265c1f161be4e6f2c29ba074127,Ashley Harrison
|
||||
sseGroupByDatasource,2023-09-07T20:02:07Z,,5cc737bb24f80514d35b1f02c7113e106a277dc6,Kyle Brandt
|
||||
requestInstrumentationStatusSource,2023-09-11T10:13:13Z,2024-02-06T08:29:41Z,8ee43f37051e6b6a016e1c38c3d585cc0df47419,Marcus Efraimsson
|
||||
wargamesTesting,2023-09-13T18:32:01Z,,2d8f5c1488142288d73a4db892e2108b7c7db03b,Carl Bergquist
|
||||
alertingInsights,2023-09-14T12:58:04Z,,5d88b8a4f58abeafc11553865f99df69b760fcd3,Virginia Cepeda
|
||||
dockedMegaMenu,2023-09-18T10:57:11Z,2024-02-06T13:43:11Z,0ceeb18269765b40eb5cf7bf2536894152d5e5e8,Laura Fernández
|
||||
lokiRunQueriesInParallel,2023-09-19T09:34:01Z,,98aa7db64ab4c4d0f3699a9bcbde9894429daa62,Travis Patterson
|
||||
pluginsAPIMetrics,2023-09-21T11:36:32Z,,8e8bd2760b8c05df9015900ebdf865c1629881de,Esteban Beltran
|
||||
externalCorePlugins,2023-09-22T08:50:13Z,,61cdfba87a36bd4b8e1bb227a14c5d88fc943aff,Andres Martinez Gotor
|
||||
httpSLOLevels,2023-09-22T08:52:28Z,2024-02-06T08:29:41Z,e5fbc4a4cd587e4b342afd0ab0136754cf8ebf76,Carl Bergquist
|
||||
idForwarding,2023-09-25T15:21:28Z,,d15661c726b582212e1329f0ebe938664445e44a,Karl Persson
|
||||
cloudwatchNewRegionsHandler,2023-09-25T18:19:12Z,2024-01-30T12:11:52Z,ef441f02d09730449f3f7bbdcf3b31dc00ed797a,Sarah Zinger
|
||||
cloudWatchWildCardDimensionValues,2023-09-27T14:41:48Z,2024-03-15T13:49:53Z,06a35f55ac56ca5699031b2af9202ac7a26918fc,Isabella Siu
|
||||
externalServiceAccounts,2023-09-28T07:26:37Z,,4563fc48afe81ff1af54b6a5bf6d65355f961155,Gabriel MABILLE
|
||||
alertingModifiedExport,2023-09-28T14:07:45Z,2023-10-12T17:17:32Z,169c5262a5e7d04234119a3733b00bb787b8bf90,Sonia Aguilar
|
||||
enableNativeHTTPHistogram,2023-10-03T18:23:55Z,,0fc403d116b9cbd0b93e497cff0ddded55f9fe60,Carl Bergquist
|
||||
transformationsVariableSupport,2023-10-04T14:28:46Z,,40cdb3033697e8d8e43d0ee0342f0d16fac7d633,Oscar Kilhed
|
||||
kubernetesPlaylists,2023-10-05T19:00:36Z,,664ebf771e2a1d2f94bc14d3dababb634c4ac40f,Todd Treece
|
||||
grafanaAPIServerWithExperimentalAPIs,2023-10-06T18:55:22Z,,717a9dd6160e352ff7c184bfcddf2410eed9d908,Ryan McKinley
|
||||
panelMonitoring,2023-10-09T05:19:08Z,,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin
|
||||
navAdminSubsections,2023-10-10T10:50:44Z,2023-11-17T10:04:34Z,f56cc6fdc01dbf2efb802f22650cb2bef0c40179,Ashley Harrison
|
||||
recoveryThreshold,2023-10-10T14:51:50Z,,810fbc3327841da6d21f945e75cad9daf040e625,Yuri Tseretyan
|
||||
libraryPanelRBAC,2023-10-11T23:30:50Z,,a12cb8cbf3a9b33841b2f2cb1522be11de78c86a,kay delaney
|
||||
awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,,2771fb940342aa152377b26b9554eb15082f90ac,Ida Štambuk
|
||||
cachingOptimizeSerializationMemoryUsage,2023-10-12T16:56:49Z,,94ce87571ddfcede0fb7a229a65502b385d5bca3,Michael Mandrus
|
||||
panelTitleSearchInV1,2023-10-13T12:04:24Z,,bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69,Arati R
|
||||
exploreContentOutline,2023-10-13T16:57:13Z,,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac
|
||||
formatString,2023-10-13T18:17:12Z,,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol
|
||||
pluginsInstrumentationStatusSource,2023-10-17T08:27:45Z,2024-02-21T11:57:40Z,f5076d1868caa14ce44a70e812315541b4199d9f,Giuseppe Guerra
|
||||
teamHttpHeaders,2023-10-17T10:23:54Z,,be5ba6813209b5b24e955e0f761032cb5826b578,Eric Leijonmarck
|
||||
costManagementUi,2023-10-17T16:15:51Z,2024-01-08T14:25:11Z,de1ed216f4bbf6f341aa22b144fa66d583a63981,Adam Bannach
|
||||
managedPluginsInstall,2023-10-18T13:17:03Z,,43add83d1a84fe0945860a9cc700df92c7e8341f,Hugo Kiyodi Oshiro
|
||||
prometheusPromQAIL,2023-10-19T15:45:32Z,,5580d061019bee46ea2e69c94041f3da14585ceb,Brendan O'Handley
|
||||
cloudWatchBatchQueries,2023-10-20T19:09:41Z,,ecbc52f51529e1f35e26895db1a10f8a1c2f4244,Isabella Siu
|
||||
alertingContactPointsV2,2023-10-25T13:57:53Z,2023-11-30T12:37:14Z,e12e40fc2493160338237b0b94e72fa530a78ef4,Gilles De Mey
|
||||
alertmanagerRemoteOnly,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
|
||||
alertmanagerRemoteSecondary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
|
||||
alertmanagerRemotePrimary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
|
||||
annotationPermissionUpdate,2023-10-31T13:30:13Z,,c51c51458e4dd103aa0c099aa48b0d41f9375f86,Ieva
|
||||
kubernetesPlaylistsAPI,2023-10-31T17:26:39Z,2023-11-08T19:14:05Z,dd773e74f120ba908cafd596a6875c2d7fa199bf,Ryan McKinley
|
||||
traceToProfiles,2023-11-01T10:14:24Z,2024-01-22T14:21:14Z,c39e9a8f527b79881b95f64fd1c413b4bff42983,Joey
|
||||
extractFieldsNameDeduplication,2023-11-02T15:47:42Z,,0eda368d32dbd7f57635cabd6b7d6870ca7b58ee,Oscar Kilhed
|
||||
dashboardSceneForViewers,2023-11-02T19:02:25Z,,6e80a3d59b14d9af235431d6acdce95835e2a792,Dominik Prokop
|
||||
panelFilterVariable,2023-11-03T12:15:54Z,,6bf4d0cbc6756ec8e8fb64b3a9d7b76f4c4194f5,Jacob Zelek
|
||||
addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,,61d63d3034d7eed4837c63e1f5cb2f6b7114758d,Victor Marin
|
||||
pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès Toulet
|
||||
newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan
|
||||
ssoSettingsApi,2023-11-08T09:50:01Z,,5285e9503be5702680acb2b52a6bda0632f4603d,Misi
|
||||
logsInfiniteScrolling,2023-11-09T10:54:03Z,,174c2ab45a2af912519153c5c3e671f04396d7d7,Matias Chomicki
|
||||
flameGraphItemCollapsing,2023-11-09T14:31:07Z,,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas
|
||||
alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8e71c780d797fabac7041a673,Gilles De Mey
|
||||
alertingSimplifiedRouting,2023-11-10T13:14:39Z,,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar
|
||||
dashboardScene,2023-11-13T08:51:21Z,,4bc322ca1d6ed63d7e79eecb1ed09f3043f9aedb,Torkel Ödegaard
|
||||
datatrails,2023-11-15T11:28:29Z,2024-04-09T18:15:18Z,1f1d348e1700735683dc9b80fce106b8a5bc4cee,Torkel Ödegaard
|
||||
pluginsSkipHostEnvVars,2023-11-15T17:09:14Z,,cb0a88a02770eaee2561fd434d8339cab268f02e,Giuseppe Guerra
|
||||
logRowsPopoverMenu,2023-11-16T09:48:10Z,,9cb303c3f701169e8651267744a4b5fe1695a066,Matias Chomicki
|
||||
lokiStructuredMetadata,2023-11-16T16:06:14Z,,a01f8c5b42bb7937139b8d52e9723cc37e5f7ae6,Sven Grossmann
|
||||
tracesEmbeddedFlameGraph,2023-11-23T13:36:53Z,2024-01-22T14:21:14Z,4f46fb412ca4e96b0b0ef3f462aead3fc146389e,Joey
|
||||
regressionTransformation,2023-11-24T14:49:16Z,,ab982e7bd36b86c94093bddcc31008f5dc49660e,Oscar Kilhed
|
||||
displayAnonymousStats,2023-11-29T16:58:41Z,2024-02-23T15:53:37Z,59bdff0280d52ca5d8918157d7697b9279b25501,Eric Leijonmarck
|
||||
influxqlStreamingParser,2023-11-29T17:29:35Z,,5845f140758473ab5ffe789bec4077032fd22839,ismail simsek
|
||||
kubernetesSnapshots,2023-12-05T22:31:49Z,,439edebcd605a1b63bf3a9b0ab5c2b83341cd5cd,Ryan McKinley
|
||||
grafanaAPIServerEnsureKubectlAccess,2023-12-06T20:21:21Z,,c4c9bfaf2e7fa12a8e453df0f089c8b4f914a3d3,Dan Cech
|
||||
unifiedStorage,2023-12-06T20:21:21Z,,c4c9bfaf2e7fa12a8e453df0f089c8b4f914a3d3,Dan Cech
|
||||
alertStateHistoryAnnotationsFromLoki,2023-12-11T19:17:01Z,2024-01-25T17:56:09Z,4c1bf86ae11696277025296b86e8514386b4bb31,William Wernert
|
||||
tableSharedCrosshair,2023-12-13T09:33:14Z,,5aff3389f4633d6970eb2b3629ac015e564c626c,Victor Marin
|
||||
lokiQueryHints,2023-12-18T20:43:16Z,,2165c9b3f000f59c9fbda80d2bfe3bc74cd6d9dc,Sven Grossmann
|
||||
canvasPanelPanZoom,2024-01-02T19:52:21Z,,2502fe4d19faa993da8fd4d21b9802d59f4b02af,Drew Slobodnjak
|
||||
alertingPreviewUpgrade,2024-01-05T18:31:05Z,2024-03-14T14:36:35Z,49891d6a72537c869628a13ab75c8d616fbac590,Matthew Jacobson
|
||||
enablePluginsTracingByDefault,2024-01-10T11:25:54Z,2024-04-11T16:40:47Z,b40d3e748717961ef68457442d71c1284189c577,Giuseppe Guerra
|
||||
cloudRBACRoles,2024-01-10T13:19:01Z,,5b3cd9f55b292f0b693f3bb5171da7b69dac4a95,Karl Persson
|
||||
alertingQueryOptimization,2024-01-10T20:52:58Z,,afa33f12b2cf50d2c7e438f498d27b0684797778,Matthew Jacobson
|
||||
newFolderPicker,2024-01-15T11:43:19Z,,ec53487c995777b314f566f5a1054e3f8e29ec05,Ashley Harrison
|
||||
kubernetesFeatureToggles,2024-01-18T05:32:44Z,,41e523bde7db5706f339d418c68d019039a8062e,Ryan McKinley
|
||||
returnToPrevious,2024-01-18T17:12:14Z,2024-05-27T15:47:57Z,5800e40fba2accf96d81328f000e35bbb7c7acf1,Laura Fernández
|
||||
jitterAlertRulesWithinGroups,2024-01-18T18:48:11Z,,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver
|
||||
jitterAlertRules,2024-01-18T18:48:11Z,2024-02-09T21:53:58Z,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver
|
||||
onPremToCloudMigrations,2024-01-22T16:09:08Z,,cf13cb9f70c2230f17450667ce59440304fb023c,Michael Mandrus
|
||||
alertingSaveStatePeriodic,2024-01-23T16:03:30Z,,aa25776f813926cb4f1947d4ae5a014f4e7728ff,Jean-Philippe Quéméner
|
||||
promQLScope,2024-01-29T20:22:17Z,,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt
|
||||
slateAutocomplete,2024-01-31T10:01:20Z,2024-02-01T13:03:11Z,39057552dc2ddfe8ee770c67a71cf55159aca741,Ashley Harrison
|
||||
nodeGraphDotLayout,2024-01-31T16:26:12Z,,cb945aa5df3453ab2a851ef7ec8bee244e3eeac4,Andrej Ocenas
|
||||
kubernetesQueryServiceRewrite,2024-01-31T18:36:51Z,2024-04-19T09:26:21Z,e013cd427cb0457177e11f19ebd30bc523b36c76,Ryan McKinley
|
||||
influxdbRunQueriesInParallel,2024-02-01T10:58:24Z,,536153c3362782ae7f79e06755e8c7e0cd3b3acd,ismail simsek
|
||||
groupToNestedTableTransformation,2024-02-07T14:28:26Z,,756cd3c28b0648f55a6c0158f5faf63b632ebe70,Kyle Cunningham
|
||||
newPDFRendering,2024-02-08T12:09:34Z,,28e66b4ad82ecebb551374325f0be4332412341c,Agnès Toulet
|
||||
autoMigrateGraphPanel,2024-02-08T22:00:48Z,,829672759c12b27f849c92c3a2aee4a6b0037920,Nathan Marrs
|
||||
dashboardSceneSolo,2024-02-11T08:08:47Z,,fe6d1460b09b403fc74fd20e95f61513eede2555,Torkel Ödegaard
|
||||
kubernetesAggregator,2024-02-12T20:59:35Z,,d6e6298103d5d6a4efd1c21a3d74f429b506f3a7,Todd Treece
|
||||
autoMigrateStatPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
|
||||
autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
|
||||
autoMigratePiechartPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
|
||||
autoMigrateTablePanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
|
||||
groupByVariable,2024-02-14T17:18:04Z,,f016f95298fe490a865612864520f3622f8e804a,Dominik Prokop
|
||||
alertingUpgradeDryrunOnStart,2024-02-16T16:29:54Z,2024-03-14T14:36:35Z,dfaf6d1e2e13b2bd11dc8f0cd4432bfcad819aa9,Matthew Jacobson
|
||||
expressionParser,2024-02-17T00:59:11Z,,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley
|
||||
sqlExpressions,2024-02-27T21:16:00Z,,70009201d44c2d0ab39cc77081808a69a6c4fd63,Scott Lepper
|
||||
aiGeneratedDashboardChanges,2024-03-05T12:01:31Z,,a7c06d26f14b2a9fa8faa929a6c9a0c355018429,Ivan Ortega Alba
|
||||
scopeFilters,2024-03-05T15:41:19Z,,b3efb4217e48656f24aacdffd5737595d7361afe,Carl Bergquist
|
||||
betterPageScrolling,2024-03-06T15:06:47Z,,6a4e0c692ab26f4d4cb99ae615013e0b6e23f90b,Josh Hunt
|
||||
emailVerificationEnforcement,2024-03-11T14:09:44Z,2024-03-22T13:30:58Z,0b55d72fb5698e1ea2cf73eaceae166cd5619daa,Karl Persson
|
||||
ssoSettingsSAML,2024-03-14T11:04:45Z,,831ee9ee1696c0aa7a6e4ca022ddfc0ab28b86dc,linoman
|
||||
publicDashboardsScene,2024-03-22T14:48:21Z,,8d4ca72f2a0e66c446d58d8bf13fadbc988fce11,Juan Cabanas
|
||||
autoMigrateXYChartPanel,2024-03-22T15:44:37Z,,d7fa99e2df8269425eefb6373a665d8ec0b219d9,Leon Sorokin
|
||||
usePrometheusFrontendPackage,2024-03-23T00:47:53Z,2024-04-15T21:45:23Z,d0845952117152e7707f964cc343790497220990,Brendan O'Handley
|
||||
oauthRequireSubClaim,2024-03-25T13:22:24Z,,2f3a01f79fda7f6c465dd64adf5cf5c7227f9d19,Karl Persson
|
||||
authAPIAccessTokenAuth,2024-04-02T15:45:15Z,,5340a6e548b1e5fcd3af66695ca34d7e943fd068,Jo
|
||||
newDashboardWithFiltersAndGroupBy,2024-04-04T11:25:21Z,,32b6ef9d153dc7def2c17b81b02cfb1c412eb315,Dominik Prokop
|
||||
prometheusCodeModeMetricNamesSearch,2024-04-04T20:38:23Z,,559fab9dc6a6c882e7d9621ba013c3a53347176a,Nick Richmond
|
||||
cloudWatchNewLabelParsing,2024-04-05T15:57:56Z,,58f32150c262605d188874b88f44a7dece4b9388,Isabella Siu
|
||||
exploreMetrics,2024-04-09T18:15:18Z,,66c0fd4dcc3202e11f41b302d27894dc162fb288,Darren Janeczek
|
||||
accessActionSets,2024-04-12T16:19:25Z,,56f4664875047d6861ea3facbc94cd921e263950,Ieva
|
||||
disableNumericMetricsSortingInExpressions,2024-04-16T14:52:47Z,,d3fee607e2818747ad02daf6b8c58cc801075076,Nick Richmond
|
||||
queryServiceRewrite,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
|
||||
queryServiceFromUI,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
|
||||
queryService,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
|
||||
grafanaManagedRecordingRules,2024-04-22T17:53:16Z,,c32953e52cf9fd46a462711bc23fde15a5c3b6bf,Alexander Weaver
|
||||
logsExploreTableDefaultVisualization,2024-05-02T15:28:15Z,,840aeddbd1957117d9b62074c155e87bb4afd4b4,Galen Kistler
|
||||
autofixDSUID,2024-05-03T11:32:07Z,,b6f899d953a0924760dc5fd5a3d40669c86d475d,Andres Martinez Gotor
|
||||
newDashboardSharingComponent,2024-05-03T15:02:18Z,,d1434fad3a68bc1d2b49725be31e45526e6ad349,Juan Cabanas
|
||||
tlsMemcached,2024-05-09T19:12:08Z,,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean.dev
|
||||
notificationBanner,2024-05-13T09:32:34Z,,f3953b4955c218cc4678e842faa3d3380fd0f8f7,Alex Khomenko
|
||||
dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
|
||||
dualWritePlaylistsMode2,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
|
||||
dashboardRestore,2024-05-16T17:36:26Z,,42d75ac737d7ac001a6d53376e25512408a01db5,Ezequiel Victorero
|
||||
datasourceProxyDisableRBAC,2024-05-21T13:05:16Z,,0072e4a92d896df343d9586522d6f7533773da78,Aaron Godin
|
||||
alertingDisableSendAlertsExternal,2024-05-23T12:29:19Z,,8421919cb552b9e8dbd4ebba20bcc67bbc5e6b4f,Steve Simpson
|
||||
datasourceQueryTypes,2024-05-23T16:46:28Z,,42b0f802de31f3d42ece68544c193a69ea381bc0,Ryan McKinley
|
||||
alertingListViewV2,2024-05-24T14:40:49Z,,99b5259cc1efa3f5c9dbb9b0153be14437818f38,Gilles De Mey
|
||||
preserveDashboardStateWhenNavigating,2024-05-27T12:28:06Z,,6e9543e0addab825f6755d4d02c5a15a1598a483,Dominik Prokop
|
||||
alertingCentralAlertHistory,2024-05-29T15:01:38Z,,289ce6185574df99acea37b86c2e0866ea5940fd,Sonia Aguilar
|
||||
pluginProxyPreserveTrailingSlash,2024-06-05T11:36:14Z,,fe3e5917f1bc83ab29b6a57578316e054718aa4a,Marcus Efraimsson
|
||||
kubernetesDashboards,2024-06-05T14:34:23Z,,41e0430f83bf7db50c4caaa1472afa3bf3d5c2dc,Ryan McKinley
|
||||
azureMonitorPrometheusExemplars,2024-06-06T16:53:17Z,,c9778c3332aa93e5dd8bc3b894264e1955f0d593,Andreas Christou
|
||||
|
+1898
-1896
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -107,6 +108,25 @@ func TestFeatureToggleFiles(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Set the dates from git history
|
||||
dates := readFlagDateInfo(t)
|
||||
for idx, item := range current.Items {
|
||||
found, ok := dates[item.Name]
|
||||
if ok {
|
||||
// current.Items[idx].ResourceVersion = fmt.Sprintf("%d", found.created.UnixMilli()+int64(idx))
|
||||
current.Items[idx].CreationTimestamp = v1.NewTime(found.created)
|
||||
if found.deleted != nil {
|
||||
tmp := v1.NewTime(*found.deleted)
|
||||
current.Items[idx].DeletionTimestamp = &tmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name -- will avoid more git conflicts
|
||||
sort.Slice(current.Items, func(i, j int) bool {
|
||||
return current.Items[i].Name < current.Items[j].Name
|
||||
})
|
||||
|
||||
out, err := json.MarshalIndent(current, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -189,6 +209,38 @@ func verifyFlagsConfiguration(t *testing.T) {
|
||||
// acronyms can be configured as needed via `ConfigureAcronym` function from `./strcase/camel.go`
|
||||
}
|
||||
|
||||
type flagDateInfo struct {
|
||||
created time.Time
|
||||
deleted *time.Time
|
||||
}
|
||||
|
||||
// Load a cached copy of the feature toggle dates
|
||||
func readFlagDateInfo(t *testing.T) map[string]flagDateInfo {
|
||||
info := make(map[string]flagDateInfo, 300)
|
||||
// This file is created by running the script in:
|
||||
// https://github.com/grafana/grafana-enterprise/blob/ff-git-log-history/scripts/sidecar/main.go#L9
|
||||
body, err := os.ReadFile("toggles-gitlog.csv")
|
||||
require.NoError(t, err)
|
||||
reader := csv.NewReader(bytes.NewBuffer(body))
|
||||
rows, err := reader.ReadAll()
|
||||
require.NoError(t, err)
|
||||
for _, row := range rows {
|
||||
if strings.HasPrefix(row[0], "#") {
|
||||
continue
|
||||
}
|
||||
d := flagDateInfo{}
|
||||
d.created, err = time.Parse(time.RFC3339, row[1])
|
||||
require.NoError(t, err)
|
||||
if row[2] != "" {
|
||||
tmp, err := time.Parse(time.RFC3339, row[2])
|
||||
require.NoError(t, err)
|
||||
d.deleted = &tmp
|
||||
}
|
||||
info[row[0]] = d
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func verifyAndGenerateFile(t *testing.T, fpath string, gen string) {
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning since this is a test and the function is only called explicitly above
|
||||
|
||||
@@ -90,7 +90,7 @@ func (e *Engine) Test(ctx context.Context, user identity.Requester, rule *models
|
||||
start := time.Now()
|
||||
|
||||
tsField := data.NewField("Time", nil, make([]time.Time, length))
|
||||
valueFields := make(map[string]*data.Field)
|
||||
valueFields := make(map[data.Fingerprint]*data.Field)
|
||||
|
||||
err = evaluator.Eval(ruleCtx, from, time.Duration(rule.IntervalSeconds)*time.Second, length, func(idx int, currentTime time.Time, results eval.Results) error {
|
||||
if idx >= length {
|
||||
|
||||
@@ -160,9 +160,10 @@ func TestNewBacktestingEvaluator(t *testing.T) {
|
||||
func TestEvaluatorTest(t *testing.T) {
|
||||
states := []eval.State{eval.Normal, eval.Alerting, eval.Pending}
|
||||
generateState := func(prefix string) *state.State {
|
||||
labels := models.GenerateAlertLabels(rand.Intn(5)+1, prefix+"-")
|
||||
return &state.State{
|
||||
CacheID: "state-" + prefix,
|
||||
Labels: models.GenerateAlertLabels(rand.Intn(5)+1, prefix+"-"),
|
||||
CacheID: labels.Fingerprint(),
|
||||
Labels: labels,
|
||||
State: states[rand.Intn(len(states))],
|
||||
}
|
||||
}
|
||||
@@ -201,10 +202,11 @@ func TestEvaluatorTest(t *testing.T) {
|
||||
var states []state.StateTransition
|
||||
|
||||
for _, s := range allStates {
|
||||
labels := models.GenerateAlertLabels(rand.Intn(5)+1, s.String()+"-")
|
||||
states = append(states, state.StateTransition{
|
||||
State: &state.State{
|
||||
CacheID: "state-" + s.String(),
|
||||
Labels: models.GenerateAlertLabels(rand.Intn(5)+1, s.String()+"-"),
|
||||
CacheID: labels.Fingerprint(),
|
||||
Labels: labels,
|
||||
State: s,
|
||||
StateReason: util.GenerateShortUID(),
|
||||
},
|
||||
@@ -226,7 +228,7 @@ func TestEvaluatorTest(t *testing.T) {
|
||||
require.Equal(t, data.FieldTypeTime, timestampField.Type())
|
||||
})
|
||||
|
||||
fieldByState := make(map[string]*data.Field, len(states))
|
||||
fieldByState := make(map[data.Fingerprint]*data.Field, len(states))
|
||||
|
||||
t.Run("should contain a field per state", func(t *testing.T) {
|
||||
for _, s := range states {
|
||||
@@ -269,11 +271,12 @@ func TestEvaluatorTest(t *testing.T) {
|
||||
from := time.Unix(0, 0)
|
||||
to := from.Add(5 * ruleInterval)
|
||||
|
||||
labels := models.GenerateAlertLabels(rand.Intn(5)+1, "test-")
|
||||
states := []state.StateTransition{
|
||||
{
|
||||
State: &state.State{
|
||||
CacheID: "state-1",
|
||||
Labels: models.GenerateAlertLabels(rand.Intn(5)+1, "test-"),
|
||||
CacheID: labels.Fingerprint(),
|
||||
Labels: labels,
|
||||
State: eval.Normal,
|
||||
StateReason: util.GenerateShortUID(),
|
||||
},
|
||||
|
||||
@@ -141,9 +141,6 @@ type ExecutionResults struct {
|
||||
// Results contains the results of all queries, reduce and math expressions
|
||||
Results map[string]data.Frames
|
||||
|
||||
// Errors contains a map of RefIDs that returned an error
|
||||
Errors map[string]error
|
||||
|
||||
// NoData contains the DatasourceUID for RefIDs that returned no data.
|
||||
NoData map[string]string
|
||||
|
||||
@@ -166,16 +163,12 @@ func (evalResults Results) HasErrors() bool {
|
||||
// HasNonRetryableErrors returns true if we have at least 1 result with:
|
||||
// 1. A `State` of `Error`
|
||||
// 2. The `Error` attribute is not nil
|
||||
// 3. The `Error` type is of `&invalidEvalResultFormatError` or `ErrSeriesMustBeWide`
|
||||
// 3. The `Error` matches IsNonRetryableError
|
||||
// Our thinking with this approach, is that we don't want to retry errors that have relation with invalid alert definition format.
|
||||
func (evalResults Results) HasNonRetryableErrors() bool {
|
||||
for _, r := range evalResults {
|
||||
if r.State == Error && r.Error != nil {
|
||||
var nonRetryableError *invalidEvalResultFormatError
|
||||
if errors.As(r.Error, &nonRetryableError) {
|
||||
return true
|
||||
}
|
||||
if errors.Is(r.Error, expr.ErrSeriesMustBeWide) {
|
||||
if IsNonRetryableError(r.Error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -183,6 +176,19 @@ func (evalResults Results) HasNonRetryableErrors() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNonRetryableError indicates whether an error is considered persistent and not worth performing evaluation retries.
|
||||
// Currently it is true if err is `&invalidEvalResultFormatError` or `ErrSeriesMustBeWide`
|
||||
func IsNonRetryableError(err error) bool {
|
||||
var nonRetryableError *invalidEvalResultFormatError
|
||||
if errors.As(err, &nonRetryableError) {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, expr.ErrSeriesMustBeWide) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasErrors returns true when Results contains at least one element and all elements are errors
|
||||
func (evalResults Results) IsError() bool {
|
||||
for _, r := range evalResults {
|
||||
@@ -407,7 +413,6 @@ type NumberValueCapture struct {
|
||||
Value *float64
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.QueryDataResponse) ExecutionResults {
|
||||
// captures contains the values of all instant queries and expressions for each dimension
|
||||
captures := make(map[string]map[data.Fingerprint]NumberValueCapture)
|
||||
@@ -433,17 +438,10 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q
|
||||
}
|
||||
|
||||
result := ExecutionResults{Results: make(map[string]data.Frames)}
|
||||
for refID, res := range execResp.Responses {
|
||||
if res.Error != nil {
|
||||
if result.Errors == nil {
|
||||
result.Errors = make(map[string]error)
|
||||
}
|
||||
result.Errors[refID] = res.Error
|
||||
if refID == c.Condition {
|
||||
result.Error = res.Error
|
||||
}
|
||||
}
|
||||
|
||||
result.Error = FindConditionError(execResp, c.Condition)
|
||||
|
||||
for refID, res := range execResp.Responses {
|
||||
// There are two possible frame formats for No Data:
|
||||
//
|
||||
// 1. A response with no frames
|
||||
@@ -526,30 +524,50 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindConditionError extracts the error from a query response that caused the given condition to fail.
|
||||
// If a condition failed because a node it depends on had an error, that error is returned instead.
|
||||
// It returns nil if there are no errors related to the condition.
|
||||
func FindConditionError(resp *backend.QueryDataResponse, condition string) error {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := make(map[string]error)
|
||||
for refID, node := range resp.Responses {
|
||||
if node.Error != nil {
|
||||
errs[refID] = node.Error
|
||||
}
|
||||
}
|
||||
|
||||
conditionErr := errs[condition]
|
||||
|
||||
// If the error of the condition is an Error that indicates the condition failed
|
||||
// because one of its dependent query or expressions failed, then we follow
|
||||
// the dependency chain to an error that is not a dependency error.
|
||||
if len(result.Errors) > 0 && result.Error != nil {
|
||||
if errors.Is(result.Error, expr.DependencyError) {
|
||||
if conditionErr != nil {
|
||||
if errors.Is(conditionErr, expr.DependencyError) {
|
||||
var utilError errutil.Error
|
||||
e := result.Error
|
||||
e := conditionErr
|
||||
for {
|
||||
errors.As(e, &utilError)
|
||||
depRefID := utilError.PublicPayload["depRefId"].(string)
|
||||
depError, ok := result.Errors[depRefID]
|
||||
depError, ok := errs[depRefID]
|
||||
if !ok {
|
||||
return result
|
||||
return conditionErr
|
||||
}
|
||||
if !errors.Is(depError, expr.DependencyError) {
|
||||
result.Error = depError
|
||||
return result
|
||||
conditionErr = depError
|
||||
return conditionErr
|
||||
}
|
||||
e = depError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return conditionErr
|
||||
}
|
||||
|
||||
// datasourceUIDsToRefIDs returns a sorted slice of Ref IDs for each Datasource UID.
|
||||
|
||||
@@ -894,6 +894,85 @@ func TestEvaluate(t *testing.T) {
|
||||
},
|
||||
EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]",
|
||||
}},
|
||||
}, {
|
||||
name: "results contains error if condition frame has error",
|
||||
cond: models.Condition{
|
||||
Condition: "B",
|
||||
},
|
||||
resp: backend.QueryDataResponse{
|
||||
Responses: backend.Responses{
|
||||
"A": {
|
||||
Frames: []*data.Frame{{
|
||||
RefID: "A",
|
||||
Fields: []*data.Field{
|
||||
data.NewField(
|
||||
"Value",
|
||||
data.Labels{"foo": "bar"},
|
||||
[]*float64{util.Pointer(10.0)},
|
||||
),
|
||||
},
|
||||
}},
|
||||
},
|
||||
"B": {
|
||||
Frames: []*data.Frame{{
|
||||
RefID: "B",
|
||||
Fields: []*data.Field{
|
||||
data.NewField(
|
||||
"Value",
|
||||
data.Labels{"foo": "bar", "bar": "baz"},
|
||||
[]*float64{util.Pointer(1.0)},
|
||||
),
|
||||
},
|
||||
}},
|
||||
Error: errors.New("some frame error"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: Results{{
|
||||
State: Error,
|
||||
Error: errors.New("some frame error"),
|
||||
EvaluationString: "",
|
||||
}},
|
||||
}, {
|
||||
name: "results contain underlying error if condition frame has error that depends on another node",
|
||||
cond: models.Condition{
|
||||
Condition: "B",
|
||||
},
|
||||
resp: backend.QueryDataResponse{
|
||||
Responses: backend.Responses{
|
||||
"A": {
|
||||
Frames: []*data.Frame{{
|
||||
RefID: "A",
|
||||
Fields: []*data.Field{
|
||||
data.NewField(
|
||||
"Value",
|
||||
data.Labels{"foo": "bar"},
|
||||
[]*float64{util.Pointer(10.0)},
|
||||
),
|
||||
},
|
||||
}},
|
||||
Error: errors.New("another error depends on me"),
|
||||
},
|
||||
"B": {
|
||||
Frames: []*data.Frame{{
|
||||
RefID: "B",
|
||||
Fields: []*data.Field{
|
||||
data.NewField(
|
||||
"Value",
|
||||
data.Labels{"foo": "bar", "bar": "baz"},
|
||||
[]*float64{util.Pointer(1.0)},
|
||||
),
|
||||
},
|
||||
}},
|
||||
Error: expr.MakeDependencyError("B", "A"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: Results{{
|
||||
State: Error,
|
||||
Error: errors.New("another error depends on me"),
|
||||
EvaluationString: "",
|
||||
}},
|
||||
}}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -65,6 +65,10 @@ func (il *InstanceLabels) StringAndHash() (string, string, error) {
|
||||
return string(b), fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (il *InstanceLabels) Fingerprint() data.Fingerprint {
|
||||
return data.Labels(*il).Fingerprint()
|
||||
}
|
||||
|
||||
// The following is based on SDK code, copied for now
|
||||
|
||||
// tupleLables is an alternative representation of Labels (map[string]string) that can be sorted
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -39,6 +40,19 @@ func TestTupleLabelsToLabels(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstanceLabelsFingerprint(t *testing.T) {
|
||||
t.Run("returns labels fingerprint", func(t *testing.T) {
|
||||
labels := InstanceLabels{
|
||||
"foo": "bar",
|
||||
"baz": "qux",
|
||||
}
|
||||
|
||||
fingerprint := labels.Fingerprint()
|
||||
expectedFingerprint := data.Labels(labels).Fingerprint()
|
||||
require.Equal(t, expectedFingerprint, fingerprint)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkTupleLabelsToLabels(b *testing.B) {
|
||||
b.Run("10 labels", func(b *testing.B) {
|
||||
in := make(tupleLabels, 0, 10)
|
||||
|
||||
@@ -215,7 +215,7 @@ func (moa *MultiOrgAlertmanager) setupClustering(cfg *setting.Cfg) error {
|
||||
return fmt.Errorf("unable to initialize gossip mesh: %w", err)
|
||||
}
|
||||
|
||||
err = peer.Join(alertingCluster.DefaultReconnectInterval, alertingCluster.DefaultReconnectTimeout)
|
||||
err = peer.Join(alertingCluster.DefaultReconnectInterval, cfg.UnifiedAlerting.HAReconnectTimeout)
|
||||
if err != nil {
|
||||
moa.logger.Error("Msg", "Unable to join gossip mesh while initializing cluster for high availability mode", "error", err)
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ func TestRuleRoutine(t *testing.T) {
|
||||
for i := 0; i < 2; i++ {
|
||||
states = append(states, &state.State{
|
||||
AlertRuleUID: rule.UID,
|
||||
CacheID: util.GenerateShortUID(),
|
||||
CacheID: data.Labels(rule.Labels).Fingerprint(),
|
||||
OrgID: rule.OrgID,
|
||||
State: s,
|
||||
StartsAt: sch.clock.Now(),
|
||||
|
||||
@@ -116,7 +116,10 @@ func (r *recordingRule) doEvaluate(ctx context.Context, ev *Evaluation) {
|
||||
logger := r.logger.FromContext(ctx).New("now", ev.scheduledAt, "fingerprint", ev.Fingerprint())
|
||||
orgID := fmt.Sprint(ev.rule.OrgID)
|
||||
evalDuration := r.metrics.EvalDuration.WithLabelValues(orgID)
|
||||
evalAttemptTotal := r.metrics.EvalAttemptTotal.WithLabelValues(orgID)
|
||||
evalAttemptFailures := r.metrics.EvalAttemptFailures.WithLabelValues(orgID)
|
||||
evalTotal := r.metrics.EvalTotal.WithLabelValues(orgID)
|
||||
evalTotalFailures := r.metrics.EvalFailures.WithLabelValues(orgID)
|
||||
evalStart := r.clock.Now()
|
||||
|
||||
defer func() {
|
||||
@@ -139,6 +142,7 @@ func (r *recordingRule) doEvaluate(ctx context.Context, ev *Evaluation) {
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
var latestError error
|
||||
for attempt := int64(1); attempt <= r.maxAttempts; attempt++ {
|
||||
logger := logger.New("attempt", attempt)
|
||||
if ctx.Err() != nil {
|
||||
@@ -147,49 +151,59 @@ func (r *recordingRule) doEvaluate(ctx context.Context, ev *Evaluation) {
|
||||
return
|
||||
}
|
||||
|
||||
evalAttemptTotal.Inc()
|
||||
err := r.tryEvaluation(ctx, ev, logger)
|
||||
latestError = err
|
||||
if err == nil {
|
||||
return
|
||||
break
|
||||
}
|
||||
|
||||
logger.Error("Failed to evaluate rule", "attempt", attempt, "error", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Error("Context has been cancelled while backing off", "attempt", attempt)
|
||||
return
|
||||
case <-time.After(retryDelay):
|
||||
continue
|
||||
evalAttemptFailures.Inc()
|
||||
|
||||
if eval.IsNonRetryableError(err) {
|
||||
break
|
||||
}
|
||||
|
||||
if attempt < r.maxAttempts {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Error("Context has been cancelled while backing off", "attempt", attempt)
|
||||
return
|
||||
case <-time.After(retryDelay):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestError != nil {
|
||||
evalTotalFailures.Inc()
|
||||
span.SetStatus(codes.Error, "rule evaluation failed")
|
||||
span.RecordError(latestError)
|
||||
if r.maxAttempts > 0 {
|
||||
logger.Error("Recording rule evaluation failed after all attempts", "lastError", latestError)
|
||||
}
|
||||
} else {
|
||||
logger.Debug("Recording rule evaluation succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recordingRule) tryEvaluation(ctx context.Context, ev *Evaluation, logger log.Logger) error {
|
||||
orgID := fmt.Sprint(ev.rule.OrgID)
|
||||
evalAttemptTotal := r.metrics.EvalAttemptTotal.WithLabelValues(orgID)
|
||||
evalAttemptFailures := r.metrics.EvalAttemptFailures.WithLabelValues(orgID)
|
||||
evalTotalFailures := r.metrics.EvalFailures.WithLabelValues(orgID)
|
||||
|
||||
evalStart := r.clock.Now()
|
||||
evalCtx := eval.NewContext(ctx, SchedulerUserFor(ev.rule.OrgID))
|
||||
result, err := r.buildAndExecutePipeline(ctx, evalCtx, ev, logger)
|
||||
evalDur := r.clock.Now().Sub(evalStart)
|
||||
|
||||
evalAttemptTotal.Inc()
|
||||
span := trace.SpanFromContext(ctx)
|
||||
|
||||
// TODO: In some cases, err can be nil but the dataframe itself contains embedded error frames. Parse these out like we do when evaluating alert rules.
|
||||
// TODO: (Maybe, refactor something in eval package so we can use shared code for this)
|
||||
if err != nil {
|
||||
evalAttemptFailures.Inc()
|
||||
// TODO: Only errors embedded in the frame can be considered retryable.
|
||||
// TODO: Since we are not handling these yet per the above TODO, we can blindly consider all errors to be non-retryable for now, and just exit.
|
||||
evalTotalFailures.Inc()
|
||||
span.SetStatus(codes.Error, "rule evaluation failed")
|
||||
span.RecordError(err)
|
||||
return fmt.Errorf("server side expressions pipeline returned an error: %w", err)
|
||||
}
|
||||
|
||||
// There might be errors in the pipeline results, even if the query succeeded.
|
||||
if err := eval.FindConditionError(result, ev.rule.Record.From); err != nil {
|
||||
return fmt.Errorf("the query failed with an error: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Recording rule evaluated", "results", result, "duration", evalDur)
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.AddEvent("rule evaluated", trace.WithAttributes(
|
||||
attribute.Int64("results", int64(len(result.Responses))),
|
||||
))
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
type ruleStates struct {
|
||||
states map[string]*State
|
||||
states map[data.Fingerprint]*State
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
@@ -89,7 +89,7 @@ func (c *cache) getOrCreate(ctx context.Context, log log.Logger, alertRule *ngMo
|
||||
}
|
||||
var states *ruleStates
|
||||
if states, ok = orgStates[stateCandidate.AlertRuleUID]; !ok {
|
||||
states = &ruleStates{states: make(map[string]*State)}
|
||||
states = &ruleStates{states: make(map[data.Fingerprint]*State)}
|
||||
c.states[stateCandidate.OrgID][stateCandidate.AlertRuleUID] = states
|
||||
}
|
||||
return states.getOrAdd(stateCandidate)
|
||||
@@ -199,18 +199,14 @@ func calculateState(ctx context.Context, log log.Logger, alertRule *ngModels.Ale
|
||||
log.Warn("Evaluation result contains either reserved labels or labels declared in the rules. Those labels from the result will be ignored", "labels", dupes)
|
||||
}
|
||||
|
||||
il := ngModels.InstanceLabels(lbs)
|
||||
id, err := il.StringKey()
|
||||
if err != nil {
|
||||
log.Error("Error getting cacheId for entry", "error", err)
|
||||
}
|
||||
cacheID := lbs.Fingerprint()
|
||||
|
||||
// For new states, we set StartsAt & EndsAt to EvaluatedAt as this is the
|
||||
// expected value for a Normal state during state transition.
|
||||
newState := State{
|
||||
AlertRuleUID: alertRule.UID,
|
||||
OrgID: alertRule.OrgID,
|
||||
CacheID: id,
|
||||
CacheID: cacheID,
|
||||
Labels: lbs,
|
||||
Annotations: annotations,
|
||||
EvaluationDuration: result.EvaluationDuration,
|
||||
@@ -279,12 +275,12 @@ func (c *cache) set(entry *State) {
|
||||
c.states[entry.OrgID] = make(map[string]*ruleStates)
|
||||
}
|
||||
if _, ok := c.states[entry.OrgID][entry.AlertRuleUID]; !ok {
|
||||
c.states[entry.OrgID][entry.AlertRuleUID] = &ruleStates{states: make(map[string]*State)}
|
||||
c.states[entry.OrgID][entry.AlertRuleUID] = &ruleStates{states: make(map[data.Fingerprint]*State)}
|
||||
}
|
||||
c.states[entry.OrgID][entry.AlertRuleUID].states[entry.CacheID] = entry
|
||||
}
|
||||
|
||||
func (c *cache) get(orgID int64, alertRuleUID, stateId string) *State {
|
||||
func (c *cache) get(orgID int64, alertRuleUID string, stateId data.Fingerprint) *State {
|
||||
c.mtxStates.RLock()
|
||||
defer c.mtxStates.RUnlock()
|
||||
ruleStates, ok := c.states[orgID][alertRuleUID]
|
||||
|
||||
@@ -180,15 +180,12 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader) {
|
||||
|
||||
rulesStates, ok := orgStates[entry.RuleUID]
|
||||
if !ok {
|
||||
rulesStates = &ruleStates{states: make(map[string]*State)}
|
||||
rulesStates = &ruleStates{states: make(map[data.Fingerprint]*State)}
|
||||
orgStates[entry.RuleUID] = rulesStates
|
||||
}
|
||||
|
||||
lbs := map[string]string(entry.Labels)
|
||||
cacheID, err := entry.Labels.StringKey()
|
||||
if err != nil {
|
||||
st.log.Error("Error getting cacheId for entry", "error", err)
|
||||
}
|
||||
cacheID := entry.Labels.Fingerprint()
|
||||
var resultFp data.Fingerprint
|
||||
if entry.ResultFingerprint != "" {
|
||||
fp, err := strconv.ParseUint(entry.ResultFingerprint, 16, 64)
|
||||
@@ -214,11 +211,12 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader) {
|
||||
statesCount++
|
||||
}
|
||||
}
|
||||
|
||||
st.cache.setAllStates(states)
|
||||
st.log.Info("State cache has been initialized", "states", statesCount, "duration", time.Since(startTime))
|
||||
}
|
||||
|
||||
func (st *Manager) Get(orgID int64, alertRuleUID, stateId string) *State {
|
||||
func (st *Manager) Get(orgID int64, alertRuleUID string, stateId data.Fingerprint) *State {
|
||||
return st.cache.get(orgID, alertRuleUID, stateId)
|
||||
}
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
if !ok { // skip if nothing to assert
|
||||
continue
|
||||
}
|
||||
expectedTransitionsMap := make(map[string]StateTransition, len(expectedTransitions))
|
||||
expectedTransitionsMap := make(map[data.Fingerprint]StateTransition, len(expectedTransitions))
|
||||
for i := range expectedTransitions {
|
||||
patchState(alertRule, expectedTransitions[i].State)
|
||||
expectedTransitionsMap[expectedTransitions[i].CacheID] = expectedTransitions[i]
|
||||
@@ -294,7 +294,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if len(expectedTransitionsMap) > 0 {
|
||||
vals := make([]string, 0, len(expectedTransitionsMap))
|
||||
vals := make([]data.Fingerprint, 0, len(expectedTransitionsMap))
|
||||
for _, s := range expectedTransitionsMap {
|
||||
vals = append(vals, s.CacheID)
|
||||
}
|
||||
@@ -2524,15 +2524,6 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
ngmodels.KeepLastErrState: baseRuleWith(ngmodels.RuleMuts.WithErrorExecAs(ngmodels.KeepLastErrState)),
|
||||
}
|
||||
|
||||
cacheID := func(lbls data.Labels) string {
|
||||
l := ngmodels.InstanceLabels(lbls)
|
||||
r, err := l.StringKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type errorTestCase struct {
|
||||
desc string
|
||||
ruleMutators []ngmodels.AlertRuleMutator
|
||||
@@ -2587,7 +2578,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
State: eval.Error,
|
||||
Error: datasourceError,
|
||||
@@ -2748,7 +2739,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
State: eval.Error,
|
||||
Error: datasourceError,
|
||||
@@ -2881,7 +2872,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
State: eval.Error,
|
||||
Error: datasourceError,
|
||||
@@ -3028,7 +3019,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Error,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
Error: datasourceError,
|
||||
State: eval.Normal,
|
||||
@@ -3148,7 +3139,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
State: eval.Error,
|
||||
Error: datasourceError,
|
||||
@@ -3244,7 +3235,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
State: eval.Error,
|
||||
Error: datasourceError,
|
||||
@@ -3330,7 +3321,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
CacheID: cacheID(labels["system + rule"]),
|
||||
CacheID: labels["system + rule"].Fingerprint(),
|
||||
Labels: labels["system + rule + datasource-error"],
|
||||
State: eval.Error,
|
||||
Error: datasourceError,
|
||||
@@ -3539,14 +3530,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
||||
}
|
||||
|
||||
func setCacheID(s *State) *State {
|
||||
if s.CacheID != "" {
|
||||
if s.CacheID != 0 {
|
||||
return s
|
||||
}
|
||||
il := ngmodels.InstanceLabels(s.Labels)
|
||||
id, err := il.StringKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.CacheID = id
|
||||
|
||||
s.CacheID = s.Labels.Fingerprint()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -973,13 +973,9 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
expectedAnnotations: 1,
|
||||
expectedStates: []*state.State{
|
||||
{
|
||||
CacheID: func() string {
|
||||
CacheID: func() data.Fingerprint {
|
||||
lbls := models.InstanceLabels(labels["system + rule + labels1"])
|
||||
r, err := lbls.StringKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
return lbls.Fingerprint()
|
||||
}(),
|
||||
Labels: mergeLabels(labels["system + rule + labels1"], data.Labels{
|
||||
"datasource_uid": "datasource_uid_1",
|
||||
@@ -1317,7 +1313,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
states := st.GetStatesForRuleUID(tc.alertRule.OrgID, tc.alertRule.UID)
|
||||
assert.Len(t, states, len(tc.expectedStates))
|
||||
|
||||
expectedStates := make(map[string]*state.State, len(tc.expectedStates))
|
||||
expectedStates := make(map[data.Fingerprint]*state.State, len(tc.expectedStates))
|
||||
for _, s := range tc.expectedStates {
|
||||
// patch all optional fields of the expected state
|
||||
setCacheID(s)
|
||||
@@ -1404,12 +1400,11 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
|
||||
require.NotEmpty(t, states)
|
||||
|
||||
savedStates := make(map[string]models.AlertInstance)
|
||||
savedStates := make(map[data.Fingerprint]models.AlertInstance)
|
||||
for _, op := range instanceStore.RecordedOps() {
|
||||
switch q := op.(type) {
|
||||
case models.AlertInstance:
|
||||
cacheId, err := q.Labels.StringKey()
|
||||
require.NoError(t, err)
|
||||
cacheId := q.Labels.Fingerprint()
|
||||
savedStates[cacheId] = q
|
||||
}
|
||||
}
|
||||
@@ -1578,7 +1573,7 @@ func TestStaleResultsHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStaleResults(t *testing.T) {
|
||||
getCacheID := func(t *testing.T, rule *models.AlertRule, result eval.Result) string {
|
||||
getCacheID := func(t *testing.T, rule *models.AlertRule, result eval.Result) data.Fingerprint {
|
||||
t.Helper()
|
||||
labels := data.Labels{}
|
||||
for key, value := range rule.Labels {
|
||||
@@ -1588,14 +1583,12 @@ func TestStaleResults(t *testing.T) {
|
||||
labels[key] = value
|
||||
}
|
||||
lbls := models.InstanceLabels(labels)
|
||||
key, err := lbls.StringKey()
|
||||
require.NoError(t, err)
|
||||
return key
|
||||
return lbls.Fingerprint()
|
||||
}
|
||||
|
||||
checkExpectedStates := func(t *testing.T, actual []*state.State, expected map[string]struct{}) map[string]*state.State {
|
||||
checkExpectedStates := func(t *testing.T, actual []*state.State, expected map[data.Fingerprint]struct{}) map[data.Fingerprint]*state.State {
|
||||
t.Helper()
|
||||
result := make(map[string]*state.State)
|
||||
result := make(map[data.Fingerprint]*state.State)
|
||||
require.Len(t, actual, len(expected))
|
||||
for _, currentState := range actual {
|
||||
_, ok := expected[currentState.CacheID]
|
||||
@@ -1604,7 +1597,7 @@ func TestStaleResults(t *testing.T) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
checkExpectedStateTransitions := func(t *testing.T, actual []state.StateTransition, expected map[string]struct{}) {
|
||||
checkExpectedStateTransitions := func(t *testing.T, actual []state.StateTransition, expected map[data.Fingerprint]struct{}) {
|
||||
t.Helper()
|
||||
require.Len(t, actual, len(expected))
|
||||
for _, currentState := range actual {
|
||||
@@ -1643,7 +1636,7 @@ func TestStaleResults(t *testing.T) {
|
||||
state2 := getCacheID(t, rule, initResults[1])
|
||||
state3 := getCacheID(t, rule, initResults[2])
|
||||
|
||||
initStates := map[string]struct{}{
|
||||
initStates := map[data.Fingerprint]struct{}{
|
||||
state1: {},
|
||||
state2: {},
|
||||
state3: {},
|
||||
@@ -1687,7 +1680,7 @@ func TestStaleResults(t *testing.T) {
|
||||
|
||||
t.Run("should remove stale states from cache", func(t *testing.T) {
|
||||
currentStates = st.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||
checkExpectedStates(t, currentStates, map[string]struct{}{
|
||||
checkExpectedStates(t, currentStates, map[data.Fingerprint]struct{}{
|
||||
getCacheID(t, rule, results[0]): {},
|
||||
})
|
||||
})
|
||||
@@ -1782,7 +1775,7 @@ func TestDeleteStateByRuleUID(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
expectedStatesMap := make(map[string]*state.State, len(tc.expectedStates))
|
||||
expectedStatesMap := make(map[data.Fingerprint]*state.State, len(tc.expectedStates))
|
||||
for _, expectedState := range tc.expectedStates {
|
||||
s := setCacheID(expectedState)
|
||||
expectedStatesMap[s.CacheID] = s
|
||||
@@ -1992,20 +1985,15 @@ func TestResetStateByRuleUID(t *testing.T) {
|
||||
}
|
||||
|
||||
func setCacheID(s *state.State) *state.State {
|
||||
if s.CacheID != "" {
|
||||
if s.CacheID != 0 {
|
||||
return s
|
||||
}
|
||||
il := models.InstanceLabels(s.Labels)
|
||||
id, err := il.StringKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.CacheID = id
|
||||
s.CacheID = s.Labels.Fingerprint()
|
||||
return s
|
||||
}
|
||||
|
||||
func stateSliceToMap(states []*state.State) map[string]*state.State {
|
||||
result := make(map[string]*state.State, len(states))
|
||||
func stateSliceToMap(states []*state.State) map[data.Fingerprint]*state.State {
|
||||
result := make(map[data.Fingerprint]*state.State, len(states))
|
||||
for _, s := range states {
|
||||
setCacheID(s)
|
||||
result[s.CacheID] = s
|
||||
|
||||
@@ -56,7 +56,7 @@ func (a *SyncStatePersister) deleteAlertStates(ctx context.Context, states []Sta
|
||||
for _, s := range states {
|
||||
key, err := s.GetAlertInstanceKey()
|
||||
if err != nil {
|
||||
a.log.Error("Failed to delete alert instance with invalid labels", "cacheID", s.CacheID, "error", err)
|
||||
a.log.Error("Failed to delete alert instance with invalid labels", "cacheID", s.CacheID, "labels", s.Labels.String(), "error", err)
|
||||
continue
|
||||
}
|
||||
toDelete = append(toDelete, key)
|
||||
|
||||
@@ -27,7 +27,7 @@ type State struct {
|
||||
|
||||
// CacheID is a unique, opaque identifier for the state, and is used to find the state
|
||||
// in the state cache. It tends to be derived from the state's labels.
|
||||
CacheID string
|
||||
CacheID data.Fingerprint
|
||||
|
||||
// State represents the current state.
|
||||
State eval.State
|
||||
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
alertmanagerDefaultClusterAddr = "0.0.0.0:9094"
|
||||
alertmanagerDefaultPeerTimeout = 15 * time.Second
|
||||
alertmanagerDefaultGossipInterval = alertingCluster.DefaultGossipInterval
|
||||
alertmanagerDefaultReconnectTimeout = alertingCluster.DefaultReconnectTimeout
|
||||
alertmanagerDefaultPushPullInterval = alertingCluster.DefaultPushPullInterval
|
||||
alertmanagerDefaultConfigPollInterval = time.Minute
|
||||
alertmanagerRedisDefaultMaxConns = 5
|
||||
@@ -71,6 +72,7 @@ type UnifiedAlertingSettings struct {
|
||||
HAPeers []string
|
||||
HAPeerTimeout time.Duration
|
||||
HAGossipInterval time.Duration
|
||||
HAReconnectTimeout time.Duration
|
||||
HAPushPullInterval time.Duration
|
||||
HALabel string
|
||||
HARedisClusterModeEnabled bool
|
||||
@@ -217,6 +219,10 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uaCfg.HAReconnectTimeout, err = gtime.ParseDuration(valueAsString(ua, "ha_reconnect_timeout", (alertmanagerDefaultReconnectTimeout).String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uaCfg.HAPushPullInterval, err = gtime.ParseDuration(valueAsString(ua, "ha_push_pull_interval", (alertmanagerDefaultPushPullInterval).String()))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -25,6 +25,7 @@ func TestCfg_ReadUnifiedAlertingSettings(t *testing.T) {
|
||||
require.Len(t, cfg.UnifiedAlerting.HAPeers, 0)
|
||||
require.Equal(t, 200*time.Millisecond, cfg.UnifiedAlerting.HAGossipInterval)
|
||||
require.Equal(t, time.Minute, cfg.UnifiedAlerting.HAPushPullInterval)
|
||||
require.Equal(t, 6*time.Hour, cfg.UnifiedAlerting.HAReconnectTimeout)
|
||||
}
|
||||
|
||||
// With peers set, it correctly parses them.
|
||||
|
||||
@@ -627,6 +627,18 @@ export class Mousetrap {
|
||||
// keep a list of which sequences were matches for later
|
||||
doNotReset[seq] = 1;
|
||||
this._fireCallback(callbacks[i].callback, e, callbacks[i].combo, seq);
|
||||
|
||||
// When matching a callback, don't reset other callbacks that starts with this prefix
|
||||
// This allows chaining of multiple shortcuts that share a prefix. e.g. if we have
|
||||
// `t left` and `t right`, allow user to hit `t left`, `right` without resetting the sequence
|
||||
const suffixPrefixIndex = seq.lastIndexOf(character);
|
||||
const sequencePrefix = seq.slice(0, suffixPrefixIndex);
|
||||
for (const [seq, level] of Object.entries(this._sequenceLevels)) {
|
||||
if (level > 0 && seq.startsWith(sequencePrefix)) {
|
||||
doNotReset[seq] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -637,6 +649,14 @@ export class Mousetrap {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't reset a sequence if this character is the start of a sequence that has already progressed.
|
||||
// This allows `t left` to be hit immediately after a `t right`
|
||||
for (const callback of this._callbacks[character] ?? []) {
|
||||
if (callback.action === e.type && callback.seq && callback.level === 0) {
|
||||
doNotReset[callback.seq] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// if the key you pressed matches the type of sequence without
|
||||
// being a modifier (ie "keyup" or "keypress") then we should
|
||||
// reset all sequences that were not matched by this event
|
||||
@@ -675,6 +695,11 @@ export class Mousetrap {
|
||||
}
|
||||
const event: KeyboardEvent = rawEvent;
|
||||
|
||||
// Don't trigger shortcuts when a key is just held down
|
||||
if (event.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
// normalize e.which for key events
|
||||
// @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
|
||||
if (typeof event.which !== 'number') {
|
||||
@@ -743,9 +768,9 @@ export class Mousetrap {
|
||||
this._ignoreNextKeyup = characterFromEvent(e);
|
||||
}
|
||||
|
||||
// weird race condition if a sequence ends with the key
|
||||
// another sequence begins with
|
||||
setTimeout(this._resetSequences, 10);
|
||||
// Reset the sequence timer and allow for this shortcut to be
|
||||
// triggered again just by repeating the last key
|
||||
this._resetSequenceTimer();
|
||||
};
|
||||
|
||||
// loop through keys one at a time and bind the appropriate callback
|
||||
|
||||
@@ -11,11 +11,11 @@ export function BrowseFilters() {
|
||||
<div>
|
||||
<ActionRow
|
||||
showStarredFilter
|
||||
showLayout
|
||||
state={searchState}
|
||||
getTagOptions={stateManager.getTagOptions}
|
||||
getSortOptions={getGrafanaSearcher().getSortOptions}
|
||||
sortPlaceholder={getGrafanaSearcher().sortPlaceholder}
|
||||
includePanels={searchState.includePanels ?? false}
|
||||
onLayoutChange={stateManager.onLayoutChange}
|
||||
onStarredFilterChange={stateManager.onStarredFilterChange}
|
||||
onSortChange={stateManager.onSortChange}
|
||||
|
||||
@@ -11,6 +11,8 @@ jest.mock('./ContentOutlineContext', () => ({
|
||||
const scrollIntoViewMock = jest.fn();
|
||||
const scrollerMock = document.createElement('div');
|
||||
|
||||
const unregisterMock = jest.fn();
|
||||
|
||||
const setup = (mergeSingleChild = false) => {
|
||||
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||
|
||||
@@ -50,6 +52,7 @@ const setup = (mergeSingleChild = false) => {
|
||||
title: 'Item 2-1',
|
||||
ref: document.createElement('div'),
|
||||
level: 'child',
|
||||
onRemove: () => unregisterMock('item-2-1'),
|
||||
},
|
||||
{
|
||||
id: 'item-2-2',
|
||||
@@ -62,7 +65,7 @@ const setup = (mergeSingleChild = false) => {
|
||||
},
|
||||
],
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
unregister: unregisterMock,
|
||||
});
|
||||
|
||||
return render(<ContentOutline scroller={scrollerMock} panelId="content-outline-container-1" />);
|
||||
@@ -143,4 +146,16 @@ describe('<ContentOutline />', () => {
|
||||
await userEvent.click(button);
|
||||
expect(button.getAttribute('aria-controls')).toBe(sectionContent.id);
|
||||
});
|
||||
|
||||
it('deletes item on delete button click', async () => {
|
||||
setup();
|
||||
const expandSectionChevrons = screen.getAllByRole('button', { name: 'Content outline item collapse button' });
|
||||
// chevron for the second item
|
||||
const button = expandSectionChevrons[1];
|
||||
await userEvent.click(button);
|
||||
const deleteButtons = screen.getAllByTestId('content-outline-item-delete-button');
|
||||
await userEvent.click(deleteButtons[0]);
|
||||
|
||||
expect(unregisterMock).toHaveBeenCalledWith('item-2-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,9 +47,11 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
(item) => item.children && !(item.mergeSingleChild && item.children?.length === 1) && item.children.length > 0
|
||||
);
|
||||
|
||||
const outlineItemsHaveDeleteButton = outlineItems.some((item) => item.children?.some((child) => child.onRemove));
|
||||
|
||||
const [sectionsExpanded, setSectionsExpanded] = useState(() => {
|
||||
return outlineItems.reduce((acc: { [key: string]: boolean }, item) => {
|
||||
acc[item.id] = false;
|
||||
acc[item.id] = !!item.expanded;
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
@@ -58,6 +60,10 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
let scrollValue = 0;
|
||||
let el: HTMLElement | null | undefined = ref;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
scrollValue += el?.offsetTop || 0;
|
||||
el = el?.offsetParent instanceof HTMLElement ? el.offsetParent : undefined;
|
||||
@@ -158,7 +164,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
title={contentOutlineExpanded ? item.title : undefined}
|
||||
contentOutlineExpanded={contentOutlineExpanded}
|
||||
className={cx(styles.buttonStyles, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded,
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]: isChildActive(item, activeSectionChildId) && !contentOutlineExpanded,
|
||||
})}
|
||||
indentStyle={cx({
|
||||
@@ -196,7 +202,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
contentOutlineExpanded={contentOutlineExpanded}
|
||||
icon={contentOutlineExpanded ? undefined : item.icon}
|
||||
className={cx(styles.buttonStyles, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded,
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]:
|
||||
isChildActive(item, activeSectionChildId) && !contentOutlineExpanded,
|
||||
})}
|
||||
@@ -211,6 +217,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
isActive={shouldBeActive(child, activeSectionId, activeSectionChildId, sectionsExpanded)}
|
||||
extraHighlight={child.highlight}
|
||||
color={child.color}
|
||||
onRemove={child.onRemove ? () => child.onRemove?.(child.id) : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -257,10 +264,10 @@ const getStyles = (theme: GrafanaTheme2, expanded: boolean) => {
|
||||
marginRight: expanded ? theme.spacing(0.5) : undefined,
|
||||
}),
|
||||
indentRoot: css({
|
||||
paddingLeft: theme.spacing(4),
|
||||
paddingLeft: theme.spacing(3),
|
||||
}),
|
||||
indentChild: css({
|
||||
paddingLeft: expanded ? theme.spacing(7) : theme.spacing(4),
|
||||
paddingLeft: expanded ? theme.spacing(5) : theme.spacing(2.75),
|
||||
}),
|
||||
itemWrapper: css({
|
||||
display: 'flex',
|
||||
@@ -275,7 +282,7 @@ const getStyles = (theme: GrafanaTheme2, expanded: boolean) => {
|
||||
borderRight: `1px solid ${theme.colors.border.medium}`,
|
||||
content: '""',
|
||||
height: '100%',
|
||||
left: 48,
|
||||
left: theme.spacing(4.75),
|
||||
position: 'absolute',
|
||||
transform: 'translateX(50%)',
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ContentOutlineContextProps {
|
||||
unregister: (id: string) => void;
|
||||
unregisterAllChildren: (parentId: string, childType: ITEM_TYPES) => void;
|
||||
updateOutlineItems: (newItems: ContentOutlineItemContextProps[]) => void;
|
||||
updateItem: (id: string, properties: Partial<Omit<ContentOutlineItemContextProps, 'id'>>) => void;
|
||||
}
|
||||
|
||||
interface ContentOutlineContextProviderProps {
|
||||
@@ -141,8 +142,11 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
ref = parent.ref;
|
||||
}
|
||||
|
||||
const childrenUpdated = [...(parent.children || []), { ...outlineItem, id, ref }];
|
||||
childrenUpdated.sort(sortElementsByDocumentPosition);
|
||||
let childrenUpdated = [{ ...outlineItem, id, ref }, ...(parent.children || [])];
|
||||
|
||||
if (!outlineItem.childOnTop) {
|
||||
childrenUpdated = sortItems(childrenUpdated);
|
||||
}
|
||||
|
||||
newItems[parentIndex] = {
|
||||
...parent,
|
||||
@@ -175,6 +179,20 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
setOutlineItems(newItems);
|
||||
}, []);
|
||||
|
||||
const updateItem = useCallback((id: string, properties: Partial<Omit<ContentOutlineItemContextProps, 'id'>>) => {
|
||||
setOutlineItems((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
...properties,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const unregisterAllChildren = useCallback((parentId: string, childType: ITEM_TYPES) => {
|
||||
setOutlineItems((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
@@ -190,7 +208,8 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
setOutlineItems((prevItems) => {
|
||||
const newItems = [...prevItems];
|
||||
for (const item of newItems) {
|
||||
item.children?.sort(sortElementsByDocumentPosition);
|
||||
const sortedItems = sortItems(item.children || []);
|
||||
item.children = sortedItems;
|
||||
}
|
||||
return newItems;
|
||||
});
|
||||
@@ -198,14 +217,14 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
|
||||
return (
|
||||
<ContentOutlineContext.Provider
|
||||
value={{ outlineItems, register, unregister, updateOutlineItems, unregisterAllChildren }}
|
||||
value={{ outlineItems, register, unregister, updateOutlineItems, unregisterAllChildren, updateItem }}
|
||||
>
|
||||
{children}
|
||||
</ContentOutlineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps, b: ContentOutlineItemContextProps) {
|
||||
function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps, b: ContentOutlineItemContextProps) {
|
||||
if (a.ref && b.ref) {
|
||||
const diff = a.ref.compareDocumentPosition(b.ref);
|
||||
if (diff === Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
@@ -217,6 +236,22 @@ export function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortItems(outlineItems: ContentOutlineItemContextProps[]): ContentOutlineItemContextProps[] {
|
||||
const [skipSort, sortable] = outlineItems.reduce<
|
||||
[ContentOutlineItemContextProps[], ContentOutlineItemContextProps[]]
|
||||
>(
|
||||
(acc, item) => {
|
||||
item.childOnTop ? acc[0].push(item) : acc[1].push(item);
|
||||
return acc;
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
|
||||
sortable.sort(sortElementsByDocumentPosition);
|
||||
|
||||
return [...skipSort, ...sortable];
|
||||
}
|
||||
|
||||
export function useContentOutlineContext() {
|
||||
return useContext(ContentOutlineContext);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ export interface ContentOutlineItemBaseProps {
|
||||
* Client can additionally mark filter actions as highlighted
|
||||
*/
|
||||
highlight?: boolean;
|
||||
onRemove?: (id: string) => void;
|
||||
/**
|
||||
* Child that will always be on top of the list
|
||||
* e.g. pinned log in Logs section
|
||||
*/
|
||||
childOnTop?: boolean;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
interface ContentOutlineItemProps extends ContentOutlineItemBaseProps {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cx, css } from '@emotion/css';
|
||||
import React, { ButtonHTMLAttributes, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { IconName, isIconName, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
import { TooltipPlacement } from '@grafana/ui/src/components/Tooltip';
|
||||
|
||||
type CommonProps = {
|
||||
@@ -20,6 +20,7 @@ type CommonProps = {
|
||||
sectionId?: string;
|
||||
toggleCollapsed?: () => void;
|
||||
color?: string;
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
export type ContentOutlineItemButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
@@ -39,6 +40,7 @@ export function ContentOutlineItemButton({
|
||||
sectionId,
|
||||
toggleCollapsed,
|
||||
color,
|
||||
onRemove,
|
||||
...rest
|
||||
}: ContentOutlineItemButtonProps) {
|
||||
const theme = useTheme2();
|
||||
@@ -83,6 +85,15 @@ export function ContentOutlineItemButton({
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{onRemove && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className={styles.deleteButton}
|
||||
icon="times"
|
||||
onClick={() => onRemove()}
|
||||
data-testid="content-outline-item-delete-button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -117,20 +128,20 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
gap: theme.spacing(1),
|
||||
overflow: 'hidden',
|
||||
gap: theme.spacing(0.25),
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
button: css({
|
||||
label: 'content-outline-item-button',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: theme.spacing(theme.components.height.md),
|
||||
padding: theme.spacing(0, 1),
|
||||
gap: theme.spacing(1),
|
||||
gap: theme.spacing(0.5),
|
||||
color: theme.colors.text.secondary,
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden',
|
||||
border: 'none',
|
||||
}),
|
||||
collapseButton: css({
|
||||
@@ -143,6 +154,7 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
color: theme.colors.text.secondary,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.text.primary,
|
||||
@@ -153,6 +165,8 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
marginLeft: theme.spacing(0.5),
|
||||
}),
|
||||
active: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
@@ -193,5 +207,11 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
left: '2px',
|
||||
},
|
||||
}),
|
||||
deleteButton: css({
|
||||
width: theme.spacing(1),
|
||||
height: theme.spacing(1),
|
||||
padding: theme.spacing(0.75, 0.75),
|
||||
marginRight: theme.spacing(0.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -188,8 +188,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
|
||||
/**
|
||||
* Used by Logs details.
|
||||
* Returns true if all queries have the filter, otherwise false.
|
||||
* TODO: In the future, we would like to return active filters based the query that produced the log line.
|
||||
* Returns true if the query identified by `refId` has a filter with the provided key and value.
|
||||
* @alpha
|
||||
*/
|
||||
isFilterLabelActive = async (key: string, value: string | number, refId?: string) => {
|
||||
@@ -235,14 +234,14 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
/**
|
||||
* Used by Logs Popover Menu.
|
||||
*/
|
||||
onClickFilterValue = (value: string | number, refId?: string) => {
|
||||
onClickFilterString = (value: string | number, refId?: string) => {
|
||||
this.onModifyQueries({ type: 'ADD_STRING_FILTER', options: { value: value.toString() } }, refId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Used by Logs Popover Menu.
|
||||
*/
|
||||
onClickFilterOutValue = (value: string | number, refId?: string) => {
|
||||
onClickFilterOutString = (value: string | number, refId?: string) => {
|
||||
this.onModifyQueries({ type: 'ADD_STRING_FILTER_OUT', options: { value: value.toString() } }, refId);
|
||||
};
|
||||
|
||||
@@ -437,8 +436,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
splitOpenFn={this.onSplitOpen('logs')}
|
||||
scrollElement={this.scrollElement}
|
||||
isFilterLabelActive={this.isFilterLabelActive}
|
||||
onClickFilterValue={this.onClickFilterValue}
|
||||
onClickFilterOutValue={this.onClickFilterOutValue}
|
||||
onClickFilterString={this.onClickFilterString}
|
||||
onClickFilterOutString={this.onClickFilterOutString}
|
||||
onPinLineCallback={() => {
|
||||
this.setState({ contentOutlineVisible: true });
|
||||
}}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
|
||||
@@ -39,12 +39,14 @@ import {
|
||||
InlineFieldRow,
|
||||
InlineSwitch,
|
||||
PanelChrome,
|
||||
PopoverContent,
|
||||
RadioButtonGroup,
|
||||
SeriesVisibilityChangeMode,
|
||||
Themeable2,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { mapMouseEventToMode } from '@grafana/ui/src/components/VizLegend/utils';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import store from 'app/core/store';
|
||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||
import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll';
|
||||
@@ -109,9 +111,10 @@ interface Props extends Themeable2 {
|
||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
logsFrames?: DataFrame[];
|
||||
range: TimeRange;
|
||||
onClickFilterValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
loadMoreLogs?(range: AbsoluteTimeRange): void;
|
||||
onPinLineCallback?: () => void;
|
||||
}
|
||||
|
||||
export type LogsVisualisationType = 'table' | 'logs';
|
||||
@@ -132,6 +135,7 @@ interface State {
|
||||
tableFrame?: DataFrame;
|
||||
visualisationType?: LogsVisualisationType;
|
||||
logsContainer?: HTMLDivElement;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
}
|
||||
|
||||
// we need to define the order of these explicitly
|
||||
@@ -156,6 +160,8 @@ const getDefaultVisualisationType = (): LogsVisualisationType => {
|
||||
return 'logs';
|
||||
};
|
||||
|
||||
const PINNED_LOGS_LIMIT = 3;
|
||||
|
||||
class UnthemedLogs extends PureComponent<Props, State> {
|
||||
flipOrderTimer?: number;
|
||||
cancelFlippingTimer?: number;
|
||||
@@ -183,6 +189,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
tableFrame: undefined,
|
||||
visualisationType: this.props.panelState?.logs?.visualisationType ?? getDefaultVisualisationType(),
|
||||
logsContainer: undefined,
|
||||
pinLineButtonTooltipTitle: 'Pin to content outline',
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -652,6 +659,56 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
this.topLogsRef.current?.scrollIntoView();
|
||||
};
|
||||
|
||||
onPinToContentOutlineClick = (row: LogRowModel) => {
|
||||
if (this.getPinnedLogsCount() === PINNED_LOGS_LIMIT) {
|
||||
this.setState({
|
||||
pinLineButtonTooltipTitle: (
|
||||
<span style={{ display: 'flex', textAlign: 'center' }}>
|
||||
❗️
|
||||
<Trans i18nKey="explore.logs.maximum-pinned-logs">
|
||||
Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another.
|
||||
</Trans>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// find the Logs parent item
|
||||
const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
||||
|
||||
//update the parent's expanded state
|
||||
if (logsParent) {
|
||||
this.context?.updateItem(logsParent.id, { expanded: true });
|
||||
}
|
||||
|
||||
this.context?.register({
|
||||
icon: 'gf-logs',
|
||||
title: 'Pinned log',
|
||||
panelId: 'Logs',
|
||||
level: 'child',
|
||||
ref: null,
|
||||
color: LogLevelColor[row.logLevel],
|
||||
childOnTop: true,
|
||||
onClick: () => this.onOpenContext(row, () => {}),
|
||||
onRemove: (id: string) => {
|
||||
this.context?.unregister(id);
|
||||
if (this.getPinnedLogsCount() < PINNED_LOGS_LIMIT) {
|
||||
this.setState({
|
||||
pinLineButtonTooltipTitle: 'Pin to content outline',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.props.onPinLineCallback?.();
|
||||
};
|
||||
|
||||
getPinnedLogsCount = () => {
|
||||
const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
||||
return logsParent?.children?.filter((child) => child.title === 'Pinned log').length ?? 0;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
width,
|
||||
@@ -943,8 +1000,10 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
scrollIntoView={this.scrollIntoView}
|
||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||
containerRendered={!!this.state.logsContainer}
|
||||
onClickFilterValue={this.props.onClickFilterValue}
|
||||
onClickFilterOutValue={this.props.onClickFilterOutValue}
|
||||
onClickFilterString={this.props.onClickFilterString}
|
||||
onClickFilterOutString={this.props.onClickFilterOutString}
|
||||
onPinLine={this.onPinToContentOutlineClick}
|
||||
pinLineButtonTooltipTitle={this.state.pinLineButtonTooltipTitle}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
@@ -952,9 +1011,9 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
{!loading && !hasData && !scanning && (
|
||||
<div className={styles.logRows}>
|
||||
<div className={styles.noData}>
|
||||
No logs found.
|
||||
<Trans i18nKey="explore.logs.no-logs-found">No logs found.</Trans>
|
||||
<Button size="sm" variant="secondary" onClick={this.onClickScan}>
|
||||
Scan for older logs
|
||||
<Trans i18nKey="explore.logs.scan-for-older-logs">Scan for older logs</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -964,7 +1023,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
<div className={styles.noData}>
|
||||
<span>{scanText}</span>
|
||||
<Button size="sm" variant="secondary" onClick={this.onClickStopScan}>
|
||||
Stop scan
|
||||
<Trans i18nKey="explore.logs.stop-scan">Stop scan</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,8 +58,9 @@ interface LogsContainerProps extends PropsFromRedux {
|
||||
splitOpenFn: SplitOpen;
|
||||
scrollElement?: HTMLDivElement;
|
||||
isFilterLabelActive: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
onClickFilterValue: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue: (value: string, refId?: string) => void;
|
||||
onClickFilterString: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString: (value: string, refId?: string) => void;
|
||||
onPinLineCallback?: () => void;
|
||||
}
|
||||
|
||||
type DataSourceInstance =
|
||||
@@ -282,6 +283,7 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
exploreId,
|
||||
logsVolume,
|
||||
scrollElement,
|
||||
onPinLineCallback,
|
||||
} = this.props;
|
||||
|
||||
if (!logRows) {
|
||||
@@ -350,8 +352,9 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
scrollElement={scrollElement}
|
||||
isFilterLabelActive={this.logDetailsFilterAvailable() ? this.props.isFilterLabelActive : undefined}
|
||||
range={range}
|
||||
onClickFilterValue={this.filterValueAvailable() ? this.props.onClickFilterValue : undefined}
|
||||
onClickFilterOutValue={this.filterOutValueAvailable() ? this.props.onClickFilterOutValue : undefined}
|
||||
onPinLineCallback={onPinLineCallback}
|
||||
onClickFilterString={this.filterValueAvailable() ? this.props.onClickFilterString : undefined}
|
||||
onClickFilterOutString={this.filterOutValueAvailable() ? this.props.onClickFilterOutString : undefined}
|
||||
/>
|
||||
</LogsCrossFadeTransition>
|
||||
</>
|
||||
|
||||
@@ -15,9 +15,9 @@ test('Does not render if the filter functions are not defined', () => {
|
||||
});
|
||||
|
||||
test('Renders copy and line contains filter', async () => {
|
||||
const onClickFilterValue = jest.fn();
|
||||
const onClickFilterString = jest.fn();
|
||||
render(
|
||||
<PopoverMenu selection="test" x={0} y={0} row={row} close={() => {}} onClickFilterValue={onClickFilterValue} />
|
||||
<PopoverMenu selection="test" x={0} y={0} row={row} close={() => {}} onClickFilterString={onClickFilterString} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('Copy selection')).toBeInTheDocument();
|
||||
@@ -25,12 +25,12 @@ test('Renders copy and line contains filter', async () => {
|
||||
|
||||
await userEvent.click(screen.getByText('Add as line contains filter'));
|
||||
|
||||
expect(onClickFilterValue).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterValue).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
expect(onClickFilterString).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterString).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
});
|
||||
|
||||
test('Renders copy and line does not contain filter', async () => {
|
||||
const onClickFilterOutValue = jest.fn();
|
||||
const onClickFilterOutString = jest.fn();
|
||||
render(
|
||||
<PopoverMenu
|
||||
selection="test"
|
||||
@@ -38,7 +38,7 @@ test('Renders copy and line does not contain filter', async () => {
|
||||
y={0}
|
||||
row={row}
|
||||
close={() => {}}
|
||||
onClickFilterOutValue={onClickFilterOutValue}
|
||||
onClickFilterOutString={onClickFilterOutString}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -47,8 +47,8 @@ test('Renders copy and line does not contain filter', async () => {
|
||||
|
||||
await userEvent.click(screen.getByText('Add as line does not contain filter'));
|
||||
|
||||
expect(onClickFilterOutValue).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterOutValue).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
expect(onClickFilterOutString).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterOutString).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
});
|
||||
|
||||
test('Renders copy, line contains filter, and line does not contain filter', () => {
|
||||
@@ -59,8 +59,8 @@ test('Renders copy, line contains filter, and line does not contain filter', ()
|
||||
y={0}
|
||||
row={row}
|
||||
close={() => {}}
|
||||
onClickFilterValue={() => {}}
|
||||
onClickFilterOutValue={() => {}}
|
||||
onClickFilterString={() => {}}
|
||||
onClickFilterOutString={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -78,8 +78,8 @@ test('Can be dismissed with escape', async () => {
|
||||
y={0}
|
||||
row={row}
|
||||
close={close}
|
||||
onClickFilterValue={() => {}}
|
||||
onClickFilterOutValue={() => {}}
|
||||
onClickFilterString={() => {}}
|
||||
onClickFilterOutString={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ interface PopoverMenuProps {
|
||||
selection: string;
|
||||
x: number;
|
||||
y: number;
|
||||
onClickFilterValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
row: LogRowModel;
|
||||
close: () => void;
|
||||
}
|
||||
@@ -20,8 +20,8 @@ interface PopoverMenuProps {
|
||||
export const PopoverMenu = ({
|
||||
x,
|
||||
y,
|
||||
onClickFilterValue,
|
||||
onClickFilterOutValue,
|
||||
onClickFilterString,
|
||||
onClickFilterOutString,
|
||||
selection,
|
||||
row,
|
||||
close,
|
||||
@@ -42,7 +42,7 @@ export const PopoverMenu = ({
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const supported = onClickFilterValue || onClickFilterOutValue;
|
||||
const supported = onClickFilterString || onClickFilterOutString;
|
||||
|
||||
if (!supported) {
|
||||
return null;
|
||||
@@ -59,21 +59,21 @@ export const PopoverMenu = ({
|
||||
track('copy', selection.length, row.datasourceType);
|
||||
}}
|
||||
/>
|
||||
{onClickFilterValue && (
|
||||
{onClickFilterString && (
|
||||
<Menu.Item
|
||||
label="Add as line contains filter"
|
||||
onClick={() => {
|
||||
onClickFilterValue(selection, row.dataFrame.refId);
|
||||
onClickFilterString(selection, row.dataFrame.refId);
|
||||
close();
|
||||
track('line_contains', selection.length, row.datasourceType);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{onClickFilterOutValue && (
|
||||
{onClickFilterOutString && (
|
||||
<Menu.Item
|
||||
label="Add as line does not contain filter"
|
||||
onClick={() => {
|
||||
onClickFilterOutValue(selection, row.dataFrame.refId);
|
||||
onClickFilterOutString(selection, row.dataFrame.refId);
|
||||
close();
|
||||
track('line_does_not_contain', selection.length, row.datasourceType);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Table, amendTable } from "./amendTimeSeries";
|
||||
|
||||
describe('amendTable', () => {
|
||||
it('should append nextTable when there is no overlap (nextTable after prevTable)', () => {
|
||||
const prevTable: Table = [
|
||||
[1, 2, 5],
|
||||
['a', 'b', 'e'],
|
||||
];
|
||||
const nextTable: Table = [
|
||||
[6, 7, 8],
|
||||
['f', 'g', 'h'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual([
|
||||
[1, 2, 5, 6, 7, 8],
|
||||
['a', 'b', 'e', 'f', 'g', 'h'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should prepend nextTable when there is no overlap (nextTable before prevTable)', () => {
|
||||
const prevTable: Table = [
|
||||
[3, 4, 5],
|
||||
['c', 'd', 'e'],
|
||||
];
|
||||
const nextTable: Table = [
|
||||
[1, 2],
|
||||
['a', 'b'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual([
|
||||
[1, 2, 3, 4, 5],
|
||||
['a', 'b', 'c', 'd', 'e'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fully replace prevTable when nextTable covers entire range', () => {
|
||||
const prevTable: Table = [
|
||||
[3, 4, 5],
|
||||
['c', 'd', 'e'],
|
||||
];
|
||||
const nextTable: Table = [
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual(nextTable);
|
||||
});
|
||||
|
||||
it('should partially replace prevTable when nextTable is within range', () => {
|
||||
const prevTable: Table = [
|
||||
[1, 2, 3, 4, 5],
|
||||
['a', 'b', 'c', 'd', 'e'],
|
||||
];
|
||||
const nextTable: Table = [
|
||||
[3, 4],
|
||||
['x', 'y'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual([
|
||||
[1, 2, 3, 4, 5],
|
||||
['a', 'b', 'x', 'y', 'e'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should append nextTable with overlap', () => {
|
||||
const prevTable: Table = [
|
||||
[1, 2, 5],
|
||||
['a', 'b', 'e'],
|
||||
];
|
||||
const nextTable: Table = [
|
||||
[2, 3, 6],
|
||||
['b', 'c', 'f'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual([
|
||||
[1, 2, 3, 6],
|
||||
['a', 'b', 'c', 'f'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should prepend nextTable with overlap', () => {
|
||||
const prevTable: Table = [
|
||||
[3, 4, 5],
|
||||
['c', 'd', 'e'],
|
||||
];
|
||||
const nextTable: Table = [
|
||||
[1, 4],
|
||||
['a', 'd'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual([
|
||||
[1, 4, 5],
|
||||
['a', 'd', 'e'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty prevTable', () => {
|
||||
const prevTable: Table = [[]];
|
||||
const nextTable: Table = [
|
||||
[1, 2, 3],
|
||||
['a', 'b', 'c'],
|
||||
];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual(nextTable);
|
||||
});
|
||||
|
||||
it('should handle empty nextTable', () => {
|
||||
const prevTable: Table = [
|
||||
[1, 2, 3],
|
||||
['a', 'b', 'c'],
|
||||
];
|
||||
const nextTable: Table = [[]];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual(prevTable);
|
||||
});
|
||||
|
||||
it('should handle both tables being empty', () => {
|
||||
const prevTable: Table = [[]];
|
||||
const nextTable: Table = [[]];
|
||||
const result = amendTable(prevTable, nextTable);
|
||||
expect(result).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,18 @@ export function amendTable(prevTable: Table, nextTable: Table): Table {
|
||||
}
|
||||
// partial replace
|
||||
else if (nStart > pStart && nEnd < pEnd) {
|
||||
// partial replace
|
||||
let startIdx = closestIdx(nStart, prevTimes);
|
||||
startIdx = prevTimes[startIdx] < nStart ? startIdx + 1 : startIdx;
|
||||
let endIdx = closestIdx(nEnd, prevTimes);
|
||||
endIdx = prevTimes[endIdx] > nEnd ? endIdx - 1 : endIdx;
|
||||
|
||||
outTable = prevTable.map((_, i) =>
|
||||
prevTable[i]
|
||||
.slice(0, startIdx)
|
||||
.concat(nextTable[i])
|
||||
.concat(prevTable[i].slice(endIdx + 1))
|
||||
) as Table;
|
||||
}
|
||||
// append, with overlap
|
||||
else if (nStart >= pStart) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||
import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui';
|
||||
import { withTheme2, Themeable2, Icon, Tooltip, PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { checkLogsError, escapeUnescapedString } from '../utils';
|
||||
|
||||
@@ -60,6 +60,7 @@ interface Props extends Themeable2 {
|
||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
pinned?: boolean;
|
||||
containerRendered?: boolean;
|
||||
handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean;
|
||||
@@ -305,6 +306,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
styles={styles}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||
pinned={this.props.pinned}
|
||||
mouseIsOver={this.state.mouseIsOver}
|
||||
onBlur={this.onMouseLeave}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FocusEvent, SyntheticEvent, useCallback } from 'react';
|
||||
|
||||
import { LogRowContextOptions, LogRowModel, getDefaultTimeRange, locationUtil, urlUtil } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { ClipboardButton, IconButton } from '@grafana/ui';
|
||||
import { ClipboardButton, IconButton, PopoverContent } from '@grafana/ui';
|
||||
import { getConfig } from 'app/core/config';
|
||||
|
||||
import { LogRowStyles } from './getLogRowStyles';
|
||||
@@ -20,10 +20,12 @@ interface Props {
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
pinned?: boolean;
|
||||
styles: LogRowStyles;
|
||||
mouseIsOver: boolean;
|
||||
onBlur: () => void;
|
||||
onPinToContentOutlineClick?: (row: LogRowModel, onOpenContext: (row: LogRowModel) => void) => void;
|
||||
}
|
||||
|
||||
export const LogRowMenuCell = React.memo(
|
||||
@@ -33,6 +35,7 @@ export const LogRowMenuCell = React.memo(
|
||||
onPermalinkClick,
|
||||
onPinLine,
|
||||
onUnpinLine,
|
||||
pinLineButtonTooltipTitle,
|
||||
pinned,
|
||||
row,
|
||||
showContextToggle,
|
||||
@@ -145,7 +148,7 @@ export const LogRowMenuCell = React.memo(
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onPinLine && onPinLine(row)}
|
||||
tooltip="Pin line"
|
||||
tooltip={pinLineButtonTooltipTitle ?? 'Pin line'}
|
||||
tooltipPlacement="top"
|
||||
aria-label="Pin line"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Highlighter from 'react-highlight-words';
|
||||
|
||||
import { CoreApp, findHighlightChunksInText, LogRowContextOptions, LogRowModel } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
import { LogRowMenuCell } from './LogRowMenuCell';
|
||||
@@ -25,6 +26,7 @@ interface Props {
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
pinned?: boolean;
|
||||
styles: LogRowStyles;
|
||||
mouseIsOver: boolean;
|
||||
@@ -88,6 +90,7 @@ export const LogRowMessage = React.memo((props: Props) => {
|
||||
onPermalinkClick,
|
||||
onUnpinLine,
|
||||
onPinLine,
|
||||
pinLineButtonTooltipTitle,
|
||||
pinned,
|
||||
mouseIsOver,
|
||||
onBlur,
|
||||
@@ -124,6 +127,7 @@ export const LogRowMessage = React.memo((props: Props) => {
|
||||
onPermalinkClick={onPermalinkClick}
|
||||
onPinLine={onPinLine}
|
||||
onUnpinLine={onUnpinLine}
|
||||
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
||||
pinned={pinned}
|
||||
styles={styles}
|
||||
mouseIsOver={mouseIsOver}
|
||||
|
||||
@@ -221,8 +221,8 @@ describe('Popover menu', () => {
|
||||
logsSortOrder={LogsSortOrder.Descending}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterOutValue={() => {}}
|
||||
onClickFilterValue={() => {}}
|
||||
onClickFilterOutString={() => {}}
|
||||
onClickFilterString={() => {}}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { withTheme2, Themeable2 } from '@grafana/ui';
|
||||
import { withTheme2, Themeable2, PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { PopoverMenu } from '../../explore/Logs/PopoverMenu';
|
||||
import { UniqueKeyMaker } from '../UniqueKeyMaker';
|
||||
@@ -50,6 +50,7 @@ export interface Props extends Themeable2 {
|
||||
onClickHideField?: (key: string) => void;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
onLogRowHover?: (row?: LogRowModel) => void;
|
||||
onOpenContext?: (row: LogRowModel, onClose: () => void) => void;
|
||||
getRowContextQuery?: (
|
||||
@@ -68,8 +69,8 @@ export interface Props extends Themeable2 {
|
||||
* Any overflowing content will be clipped at the table boundary.
|
||||
*/
|
||||
overflowingContent?: boolean;
|
||||
onClickFilterValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -107,7 +108,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
if (!config.featureToggles.logRowsPopoverMenu || this.props.app !== CoreApp.Explore) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(this.props.onClickFilterOutValue || this.props.onClickFilterValue);
|
||||
return Boolean(this.props.onClickFilterOutString || this.props.onClickFilterString);
|
||||
}
|
||||
|
||||
handleSelection = (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel): boolean => {
|
||||
@@ -217,8 +218,8 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
row={this.state.selectedRow}
|
||||
selection={this.state.selection}
|
||||
{...this.state.popoverMenuCoordinates}
|
||||
onClickFilterValue={rest.onClickFilterValue}
|
||||
onClickFilterOutValue={rest.onClickFilterOutValue}
|
||||
onClickFilterString={rest.onClickFilterString}
|
||||
onClickFilterOutString={rest.onClickFilterOutString}
|
||||
/>
|
||||
)}
|
||||
<table className={cx(styles.logsRowsTable, this.props.overflowingContent ? '' : styles.logsRowsTableContain)}>
|
||||
@@ -238,6 +239,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
permalinkedRowId={this.props.permalinkedRowId}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||
pinned={this.props.pinnedRowId === row.uid}
|
||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
||||
@@ -260,6 +262,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
permalinkedRowId={this.props.permalinkedRowId}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||
pinned={this.props.pinnedRowId === row.uid}
|
||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { FilterInput } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { ActionRow } from 'app/features/search/page/components/ActionRow';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service';
|
||||
|
||||
@@ -37,19 +35,11 @@ const RecentlyDeletedPage = memo(() => {
|
||||
return (
|
||||
<Page navId="dashboards/recentlyDeleted">
|
||||
<Page.Contents>
|
||||
<FilterInput
|
||||
placeholder={t('recentlyDeleted.filter.placeholder', 'Search for dashboards')}
|
||||
value={searchState.query}
|
||||
escapeRegex={false}
|
||||
onChange={(e) => stateManager.onQueryChange(e)}
|
||||
/>
|
||||
<ActionRow
|
||||
showStarredFilter={false}
|
||||
state={searchState}
|
||||
getTagOptions={stateManager.getTagOptions}
|
||||
getSortOptions={getGrafanaSearcher().getSortOptions}
|
||||
sortPlaceholder={getGrafanaSearcher().sortPlaceholder}
|
||||
includePanels={false}
|
||||
onLayoutChange={stateManager.onLayoutChange}
|
||||
onSortChange={stateManager.onSortChange}
|
||||
onTagFilterChange={stateManager.onTagFilterChange}
|
||||
|
||||
@@ -21,21 +21,21 @@ function getLayoutOptions() {
|
||||
];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
interface ActionRowProps {
|
||||
state: SearchState;
|
||||
showStarredFilter?: boolean;
|
||||
showLayout?: boolean;
|
||||
sortPlaceholder?: string;
|
||||
|
||||
onLayoutChange: (layout: SearchLayout) => void;
|
||||
onSortChange: (value?: string) => void;
|
||||
onStarredFilterChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||
onTagFilterChange: (tags: string[]) => void;
|
||||
getTagOptions: () => Promise<TermCount[]>;
|
||||
getSortOptions: () => Promise<SelectableValue[]>;
|
||||
sortPlaceholder?: string;
|
||||
onDatasourceChange: (ds?: string) => void;
|
||||
onPanelTypeChange: (pt?: string) => void;
|
||||
includePanels: boolean;
|
||||
onSetIncludePanels: (v: boolean) => void;
|
||||
state: SearchState;
|
||||
showStarredFilter?: boolean;
|
||||
hideLayout?: boolean;
|
||||
}
|
||||
|
||||
export function getValidQueryLayout(q: SearchState): SearchLayout {
|
||||
@@ -52,20 +52,20 @@ export function getValidQueryLayout(q: SearchState): SearchLayout {
|
||||
}
|
||||
|
||||
export const ActionRow = ({
|
||||
state,
|
||||
showStarredFilter,
|
||||
showLayout,
|
||||
sortPlaceholder,
|
||||
onLayoutChange,
|
||||
onSortChange,
|
||||
onStarredFilterChange = () => {},
|
||||
onTagFilterChange,
|
||||
getTagOptions,
|
||||
getSortOptions,
|
||||
sortPlaceholder,
|
||||
onDatasourceChange,
|
||||
onPanelTypeChange,
|
||||
onSetIncludePanels,
|
||||
state,
|
||||
showStarredFilter,
|
||||
hideLayout,
|
||||
}: Props) => {
|
||||
}: ActionRowProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const layout = getValidQueryLayout(state);
|
||||
|
||||
@@ -113,7 +113,7 @@ export const ActionRow = ({
|
||||
</Stack>
|
||||
|
||||
<Stack gap={2}>
|
||||
{!hideLayout && (
|
||||
{showLayout && (
|
||||
<RadioButtonGroup
|
||||
options={getLayoutOptions()}
|
||||
disabledOptions={disabledOptions}
|
||||
|
||||
@@ -92,12 +92,13 @@ export const BarChartPanel = (props: PanelProps<Options>) => {
|
||||
|
||||
const xGroupsCount = vizSeries[0]?.length ?? 0;
|
||||
const seriesCount = vizSeries[0]?.fields.length ?? 0;
|
||||
const totalSeries = info.series[0].fields.length - 1;
|
||||
|
||||
let { builder, prepData } = useMemo(
|
||||
() => {
|
||||
return xGroupsCount === 0
|
||||
? { builder: null, prepData: null }
|
||||
: prepConfig({ series: vizSeries, color: info.color, orientation, options, timeZone, theme });
|
||||
: prepConfig({ series: vizSeries, totalSeries, color: info.color, orientation, options, timeZone, theme });
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@@ -105,6 +106,7 @@ export const BarChartPanel = (props: PanelProps<Options>) => {
|
||||
timeZone,
|
||||
props.data.structureRev,
|
||||
|
||||
totalSeries,
|
||||
seriesCount,
|
||||
xGroupsCount,
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ describe('BarChart utils', () => {
|
||||
describe('preparePlotConfigBuilder', () => {
|
||||
const config: PrepConfigOpts = {
|
||||
series: [mockDataFrame()],
|
||||
totalSeries: 2,
|
||||
// color?: Field | null;
|
||||
timeZone: defaultTimeZone,
|
||||
theme: createTheme(),
|
||||
|
||||
@@ -148,7 +148,8 @@ export function prepSeries(
|
||||
}
|
||||
|
||||
export interface PrepConfigOpts {
|
||||
series: DataFrame[];
|
||||
series: DataFrame[]; // series with hideFrom.viz: false
|
||||
totalSeries: number; // total series count (including hidden)
|
||||
color?: Field | null;
|
||||
orientation: VizOrientation;
|
||||
options: Options;
|
||||
@@ -156,7 +157,7 @@ export interface PrepConfigOpts {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
export const prepConfig = ({ series, color, orientation, options, timeZone, theme }: PrepConfigOpts) => {
|
||||
export const prepConfig = ({ series, totalSeries, color, orientation, options, timeZone, theme }: PrepConfigOpts) => {
|
||||
let {
|
||||
showValue,
|
||||
groupWidth,
|
||||
@@ -205,8 +206,11 @@ export const prepConfig = ({ series, color, orientation, options, timeZone, them
|
||||
const vizOrientation = getScaleOrientation(orientation);
|
||||
|
||||
// Use bar width when only one field
|
||||
if (frame.fields.length === 2) {
|
||||
groupWidth = barWidth;
|
||||
if (frame.fields.length === 2 && stacking === StackingMode.None) {
|
||||
if (totalSeries === 1) {
|
||||
groupWidth = barWidth;
|
||||
}
|
||||
|
||||
barWidth = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -324,6 +324,63 @@ describe('LogsPanel', () => {
|
||||
expect(jest.mocked(styles.getLogRowStyles).mock.calls.length).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filters', () => {
|
||||
const series = [
|
||||
createDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: ['2019-04-26T09:28:11.352440161Z'],
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: FieldType.string,
|
||||
values: ['logline text'],
|
||||
labels: {
|
||||
app: 'common_app',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
it('allow to filter for a value or filter out a value', async () => {
|
||||
const filterForMock = jest.fn();
|
||||
const filterOutMock = jest.fn();
|
||||
const isFilterLabelActiveMock = jest.fn();
|
||||
setup({
|
||||
data: {
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickFilterLabel: filterForMock,
|
||||
onClickFilterOutLabel: filterOutMock,
|
||||
isFilterLabelActive: isFilterLabelActiveMock,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('logline text'));
|
||||
await userEvent.click(screen.getByLabelText('Filter for value in query A'));
|
||||
expect(filterForMock).toHaveBeenCalledTimes(1);
|
||||
await userEvent.click(screen.getByLabelText('Filter out value in query A'));
|
||||
expect(filterOutMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(isFilterLabelActiveMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const setup = (propsOverrides?: {}) => {
|
||||
|
||||
@@ -29,10 +29,34 @@ import { LogLabels } from '../../../features/logs/components/LogLabels';
|
||||
import { LogRows } from '../../../features/logs/components/LogRows';
|
||||
import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel';
|
||||
|
||||
import { Options } from './types';
|
||||
import {
|
||||
isIsFilterLabelActive,
|
||||
isOnClickFilterLabel,
|
||||
isOnClickFilterOutLabel,
|
||||
isOnClickFilterOutString,
|
||||
isOnClickFilterString,
|
||||
Options,
|
||||
} from './types';
|
||||
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
|
||||
|
||||
interface LogsPanelProps extends PanelProps<Options> {}
|
||||
interface LogsPanelProps extends PanelProps<Options> {
|
||||
/**
|
||||
* Adds a key => value filter to the query referenced by the provided DataFrame refId. Used by Log details and Logs table.
|
||||
* onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
||||
*
|
||||
* Adds a negative key => value filter to the query referenced by the provided DataFrame refId. Used by Log details and Logs table.
|
||||
* onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
||||
*
|
||||
* Adds a string filter to the query referenced by the provided DataFrame refId. Used by the Logs popover menu.
|
||||
* onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
*
|
||||
* Removes a string filter to the query referenced by the provided DataFrame refId. Used by the Logs popover menu.
|
||||
* onClickFilterString?: (value: string, refId?: string) => void;
|
||||
*
|
||||
* Determines if a given key => value filter is active in a given query. Used by Log details.
|
||||
* isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
*/
|
||||
}
|
||||
interface LogsPermalinkUrlState {
|
||||
logs?: {
|
||||
id?: string;
|
||||
@@ -55,6 +79,11 @@ export const LogsPanel = ({
|
||||
dedupStrategy,
|
||||
enableLogDetails,
|
||||
showLogContextToggle,
|
||||
onClickFilterLabel,
|
||||
onClickFilterOutLabel,
|
||||
onClickFilterOutString,
|
||||
onClickFilterString,
|
||||
isFilterLabelActive,
|
||||
},
|
||||
id,
|
||||
}: LogsPanelProps) => {
|
||||
@@ -68,7 +97,7 @@ export const LogsPanel = ({
|
||||
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
|
||||
let closeCallback = useRef<() => void>();
|
||||
|
||||
const { eventBus } = usePanelContext();
|
||||
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
||||
const onLogRowHover = useCallback(
|
||||
(row?: LogRowModel) => {
|
||||
if (!row) {
|
||||
@@ -220,6 +249,28 @@ export const LogsPanel = ({
|
||||
[scrollElement]
|
||||
);
|
||||
|
||||
const defaultOnClickFilterLabel = useCallback(
|
||||
(key: string, value: string) => {
|
||||
onAddAdHocFilter?.({
|
||||
key,
|
||||
value,
|
||||
operator: '=',
|
||||
});
|
||||
},
|
||||
[onAddAdHocFilter]
|
||||
);
|
||||
|
||||
const defaultOnClickFilterOutLabel = useCallback(
|
||||
(key: string, value: string) => {
|
||||
onAddAdHocFilter?.({
|
||||
key,
|
||||
value,
|
||||
operator: '!=',
|
||||
});
|
||||
},
|
||||
[onAddAdHocFilter]
|
||||
);
|
||||
|
||||
if (!data || logRows.length === 0) {
|
||||
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
||||
}
|
||||
@@ -275,6 +326,17 @@ export const LogsPanel = ({
|
||||
onLogRowHover={onLogRowHover}
|
||||
app={CoreApp.Dashboard}
|
||||
onOpenContext={onOpenContext}
|
||||
onClickFilterLabel={
|
||||
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
|
||||
}
|
||||
onClickFilterOutLabel={
|
||||
isOnClickFilterOutLabel(onClickFilterOutLabel) ? onClickFilterOutLabel : defaultOnClickFilterOutLabel
|
||||
}
|
||||
onClickFilterString={isOnClickFilterString(onClickFilterString) ? onClickFilterString : undefined}
|
||||
onClickFilterOutString={
|
||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||
}
|
||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
/>
|
||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,12 @@ composableKinds: PanelCfg: {
|
||||
enableLogDetails: bool
|
||||
sortOrder: common.LogsSortOrder
|
||||
dedupStrategy: common.LogsDedupStrategy
|
||||
// TODO: figure out how to define callbacks
|
||||
onClickFilterLabel?: _
|
||||
onClickFilterOutLabel?: _
|
||||
isFilterLabelActive?: _
|
||||
onClickFilterString?: _
|
||||
onClickFilterOutString?: _
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -13,6 +13,14 @@ import * as common from '@grafana/schema';
|
||||
export interface Options {
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
/**
|
||||
* TODO: figure out how to define callbacks
|
||||
*/
|
||||
onClickFilterLabel?: unknown;
|
||||
onClickFilterOutLabel?: unknown;
|
||||
onClickFilterOutString?: unknown;
|
||||
onClickFilterString?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showLabels: boolean;
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
import { DataFrame } from '@grafana/data';
|
||||
|
||||
export { Options } from './panelcfg.gen';
|
||||
|
||||
type onClickFilterLabelType = (key: string, value: string, frame?: DataFrame) => void;
|
||||
type onClickFilterOutLabelType = (key: string, value: string, frame?: DataFrame) => void;
|
||||
type onClickFilterValueType = (value: string, refId?: string) => void;
|
||||
type onClickFilterOutStringType = (value: string, refId?: string) => void;
|
||||
type isFilterLabelActiveType = (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
|
||||
export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnClickFilterOutLabel(callback: unknown): callback is onClickFilterOutLabelType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnClickFilterString(callback: unknown): callback is onClickFilterValueType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnClickFilterOutString(callback: unknown): callback is onClickFilterOutStringType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isIsFilterLabelActive(callback: unknown): callback is isFilterLabelActiveType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
@@ -497,6 +497,12 @@
|
||||
"title": "Add query to Query Library",
|
||||
"visibility": "Visibility"
|
||||
},
|
||||
"logs": {
|
||||
"maximum-pinned-logs": "Maximum of {{PINNED_LOGS_LIMIT}} pinned logs reached. Unpin a log to add another.",
|
||||
"no-logs-found": "No logs found.",
|
||||
"scan-for-older-logs": "Scan for older logs",
|
||||
"stop-scan": "Stop scan"
|
||||
},
|
||||
"rich-history": {
|
||||
"close-tooltip": "Close query history",
|
||||
"datasource-a-z": "Data source A-Z",
|
||||
@@ -1479,11 +1485,6 @@
|
||||
},
|
||||
"query-editor-not-exported": "Data source plugin does not export any Query Editor component"
|
||||
},
|
||||
"recentlyDeleted": {
|
||||
"filter": {
|
||||
"placeholder": "Search for dashboards"
|
||||
}
|
||||
},
|
||||
"refresh-picker": {
|
||||
"aria-label": {
|
||||
"choose-interval": "Auto refresh turned off. Choose refresh time interval",
|
||||
|
||||
@@ -497,6 +497,12 @@
|
||||
"title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy",
|
||||
"visibility": "Vįşįþįľįŧy"
|
||||
},
|
||||
"logs": {
|
||||
"maximum-pinned-logs": "Mäχįmūm őƒ {{PINNED_LOGS_LIMIT}} pįʼnʼnęđ ľőģş řęäčĥęđ. Ůʼnpįʼn ä ľőģ ŧő äđđ äʼnőŧĥęř.",
|
||||
"no-logs-found": "Ńő ľőģş ƒőūʼnđ.",
|
||||
"scan-for-older-logs": "Ŝčäʼn ƒőř őľđęř ľőģş",
|
||||
"stop-scan": "Ŝŧőp şčäʼn"
|
||||
},
|
||||
"rich-history": {
|
||||
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
|
||||
"datasource-a-z": "Đäŧä şőūřčę Å-Ż",
|
||||
@@ -1479,11 +1485,6 @@
|
||||
},
|
||||
"query-editor-not-exported": "Đäŧä şőūřčę pľūģįʼn đőęş ʼnőŧ ęχpőřŧ äʼny Qūęřy Ēđįŧőř čőmpőʼnęʼnŧ"
|
||||
},
|
||||
"recentlyDeleted": {
|
||||
"filter": {
|
||||
"placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş"
|
||||
}
|
||||
},
|
||||
"refresh-picker": {
|
||||
"aria-label": {
|
||||
"choose-interval": "Åūŧő řęƒřęşĥ ŧūřʼnęđ őƒƒ. Cĥőőşę řęƒřęşĥ ŧįmę įʼnŧęřväľ",
|
||||
|
||||
Reference in New Issue
Block a user