diff --git a/grafana_dashboards/schema/template/__init__.py b/grafana_dashboards/schema/template/__init__.py index 441ad3a..2dfc2b0 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.interval import Interval from grafana_dashboards.schema.template.query import Query @@ -42,6 +43,8 @@ class Template(object): if template['type'] == 'query': schema = Query().get_schema() + if template['type'] == 'interval': + schema = Interval().get_schema() res['list'].append(schema(template)) diff --git a/grafana_dashboards/schema/template/base.py b/grafana_dashboards/schema/template/base.py index 4cb26e7..a3a67d7 100644 --- a/grafana_dashboards/schema/template/base.py +++ b/grafana_dashboards/schema/template/base.py @@ -20,7 +20,7 @@ class Base(object): def __init__(self): self.base = { v.Required('name'): v.All(str, v.Length(min=1)), - v.Required('type'): v.Any('query'), + v.Required('type'): v.Any('query', 'interval'), } def get_schema(self): diff --git a/grafana_dashboards/schema/template/interval.py b/grafana_dashboards/schema/template/interval.py new file mode 100644 index 0000000..faadb83 --- /dev/null +++ b/grafana_dashboards/schema/template/interval.py @@ -0,0 +1,152 @@ +# Copyright 2015 IBM Corp. +# +# 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 + + +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) + + if len(options): + selected_options = [x for x in options if x.get('selected')] + # If the user did not specify any selected options, mark + # the first one as selected. + if len(selected_options) == 0: + options[0]['selected'] = True + elif len(selected_options) > 1: + raise v.Invalid("No more than one option must be selected") + + return options + + def _validate(self, data): + # This method performs some validation but also coerces some + # values as needed to be friendlier. + + interval = { + v.Required('allFormat', default='glob'): v.Any('glob'), + # This will be automatically supplied based on the options: + v.Required('auto'): v.Any(bool), + v.Required('auto_count', default=10): v.Any(int), + # This will be automatically supplied based on the options: + v.Required('current'): v.Any(self.current), + # NOTE(jeblair): I don't know what datasource means in this context + v.Required('datasource', default=None): v.All(None), + v.Required('hideLabel', default=False): v.Any(bool), + v.Required('includeAll', default=False): v.All(bool), + v.Required('label', default=''): v.Any(str), + v.Required('multi', default=False): v.All(bool), + v.Required('multiFormat', default='glob'): v.Any('glob'), + v.Required('options', default=[]): self.validate_options, + # This will be automatically supplied based on the options: + v.Required('query'): v.Any(str), + # NOTE(jeblair): I don't know what refresh means in this context + v.Required('refresh', default=False): v.All(bool), + } + interval.update(self.base) + + # Make sure we have the minimum we need before we start + # messing with the data. + rudimentary_interval_schema = { + v.Required('options', default=[]): self.validate_options, + } + data = v.Schema(rudimentary_interval_schema, extra=True)(data) + + # There are some values that would be annoying to be required + # to be included by the user because they are calculable from + # other values, and ought to be identical. Therefore, if they + # are not supplied, we will calculate them for the user. + auto = False + selected = None + options = data.get('options', []) + query = [] + for option in options: + if option['value'] == AUTO_INTERVAL: + auto = True + else: + query.append(option['value']) + if option.get('selected'): + selected = option + query = ','.join(query) + + # If 'auto' is not supplied, set it based on the presense of + # an 'auto' option in the options list. + if 'auto' not in data: + data['auto'] = auto + + # If 'current' is not supplied, set it based on which of the + # options was marked 'selected'. + if 'current' not in data: + if selected: + data['current'] = dict(text=selected['text'], + value=selected['value']) + else: + data['current'] = dict() + + # If 'query' is not supplied, compose it from the list of options. + if 'query' not in data: + data['query'] = query + + data = v.Schema(interval)(data) + return data + + def get_schema(self): + return v.Schema(self._validate) diff --git a/tests/schema/fixtures/dashboard-0015.json b/tests/schema/fixtures/dashboard-0015.json new file mode 100644 index 0000000..1222203 --- /dev/null +++ b/tests/schema/fixtures/dashboard-0015.json @@ -0,0 +1,91 @@ +{ + "dashboard": { + "new-dashboard": { + "rows": [], + "templating": { + "enabled": true, + "list": [ + { + "allFormat": "glob", + "auto": true, + "auto_count": 10, + "current": { + "text": "auto", + "value": "$__auto_interval" + }, + "datasource": null, + "hideLabel": false, + "includeAll": false, + "label": "", + "multi": false, + "multiFormat": "glob", + "name": "auto_summarize", + "options": [ + { + "selected": true, + "text": "auto", + "value": "$__auto_interval" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + } + ], + "query": "1m,1h,1d", + "refresh": false, + "type": "interval" + }, + { + "allFormat": "glob", + "auto": false, + "auto_count": 10, + "current": { + "text": "1m", + "value": "1m" + }, + "datasource": null, + "hideLabel": false, + "includeAll": false, + "label": "", + "multi": false, + "multiFormat": "glob", + "name": "summarize", + "options": [ + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + } + ], + "query": "1m,1h,1d", + "refresh": false, + "type": "interval" + } + ] + }, + "timezone": "utc", + "title": "New dashboard" + } + } +} diff --git a/tests/schema/fixtures/dashboard-0015.yaml b/tests/schema/fixtures/dashboard-0015.yaml new file mode 100644 index 0000000..a20bd09 --- /dev/null +++ b/tests/schema/fixtures/dashboard-0015.yaml @@ -0,0 +1,16 @@ +dashboard: + templating: + - name: auto_summarize + type: interval + options: + - auto + - 1m + - 1h + - 1d + - name: summarize + type: interval + options: + - 1m + - 1h + - 1d + title: New dashboard