diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3d775fa7..0e980cf0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,11 +2,27 @@ Changelog ========= -<<<<<<< HEAD 0.x.x (?) -================== -* Added ... +* Add `QueryMode` parameter in CloudwatchMetricsTarget +* Added support `alias` via the `legendFormat` option for `Target` +* Added `neutral` option for `GaugePanel` (supported by Grafana 9.3.0 - https://github.com/grafana/grafana/discussions/38273) +* Added support `alias` via the `legendFormat` option for `Target` +* **Breaking change:** Fixed spelling errors for temperature units, corrected 'CELSUIS' to 'CELSIUS' and 'FARENHEIT' to 'FAHRENHEIT'. +* Added ``tooltipSort`` parameter to PieChartv2 panel +* Fix mappings for Table +* Added support for AWS Cross-Account in CloudwatchMetricsTarget +* Added `LokiTarget` + +0.7.1 2024-01-12 +================ + +* Extended DashboardLink to support links to dashboards and urls, as per the docs_ + +.. _`docs`: https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/manage-dashboard-links/#dashboard-links + +* Fix default options for Heatmap +* Add Unit option for Graph panel * Added Minimum option for Timeseries * Added Maximum option for Timeseries * Added Number of decimals displays option for Timeseries @@ -14,10 +30,19 @@ Changelog * Extended SqlTarget to support parsing queries from files * Fix AlertCondition backwards compatibility (``useNewAlerts`` default to ``False``) * Added RateMetricAgg_ for ElasticSearch +* added axisSoftMin and axisSoftMax options for TimeSeries +* Added support for Azure Data Explorer datasource plugin (https://github.com/grafana/azure-data-explorer-datasource) +* Added ``sortBy`` parameter to Table panel +* Added ``tooltipSort`` parameter to TimeSeries panel +* Added unit parameter to the Table class in core +* Added a hide parameter to ElasticsearchTarget +* Fix value literal GAUGE_CALC_TOTAL to sum instead of total +* Fix `BarGauge` orientation validation to accept `'auto'` .. _`Bar_Chart`: https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/bar-chart/ .. _`RateMetricAgg`: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-rate-aggregation.html + 0.7.0 (2022-10-02) ================== diff --git a/README.rst b/README.rst index 5762d975..4daab8f7 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ Support This library is in its very early stages. We'll probably make changes that break backwards compatibility, although we'll try hard not to. -grafanalib works with Python 3.6 through 3.10. +grafanalib works with Python 3.6 through 3.11. Developing ========== diff --git a/docs/requirements.txt b/docs/requirements.txt index a3f75dea..63500b97 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx == 6.1.3 -sphinx_rtd_theme == 1.2.2 \ No newline at end of file +sphinx == 7.4.7 +sphinx_rtd_theme == 2.0.0 diff --git a/grafanalib/azuredataexplorer.py b/grafanalib/azuredataexplorer.py new file mode 100644 index 00000000..9589d25a --- /dev/null +++ b/grafanalib/azuredataexplorer.py @@ -0,0 +1,41 @@ +"""Helpers to create Azure Data Explorer specific Grafana queries.""" + +import attr + +TIME_SERIES_RESULT_FORMAT = 'time_series' +TABLE_RESULT_FORMAT = 'table' +ADX_TIME_SERIES_RESULT_FORMAT = 'time_series_adx_series' + + +@attr.s +class AzureDataExplorerTarget(object): + """ + Generates Azure Data Explorer target JSON structure. + + Link to Azure Data Explorer datasource Grafana plugin: + https://grafana.com/grafana/plugins/grafana-azure-data-explorer-datasource/ + + Azure Data Explorer docs on query language (KQL): + https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/ + + :param database: Database to execute query on + :param query: Query in Kusto Query Language (KQL) + :param resultFormat: Output format of the query result + :param alias: legend alias + :param refId: target reference id + """ + + database = attr.ib(default="") + query = attr.ib(default="") + resultFormat = attr.ib(default=TIME_SERIES_RESULT_FORMAT) + alias = attr.ib(default="") + refId = attr.ib(default="") + + def to_json_data(self): + return { + 'database': self.database, + 'query': self.query, + 'resultFormat': self.resultFormat, + 'alias': self.alias, + 'refId': self.refId + } diff --git a/grafanalib/cloudwatch.py b/grafanalib/cloudwatch.py index a9a22248..9aadae4f 100644 --- a/grafanalib/cloudwatch.py +++ b/grafanalib/cloudwatch.py @@ -1,8 +1,8 @@ """Helpers to create Cloudwatch-specific Grafana queries.""" import attr - from attr.validators import instance_of + from grafanalib.core import Target @@ -22,6 +22,8 @@ class CloudwatchMetricsTarget(Target): :param expression: Cloudwatch Metric math expressions :param id: unique id :param matchExact: Only show metrics that exactly match all defined dimension names. + :param account: AWS Account where Cloudwatch is used + :param accountId: AWS Account ID where Cloudwatch is used :param metricName: Cloudwatch metric name :param namespace: Cloudwatch namespace :param period: Cloudwatch data period @@ -31,12 +33,16 @@ class CloudwatchMetricsTarget(Target): :param statistic: Cloudwatch mathematic statistic :param hide: controls if given metric is displayed on visualization :param datasource: Grafana datasource name + :param queryMode: queryMode for cloudwatch metric request """ + alias = attr.ib(default="") dimensions = attr.ib(factory=dict, validator=instance_of(dict)) expression = attr.ib(default="") id = attr.ib(default="") matchExact = attr.ib(default=True, validator=instance_of(bool)) + account = attr.ib(default="") + accountId = attr.ib(default="") metricName = attr.ib(default="") namespace = attr.ib(default="") period = attr.ib(default="") @@ -46,15 +52,17 @@ class CloudwatchMetricsTarget(Target): statistic = attr.ib(default="Average") hide = attr.ib(default=False, validator=instance_of(bool)) datasource = attr.ib(default=None) + queryMode = attr.ib(default="") def to_json_data(self): - return { "alias": self.alias, "dimensions": self.dimensions, "expression": self.expression, "id": self.id, "matchExact": self.matchExact, + "account": self.account, + "accountId": self.accountId, "metricName": self.metricName, "namespace": self.namespace, "period": self.period, @@ -64,6 +72,7 @@ def to_json_data(self): "statistic": self.statistic, "hide": self.hide, "datasource": self.datasource, + "queryMode": self.queryMode, } @@ -88,6 +97,7 @@ class CloudwatchLogsInsightsTarget(Target): :param hide: controls if given metric is displayed on visualization :param datasource: Grafana datasource name """ + expression = attr.ib(default="") id = attr.ib(default="") logGroupNames = attr.ib(factory=list, validator=instance_of(list)) @@ -99,7 +109,6 @@ class CloudwatchLogsInsightsTarget(Target): datasource = attr.ib(default=None) def to_json_data(self): - return { "expression": self.expression, "id": self.id, diff --git a/grafanalib/core.py b/grafanalib/core.py index 5f0a7cf1..3d5acc2f 100644 --- a/grafanalib/core.py +++ b/grafanalib/core.py @@ -5,12 +5,13 @@ encourage it by way of some defaults. Rather, they are ways of building arbitrary Grafana JSON. """ - +from __future__ import annotations import itertools import math import string import warnings from numbers import Number +from typing import Literal import attr from attr.validators import in_, instance_of @@ -73,7 +74,7 @@ def to_json_data(self): FLOT = 'flot' ABSOLUTE_TYPE = 'absolute' -DASHBOARD_TYPE = 'dashboard' +DASHBOARD_TYPE = Literal['dashboards', 'link'] ROW_TYPE = 'row' GRAPH_TYPE = 'graph' DISCRETE_TYPE = 'natel-discrete-panel' @@ -277,7 +278,7 @@ def to_json_data(self): GAUGE_CALC_MIN = 'min' GAUGE_CALC_MAX = 'max' GAUGE_CALC_MEAN = 'mean' -GAUGE_CALC_TOTAL = 'total' +GAUGE_CALC_TOTAL = 'sum' GAUGE_CALC_COUNT = 'count' GAUGE_CALC_RANGE = 'range' GAUGE_CALC_DELTA = 'delta' @@ -289,6 +290,7 @@ def to_json_data(self): ORIENTATION_HORIZONTAL = 'horizontal' ORIENTATION_VERTICAL = 'vertical' +ORIENTATION_AUTO = 'auto' GAUGE_DISPLAY_MODE_BASIC = 'basic' GAUGE_DISPLAY_MODE_LCD = 'lcd' @@ -301,6 +303,9 @@ def to_json_data(self): DEFAULT_AUTO_COUNT = 30 DEFAULT_MIN_AUTO_INTERVAL = '10s' +DASHBOARD_LINK_ICON = Literal['bolt', 'cloud', 'dashboard', 'doc', + 'external link', 'info', 'question'] + @attr.s class Mapping(object): @@ -569,6 +574,7 @@ class Target(object): Metric to show. :param target: Graphite way to select data + :param legendFormat: Target alias. Prometheus use legendFormat, other like Influx use alias. This set legendFormat as well as alias. """ expr = attr.ib(default="") @@ -595,6 +601,7 @@ def to_json_data(self): 'interval': self.interval, 'intervalFactor': self.intervalFactor, 'legendFormat': self.legendFormat, + 'alias': self.legendFormat, 'metric': self.metric, 'refId': self.refId, 'step': self.step, @@ -603,6 +610,29 @@ def to_json_data(self): } +# Currently not deriving from `Target` because Grafana errors if fields like `query` are added to Loki targets +@attr.s +class LokiTarget(object): + """ + Target for Loki LogQL queries + """ + + datasource = attr.ib(default='', validator=instance_of(str)) + expr = attr.ib(default='', validator=instance_of(str)) + hide = attr.ib(default=False, validator=instance_of(bool)) + + def to_json_data(self): + return { + 'datasource': { + 'type': 'loki', + 'uid': self.datasource, + }, + 'expr': self.expr, + 'hide': self.hide, + 'queryType': 'range', + } + + @attr.s class CloudWatchTarget(object): region = attr.ib(default="") @@ -912,24 +942,65 @@ def to_json_data(self): @attr.s class DashboardLink(object): - dashboard = attr.ib() - uri = attr.ib() - keepTime = attr.ib( + """Create a link to other dashboards, or external resources. + + Dashboard Links come in two flavours; a list of dashboards, or a direct + link to an arbitrary URL. These are controlled by the ``type`` parameter. + A dashboard list targets a given set of tags, whereas for a link you must + also provide the URL. + + See `the documentation ` + for more information. + + :param asDropdown: Controls if the list appears in a dropdown rather than + tiling across the dashboard. Affects 'dashboards' type only. Defaults + to False + :param icon: Set the icon, from a predefined list. See + ``grafanalib.core.DASHBOARD_LINK_ICON`` for allowed values. Affects + the 'link' type only. Defaults to 'external link' + :param includeVars: Controls if data variables from the current dashboard + are passed as query parameters to the linked target. Defaults to False + :param keepTime: Controls if the current time range is passed as query + parameters to the linked target. Defaults to False + :param tags: A list of tags used to select dashboards for the link. + Affects the 'dashboards' type only. Defaults to an empty list + :param targetBlank: Controls if the link opens in a new tab. Defaults + to False + :param tooltip: Tooltip text that appears when hovering over the link. + Affects the 'link' type only. Defaults to an empty string + :param type: Controls the type of DashboardLink generated. Must be + one of 'dashboards' or 'link'. + :param uri: The url target of the external link. Affects the 'link' + type only. + """ + asDropdown: bool = attr.ib(default=False, validator=instance_of(bool)) + icon: DASHBOARD_LINK_ICON = attr.ib(default='external link', + validator=in_(DASHBOARD_LINK_ICON.__args__)) + includeVars: bool = attr.ib(default=False, validator=instance_of(bool)) + keepTime: bool = attr.ib( default=True, validator=instance_of(bool), ) - title = attr.ib(default=None) - type = attr.ib(default=DASHBOARD_TYPE) + tags: list[str] = attr.ib(factory=list, validator=instance_of(list)) + targetBlank: bool = attr.ib(default=False, validator=instance_of(bool)) + title: str = attr.ib(default="") + tooltip: str = attr.ib(default="", validator=instance_of(str)) + type: DASHBOARD_TYPE = attr.ib(default='dashboards', + validator=in_(DASHBOARD_TYPE.__args__)) + uri: str = attr.ib(default="", validator=instance_of(str)) def to_json_data(self): - title = self.dashboard if self.title is None else self.title return { - 'dashUri': self.uri, - 'dashboard': self.dashboard, + 'asDropdown': self.asDropdown, + 'icon': self.icon, + 'includeVars': self.includeVars, 'keepTime': self.keepTime, - 'title': title, + 'tags': self.tags, + 'targetBlank': self.targetBlank, + 'title': self.title, + 'tooltip': self.tooltip, 'type': self.type, - 'url': self.uri, + 'url': self.uri } @@ -2105,7 +2176,7 @@ class Graph(Panel): :param stack: Each series is stacked on top of another :param percentage: Available when Stack is selected. Each series is drawn as a percentage of the total of all series :param thresholds: List of GraphThresholds - Only valid when alert not defined - + :param unit: Set Y Axis Unit """ alert = attr.ib(default=None) @@ -2139,6 +2210,7 @@ class Graph(Panel): validator=instance_of(Tooltip), ) thresholds = attr.ib(default=attr.Factory(list)) + unit = attr.ib(default='', validator=instance_of(str)) xAxis = attr.ib(default=attr.Factory(XAxis), validator=instance_of(XAxis)) try: yAxes = attr.ib( @@ -2162,6 +2234,11 @@ def to_json_data(self): 'description': self.description, 'editable': self.editable, 'error': self.error, + 'fieldConfig': { + 'defaults': { + 'unit': self.unit + }, + }, 'fill': self.fill, 'grid': self.grid, 'isNew': self.isNew, @@ -2267,11 +2344,15 @@ class TimeSeries(Panel): :param thresholds: single stat thresholds :param tooltipMode: When you hover your cursor over the visualization, Grafana can display tooltips single (Default), multi, none + :param tooltipSort: To sort the tooltips + none (Default), asc, desc :param unit: units :param thresholdsStyleMode: thresholds style mode off (Default), area, line, line+area :param valueMin: Minimum value for Panel :param valueMax: Maximum value for Panel :param valueDecimals: Number of display decimals + :param axisSoftMin: soft minimum Y axis value + :param axisSoftMax: soft maximum Y axis value """ axisPlacement = attr.ib(default='auto', validator=instance_of(str)) @@ -2322,12 +2403,15 @@ class TimeSeries(Panel): showPoints = attr.ib(default='auto', validator=instance_of(str)) stacking = attr.ib(factory=dict, validator=instance_of(dict)) tooltipMode = attr.ib(default='single', validator=instance_of(str)) + tooltipSort = attr.ib(default='none', validator=instance_of(str)) unit = attr.ib(default='', validator=instance_of(str)) thresholdsStyleMode = attr.ib(default='off', validator=instance_of(str)) valueMin = attr.ib(default=None, validator=attr.validators.optional(instance_of(int))) valueMax = attr.ib(default=None, validator=attr.validators.optional(instance_of(int))) valueDecimals = attr.ib(default=None, validator=attr.validators.optional(instance_of(int))) + axisSoftMin = attr.ib(default=None, validator=attr.validators.optional(instance_of(int))) + axisSoftMax = attr.ib(default=None, validator=attr.validators.optional(instance_of(int))) def to_json_data(self): return self.panel_json( @@ -2362,6 +2446,8 @@ def to_json_data(self): 'thresholdsStyle': { 'mode': self.thresholdsStyleMode }, + 'axisSoftMin': self.axisSoftMin, + 'axisSoftMax': self.axisSoftMax }, 'mappings': self.mappings, "min": self.valueMin, @@ -2378,7 +2464,8 @@ def to_json_data(self): 'calcs': self.legendCalcs }, 'tooltip': { - 'mode': self.tooltipMode + 'mode': self.tooltipMode, + 'sort': self.tooltipSort } }, 'type': TIMESERIES_TYPE, @@ -2812,6 +2899,7 @@ class Stat(Panel): """ alignment = attr.ib(default='auto') + color = attr.ib(default=None) colorMode = attr.ib(default='value') decimals = attr.ib(default=None) format = attr.ib(default='none') @@ -2830,6 +2918,7 @@ def to_json_data(self): { 'fieldConfig': { 'defaults': { + 'color': self.color, 'custom': {}, 'decimals': self.decimals, 'mappings': self.mappings, @@ -3306,7 +3395,19 @@ def _style_columns(columns): @attr.s -class Table(object): +class TableSortByField(object): + displayName = attr.ib(default="") + desc = attr.ib(default=False) + + def to_json_data(self): + return { + 'displayName': self.displayName, + 'desc': self.desc, + } + + +@attr.s +class Table(Panel): """Generates Table panel json structure Grafana doc on table: http://docs.grafana.org/reference/table_panel/ @@ -3331,6 +3432,8 @@ class Table(object): :param title: panel title :param transform: table style :param transparent: defines if panel should be transparent + :param unit: units + :param sortBy: Sort rows by table fields """ dataSource = attr.ib() @@ -3349,27 +3452,12 @@ class Table(object): repeat = attr.ib(default=None) scroll = attr.ib(default=True, validator=instance_of(bool)) showHeader = attr.ib(default=True, validator=instance_of(bool)) - span = attr.ib(default=6) - sort = attr.ib( - default=attr.Factory(ColumnSort), validator=instance_of(ColumnSort)) - styles = attr.ib() - timeFrom = attr.ib(default=None) - - transform = attr.ib(default=COLUMNS_TRANSFORM) - transparent = attr.ib(default=False, validator=instance_of(bool)) - - @styles.default - def styles_default(self): - return [ - ColumnStyle( - alias="Time", - pattern="time", - type=DateColumnStyleType(), - ), - ColumnStyle( - pattern="/.*/", - ), - ] + span = attr.ib(default=6), + unit = attr.ib(default='', validator=instance_of(str)) + sortBy = attr.ib(default=attr.Factory(list), validator=attr.validators.deep_iterable( + member_validator=instance_of(TableSortByField), + iterable_validator=instance_of(list) + )) @classmethod def with_styled_columns(cls, columns, styles=None, **kwargs): @@ -3389,31 +3477,35 @@ def with_styled_columns(cls, columns, styles=None, **kwargs): return cls(columns=columns, styles=styles + extraStyles, **kwargs) def to_json_data(self): - return { - 'columns': self.columns, - 'datasource': self.dataSource, - 'description': self.description, - 'editable': self.editable, - 'fontSize': self.fontSize, - 'height': self.height, - 'hideTimeOverride': self.hideTimeOverride, - 'id': self.id, - 'links': self.links, - 'minSpan': self.minSpan, - 'pageSize': self.pageSize, - 'repeat': self.repeat, - 'scroll': self.scroll, - 'showHeader': self.showHeader, - 'span': self.span, - 'sort': self.sort, - 'styles': self.styles, - 'targets': self.targets, - 'timeFrom': self.timeFrom, - 'title': self.title, - 'transform': self.transform, - 'transparent': self.transparent, - 'type': TABLE_TYPE, - } + return self.panel_json( + { + "color": { + "mode": self.colorMode + }, + 'columns': self.columns, + 'fontSize': self.fontSize, + 'fieldConfig': { + 'defaults': { + 'custom': { + 'align': self.align, + 'displayMode': self.displayMode, + 'filterable': self.filterable, + }, + 'unit': self.unit, + 'mappings': self.mappings + }, + 'overrides': self.overrides + }, + 'hideTimeOverride': self.hideTimeOverride, + 'mappings': self.mappings, + 'minSpan': self.minSpan, + 'options': { + 'showHeader': self.showHeader, + 'sortBy': self.sortBy + }, + 'type': TABLE_TYPE, + } + ) @attr.s @@ -3460,7 +3552,9 @@ class BarGauge(Panel): min = attr.ib(default=0) orientation = attr.ib( default=ORIENTATION_HORIZONTAL, - validator=in_([ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL]), + validator=in_([ORIENTATION_HORIZONTAL, + ORIENTATION_VERTICAL, + ORIENTATION_AUTO]), ) rangeMaps = attr.ib(default=attr.Factory(list)) thresholdLabels = attr.ib(default=False, validator=instance_of(bool)) @@ -3525,6 +3619,7 @@ class GaugePanel(Panel): :param thresholdMarkers: option to show marker of level on gauge :param thresholds: single stat thresholds :param valueMaps: the list of value to text mappings + :param neutral: neutral point of gauge, leave empty to use Min as neutral point """ allValues = attr.ib(default=False, validator=instance_of(bool)) @@ -3549,6 +3644,7 @@ class GaugePanel(Panel): validator=instance_of(list), ) valueMaps = attr.ib(default=attr.Factory(list)) + neutral = attr.ib(default=None) def to_json_data(self): return self.panel_json( @@ -3566,6 +3662,9 @@ def to_json_data(self): 'mappings': self.valueMaps, 'override': {}, 'values': self.allValues, + 'custom': { + 'neutral': self.neutral, + }, }, 'showThresholdLabels': self.thresholdLabels, 'showThresholdMarkers': self.thresholdMarkers, @@ -3650,7 +3749,7 @@ class Heatmap(Panel): heatmap = {} hideZeroBuckets = attr.ib(default=False) highlightCards = attr.ib(default=True) - options = attr.ib(default=None) + options = attr.ib(default=attr.Factory(list)) xAxis = attr.ib( default=attr.Factory(XAxis), @@ -3919,6 +4018,8 @@ class PieChartv2(Panel): :param reduceOptionsValues: Calculate a single value per column or series or show each row :param tooltipMode: Tooltip mode single (Default), multi, none + :param tooltipSort: To sort the tooltips + none (Default), asc, desc :param unit: units """ @@ -3934,6 +4035,7 @@ class PieChartv2(Panel): reduceOptionsFields = attr.ib(default='', validator=instance_of(str)) reduceOptionsValues = attr.ib(default=False, validator=instance_of(bool)) tooltipMode = attr.ib(default='single', validator=instance_of(str)) + tooltipSort = attr.ib(default='none', validator=instance_of(str)) unit = attr.ib(default='', validator=instance_of(str)) def to_json_data(self): @@ -3958,7 +4060,8 @@ def to_json_data(self): }, 'pieType': self.pieType, 'tooltip': { - 'mode': self.tooltipMode + 'mode': self.tooltipMode, + 'sort': self.tooltipSort }, 'legend': { 'displayMode': self.legendDisplayMode, diff --git a/grafanalib/elasticsearch.py b/grafanalib/elasticsearch.py index 35f1a9d4..bf4cb297 100644 --- a/grafanalib/elasticsearch.py +++ b/grafanalib/elasticsearch.py @@ -363,6 +363,7 @@ class ElasticsearchTarget(object): :param query: query :param refId: target reference id :param timeField: name of the elasticsearch time field + :param hide: show/hide the target result in the final panel display """ alias = attr.ib(default=None) @@ -373,6 +374,7 @@ class ElasticsearchTarget(object): query = attr.ib(default="", validator=instance_of(str)) refId = attr.ib(default="", validator=instance_of(str)) timeField = attr.ib(default="@timestamp", validator=instance_of(str)) + hide = attr.ib(default=False, validator=instance_of(bool)) def _map_bucket_aggs(self, f): return attr.evolve(self, bucketAggs=list(map(f, self.bucketAggs))) @@ -407,6 +409,7 @@ def to_json_data(self): 'query': self.query, 'refId': self.refId, 'timeField': self.timeField, + 'hide': self.hide, } diff --git a/grafanalib/formatunits.py b/grafanalib/formatunits.py index 54112427..3fbd76ec 100644 --- a/grafanalib/formatunits.py +++ b/grafanalib/formatunits.py @@ -238,9 +238,9 @@ RADS_PER_SEC = 'rotrads' # rad/s DEGREES_PER_SECOND = 'rotdegs' # °/s # Temperature -CELSUIS = 'celsius' # °C -FARENHEIT = 'fahrenheit' # °F -KELVIN = 'kelvin' # K +CELSIUS = 'celsius' # °C +FAHRENHEIT = 'fahrenheit' # °F +KELVIN = 'kelvin' # K # Time HERTZ = 'hertz' # Hz NANO_SECONDS = 'ns' # ns diff --git a/grafanalib/tests/test_azuredataexplorer.py b/grafanalib/tests/test_azuredataexplorer.py new file mode 100644 index 00000000..ed0c3c59 --- /dev/null +++ b/grafanalib/tests/test_azuredataexplorer.py @@ -0,0 +1,18 @@ +import grafanalib.core as G +import grafanalib.azuredataexplorer as A +from grafanalib import _gen +from io import StringIO + + +def test_serialization_azuredataexplorer_metrics_target(): + """Serializing a graph doesn't explode.""" + graph = G.Graph( + title="Azure Data Explorer graph", + dataSource="default", + targets=[ + A.AzureDataExplorerTarget() + ], + ) + stream = StringIO() + _gen.write_dashboard(graph, stream) + assert stream.getvalue() != '' diff --git a/grafanalib/tests/test_core.py b/grafanalib/tests/test_core.py index 6b640645..76b116f8 100644 --- a/grafanalib/tests/test_core.py +++ b/grafanalib/tests/test_core.py @@ -1123,6 +1123,32 @@ def test_target_invalid(): ) +def test_loki_target(): + t = G.Dashboard( + title='unittest', + uid='unit-test-uid', + timezone='browser', + panels=[ + G.TimeSeries( + title='Some logs', + targets=[ + G.LokiTarget( + datasource='my-logs', + expr='{pod="unittest"} |= "hello"', + ), + ], + gridPos=G.GridPos(h=10, w=24, x=0, y=0), + ), + ], + ).auto_panel_ids() + + dashboard_json = t.to_json_data() + target_json = dashboard_json['panels'][0].targets[0].to_json_data() + # Grafana wants type/uid fields for Loki targets (as of 2024-04) + assert target_json['datasource']['type'] == 'loki' + assert target_json['datasource']['uid'] == 'my-logs' + + def test_sql_target(): t = G.Table( dataSource="some data source", @@ -1161,3 +1187,43 @@ def test_sql_target_with_source_files(): assert t.to_json_data()["targets"][0].rawQuery is True assert t.to_json_data()["targets"][0].rawSql == "SELECT example\nFROM test\nWHERE example='example' AND example_date BETWEEN '1970-01-01' AND '1971-01-01';\n" print(t.to_json_data()["targets"][0]) + + +def test_default_heatmap(): + h = G.Heatmap() + + assert h.to_json_data()["options"] == [] + + +class TestDashboardLink(): + + def test_validators(self): + with pytest.raises(ValueError): + G.DashboardLink( + type='dashboard', + ) + with pytest.raises(ValueError): + G.DashboardLink( + icon='not an icon' + ) + + def test_initialisation(self): + dl = G.DashboardLink().to_json_data() + assert dl['asDropdown'] is False + assert dl['icon'] == 'external link' + assert dl['includeVars'] is False + assert dl['keepTime'] is True + assert not dl['tags'] + assert dl['targetBlank'] is False + assert dl['title'] == "" + assert dl['tooltip'] == "" + assert dl['type'] == 'dashboards' + assert dl['url'] == "" + + url = 'https://grafana.com' + dl = G.DashboardLink( + uri=url, + type='link' + ).to_json_data() + assert dl['url'] == url + assert dl['type'] == 'link' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..41ccc72f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +tox +pytest +flake8 diff --git a/setup.py b/setup.py index 0eb10c4e..a1c0d6b6 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def local_file(name): # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/guides/single-sourcing-package-version/ - version='0.7.0', + version='0.7.1', description='Library for building Grafana dashboards', long_description=open(README).read(), url='https://github.com/weaveworks/grafanalib', @@ -33,7 +33,6 @@ def local_file(name): 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10',