Skip to content

Commit

Permalink
Added custom dashboard mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
ar4s committed Mar 14, 2016
1 parent 51165b7 commit cc1e2f6
Show file tree
Hide file tree
Showing 36 changed files with 683 additions and 5 deletions.
3 changes: 2 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"paper-menu": "PolymerElements/paper-menu#~1.2.2",
"polymer": "Polymer/polymer#~1.2.0",
"iron-flex-layout": "PolymerElements/iron-flex-layout#~1.2.2",
"iron-form": "PolymerElements/iron-form#~1.0.13"
"iron-form": "PolymerElements/iron-form#~1.0.13",
"chartist": "~0.9.5"
},
"devDependencies": {
"qunit": "~1.18.0",
Expand Down
Binary file added docs/img/dashboard-create-dasboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/dashboard-create-graph-dc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/dashboard-final-dc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/dashboard-link.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions docs/user/dashboards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Dashboards

The dashboard provide basic mechanism for displaying data via bar or pie charts.


## Getting started
All example data in this tutorial was generated by Ralph's command - ``ralph make_demo_data``.

### Goal
Display graphs with quantity assets in each data centers.

### First dashboard
First of all we must create new dasboard object in Ralph by clicking in menu``Dashboards > Dashboards`` next click ``Add new dashboard`` to add new one.

![add-dashboard](/img/dashboard-create-dasboard.png "Add dashboard")


Next steps is create graph and configure it.
![add-first-graph](/img/dashboard-create-graph-dc.png "Add first-graph")

The important field of form above is ``Params`` - this field accepted configuration of graph in JSON format. Keys ``labels``, ``series``, ``filters`` are required.
Below short description of these fields:

- ``labels`` - which field in model are string representation,
- ``series`` - aggregate by this field,
- ``filters`` - Django ORM-like lookup (visit [Django documentation](https://docs.djangoproject.com/en/1.8/ref/models/querysets/#id4)).

OK, after save go our new dashboard object. Now we can see item (``DC Capacity``) in ``Graphs`` fields - select them. After save go to ``Dashboards > Dashboards`` in list view click ``Link``.
![link-to-dashboard](/img/dashboard-link.png "Link")

Final result:
![link-dashboard-final](/img/dashboard-final-dc.png "Final dashboard")


## Special filters
Special filters are some helpers to

### from_now
``from_now`` works only with date and date-time fields in ``filters`` section, e.g.:
```json
{
"labels": "name",
"series": "serverroom__rack__datacenterasset",
"filters": {
"created__gt|from_now": "-1y",
},
}
```
The filter above limit query to objects which created from one year ago to now. Possible variants of period:

- ``y`` - years,
- ``m`` - months,
- ``d`` - days,



# TODO

- free assets aggregate by model name with invoice date from 2 years ago
- from GET params to dashboard
2 changes: 2 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var sass_config = {
includePaths: [
config.bowerDir + 'foundation/scss',
config.bowerDir + 'fontawesome/scss',
config.bowerDir + 'chartist/dist/scss',
]
}

Expand Down Expand Up @@ -62,6 +63,7 @@ gulp.task('js', function(){
'./bower_components/foundation-datepicker/js/foundation-datepicker.js',
'./bower_components/angular-loading-bar/build/loading-bar.min.js',
'./bower_components/raven-js/dist/raven.min.js',
'./bower_components/chartist/dist/chartist.js',
];
gulp.src(vendorFiles)
.pipe(gulp.dest(config.vendorRoot + 'js/'));
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pages:
- Quickstart: user/quickstart.md
- Advanced guide: user/guide.md
- Domains management: user/domains.md
- Dashboards: user/dashboards.md
- API: user/api.md
- Developer guide:
- Before you start: CONTRIBUTING.md
Expand Down
10 changes: 10 additions & 0 deletions src/ralph/admin/sitetrees.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ def section(section_name, app, model):
section(_('Types'), 'operations', 'OperationType'),
]
),
ralph_item(
title=_('Dashboards'),
url='#',
url_as_pattern=False,
perms_mode_all=False,
children=[
section(_('Dashboards'), 'dashboards', 'Dashboard'),
section(_('Graphs'), 'dashboards', 'Graph'),
]
),
ralph_item(
title=_('Settings'),
url='#',
Expand Down
2 changes: 1 addition & 1 deletion src/ralph/admin/templatetags/dashboard_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def get_used_space_in_data_centers(data_centers):
@register.inclusion_tag('admin/templatetags/dc_capacity.html')
def dc_capacity(data_centers=None, size='big'):
color = cycle(COLORS)
if data_centers is None:
if not data_centers:
data_centers = DataCenter.objects.all()
if not isinstance(data_centers, Iterable):
data_centers = [data_centers]
Expand Down
8 changes: 7 additions & 1 deletion src/ralph/assets/models/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class Environment(NamedMixin, TimeStampMixin, models.Model):

class Service(NamedMixin, TimeStampMixin, models.Model):
# Fixme: let's do service catalog replacement from that
_allow_in_dashboard = True

active = models.BooleanField(default=True)
uid = NullableCharField(max_length=40, unique=True, blank=True, null=True)
profit_center = models.ForeignKey(ProfitCenter, null=True, blank=True)
Expand Down Expand Up @@ -109,7 +111,7 @@ def get_autocomplete_queryset(cls):


class Manufacturer(NamedMixin, TimeStampMixin, models.Model):
pass
_allow_in_dashboard = True


class AssetModel(
Expand All @@ -119,6 +121,8 @@ class AssetModel(
models.Model
):
# TODO: should type be determined based on category?
_allow_in_dashboard = True

type = models.PositiveIntegerField(
verbose_name=_('type'), choices=ObjectModelType(),
)
Expand Down Expand Up @@ -182,6 +186,8 @@ def get_back_layout_class(self):


class Category(MPTTModel, NamedMixin.NonUnique, TimeStampMixin, models.Model):
_allow_in_dashboard = True

code = models.CharField(max_length=4, blank=True, default='')
parent = TreeForeignKey(
'self',
Expand Down
4 changes: 3 additions & 1 deletion src/ralph/back_office/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@


class Warehouse(NamedMixin, TimeStampMixin, models.Model):
pass
_allow_in_dashboard = True


class BackOfficeAssetStatus(Choices):
Expand Down Expand Up @@ -117,6 +117,8 @@ def autocomplete_if_release_report(actions, objects, field_name='user'):


class BackOfficeAsset(Regionalizable, Asset):
_allow_in_dashboard = True

warehouse = models.ForeignKey(Warehouse, on_delete=models.PROTECT)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, blank=True,
Expand Down
Empty file.
70 changes: 70 additions & 0 deletions src/ralph/dashboards/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import json
from itertools import groupby

from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _

from ralph.admin import RalphAdmin, register
from ralph.admin.mixins import RalphAdminForm
from ralph.dashboards.models import Dashboard, Graph


class GraphForm(RalphAdminForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
choices = [
ct for ct in ContentType.objects.all()
if getattr(ct.model_class(), '_allow_in_dashboard', False)
]

def keyfunc(x):
return x.app_label

data = sorted(choices, key=keyfunc)
self.fields['model'] = forms.ChoiceField(choices=(
(k.capitalize(), list(map(lambda x: (x.id, x), g)))
for k, g in groupby(data, keyfunc)
))

def clean_model(self):
ct_id = self.cleaned_data.get('model')
return ContentType.objects.get(pk=ct_id)

def clean_params(self):
params = self.cleaned_data.get('params', '{}')
try:
params_dict = json.loads(params)
except json.decoder.JSONDecodeError as e:
raise forms.ValidationError(e.msg)
if not params_dict.get('labels', None):
raise forms.ValidationError('Please specify `labels` key')
if not params_dict.get('series', None):
raise forms.ValidationError('Please specify `series` key')
return params

class Meta:
model = Graph
fields = [
'name', 'description', 'model', 'aggregate_type', 'chart_type',
'params', 'active'
]


@register(Graph)
class GraphAdmin(RalphAdmin):
form = GraphForm
list_display = ['name', 'description', 'active']


@register(Dashboard)
class DashboardAdmin(RalphAdmin):
list_display = ['name', 'description', 'active', 'get_link']

def get_link(self, obj):
return _('<a href="{}" target="_blank">Dashboard</a>'.format(reverse(
'dashboard_view', args=(obj.pk,)
)))
get_link.short_description = _('Link')
get_link.allow_tags = True
38 changes: 38 additions & 0 deletions src/ralph/dashboards/filter_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import datetime
import re

from dateutil.relativedelta import relativedelta

FILTER_FROM_NOW = re.compile(r'([+-]?\d+)(\w)')


class FilterParser(object):

def __init__(self, queryset, filters):
self.queryset = queryset
self.filters = filters

def get_queryset(self):
parsed_filters = {}
for key, value in self.filters.items():
params = key.split('|')
if len(params) == 1:
parsed_filters[key] = value
else:
filter_func = getattr(self, 'filter_' + params[1], None)
if not filter_func:
continue
parsed_filters[key] = filter_func(value)
return self.queryset.filter(**parsed_filters)

def filter_from_now(self, value):
period_mapper = {
'd': 'days',
'm': 'months',
'y': 'years',
}
val, period = FILTER_FROM_NOW.match(value).groups()
result = datetime.date.today() + relativedelta(**{
period_mapper.get(period): int(val)
})
return result.strftime('%Y-%m-%d')
55 changes: 55 additions & 0 deletions src/ralph/dashboards/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
import django_extensions.db.fields.json


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]

operations = [
migrations.CreateModel(
name='Dashboard',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name', unique=True)),
('created', models.DateTimeField(auto_now_add=True, verbose_name='date created')),
('modified', models.DateTimeField(verbose_name='last modified', auto_now=True)),
('active', models.BooleanField(default=True)),
('description', models.CharField(max_length=250, verbose_name='description', blank=True)),
('interval', models.PositiveSmallIntegerField(default=60)),
],
options={
'abstract': False,
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Graph',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name', unique=True)),
('created', models.DateTimeField(auto_now_add=True, verbose_name='date created')),
('modified', models.DateTimeField(verbose_name='last modified', auto_now=True)),
('description', models.CharField(max_length=250, verbose_name='description', blank=True)),
('aggregate_type', models.PositiveIntegerField(choices=[(1, 'Count'), (2, 'Max'), (3, 'Sum')])),
('chart_type', models.PositiveIntegerField(choices=[(1, 'Verical Bar'), (2, 'Horizontal Bar'), (3, 'Pie Chart')])),
('params', django_extensions.db.fields.json.JSONField(blank=True)),
('active', models.BooleanField(default=True)),
('model', models.ForeignKey(to='contenttypes.ContentType')),
],
options={
'abstract': False,
'ordering': ['name'],
},
),
migrations.AddField(
model_name='dashboard',
name='graphs',
field=models.ManyToManyField(to='dashboards.Graph', blank=True),
),
]
Empty file.
Loading

0 comments on commit cc1e2f6

Please sign in to comment.