Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard View #191

Merged
merged 39 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
35de9fe
First set of dashboard view.
jvanderaa Jul 7, 2022
b6120df
Moves to using queryset to gather maintenances
jvanderaa Jul 7, 2022
f1b4c95
Calculate future maintenances
jvanderaa Jul 7, 2022
2a2f601
Finishes the dashboard page.
jvanderaa Jul 9, 2022
7c72b1c
Adds dashboard as home view
jvanderaa Jul 9, 2022
21bfa49
Updates for tests.
jvanderaa Jul 9, 2022
5aaffb8
Merge branch 'develop' into jv_cm_dashboard
jvanderaa Aug 11, 2022
3e6fa1f
Updates for the dashboard.
jvanderaa Aug 18, 2022
9f12c93
Updates flake8 setup
jvanderaa Aug 19, 2022
a72f167
Updates views for QA
jvanderaa Aug 19, 2022
87d592b
Remove left over learning.
jvanderaa Aug 26, 2022
095902d
Updates for views and documentation.
jvanderaa Aug 26, 2022
70a5b86
Updates and adds tests!
jvanderaa Aug 26, 2022
c83c9b3
Updates dashboard name in menu.
jvanderaa Aug 27, 2022
a910015
Adds test for historical gathering.
jvanderaa Aug 27, 2022
109859b
Adds test for future_maintenances
jvanderaa Aug 27, 2022
a047948
Adds tests for new methods.
jvanderaa Aug 27, 2022
f804f2e
Get tests to pass.
jvanderaa Aug 27, 2022
8085f53
Updates tests.
jvanderaa Aug 27, 2022
8c0d010
Style update.
jvanderaa Aug 27, 2022
6623b0f
Remove unused imports.
jvanderaa Aug 27, 2022
6a48cee
Updates for pylint and flake8
jvanderaa Aug 27, 2022
a163ee9
Move to using queryset all to alleviate test cache
jvanderaa Aug 28, 2022
bc250ec
Updates test to ignore too many pub methods
jvanderaa Aug 28, 2022
390afe5
Updates comments.
jvanderaa Aug 29, 2022
24e9017
Updates pylint control.
jvanderaa Aug 29, 2022
7e3646a
Updates messaging
jvanderaa Aug 29, 2022
ea322ab
Update pyproject.toml
jvanderaa Sep 1, 2022
de1f9cc
Uses total_seconds() method instead of seconds.
jvanderaa Sep 1, 2022
b73f8e0
Merge branch 'jv_cm_dashboard' of github.com:jvanderaa/nautobot-plugi…
jvanderaa Sep 1, 2022
48a8f5a
Update README.md
jvanderaa Sep 2, 2022
160abea
Updates for many comments.
jvanderaa Sep 4, 2022
433a20e
Merge branch 'jv_cm_dashboard' of github.com:jvanderaa/nautobot-plugi…
jvanderaa Sep 4, 2022
fd97fb1
Simplifies month calculations.
jvanderaa Sep 5, 2022
5636abc
Update nautobot_circuit_maintenance/templates/nautobot_circuit_mainte…
jvanderaa Sep 6, 2022
2c8dcc1
Updates to remove unnecessary code.
jvanderaa Sep 6, 2022
7343b52
Updates tests for one without all the views.
jvanderaa Sep 8, 2022
0759785
Removes unused js import (for now).
jvanderaa Sep 9, 2022
f6fac8a
Update nautobot_circuit_maintenance/templates/nautobot_circuit_mainte…
jvanderaa Sep 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nautobot_circuit_maintenance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ class CircuitMaintenanceConfig(PluginConfig):
default_settings = {
"raw_notification_initial_days_since": 7,
"raw_notification_size": 8192,
"dashboard_n_days": 30,
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
}
caching_config = {}
home_view_name = "plugins:nautobot_circuit_maintenance:circuitmaintenance_overview"
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved

def ready(self):
super().ready()
Expand Down
5 changes: 5 additions & 0 deletions nautobot_circuit_maintenance/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from nautobot.utilities.choices import ButtonColorChoices

menu_items = (
PluginMenuItem(
link="plugins:nautobot_circuit_maintenance:circuitmaintenance_overview",
link_text="Circuit Maintenance Dashboard",
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
permissions=["nautobot_circuit_maintenance.view_circuitmaintenance"],
),
PluginMenuItem(
link="plugins:nautobot_circuit_maintenance:circuitmaintenance_list",
link_text="Circuit Maintenances",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
{% extends 'base.html' %}
{% load buttons %}
{% load static %}
{% load custom_links %}
{% load helpers %}
{% load plugins %}

{% block header %}
{% endblock %}
{% block content %}
<div class="pull-right noprint">
{% if request.user.is_authenticated and table_config_form %}
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#ObjectTable_config"
title="Configure table"><i class="mdi mdi-cog"></i>Configure</button>
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
{% endif %}
{% if request.user.is_authenticated and 'export' in action_buttons %}
{% export_button content_type %}
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
{% endif %}
</div>

<h1>{% block title %}Circuit Maintenance Dashboard{% endblock %}</h1>
<div class="col-md-3">
<!-- Visual block -->
<div class="card">
<div class="container">
<h3 class="text s-2 p-3">Circuits With Maintenances in the Next {{ n_days }} Days</h3>
<table class="table table-hover table-headings">
<thead>
<tr class="even">
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
<th>Circuit ID</th>
<th>Sites</th>
<th>Circuit Status</th>
<th>Maintenance Start</th>
</tr>
</thead>
<tbody>
{% for maintenance in upcoming_maintenances %}
{% for circuit in maintenance.circuits %}
<tr class="even">
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
<td>
<a href="{{ circuit.get_absolute_url }}">{{ circuit.cid }}</a>
</td>
<td>
{% if circuit.termination_a %}
{% with circuit_term_a_site=circuit.termination_a.connected_endpoint.parent.site %}
<a href="{{ circuit_term_a_site.get_absolute_url }}">{{ circuit_term_a_site.name }}</a>{% if circuit.termination_z.connected_endpoint and circuit_term_a_site != circuit.termination_z.connected_endpoint.parent.site %}, <a href="{{ circuit.termination_z.connected_endpoint.parent.site.get_absolute_url }}">{{ circuit.termination_z.connected_endpoint.parent.site.name}}</a>{% endif %}
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
{% endwith %}
{% endif %}
</td>
<td>
{{ circuit.status.name }}
</td>
<td>
{{ maintenance.start_time | date:'Y-m-d H:i' }}
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="container card-body">
<h3 class="text-left m-2 p-3">Devices with Maintenances in the Next {{ n_days }} Days</h3>
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
<table class="table table-hover table-headings">
<thead>
<tr>
<th>Device Name</th>
<th>Maintenance Start</th>
</tr>
</thead>
<tbody>
{% for maintenance in upcoming_maintenances %}
{% for circuit in maintenance.circuits %}
{% if circuit.termination_a %}
glennmatthews marked this conversation as resolved.
Show resolved Hide resolved
<tr class="even">
<td>
<!-- TODO: Add this as a link instead of just a text field -->
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
{% with device_obj=circuit.termination_a.connected_endpoint.parent %}
<a href="{{ device_obj.get_absolute_url }}">{{ device_obj.name }}</a>
{% endwith %}
</td>
<td>
{{ maintenance.start_time | date:'Y-m-d H:i' }}
</td>
</tr>
{% endif %}
{% if circuit.termination_z.connected_endpoint %}
<tr class="even">
<td>
{% with device_obj=circuit.termination_z.connected_endpoint.parent %}
<a href="{{ device_obj.get_absolute_url }}">{{ device_obj.name }}</a>
{% endwith %}
</td>
<td>
{{ maintenance.start_time | date:'Y-m-d H:i' }}
</td>
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="container">
<h3 class="text-left m-2 p-3">Metrics</h3>
<table class="table table-hover table-headings">
<thead>
<tr class="even">
<th>Metric</th>
<th>Metric Value</th>
</tr>
</thead>
<tbody>
{% for cm_metric_key, cm_metric_value in circuit_maint_metric_data.items %}
<tr class="even">
<td>{{ cm_metric_key }}</td>
<td>{{ cm_metric_value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>

{% table_config_form table table_name="ObjectTable" %}
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
{% endblock %}
{% block javascript %}
<script src="{% static 'js/tableconfig.js' %}"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jvanderaa , could you give explain (point to doc) what are we using this for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itdependsnetworks I took this from the GC plugin. Any insight?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it for now. It is used for table selection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get this merged and released?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used for customizing a table view to specify which columns are displayed. If you're removing it, you should also remove the table_config_form template tag as well.

{% endblock %}
3 changes: 3 additions & 0 deletions nautobot_circuit_maintenance/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from .models import CircuitMaintenance, CircuitImpact, Note, NotificationSource

urlpatterns = [
# Overview
# path("config-compliance/overview/", views.ConfigComplianceOverview.as_view(), name="configcompliance_report"),
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
path("maintenance/overview/", views.CircuitMaintenanceOverview.as_view(), name="circuitmaintenance_overview"),
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
#
# Maintenance
path("maintenance/", views.CircuitMaintenanceListView.as_view(), name="circuitmaintenance_list"),
Expand Down
179 changes: 178 additions & 1 deletion nautobot_circuit_maintenance/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,197 @@
"""Views for Circuit Maintenance."""
from datetime import datetime as datet
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
import datetime
import logging

import google_auth_oauthlib

from django.conf import settings
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from nautobot.core.views import generic
from nautobot.circuits.models import Provider
from nautobot.circuits.models import Circuit, Provider
from nautobot_circuit_maintenance import filters, forms, models, tables
from nautobot_circuit_maintenance.handle_notifications.sources import RedirectAuthorize, Source
from nautobot_circuit_maintenance.models import CircuitMaintenance


logger = logging.getLogger(__name__)


class CircuitMaintenanceOverview(generic.ObjectListView):
"""View for an overview dashboard of summary view."""
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved

action_buttons = ("export",)
filterset = filters.CircuitMaintenanceFilterSet
filterset_form = forms.CircuitMaintenanceFilterForm
table = tables.CircuitMaintenanceTable
template_name = "nautobot_circuit_maintenance/circuit_maintenance_overview.html"
queryset = models.CircuitMaintenance.objects.all() # Needs to remain all objects, otherwise other calcs will fail.

def setup(self, request, *args, **kwargs):
"""Using request object to perform filtering based on query params."""
super().setup(request, *args, **kwargs)
n_days = settings.PLUGINS_CONFIG.get("nautobot_circuit_maintenance", {}).get("dashboard_n_days", 7)
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
upcoming_days_maintenances = self.get_maintenances_next_n_days(n_days=n_days)
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved

# Get historical matrix for number of maintenances, includes calculating the average number per month
historical_matrix = self._get_historical_matrix()

###############################################################
# Get Average duration for the maintenances
###############################################################
total_duration_in_minutes = 0

for ckt_maint in self.queryset:
duration = ckt_maint.end_time - ckt_maint.start_time
total_duration_in_minutes += round(duration.seconds / 60.0, 0)

circuit_maint_count = CircuitMaintenance.objects.count()
if circuit_maint_count > 0:
average_maintenance_duration = str(round(total_duration_in_minutes / circuit_maint_count, 2)) + " minutes"
else:
average_maintenance_duration = "No maintenances found."

# Get count of upcoming maintenances
upcoming_maintenance_count = self.calculate_future_maintenances()

# Build up a dictionary of metrics to pass into the loop within the template
metric_values = {
"7 Day Upcoming Maintenances": len(upcoming_days_maintenances),
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
"Historical - 7 Day": len(historical_matrix["past_7_days_maintenance"]),
"Historical - 30 Days": len(historical_matrix["past_30_days_maintenance"]),
"Historical - 365 Days": len(historical_matrix["past_365_days_maintenance"]),
"Average Duration of Maintenances": average_maintenance_duration,
"Future Maintenances": upcoming_maintenance_count,
"Average Number of Maintenances Per Month": round(self.get_maintenances_per_month(), 1),
"Next 30 Days, Maintenance to Circuit Ratio": round(
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
len(self.get_maintenances_next_n_days(n_days=n_days)) / Circuit.objects.count(), 2
),
}

# Build out the extra content, but this does require that there is a method of `extra_content` to be created.
# If this method is not defined, and returning the extra_content value, then the data will not be passed to the
# template.
self.extra_content = {
"upcoming_maintenances": upcoming_days_maintenances,
"circuit_maint_metric_data": metric_values,
"n_days": n_days,
}

def extra_context(self):
"""Extra content method on."""
# add global aggregations to extra context.

return self.extra_content

def get_maintenances_next_n_days(self, n_days: int):
"""Gets maintenances in the next n number of days.

Args:
n_days (int): Number of days up coming

Returns:
Set: Set of maintenances that are up coming
"""
today = datetime.date.today()
end_date = today + datetime.timedelta(days=n_days)
maintenances = self.queryset
return_list = []
for maintenance in maintenances:
if today <= maintenance.start_time.date() <= end_date:
return_list.append(maintenance)

return return_list

def get_maintenance_past_n_days(self, n_days: int):
"""Gets maintenances in the past n number of days.

Args:
n_days (int): Should be a negative number for the number of days.
"""
today = datetime.date.today()
end_date = today + datetime.timedelta(days=n_days)
maintenances = self.queryset
return_list = []
for maintenance in maintenances:
if end_date <= maintenance.start_time.date() < today:
return_list.append(maintenance)

return return_list

def _get_historical_matrix(self):
"""Gets the historical matrix of the past maintenances.

Returns:
dict: A dictionary that represents the historical matrix maintenance record data.

{
"7 Days": count of past 7 days of maintenance,
"30 Days": count of past 30 days of maintenance,
"365 Days": count of past 30 days of maintenances
}
"""
# TODO: Move to a generic function set up, since this is something that should be exposed via the Capacity
# Metrics plugin when enabled.
return_dict = {
"past_7_days_maintenance": self.get_maintenance_past_n_days(-7),
"past_30_days_maintenance": self.get_maintenance_past_n_days(-30),
"past_365_days_maintenance": self.get_maintenance_past_n_days(-365),
}
return return_dict

def calculate_future_maintenances(self):
"""Method to calculate future maintenances.

Returns:
int: Count of future maintenances
"""
today = datetime.date.today()
count = 0
for ckt_maint in self.queryset:
if ckt_maint.start_time.date() > today:
count += 1

return count

@staticmethod
def get_month_list():
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
"""Gets the list of months that circuit maintenances have happened.

In order to know which months there are needed for a calculate average number of maintenances per month.

Returns:
list: List of months from first to last maintenance.
"""
ordered_ckt_maintenance = CircuitMaintenance.objects.order_by("start_time")
dates = [
str(ordered_ckt_maintenance.first().start_time.date()),
str(ordered_ckt_maintenance.last().start_time.date()),
]
start, end = [datet.strptime(_, "%Y-%m-%d") for _ in dates]
total_months = lambda dt: dt.month + 12 * dt.year # noqa: E731
month_list = []
for tot_m in range(total_months(start) - 1, total_months(end)):
year, month = divmod(tot_m, 12)
month_list.append(datet(year, month + 1, 1).strftime("%Y-%m"))
return month_list

def get_maintenances_per_month(self):
"""Calculates the number of circuit maintenances per month.

Returns:
float: Average maintenances per month
"""
# Initialize each month of maintenances
months = self.get_month_list()
glennmatthews marked this conversation as resolved.
Show resolved Hide resolved

if len(months) == 0:
return 0

return len(self.queryset) / len(months)


class CircuitMaintenanceListView(generic.ObjectListView):
"""View for listing the config circuitmaintenance feature definition."""

Expand Down
Loading