From dd6582e9ccb2b5fcb31e3833166df3af5aec9eff Mon Sep 17 00:00:00 2001 From: akrzos Date: Fri, 9 Feb 2018 16:17:25 -0500 Subject: [PATCH] Add Custom Template Type Allow custom templated vars. Custom templated vars are similiar to interval templated vars in that they have "options" however they can be multi-selected vs just a singular selection as with an interval var. Change-Id: Ic89c5d192f87890da950a9d2d5f9bc4a96a3e174 --- .../schema/template/__init__.py | 3 + grafana_dashboards/schema/template/base.py | 47 +++++- grafana_dashboards/schema/template/custom.py | 72 ++++++++ .../schema/template/interval.py | 43 +---- grafana_dashboards/schema/template/query.py | 12 +- tests/schema/fixtures/dashboard-0014.json | 2 +- tests/schema/fixtures/dashboard-0027.json | 159 ++++++++++++++++++ tests/schema/fixtures/dashboard-0027.yaml | 44 +++++ 8 files changed, 338 insertions(+), 44 deletions(-) create mode 100644 grafana_dashboards/schema/template/custom.py create mode 100644 tests/schema/fixtures/dashboard-0027.json create mode 100644 tests/schema/fixtures/dashboard-0027.yaml diff --git a/grafana_dashboards/schema/template/__init__.py b/grafana_dashboards/schema/template/__init__.py index 2dfc2b0..7e5dd2a 100644 --- a/grafana_dashboards/schema/template/__init__.py +++ b/grafana_dashboards/schema/template/__init__.py @@ -15,6 +15,7 @@ import voluptuous as v from grafana_dashboards.schema.template.base import Base +from grafana_dashboards.schema.template.custom import Custom from grafana_dashboards.schema.template.interval import Interval from grafana_dashboards.schema.template.query import Query @@ -45,6 +46,8 @@ class Template(object): schema = Query().get_schema() if template['type'] == 'interval': schema = Interval().get_schema() + if template['type'] == 'custom': + schema = Custom().get_schema() res['list'].append(schema(template)) diff --git a/grafana_dashboards/schema/template/base.py b/grafana_dashboards/schema/template/base.py index a3a67d7..985f22f 100644 --- a/grafana_dashboards/schema/template/base.py +++ b/grafana_dashboards/schema/template/base.py @@ -15,12 +15,57 @@ import voluptuous as v +AUTO_INTERVAL = '$__auto_interval' +ALL_CUSTOM = '$__all' + + class Base(object): + option = { + v.Required('text'): v.All(str, v.Length(min=1)), + v.Required('value'): v.All(str, v.Length(min=1)), + v.Required('selected', default=False): v.All(bool), + } + options = [option] + + def _validate_options(self, options): + # Most of the time this is going to be a simple list, so if + # the user supplied a list of strings, let's turn that into + # the requisite list of dicts. + try: + v.Schema([str])(options) + options = [dict(text=o) for o in options] + except v.Invalid: + pass + + # Ensure this is a list of dicts before we start messing with + # them. + v.Schema([dict])(options) + + # This performs some automatic cleanup to make things easier. + for option in options: + # Let's not make our users type "$__auto_interval". Instead, + # if they specify an option name of 'auto' with no value, + # supply it for them. NB: if a user wants 'auto' with value + # 'foobar', they can just override this by simply including + # 'value: foobar'. + if option.get('text') == 'auto' and 'value' not in option: + option['value'] = AUTO_INTERVAL + + if option.get('text') == 'all' and 'value' not in option: + option['value'] = ALL_CUSTOM + + # Let's also not make our users type every option twice. For + # each option with a text entry but no value, copy the next + # entry to that value. + if option.get('text') and 'value' not in option: + option['value'] = option['text'] + + return v.Schema(self.options)(options) def __init__(self): self.base = { v.Required('name'): v.All(str, v.Length(min=1)), - v.Required('type'): v.Any('query', 'interval'), + v.Required('type'): v.Any('query', 'interval', 'custom'), } def get_schema(self): diff --git a/grafana_dashboards/schema/template/custom.py b/grafana_dashboards/schema/template/custom.py new file mode 100644 index 0000000..8095784 --- /dev/null +++ b/grafana_dashboards/schema/template/custom.py @@ -0,0 +1,72 @@ +# Copyright 2018 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import voluptuous as v + +from grafana_dashboards.schema.template.base import Base + + +class Custom(Base): + current = { + v.Required('text'): v.All(str, v.Length(min=1)), + v.Required('value'): v.All([str]), + } + + def validate_options(self, options): + options = self._validate_options(options) + + if len(options): + selected_options = [x for x in options if x.get('selected')] + # Default to first option as selected (if nothing selected) + if len(selected_options) == 0: + options[0]['selected'] = True + + return options + + def _validate(self, data): + custom = { + v.Required('current'): v.Any(self.current), + v.Required('includeAll', default=False): v.All(bool), + v.Required('multi', default=False): v.All(bool), + v.Required('options', default=[]): self.validate_options, + v.Required('query', default=''): v.All(str), + v.Optional('allValue'): v.All(str), + v.Optional('hide'): v.All(int, v.Range(min=0, max=2)), + v.Optional('label', default=''): v.All(str), + + } + custom.update(self.base) + + custom_options_schema = { + v.Required('options', default=[]): self.validate_options, + } + data = v.Schema(custom_options_schema, extra=True)(data) + + # If 'query' is not supplied, compose it from the list of options. + if 'query' not in data: + query = [option['text'] + for option in data.get('options') + if option['text'] != 'All'] + data['query'] = ','.join(query) + + if 'current' not in data: + selected = [option['text'] + for option in data.get('options') + if option['selected']] + data['current'] = dict(text='+'.join(selected), value=selected) + + return v.Schema(custom)(data) + + def get_schema(self): + return v.Schema(self._validate) diff --git a/grafana_dashboards/schema/template/interval.py b/grafana_dashboards/schema/template/interval.py index faadb83..9a33f6d 100644 --- a/grafana_dashboards/schema/template/interval.py +++ b/grafana_dashboards/schema/template/interval.py @@ -14,57 +14,18 @@ import voluptuous as v +from grafana_dashboards.schema.template.base import AUTO_INTERVAL from grafana_dashboards.schema.template.base import Base -AUTO_INTERVAL = '$__auto_interval' - - class Interval(Base): - option = { - v.Required('text'): v.All(str, v.Length(min=1)), - v.Required('value'): v.All(str, v.Length(min=1)), - v.Required('selected', default=False): v.All(bool), - } - options = [option] - current = { v.Required('text'): v.All(str, v.Length(min=1)), v.Required('value'): v.All(str, v.Length(min=1)), } def validate_options(self, options): - # Most of the time this is going to be a simple list, so if - # the user supplied a list of strings, let's turn that into - # the requisite list of dicts. - try: - v.Schema([str])(options) - options = [dict(text=o) for o in options] - except v.Invalid: - pass - - # Ensure this is a list of dicts before we start messing with - # them. - v.Schema([dict])(options) - - # This performs some automatic cleanup to make things easier. - for option in options: - # Let's not make our users type "$__auto_interval". Instead, - # if they specify an option name of 'auto' with no value, - # supply it for them. NB: if a user wants 'auto' with value - # 'foobar', they can just override this by simply including - # 'value: foobar'. - if option.get('text') == 'auto' and 'value' not in option: - option['value'] = AUTO_INTERVAL - - # Let's also not make our users type every option twice. For - # each option with a text entry but no value, copy the next - # entry to that value. - if option.get('text') and 'value' not in option: - option['value'] = option['text'] - - # Now we should have something that matches our actual schema. - options = v.Schema(self.options)(options) + options = self._validate_options(options) if len(options): selected_options = [x for x in options if x.get('selected')] diff --git a/grafana_dashboards/schema/template/query.py b/grafana_dashboards/schema/template/query.py index fd19827..914fb7f 100644 --- a/grafana_dashboards/schema/template/query.py +++ b/grafana_dashboards/schema/template/query.py @@ -12,19 +12,29 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + import voluptuous as v from grafana_dashboards.schema.template.base import Base +LOG = logging.getLogger(__name__) + class Query(Base): + def validate_refresh(self, data): + v.Schema(v.Any(v.All(int, v.Range(min=0, max=2)), bool))(data) + if isinstance(data, bool): + LOG.warn('templating query refresh type bool is deprecated') + return data + def get_schema(self): query = { v.Required('includeAll', default=False): v.All(bool), v.Required('multi', default=False): v.All(bool), v.Required('query', default=''): v.All(str), - v.Required('refresh', default=False): v.All(bool), + v.Required('refresh', default=0): self.validate_refresh, v.Optional('datasource'): v.All(str), v.Optional('hide'): v.All(int, v.Range(min=0, max=2)), } diff --git a/tests/schema/fixtures/dashboard-0014.json b/tests/schema/fixtures/dashboard-0014.json index 819de9b..48fcd96 100644 --- a/tests/schema/fixtures/dashboard-0014.json +++ b/tests/schema/fixtures/dashboard-0014.json @@ -10,7 +10,7 @@ "multi": false, "name": "foobar", "query": "foobar.*", - "refresh": false, + "refresh": 0, "type": "query" } ] diff --git a/tests/schema/fixtures/dashboard-0027.json b/tests/schema/fixtures/dashboard-0027.json new file mode 100644 index 0000000..7b1adbf --- /dev/null +++ b/tests/schema/fixtures/dashboard-0027.json @@ -0,0 +1,159 @@ +{ + "dashboard": { + "new-dashboard": { + "rows": [ + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "bars": false, + "datasource": "graphite", + "editable": true, + "error": false, + "fill": 1, + "lines": true, + "linewidth": 2, + "percentage": false, + "pointradius": 5, + "points": false, + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "target": "$hostname.Cpu.cpu_prct_used" + } + ], + "title": "no title (click here)", + "type": "graph", + "x-axis": true, + "y-axis": true + } + ], + "showTitle": false, + "title": "New row" + } + ], + "templating": { + "enabled": true, + "list": [ + { + "includeAll": false, + "multi": false, + "name": "hostname", + "query": "*", + "refresh": 2, + "type": "query" + }, + { + "current": { + "text": "undercloud", + "value": [ + "undercloud" + ] + }, + "includeAll": false, + "label": "", + "multi": false, + "name": "test_custom_1", + "options": [ + { + "selected": true, + "text": "undercloud", + "value": "undercloud" + }, + { + "selected": false, + "text": "controller", + "value": "controller" + }, + { + "selected": false, + "text": "*", + "value": "*" + } + ], + "query": "undercloud,controller,*", + "type": "custom" + }, + { + "current": { + "text": "undercloud+controller", + "value": [ + "undercloud", + "controller" + ] + }, + "includeAll": false, + "label": "", + "multi": true, + "name": "test_custom_2", + "options": [ + { + "selected": true, + "text": "undercloud", + "value": "undercloud" + }, + { + "selected": true, + "text": "controller", + "value": "controller" + }, + { + "selected": false, + "text": "*", + "value": "*" + } + ], + "query": "undercloud,controller,*", + "type": "custom" + }, + { + "current": { + "text": "All", + "value": [ + "All" + ] + }, + "includeAll": true, + "label": "", + "multi": true, + "name": "test_custom_include_all", + "options": [ + { + "selected": true, + "text": "All", + "value": "All" + }, + { + "selected": false, + "text": "undercloud", + "value": "undercloud" + }, + { + "selected": false, + "text": "controller", + "value": "controller" + }, + { + "selected": false, + "text": "*", + "value": "*" + } + ], + "query": "undercloud,controller,*", + "type": "custom" + } + ] + }, + "time": { + "from": "2018-02-07T08:42:27.000Z", + "to": "2018-02-07T13:48:32.000Z" + }, + "timezone": "utc", + "title": "New dashboard" + } + } +} diff --git a/tests/schema/fixtures/dashboard-0027.yaml b/tests/schema/fixtures/dashboard-0027.yaml new file mode 100644 index 0000000..93081c2 --- /dev/null +++ b/tests/schema/fixtures/dashboard-0027.yaml @@ -0,0 +1,44 @@ +dashboard: + time: + from: "2018-02-07T08:42:27.000Z" + to: "2018-02-07T13:48:32.000Z" + templating: + - name: hostname + type: query + query: "*" + refresh: 2 + - name: test_custom_1 + type: custom + options: + - undercloud + - controller + - "*" + - name: test_custom_2 + type: custom + multi: true + options: + - text: undercloud + selected: true + - text: controller + selected: true + - text: "*" + - name: test_custom_include_all + type: custom + includeAll: true + multi: true + options: + - text: All + selected: true + - text: undercloud + - text: controller + - text: "*" + title: New dashboard + rows: + - title: New row + height: 250px + panels: + - title: no title (click here) + type: graph + datasource: graphite + targets: + - target: $hostname.Cpu.cpu_prct_used