Przeglądaj źródła

Closes #4349: Drop support for embedded graphs

Jeremy Stretch 5 lat temu
rodzic
commit
ec66e1a5c0
36 zmienionych plików z 33 dodań i 595 usunięć
  1. 0 30
      docs/additional-features/graphs.md
  2. 3 0
      docs/release-notes/version-2.10.md
  3. 0 1
      mkdocs.yml
  4. 0 15
      netbox/circuits/api/views.py
  5. 1 1
      netbox/circuits/models.py
  6. 0 24
      netbox/circuits/tests/test_api.py
  7. 1 4
      netbox/circuits/views.py
  8. 0 33
      netbox/dcim/api/views.py
  9. 1 1
      netbox/dcim/models/device_components.py
  10. 1 1
      netbox/dcim/models/devices.py
  11. 1 1
      netbox/dcim/models/sites.py
  12. 0 63
      netbox/dcim/tests/test_api.py
  13. 0 5
      netbox/dcim/views.py
  14. 1 40
      netbox/extras/admin.py
  15. 0 9
      netbox/extras/api/nested_serializers.py
  16. 1 38
      netbox/extras/api/serializers.py
  17. 0 3
      netbox/extras/api/urls.py
  18. 1 12
      netbox/extras/api/views.py
  19. 0 15
      netbox/extras/choices.py
  20. 0 1
      netbox/extras/constants.py
  21. 1 9
      netbox/extras/filters.py
  22. 16 0
      netbox/extras/migrations/0049_remove_graph.py
  23. 1 2
      netbox/extras/models/__init__.py
  24. 0 63
      netbox/extras/models/models.py
  25. 1 34
      netbox/extras/tests/test_api.py
  26. 1 38
      netbox/extras/tests/test_filters.py
  27. 1 44
      netbox/extras/tests/test_models.py
  28. 0 26
      netbox/project-static/js/graphs.js
  29. 0 10
      netbox/templates/circuits/provider.html
  30. 0 8
      netbox/templates/dcim/device.html
  31. 0 7
      netbox/templates/dcim/inc/interface.html
  32. 0 11
      netbox/templates/dcim/site.html
  33. 0 5
      netbox/templates/virtualization/inc/vminterface.html
  34. 0 15
      netbox/virtualization/api/views.py
  35. 1 1
      netbox/virtualization/models.py
  36. 0 25
      netbox/virtualization/tests/test_api.py

+ 0 - 30
docs/additional-features/graphs.md

@@ -1,30 +0,0 @@
-# Graphs
-
-!!! warning
-    Native support for embedded graphs is due to be removed in NetBox v2.10. It will likely be superseded by a plugin providing similar functionality.
-
-NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
-
-* **Type:** Site, device, provider, or interface. This determines in which view the graph will be displayed.
-* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
-* **Name:** The title to display above the graph.
-* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
-* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
-
-Graph names and links can be rendered using Jinja2 or [Django's template language](https://docs.djangoproject.com/en/stable/ref/templates/language/).
-
-## Examples
-
-You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
-
-```
-https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
-```
-
-You can define several graphs to provide multiple contexts when viewing an object. For example:
-
-```
-https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
-https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
-https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
-```

+ 3 - 0
docs/release-notes/version-2.10.md

@@ -2,8 +2,11 @@
 
 
 ## v2.10-beta1 (FUTURE)
 ## v2.10-beta1 (FUTURE)
 
 
+**NOTE:** This release completely removes support for embedded graphs.
+
 ### Other Changes
 ### Other Changes
 
 
+* [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs
 * [#4360](https://github.com/netbox-community/netbox/issues/4360) - Remove support for the Django template language from export templates
 * [#4360](https://github.com/netbox-community/netbox/issues/4360) - Remove support for the Django template language from export templates
 * [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method
 * [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method
 
 

+ 0 - 1
mkdocs.yml

@@ -49,7 +49,6 @@ nav:
         - Custom Links: 'additional-features/custom-links.md'
         - Custom Links: 'additional-features/custom-links.md'
         - Custom Scripts: 'additional-features/custom-scripts.md'
         - Custom Scripts: 'additional-features/custom-scripts.md'
         - Export Templates: 'additional-features/export-templates.md'
         - Export Templates: 'additional-features/export-templates.md'
-        - Graphs: 'additional-features/graphs.md'
         - NAPALM: 'additional-features/napalm.md'
         - NAPALM: 'additional-features/napalm.md'
         - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
         - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
         - Reports: 'additional-features/reports.md'
         - Reports: 'additional-features/reports.md'

+ 0 - 15
netbox/circuits/api/views.py

@@ -1,14 +1,9 @@
 from django.db.models import Count, Prefetch
 from django.db.models import Count, Prefetch
-from django.shortcuts import get_object_or_404
-from rest_framework.decorators import action
-from rest_framework.response import Response
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from circuits import filters
 from circuits import filters
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
-from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
-from extras.models import Graph
 from utilities.api import ModelViewSet
 from utilities.api import ModelViewSet
 from . import serializers
 from . import serializers
 
 
@@ -32,16 +27,6 @@ class ProviderViewSet(CustomFieldModelViewSet):
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
     filterset_class = filters.ProviderFilterSet
     filterset_class = filters.ProviderFilterSet
 
 
-    @action(detail=True)
-    def graphs(self, request, pk):
-        """
-        A convenience method for rendering graphs for a particular provider.
-        """
-        provider = get_object_or_404(self.queryset, pk=pk)
-        queryset = Graph.objects.restrict(request.user).filter(type__model='provider')
-        serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
-        return Response(serializer.data)
-
 
 
 #
 #
 #  Circuit Types
 #  Circuit Types

+ 1 - 1
netbox/circuits/models.py

@@ -22,7 +22,7 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
 class Provider(ChangeLoggedModel, CustomFieldModel):
 class Provider(ChangeLoggedModel, CustomFieldModel):
     """
     """
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model

+ 0 - 24
netbox/circuits/tests/test_api.py

@@ -1,11 +1,8 @@
-from django.contrib.contenttypes.models import ContentType
-from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 
 
 from circuits.choices import *
 from circuits.choices import *
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from dcim.models import Site
 from dcim.models import Site
-from extras.models import Graph
 from utilities.testing import APITestCase, APIViewTestCases
 from utilities.testing import APITestCase, APIViewTestCases
 
 
 
 
@@ -46,27 +43,6 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
         )
         )
         Provider.objects.bulk_create(providers)
         Provider.objects.bulk_create(providers)
 
 
-    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
-    def test_get_provider_graphs(self):
-        """
-        Test retrieval of Graphs assigned to Providers.
-        """
-        provider = self.model.objects.first()
-        ct = ContentType.objects.get(app_label='circuits', model='provider')
-        graphs = (
-            Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'),
-            Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'),
-            Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-        self.add_permissions('circuits.view_provider')
-        url = reverse('circuits-api:provider-graphs', kwargs={'pk': provider.pk})
-        response = self.client.get(url, **self.header)
-
-        self.assertEqual(len(response.data), 3)
-        self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?provider=provider-1&foo=1')
-
 
 
 class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
 class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
     model = CircuitType
     model = CircuitType

+ 1 - 4
netbox/circuits/views.py

@@ -1,11 +1,10 @@
 from django.conf import settings
 from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.db import transaction
 from django.db import transaction
-from django.db.models import Count, Prefetch
+from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
-from extras.models import Graph
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
 from utilities.views import (
@@ -38,7 +37,6 @@ class ProviderView(ObjectView):
         ).prefetch_related(
         ).prefetch_related(
             'type', 'tenant', 'terminations__site'
             'type', 'tenant', 'terminations__site'
         ).annotate_sites()
         ).annotate_sites()
-        show_graphs = Graph.objects.filter(type__model='provider').exists()
 
 
         circuits_table = tables.CircuitTable(circuits)
         circuits_table = tables.CircuitTable(circuits)
         circuits_table.columns.hide('provider')
         circuits_table.columns.hide('provider')
@@ -52,7 +50,6 @@ class ProviderView(ObjectView):
         return render(request, 'circuits/provider.html', {
         return render(request, 'circuits/provider.html', {
             'provider': provider,
             'provider': provider,
             'circuits_table': circuits_table,
             'circuits_table': circuits_table,
-            'show_graphs': show_graphs,
         })
         })
 
 
 
 

+ 0 - 33
netbox/dcim/api/views.py

@@ -23,9 +23,7 @@ from dcim.models import (
     PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     VirtualChassis,
     VirtualChassis,
 )
 )
-from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
-from extras.models import Graph
 from ipam.models import Prefix, VLAN
 from ipam.models import Prefix, VLAN
 from utilities.api import (
 from utilities.api import (
     get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
     get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
@@ -113,16 +111,6 @@ class SiteViewSet(CustomFieldModelViewSet):
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
     filterset_class = filters.SiteFilterSet
     filterset_class = filters.SiteFilterSet
 
 
-    @action(detail=True)
-    def graphs(self, request, pk):
-        """
-        A convenience method for rendering graphs for a particular site.
-        """
-        site = get_object_or_404(self.queryset, pk=pk)
-        queryset = Graph.objects.restrict(request.user).filter(type__model='site')
-        serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
-        return Response(serializer.data)
-
 
 
 #
 #
 # Rack groups
 # Rack groups
@@ -363,17 +351,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
 
 
         return serializers.DeviceWithConfigContextSerializer
         return serializers.DeviceWithConfigContextSerializer
 
 
-    @action(detail=True)
-    def graphs(self, request, pk):
-        """
-        A convenience method for rendering graphs for a particular Device.
-        """
-        device = get_object_or_404(self.queryset, pk=pk)
-        queryset = Graph.objects.restrict(request.user).filter(type__model='device')
-        serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
-
-        return Response(serializer.data)
-
     @swagger_auto_schema(
     @swagger_auto_schema(
         manual_parameters=[
         manual_parameters=[
             Parameter(
             Parameter(
@@ -527,16 +504,6 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
     serializer_class = serializers.InterfaceSerializer
     serializer_class = serializers.InterfaceSerializer
     filterset_class = filters.InterfaceFilterSet
     filterset_class = filters.InterfaceFilterSet
 
 
-    @action(detail=True)
-    def graphs(self, request, pk):
-        """
-        A convenience method for rendering graphs for a particular interface.
-        """
-        interface = get_object_or_404(self.queryset, pk=pk)
-        queryset = Graph.objects.restrict(request.user).filter(type__model='interface')
-        serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
-        return Response(serializer.data)
-
 
 
 class FrontPortViewSet(CableTraceMixin, ModelViewSet):
 class FrontPortViewSet(CableTraceMixin, ModelViewSet):
     queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
     queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')

+ 1 - 1
netbox/dcim/models/device_components.py

@@ -582,7 +582,7 @@ class BaseInterface(models.Model):
         abstract = True
         abstract = True
 
 
 
 
-@extras_features('graphs', 'export_templates', 'webhooks')
+@extras_features('export_templates', 'webhooks')
 class Interface(CableTermination, ComponentModel, BaseInterface):
 class Interface(CableTermination, ComponentModel, BaseInterface):
     """
     """
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.

+ 1 - 1
netbox/dcim/models/devices.py

@@ -450,7 +450,7 @@ class Platform(ChangeLoggedModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
 class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
 class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     """
     """
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,

+ 1 - 1
netbox/dcim/models/sites.py

@@ -91,7 +91,7 @@ class Region(MPTTModel, ChangeLoggedModel):
 # Sites
 # Sites
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
 class Site(ChangeLoggedModel, CustomFieldModel):
 class Site(ChangeLoggedModel, CustomFieldModel):
     """
     """
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     A Site represents a geographic location within a network; typically a building or campus. The optional facility

+ 0 - 63
netbox/dcim/tests/test_api.py

@@ -1,6 +1,4 @@
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
-from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
@@ -14,7 +12,6 @@ from dcim.models import (
     Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
     Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
 )
 )
 from ipam.models import VLAN
 from ipam.models import VLAN
-from extras.models import Graph
 from utilities.testing import APITestCase, APIViewTestCases
 from utilities.testing import APITestCase, APIViewTestCases
 from virtualization.models import Cluster, ClusterType
 from virtualization.models import Cluster, ClusterType
 
 
@@ -132,26 +129,6 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
             },
             },
         ]
         ]
 
 
-    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
-    def test_get_site_graphs(self):
-        """
-        Test retrieval of Graphs assigned to Sites.
-        """
-        ct = ContentType.objects.get_for_model(Site)
-        graphs = (
-            Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'),
-            Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'),
-            Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-        self.add_permissions('dcim.view_site')
-        url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.first().pk})
-        response = self.client.get(url, **self.header)
-
-        self.assertEqual(len(response.data), 3)
-        self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?site=site-1&foo=1')
-
 
 
 class RackGroupTest(APIViewTestCases.APIViewTestCase):
 class RackGroupTest(APIViewTestCases.APIViewTestCase):
     model = RackGroup
     model = RackGroup
@@ -902,26 +879,6 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
             },
             },
         ]
         ]
 
 
-    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
-    def test_get_device_graphs(self):
-        """
-        Test retrieval of Graphs assigned to Devices.
-        """
-        ct = ContentType.objects.get_for_model(Device)
-        graphs = (
-            Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?device={{ obj.name }}&foo=1'),
-            Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?device={{ obj.name }}&foo=2'),
-            Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?device={{ obj.name }}&foo=3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-        self.add_permissions('dcim.view_device')
-        url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.first().pk})
-        response = self.client.get(url, **self.header)
-
-        self.assertEqual(len(response.data), 3)
-        self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?device=Device 1&foo=1')
-
     def test_config_context_included_by_default_in_list_view(self):
     def test_config_context_included_by_default_in_list_view(self):
         """
         """
         Check that config context data is included by default in the devices list.
         Check that config context data is included by default in the devices list.
@@ -1159,26 +1116,6 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
             },
             },
         ]
         ]
 
 
-    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
-    def test_get_interface_graphs(self):
-        """
-        Test retrieval of Graphs assigned to Devices.
-        """
-        ct = ContentType.objects.get_for_model(Interface)
-        graphs = (
-            Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'),
-            Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'),
-            Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-        self.add_permissions('dcim.view_interface')
-        url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.first().pk})
-        response = self.client.get(url, **self.header)
-
-        self.assertEqual(len(response.data), 3)
-        self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')
-
 
 
 class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
 class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
     model = FrontPort
     model = FrontPort

+ 0 - 5
netbox/dcim/views.py

@@ -14,7 +14,6 @@ from django.utils.safestring import mark_safe
 from django.views.generic import View
 from django.views.generic import View
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
-from extras.models import Graph
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
 from ipam.models import IPAddress, Prefix, Service, VLAN
 from ipam.models import IPAddress, Prefix, Service, VLAN
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
@@ -172,13 +171,11 @@ class SiteView(ObjectView):
         rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate(
         rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate(
             rack_count=Count('racks')
             rack_count=Count('racks')
         )
         )
-        show_graphs = Graph.objects.filter(type__model='site').exists()
 
 
         return render(request, 'dcim/site.html', {
         return render(request, 'dcim/site.html', {
             'site': site,
             'site': site,
             'stats': stats,
             'stats': stats,
             'rack_groups': rack_groups,
             'rack_groups': rack_groups,
-            'show_graphs': show_graphs,
         })
         })
 
 
 
 
@@ -1082,8 +1079,6 @@ class DeviceView(ObjectView):
             'secrets': secrets,
             'secrets': secrets,
             'vc_members': vc_members,
             'vc_members': vc_members,
             'related_devices': related_devices,
             'related_devices': related_devices,
-            'show_graphs': Graph.objects.filter(type__model='device').exists(),
-            'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(),
         })
         })
 
 
 
 

+ 1 - 40
netbox/extras/admin.py

@@ -2,7 +2,7 @@ from django import forms
 from django.contrib import admin
 from django.contrib import admin
 
 
 from utilities.forms import LaxURLField
 from utilities.forms import LaxURLField
-from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, JobResult, Webhook
+from .models import CustomField, CustomFieldChoice, CustomLink, ExportTemplate, JobResult, Webhook
 
 
 
 
 def order_content_types(field):
 def order_content_types(field):
@@ -150,45 +150,6 @@ class CustomLinkAdmin(admin.ModelAdmin):
     form = CustomLinkForm
     form = CustomLinkForm
 
 
 
 
-#
-# Graphs
-#
-
-class GraphForm(forms.ModelForm):
-
-    class Meta:
-        model = Graph
-        exclude = ()
-        help_texts = {
-            'template_language': "<a href=\"https://jinja.palletsprojects.com\">Jinja2</a> is strongly recommended for "
-                                 "new graphs."
-        }
-        widgets = {
-            'source': forms.Textarea,
-            'link': forms.Textarea,
-        }
-
-
-@admin.register(Graph)
-class GraphAdmin(admin.ModelAdmin):
-    fieldsets = (
-        ('Graph', {
-            'fields': ('type', 'name', 'weight')
-        }),
-        ('Templates', {
-            'fields': ('template_language', 'source', 'link'),
-            'classes': ('monospace',)
-        })
-    )
-    form = GraphForm
-    list_display = [
-        'name', 'type', 'weight', 'template_language', 'source',
-    ]
-    list_filter = [
-        'type', 'template_language',
-    ]
-
-
 #
 #
 # Export templates
 # Export templates
 #
 #

+ 0 - 9
netbox/extras/api/nested_serializers.py

@@ -7,7 +7,6 @@ from utilities.api import ChoiceField, WritableNestedSerializer
 __all__ = [
 __all__ = [
     'NestedConfigContextSerializer',
     'NestedConfigContextSerializer',
     'NestedExportTemplateSerializer',
     'NestedExportTemplateSerializer',
-    'NestedGraphSerializer',
     'NestedImageAttachmentSerializer',
     'NestedImageAttachmentSerializer',
     'NestedJobResultSerializer',
     'NestedJobResultSerializer',
     'NestedTagSerializer',
     'NestedTagSerializer',
@@ -30,14 +29,6 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'name']
         fields = ['id', 'url', 'name']
 
 
 
 
-class NestedGraphSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:graph-detail')
-
-    class Meta:
-        model = models.Graph
-        fields = ['id', 'url', 'name']
-
-
 class NestedImageAttachmentSerializer(WritableNestedSerializer):
 class NestedImageAttachmentSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
 
 

+ 1 - 38
netbox/extras/api/serializers.py

@@ -10,7 +10,7 @@ from dcim.api.nested_serializers import (
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from extras.choices import *
 from extras.choices import *
 from extras.models import (
 from extras.models import (
-    ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
+    ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
 )
 )
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
@@ -25,43 +25,6 @@ from virtualization.models import Cluster, ClusterGroup
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 
 
-#
-# Graphs
-#
-
-class GraphSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:graph-detail')
-    type = ContentTypeField(
-        queryset=ContentType.objects.filter(FeatureQuery('graphs').get_query()),
-    )
-
-    class Meta:
-        model = Graph
-        fields = ['id', 'url', 'type', 'weight', 'name', 'template_language', 'source', 'link']
-
-
-class RenderedGraphSerializer(serializers.ModelSerializer):
-    embed_url = serializers.SerializerMethodField(
-        read_only=True
-    )
-    embed_link = serializers.SerializerMethodField(
-        read_only=True
-    )
-    type = ContentTypeField(
-        read_only=True
-    )
-
-    class Meta:
-        model = Graph
-        fields = ['id', 'type', 'weight', 'name', 'embed_url', 'embed_link']
-
-    def get_embed_url(self, obj):
-        return obj.embed_url(self.context['graphed_object'])
-
-    def get_embed_link(self, obj):
-        return obj.embed_link(self.context['graphed_object'])
-
-
 #
 #
 # Export templates
 # Export templates
 #
 #

+ 0 - 3
netbox/extras/api/urls.py

@@ -8,9 +8,6 @@ router.APIRootView = views.ExtrasRootView
 # Custom field choices
 # Custom field choices
 router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
 router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
 
 
-# Graphs
-router.register('graphs', views.GraphViewSet)
-
 # Export templates
 # Export templates
 router.register('export-templates', views.ExportTemplateViewSet)
 router.register('export-templates', views.ExportTemplateViewSet)
 
 

+ 1 - 12
netbox/extras/api/views.py

@@ -15,7 +15,7 @@ from rq import Worker
 from extras import filters
 from extras import filters
 from extras.choices import JobResultStatusChoices
 from extras.choices import JobResultStatusChoices
 from extras.models import (
 from extras.models import (
-    ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
+    ConfigContext, CustomFieldChoice, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
 )
 )
 from extras.reports import get_report, get_reports, run_report
 from extras.reports import get_report, get_reports, run_report
 from extras.scripts import get_script, get_scripts, run_script
 from extras.scripts import get_script, get_scripts, run_script
@@ -98,17 +98,6 @@ class CustomFieldModelViewSet(ModelViewSet):
         return super().get_queryset().prefetch_related('custom_field_values__field')
         return super().get_queryset().prefetch_related('custom_field_values__field')
 
 
 
 
-#
-# Graphs
-#
-
-class GraphViewSet(ModelViewSet):
-    metadata_class = ContentTypeMetadata
-    queryset = Graph.objects.all()
-    serializer_class = serializers.GraphSerializer
-    filterset_class = filters.GraphFilterSet
-
-
 #
 #
 # Export templates
 # Export templates
 #
 #

+ 0 - 15
netbox/extras/choices.py

@@ -79,21 +79,6 @@ class ObjectChangeActionChoices(ChoiceSet):
     )
     )
 
 
 
 
-#
-# ExportTemplates
-#
-
-class TemplateLanguageChoices(ChoiceSet):
-
-    LANGUAGE_JINJA2 = 'jinja2'
-    LANGUAGE_DJANGO = 'django'
-
-    CHOICES = (
-        (LANGUAGE_JINJA2, 'Jinja2'),
-        (LANGUAGE_DJANGO, 'Django (Legacy)'),
-    )
-
-
 #
 #
 # Log Levels for Reports and Scripts
 # Log Levels for Reports and Scripts
 #
 #

+ 0 - 1
netbox/extras/constants.py

@@ -6,7 +6,6 @@ EXTRAS_FEATURES = [
     'custom_fields',
     'custom_fields',
     'custom_links',
     'custom_links',
     'export_templates',
     'export_templates',
-    'graphs',
     'job_results',
     'job_results',
     'webhooks'
     'webhooks'
 ]
 ]

+ 1 - 9
netbox/extras/filters.py

@@ -7,7 +7,7 @@ from tenancy.models import Tenant, TenantGroup
 from utilities.filters import BaseFilterSet
 from utilities.filters import BaseFilterSet
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from .choices import *
 from .choices import *
-from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag
+from .models import ConfigContext, CustomField, ExportTemplate, ObjectChange, JobResult, Tag
 
 
 
 
 __all__ = (
 __all__ = (
@@ -16,7 +16,6 @@ __all__ = (
     'CustomFieldFilter',
     'CustomFieldFilter',
     'CustomFieldFilterSet',
     'CustomFieldFilterSet',
     'ExportTemplateFilterSet',
     'ExportTemplateFilterSet',
-    'GraphFilterSet',
     'LocalConfigContextFilterSet',
     'LocalConfigContextFilterSet',
     'ObjectChangeFilterSet',
     'ObjectChangeFilterSet',
     'TagFilterSet',
     'TagFilterSet',
@@ -90,13 +89,6 @@ class CustomFieldFilterSet(django_filters.FilterSet):
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
 
 
 
 
-class GraphFilterSet(BaseFilterSet):
-
-    class Meta:
-        model = Graph
-        fields = ['id', 'type', 'name', 'template_language']
-
-
 class ExportTemplateFilterSet(BaseFilterSet):
 class ExportTemplateFilterSet(BaseFilterSet):
 
 
     class Meta:
     class Meta:

+ 16 - 0
netbox/extras/migrations/0049_remove_graph.py

@@ -0,0 +1,16 @@
+# Generated by Django 3.1 on 2020-08-21 15:47
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0048_exporttemplate_remove_template_language'),
+    ]
+
+    operations = [
+        migrations.DeleteModel(
+            name='Graph',
+        ),
+    ]

+ 1 - 2
netbox/extras/models/__init__.py

@@ -1,7 +1,7 @@
 from .change_logging import ChangeLoggedModel, ObjectChange
 from .change_logging import ChangeLoggedModel, ObjectChange
 from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
 from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
 from .models import (
 from .models import (
-    ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, JobResult, Report, Script,
+    ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
     Webhook,
     Webhook,
 )
 )
 from .tags import Tag, TaggedItem
 from .tags import Tag, TaggedItem
@@ -16,7 +16,6 @@ __all__ = (
     'CustomFieldValue',
     'CustomFieldValue',
     'CustomLink',
     'CustomLink',
     'ExportTemplate',
     'ExportTemplate',
-    'Graph',
     'ImageAttachment',
     'ImageAttachment',
     'JobResult',
     'JobResult',
     'ObjectChange',
     'ObjectChange',

+ 0 - 63
netbox/extras/models/models.py

@@ -203,69 +203,6 @@ class CustomLink(models.Model):
         return self.name
         return self.name
 
 
 
 
-#
-# Graphs
-#
-
-class Graph(models.Model):
-    type = models.ForeignKey(
-        to=ContentType,
-        on_delete=models.CASCADE,
-        limit_choices_to=FeatureQuery('graphs')
-    )
-    weight = models.PositiveSmallIntegerField(
-        default=1000
-    )
-    name = models.CharField(
-        max_length=100,
-        verbose_name='Name'
-    )
-    template_language = models.CharField(
-        max_length=50,
-        choices=TemplateLanguageChoices,
-        default=TemplateLanguageChoices.LANGUAGE_JINJA2
-    )
-    source = models.CharField(
-        max_length=500,
-        verbose_name='Source URL'
-    )
-    link = models.URLField(
-        blank=True,
-        verbose_name='Link URL'
-    )
-
-    objects = RestrictedQuerySet.as_manager()
-
-    class Meta:
-        ordering = ('type', 'weight', 'name', 'pk')  # (type, weight, name) may be non-unique
-
-    def __str__(self):
-        return self.name
-
-    def embed_url(self, obj):
-        context = {'obj': obj}
-
-        if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO:
-            template = Template(self.source)
-            return template.render(Context(context))
-
-        elif self.template_language == TemplateLanguageChoices.LANGUAGE_JINJA2:
-            return render_jinja2(self.source, context)
-
-    def embed_link(self, obj):
-        if self.link is None:
-            return ''
-
-        context = {'obj': obj}
-
-        if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO:
-            template = Template(self.link)
-            return template.render(Context(context))
-
-        elif self.template_language == TemplateLanguageChoices.LANGUAGE_JINJA2:
-            return render_jinja2(self.link, context)
-
-
 #
 #
 # Export templates
 # Export templates
 #
 #

+ 1 - 34
netbox/extras/tests/test_api.py

@@ -10,7 +10,7 @@ from rq import Worker
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
 from extras.api.views import ReportViewSet, ScriptViewSet
 from extras.api.views import ReportViewSet, ScriptViewSet
-from extras.models import ConfigContext, ExportTemplate, Graph, ImageAttachment, Tag
+from extras.models import ConfigContext, ExportTemplate, ImageAttachment, Tag
 from extras.reports import Report
 from extras.reports import Report
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from utilities.testing import APITestCase, APIViewTestCases
 from utilities.testing import APITestCase, APIViewTestCases
@@ -29,39 +29,6 @@ class AppTest(APITestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
-class GraphTest(APIViewTestCases.APIViewTestCase):
-    model = Graph
-    brief_fields = ['id', 'name', 'url']
-    create_data = [
-        {
-            'type': 'dcim.site',
-            'name': 'Graph 4',
-            'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4',
-        },
-        {
-            'type': 'dcim.site',
-            'name': 'Graph 5',
-            'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=5',
-        },
-        {
-            'type': 'dcim.site',
-            'name': 'Graph 6',
-            'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=6',
-        },
-    ]
-
-    @classmethod
-    def setUpTestData(cls):
-        ct = ContentType.objects.get_for_model(Site)
-
-        graphs = (
-            Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'),
-            Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?site={{ obj.name }}&foo=2'),
-            Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?site={{ obj.name }}&foo=3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-
 class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
 class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
     model = ExportTemplate
     model = ExportTemplate
     brief_fields = ['id', 'name', 'url']
     brief_fields = ['id', 'name', 'url']

+ 1 - 38
netbox/extras/tests/test_filters.py

@@ -2,49 +2,12 @@ from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
-from extras.choices import *
 from extras.filters import *
 from extras.filters import *
-from extras.utils import FeatureQuery
-from extras.models import ConfigContext, ExportTemplate, Graph, Tag
+from extras.models import ConfigContext, ExportTemplate, Tag
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 
 
-class GraphTestCase(TestCase):
-    queryset = Graph.objects.all()
-    filterset = GraphFilterSet
-
-    @classmethod
-    def setUpTestData(cls):
-
-        # Get the first three available types
-        content_types = ContentType.objects.filter(FeatureQuery('graphs').get_query())[:3]
-
-        graphs = (
-            Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
-            Graph(name='Graph 2', type=content_types[1], template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, source='http://example.com/2'),
-            Graph(name='Graph 3', type=content_types[2], template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, source='http://example.com/3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-    def test_id(self):
-        params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-    def test_name(self):
-        params = {'name': ['Graph 1', 'Graph 2']}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-    def test_type(self):
-        content_type = ContentType.objects.filter(FeatureQuery('graphs').get_query()).first()
-        params = {'type': content_type.pk}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
-    def test_template_language(self):
-        params = {'template_language': TemplateLanguageChoices.LANGUAGE_JINJA2}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-
 class ExportTemplateTestCase(TestCase):
 class ExportTemplateTestCase(TestCase):
     queryset = ExportTemplate.objects.all()
     queryset = ExportTemplate.objects.all()
     filterset = ExportTemplateFilterSet
     filterset = ExportTemplateFilterSet

+ 1 - 44
netbox/extras/tests/test_models.py

@@ -1,49 +1,6 @@
-from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 
 
-from dcim.models import Site
-from extras.choices import TemplateLanguageChoices
-from extras.models import Graph, Tag
-
-
-class GraphTest(TestCase):
-
-    def setUp(self):
-
-        self.site = Site(name='Site 1', slug='site-1')
-
-    def test_graph_render_django(self):
-
-        # Using the pluralize filter as a sanity check (it's only available in Django)
-        TEMPLATE_TEXT = "{{ obj.name|lower }} thing{{ 2|pluralize }}"
-        RENDERED_TEXT = "site 1 things"
-
-        graph = Graph(
-            type=ContentType.objects.get(app_label='dcim', model='site'),
-            name='Graph 1',
-            template_language=TemplateLanguageChoices.LANGUAGE_DJANGO,
-            source=TEMPLATE_TEXT,
-            link=TEMPLATE_TEXT
-        )
-
-        self.assertEqual(graph.embed_url(self.site), RENDERED_TEXT)
-        self.assertEqual(graph.embed_link(self.site), RENDERED_TEXT)
-
-    def test_graph_render_jinja2(self):
-
-        TEMPLATE_TEXT = "{{ [obj.name, obj.slug]|join(',') }}"
-        RENDERED_TEXT = "Site 1,site-1"
-
-        graph = Graph(
-            type=ContentType.objects.get(app_label='dcim', model='site'),
-            name='Graph 1',
-            template_language=TemplateLanguageChoices.LANGUAGE_JINJA2,
-            source=TEMPLATE_TEXT,
-            link=TEMPLATE_TEXT
-        )
-
-        self.assertEqual(graph.embed_url(self.site), RENDERED_TEXT)
-        self.assertEqual(graph.embed_link(self.site), RENDERED_TEXT)
+from extras.models import Tag
 
 
 
 
 class TagTest(TestCase):
 class TagTest(TestCase):

+ 0 - 26
netbox/project-static/js/graphs.js

@@ -1,26 +0,0 @@
-$('#graphs_modal').on('show.bs.modal', function (event) {
-    var button = $(event.relatedTarget);
-    var obj = button.data('obj');
-    var url = button.data('url');
-    var modal_title = $(this).find('.modal-title');
-    var modal_body = $(this).find('.modal-body');
-    modal_title.text(obj);
-    modal_body.empty();
-    $.ajax({
-        url: url,
-        dataType: 'json',
-        success: function(json) {
-            $.each(json, function(i, graph) {
-                // Build in a 500ms delay per graph to avoid hammering the server
-                setTimeout(function() {
-                    modal_body.append('<h4 class="text-center">' + graph.name + '</h4>');
-                    if (graph.embed_link) {
-                        modal_body.append('<a href="' + graph.embed_link + '"><img src="' + graph.embed_url + '" /></a>');
-                    } else {
-                        modal_body.append('<img src="' + graph.embed_url + '" />');
-                    }
-                }, i*500);
-            })
-        }
-    });
-});

+ 0 - 10
netbox/templates/circuits/provider.html

@@ -30,11 +30,6 @@
     </div>
     </div>
     <div class="pull-right noprint">
     <div class="pull-right noprint">
         {% plugin_buttons provider %}
         {% plugin_buttons provider %}
-        {% if show_graphs %}
-            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
-                <i class="fa fa-signal" aria-hidden="true"></i> Graphs
-            </button>
-        {% endif %}
         {% if perms.circuits.add_provider %}
         {% if perms.circuits.add_provider %}
             {% clone_button provider %}
             {% clone_button provider %}
         {% endif %}
         {% endif %}
@@ -138,14 +133,9 @@
     {% plugin_right_page provider %}
     {% plugin_right_page provider %}
     </div>
     </div>
 </div>
 </div>
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
 <div class="row">
 <div class="row">
     <div class="col-md-12">
     <div class="col-md-12">
         {% plugin_full_width_page provider %}
         {% plugin_full_width_page provider %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
-
-{% block javascript %}
-<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 0 - 8
netbox/templates/dcim/device.html

@@ -38,12 +38,6 @@
     </div>
     </div>
     <div class="pull-right noprint">
     <div class="pull-right noprint">
         {% plugin_buttons device %}
         {% plugin_buttons device %}
-        {% if show_graphs %}
-            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }}" data-url="{% url 'dcim-api:device-graphs' pk=device.pk %}" title="Show graphs">
-                <i class="fa fa-signal" aria-hidden="true"></i>
-                Graphs
-            </button>
-        {% endif %}
         {% if perms.dcim.change_device %}
         {% if perms.dcim.change_device %}
             <div class="btn-group">
             <div class="btn-group">
                 <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                 <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -958,7 +952,6 @@
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
 {% include 'secrets/inc/private_key_modal.html' %}
 {% include 'secrets/inc/private_key_modal.html' %}
 {% endblock %}
 {% endblock %}
 
 
@@ -1012,6 +1005,5 @@ $(".cable-toggle").click(function() {
 });
 });
 </script>
 </script>
 <script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
-<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}
 {% endblock %}

+ 0 - 7
netbox/templates/dcim/inc/interface.html

@@ -138,13 +138,6 @@
 
 
     {# Buttons #}
     {# Buttons #}
     <td class="text-right text-nowrap noprint">
     <td class="text-right text-nowrap noprint">
-        {% if show_interface_graphs %}
-            {% if iface.connected_endpoint %}
-                <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
-                    <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-                </button>
-            {% endif %}
-        {% endif %}
         {% if perms.ipam.add_ipaddress %}
         {% if perms.ipam.add_ipaddress %}
             <a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
             <a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
                 <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
                 <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>

+ 0 - 11
netbox/templates/dcim/site.html

@@ -35,12 +35,6 @@
     </div>
     </div>
     <div class="pull-right noprint">
     <div class="pull-right noprint">
         {% plugin_buttons site %}
         {% plugin_buttons site %}
-        {% if show_graphs %}
-            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
-                <i class="fa fa-signal" aria-hidden="true"></i>
-                Graphs
-            </button>
-        {% endif %}
         {% if perms.dcim.add_site %}
         {% if perms.dcim.add_site %}
             {% clone_button site %}
             {% clone_button site %}
         {% endif %}
         {% endif %}
@@ -292,14 +286,9 @@
         {% plugin_right_page site %}
         {% plugin_right_page site %}
 	</div>
 	</div>
 </div>
 </div>
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
 <div class="row">
 <div class="row">
     <div class="col-md-12">
     <div class="col-md-12">
         {% plugin_full_width_page site %}
         {% plugin_full_width_page site %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
-
-{% block javascript %}
-<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 0 - 5
netbox/templates/virtualization/inc/vminterface.html

@@ -38,11 +38,6 @@
 
 
     {# Buttons #}
     {# Buttons #}
     <td class="text-right text-nowrap noprint">
     <td class="text-right text-nowrap noprint">
-        {% if show_interface_graphs %}
-            <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ virtualmachine.name }} - {{ iface.name }}" data-url="{% url 'virtualization-api:vminterface-graphs' pk=iface.pk %}" title="Show graphs">
-                <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-            </button>
-        {% endif %}
         {% if perms.ipam.add_ipaddress %}
         {% if perms.ipam.add_ipaddress %}
             <a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
             <a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
                 <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
                 <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>

+ 0 - 15
netbox/virtualization/api/views.py

@@ -1,13 +1,8 @@
 from django.db.models import Count
 from django.db.models import Count
-from django.shortcuts import get_object_or_404
-from rest_framework.decorators import action
-from rest_framework.response import Response
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from dcim.models import Device
 from dcim.models import Device
-from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
-from extras.models import Graph
 from utilities.api import ModelViewSet
 from utilities.api import ModelViewSet
 from utilities.utils import get_subquery
 from utilities.utils import get_subquery
 from virtualization import filters
 from virtualization import filters
@@ -91,13 +86,3 @@ class VMInterfaceViewSet(ModelViewSet):
     )
     )
     serializer_class = serializers.VMInterfaceSerializer
     serializer_class = serializers.VMInterfaceSerializer
     filterset_class = filters.VMInterfaceFilterSet
     filterset_class = filters.VMInterfaceFilterSet
-
-    @action(detail=True)
-    def graphs(self, request, pk):
-        """
-        A convenience method for rendering graphs for a particular VM interface.
-        """
-        vminterface = get_object_or_404(self.queryset, pk=pk)
-        queryset = Graph.objects.restrict(request.user).filter(type__model='vminterface')
-        serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': vminterface})
-        return Response(serializer.data)

+ 1 - 1
netbox/virtualization/models.py

@@ -381,7 +381,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
 # Interfaces
 # Interfaces
 #
 #
 
 
-@extras_features('graphs', 'export_templates', 'webhooks')
+@extras_features('export_templates', 'webhooks')
 class VMInterface(BaseInterface):
 class VMInterface(BaseInterface):
     virtual_machine = models.ForeignKey(
     virtual_machine = models.ForeignKey(
         to='virtualization.VirtualMachine',
         to='virtualization.VirtualMachine',

+ 0 - 25
netbox/virtualization/tests/test_api.py

@@ -1,10 +1,7 @@
-from django.contrib.contenttypes.models import ContentType
-from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
 from dcim.choices import InterfaceModeChoices
 from dcim.choices import InterfaceModeChoices
-from extras.models import Graph
 from ipam.models import VLAN
 from ipam.models import VLAN
 from utilities.testing import APITestCase, APIViewTestCases
 from utilities.testing import APITestCase, APIViewTestCases
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -244,25 +241,3 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
                 'untagged_vlan': vlans[2].pk,
                 'untagged_vlan': vlans[2].pk,
             },
             },
         ]
         ]
-
-    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
-    def test_get_vminterface_graphs(self):
-        """
-        Test retrieval of Graphs assigned to VM interfaces.
-        """
-        ct = ContentType.objects.get_for_model(VMInterface)
-        graphs = (
-            Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'),
-            Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'),
-            Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'),
-        )
-        Graph.objects.bulk_create(graphs)
-
-        self.add_permissions('virtualization.view_vminterface')
-        url = reverse('virtualization-api:vminterface-graphs', kwargs={
-            'pk': VMInterface.objects.first().pk
-        })
-        response = self.client.get(url, **self.header)
-
-        self.assertEqual(len(response.data), 3)
-        self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')