ipfabric_netbox 3.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipfabric_netbox might be problematic. Click here for more details.

Files changed (59) hide show
  1. ipfabric_netbox/__init__.py +42 -0
  2. ipfabric_netbox/api/__init__.py +2 -0
  3. ipfabric_netbox/api/nested_serializers.py +99 -0
  4. ipfabric_netbox/api/serializers.py +160 -0
  5. ipfabric_netbox/api/urls.py +21 -0
  6. ipfabric_netbox/api/views.py +111 -0
  7. ipfabric_netbox/choices.py +226 -0
  8. ipfabric_netbox/filtersets.py +125 -0
  9. ipfabric_netbox/forms.py +1063 -0
  10. ipfabric_netbox/jobs.py +95 -0
  11. ipfabric_netbox/migrations/0001_initial.py +342 -0
  12. ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +17 -0
  13. ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +49 -0
  14. ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +17 -0
  15. ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +64 -0
  16. ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +48 -0
  17. ipfabric_netbox/migrations/__init__.py +0 -0
  18. ipfabric_netbox/models.py +874 -0
  19. ipfabric_netbox/navigation.py +62 -0
  20. ipfabric_netbox/signals.py +68 -0
  21. ipfabric_netbox/tables.py +208 -0
  22. ipfabric_netbox/template_content.py +13 -0
  23. ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +72 -0
  24. ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +20 -0
  25. ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +6 -0
  26. ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +22 -0
  27. ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +70 -0
  28. ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +61 -0
  29. ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +60 -0
  30. ipfabric_netbox/templates/ipfabric_netbox/inc/sync_delete.html +19 -0
  31. ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +11 -0
  32. ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +11 -0
  33. ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +55 -0
  34. ipfabric_netbox/templates/ipfabric_netbox/ipfabricbranch.html +141 -0
  35. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +105 -0
  36. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +111 -0
  37. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +103 -0
  38. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +41 -0
  39. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +17 -0
  40. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +59 -0
  41. ipfabric_netbox/templates/ipfabric_netbox/partials/branch_all.html +10 -0
  42. ipfabric_netbox/templates/ipfabric_netbox/partials/branch_progress.html +19 -0
  43. ipfabric_netbox/templates/ipfabric_netbox/partials/branch_status.html +1 -0
  44. ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +53 -0
  45. ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_branch.html +1 -0
  46. ipfabric_netbox/templates/ipfabric_netbox/sync_list.html +126 -0
  47. ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +9 -0
  48. ipfabric_netbox/tests/__init__.py +0 -0
  49. ipfabric_netbox/tests/test_models.py +1340 -0
  50. ipfabric_netbox/urls.py +141 -0
  51. ipfabric_netbox/utilities/__init__.py +0 -0
  52. ipfabric_netbox/utilities/ipfutils.py +591 -0
  53. ipfabric_netbox/utilities/logging.py +93 -0
  54. ipfabric_netbox/utilities/nbutils.py +105 -0
  55. ipfabric_netbox/utilities/transform_map.py +35 -0
  56. ipfabric_netbox/views.py +845 -0
  57. ipfabric_netbox-3.1.2.dist-info/METADATA +88 -0
  58. ipfabric_netbox-3.1.2.dist-info/RECORD +59 -0
  59. ipfabric_netbox-3.1.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,62 @@
1
+ from netbox.plugins import PluginMenu
2
+ from netbox.plugins import PluginMenuButton
3
+ from netbox.plugins import PluginMenuItem
4
+
5
+
6
+ sync_buttons = [
7
+ PluginMenuButton(
8
+ link="plugins:ipfabric_netbox:ipfabricsync_add",
9
+ title="Add",
10
+ icon_class="mdi mdi-plus-thick",
11
+ permissions=["ipfabric_netbox.add_ipfabricsync"],
12
+ )
13
+ ]
14
+
15
+ source_buttons = [
16
+ PluginMenuButton(
17
+ link="plugins:ipfabric_netbox:ipfabricsource_add",
18
+ title="Add",
19
+ icon_class="mdi mdi-plus-thick",
20
+ permissions=["ipfabric_netbox.add_ipfabricsource"],
21
+ )
22
+ ]
23
+
24
+ source = PluginMenuItem(
25
+ link="plugins:ipfabric_netbox:ipfabricsource_list",
26
+ link_text="Sources",
27
+ buttons=source_buttons,
28
+ permissions=["ipfabric_netbox.view_ipfabricsource"],
29
+ )
30
+
31
+ snapshot = PluginMenuItem(
32
+ link="plugins:ipfabric_netbox:ipfabricsnapshot_list",
33
+ link_text="Snapshots",
34
+ permissions=["ipfabric_netbox.view_ipfabricsnapshot"],
35
+ )
36
+
37
+
38
+ ingestion = PluginMenuItem(
39
+ link="plugins:ipfabric_netbox:ipfabricsync_list",
40
+ link_text="Ingestion",
41
+ buttons=sync_buttons,
42
+ permissions=["ipfabric_netbox.view_ipfabricsync"],
43
+ )
44
+
45
+ tm = PluginMenuItem(
46
+ link="plugins:ipfabric_netbox:ipfabrictransformmap_list",
47
+ link_text="Transform Maps",
48
+ permissions=["ipfabric_netbox.view_ipfabrictransformmap"],
49
+ buttons=[
50
+ PluginMenuButton(
51
+ link="plugins:ipfabric_netbox:ipfabrictransformmap_add",
52
+ title="Add",
53
+ icon_class="mdi mdi-plus-thick",
54
+ permissions=["ipfabric_netbox.add_ipfabrictransformmap"],
55
+ )
56
+ ],
57
+ )
58
+ menu = PluginMenu(
59
+ label="IP Fabric",
60
+ icon_class="mdi mdi-cloud-sync",
61
+ groups=(("IP Fabric", (source, snapshot, ingestion, tm)),),
62
+ )
@@ -0,0 +1,68 @@
1
+ from typing import Optional
2
+
3
+ from core.models import ObjectType
4
+ from dcim.models import Device
5
+ from dcim.models import Site
6
+ from extras.choices import CustomFieldTypeChoices
7
+ from extras.choices import CustomFieldUIEditableChoices
8
+ from extras.choices import CustomFieldUIVisibleChoices
9
+ from extras.choices import CustomLinkButtonClassChoices
10
+ from extras.models import CustomField
11
+ from extras.models import CustomLink
12
+
13
+ from .models import IPFabricBranch
14
+ from .models import IPFabricSource
15
+
16
+
17
+ def create_custom_field(
18
+ field_name: str,
19
+ label: str,
20
+ models: list,
21
+ object_type=None,
22
+ cf_type: Optional[str] = "type_text",
23
+ ui_visibility: Optional[str] = "VISIBILITY_READ_ONLY",
24
+ ):
25
+ defaults = {
26
+ "label": label,
27
+ "related_object_type": ObjectType.objects.get_for_model(object_type)
28
+ if object_type
29
+ else None,
30
+ "ui_visible": getattr(CustomFieldUIVisibleChoices, "ALWAYS"),
31
+ "ui_editable": getattr(CustomFieldUIEditableChoices, "NO"),
32
+ }
33
+
34
+ custom_field, _ = CustomField.objects.update_or_create(
35
+ type=getattr(CustomFieldTypeChoices, cf_type.upper()),
36
+ name=field_name,
37
+ defaults=defaults,
38
+ )
39
+
40
+ for model in models:
41
+ custom_field.object_types.add(ObjectType.objects.get_for_model(model))
42
+
43
+
44
+ def ipfabric_netbox_init():
45
+ create_custom_field(
46
+ "ipfabric_source",
47
+ "IP Fabric Source",
48
+ [Device, Site],
49
+ cf_type="type_object",
50
+ object_type=IPFabricSource,
51
+ )
52
+ create_custom_field(
53
+ "ipfabric_branch",
54
+ "IP Fabric Last Sync",
55
+ [Device, Site],
56
+ cf_type="type_object",
57
+ object_type=IPFabricBranch,
58
+ )
59
+ cl, _ = CustomLink.objects.update_or_create(
60
+ defaults={
61
+ "link_text": "{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}IP Fabric{% endif %}{% endif %}",
62
+ "link_url": '{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}{% set BASE_URL = object.custom_fields.filter(related_object_type__model="ipfabricsource").first().related_object_type.model_class().objects.get(pk=SOURCE_ID).url %}{{ BASE_URL }}/inventory/devices?options={"filters":{"sn": ["like","{{ object.serial }}"]}}{% endif %}{%endif%}',
63
+ "new_window": True,
64
+ "button_class": CustomLinkButtonClassChoices.BLUE,
65
+ },
66
+ name="ipfabric",
67
+ )
68
+ cl.object_types.add(ObjectType.objects.get_for_model(Device))
@@ -0,0 +1,208 @@
1
+ import django_tables2 as tables
2
+ from django.utils.translation import gettext_lazy as _
3
+ from django_tables2 import Column
4
+ from extras.models import StagedChange
5
+ from netbox.tables import columns
6
+ from netbox.tables import NetBoxTable
7
+
8
+ from .models import IPFabricBranch
9
+ from .models import IPFabricData
10
+ from .models import IPFabricRelationshipField
11
+ from .models import IPFabricSnapshot
12
+ from .models import IPFabricSource
13
+ from .models import IPFabricSync
14
+ from .models import IPFabricTransformField
15
+ from .models import IPFabricTransformMap
16
+
17
+
18
+ DIFF_BUTTON = """
19
+ <a href="#"
20
+ hx-get="{% url 'plugins:ipfabric_netbox:ipfabricbranch_change_diff' pk=record.branch.pk change_pk=record.pk %}"
21
+ hx-target="#htmx-modal-content"
22
+ data-bs-toggle="modal"
23
+ data-bs-target="#htmx-modal"
24
+ class="btn btn-success btn-sm"
25
+ >
26
+ <i class="mdi mdi-code-tags">Diff</i>
27
+ </a>
28
+ """
29
+
30
+ DATA_BUTTON = """
31
+ <a href="#"
32
+ hx-get="{% url 'plugins:ipfabric_netbox:ipfabricdata_data' pk=record.pk %}"
33
+ hx-target="#htmx-modal-content"
34
+ data-bs-toggle="modal"
35
+ data-bs-target="#htmx-modal"
36
+ class="btn btn-success btn-sm"
37
+ >
38
+ <i class="mdi mdi-code-tags">JSON</i>
39
+ </a>
40
+ """
41
+
42
+
43
+ class IPFabricRelationshipFieldTable(NetBoxTable):
44
+ actions = columns.ActionsColumn(actions=("edit", "delete"))
45
+
46
+ class Meta(NetBoxTable.Meta):
47
+ model = IPFabricRelationshipField
48
+ fields = ("source_model", "target_field", "coalesce", "actions")
49
+ default_columns = ("source_model", "target_field", "coalesce", "actions")
50
+
51
+
52
+ class IPFabricTransformFieldTable(NetBoxTable):
53
+ id = tables.Column()
54
+ actions = columns.ActionsColumn(actions=("edit", "delete"))
55
+
56
+ class Meta(NetBoxTable.Meta):
57
+ model = IPFabricTransformField
58
+ fields = ("id", "source_field", "target_field", "coalesce", "actions")
59
+ default_columns = ("source_field", "target_field", "coalesce", "actions")
60
+
61
+
62
+ class IPFabricTransformMapTable(NetBoxTable):
63
+ name = tables.Column(linkify=True)
64
+
65
+ class Meta(NetBoxTable.Meta):
66
+ model = IPFabricTransformMap
67
+ fields = ("name", "source_model", "target_model", "status")
68
+ default_columns = ("name", "source_model", "target_model", "status")
69
+
70
+
71
+ class BranchTable(NetBoxTable):
72
+ name = tables.Column(linkify=True)
73
+ sync = tables.Column(verbose_name="IP Fabric Sync", linkify=True)
74
+ changes = tables.Column(accessor="staged_changes", verbose_name="Number of Changes")
75
+ actions = columns.ActionsColumn(actions=("delete",))
76
+
77
+ def render_changes(self, value):
78
+ return value.count()
79
+
80
+ class Meta(NetBoxTable.Meta):
81
+ model = IPFabricBranch
82
+ fields = ("name", "sync", "description", "user", "changes")
83
+ default_columns = ("name", "sync", "description", "user", "changes")
84
+
85
+
86
+ class IPFabricSnapshotTable(NetBoxTable):
87
+ name = tables.Column(linkify=True)
88
+ source = tables.Column(linkify=True)
89
+ tags = columns.TagColumn(url_name="core:datasource_list")
90
+ actions = columns.ActionsColumn(actions=("delete",))
91
+ status = columns.ChoiceFieldColumn()
92
+
93
+ class Meta(NetBoxTable.Meta):
94
+ model = IPFabricSnapshot
95
+ fields = (
96
+ "pk",
97
+ "id",
98
+ "name",
99
+ "snapshot_id",
100
+ "status",
101
+ "date",
102
+ "created",
103
+ "last_updated",
104
+ )
105
+ default_columns = ("pk", "name", "source", "snapshot_id", "status", "date")
106
+
107
+
108
+ class IPFabricSourceTable(NetBoxTable):
109
+ name = tables.Column(linkify=True)
110
+ status = columns.ChoiceFieldColumn()
111
+ snapshot_count = tables.Column(verbose_name="Snapshots")
112
+ tags = columns.TagColumn(url_name="core:datasource_list")
113
+
114
+ class Meta(NetBoxTable.Meta):
115
+ model = IPFabricSource
116
+ fields = (
117
+ "pk",
118
+ "id",
119
+ "name",
120
+ "status",
121
+ "description",
122
+ "comments",
123
+ "created",
124
+ "last_updated",
125
+ )
126
+ default_columns = ("pk", "name", "status", "description", "snapshot_count")
127
+
128
+
129
+ class SyncTable(NetBoxTable):
130
+ actions = None
131
+ status = columns.ChoiceFieldColumn()
132
+ snapshot_name = tables.Column(
133
+ verbose_name="Snapshot Name", accessor="snapshot_data"
134
+ )
135
+
136
+ def render_snapshot_name(self, value):
137
+ return value.get("name", "---")
138
+
139
+ class Meta(NetBoxTable.Meta):
140
+ model = IPFabricSync
141
+ fields = ("id", "status", "snapshot_name")
142
+ default_columns = ("id", "status", "snapshot_name")
143
+
144
+
145
+ class StagedChangesTable(NetBoxTable):
146
+ pk = None
147
+ object_type = tables.Column(
148
+ accessor="object_type.model", verbose_name="Object Type"
149
+ )
150
+ name = tables.Column(accessor="object_type.name", verbose_name="Name")
151
+ actions = columns.TemplateColumn(template_code=DIFF_BUTTON)
152
+
153
+ def render_name(self, value, record):
154
+ models_with_names = [
155
+ "site",
156
+ "manufacturer",
157
+ "device role",
158
+ "device",
159
+ "interface",
160
+ "platform",
161
+ "inventory item",
162
+ "VLAN",
163
+ ]
164
+ if record.data:
165
+ if value in models_with_names:
166
+ name = record.data["name"]
167
+ elif value == "device type":
168
+ name = record.data["model"]
169
+ elif value == "IP address":
170
+ name = record.data["address"]
171
+ elif value == "tagged item":
172
+ name = f"Tagging object ({record.data['object_id']})"
173
+ elif value == "prefix":
174
+ name = f"{record.data['prefix']} ({record.data['vrf']})"
175
+ else:
176
+ name = record.data
177
+ else:
178
+ name = record.object.__str__()
179
+ return name
180
+
181
+ class Meta(NetBoxTable.Meta):
182
+ model = StagedChange
183
+ name = "staged_changes"
184
+ fields = ("name", "action", "object_type", "actions")
185
+ default_columns = ("name", "action", "object_type", "actions")
186
+
187
+
188
+ class DeviceIPFTable(tables.Table):
189
+ hostname = Column()
190
+
191
+ class Meta:
192
+ attrs = {
193
+ "class": "table table-hover object-list",
194
+ }
195
+ empty_text = _("No results found")
196
+
197
+ def __init__(self, data, **kwargs):
198
+ super().__init__(data, **kwargs)
199
+
200
+
201
+ class IPFabricDataTable(NetBoxTable):
202
+ JSON = columns.TemplateColumn(template_code=DATA_BUTTON)
203
+ actions = columns.ActionsColumn(actions=("delete",))
204
+
205
+ class Meta(NetBoxTable.Meta):
206
+ model = IPFabricData
207
+ fields = ("snapshot_data", "type", "JSON")
208
+ default_columns = ("snapshot_data", "type", "JSON")
@@ -0,0 +1,13 @@
1
+ from netbox.plugins import PluginTemplateExtension
2
+
3
+
4
+ class SiteTopologyButtons(PluginTemplateExtension):
5
+ model = "dcim.site"
6
+
7
+ def buttons(self):
8
+ return self.render(
9
+ "ipfabric_netbox/inc/site_topology_button.html", extra_context={}
10
+ )
11
+
12
+
13
+ template_extensions = [SiteTopologyButtons]
@@ -0,0 +1,72 @@
1
+ <div class="modal-content" id="htmx-modal-content">
2
+ <div class="modal-header">
3
+ <h5 class="modal-title">Change Diff {{ change }}</h5>
4
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
5
+ </div>
6
+ <div class="modal-body">
7
+ <div class="row mb-3">
8
+ <div class="col-6">
9
+ <div class="card">
10
+ <h5 class="card-header">Pre-Change Data</h5>
11
+ <div class="card-body">
12
+ {% if prechange_data %}
13
+ <pre class="change-data">{% for k, v in prechange_data.items %}{% spaceless %}
14
+ <span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
15
+ {% endspaceless %}{% endfor %}
16
+ </pre>
17
+ {% else %}
18
+ <span class="text-muted">None</span>
19
+ {% endif %}
20
+ </div>
21
+ </div>
22
+ </div>
23
+ <div class="col-6">
24
+ <div class="card">
25
+ <h5 class="card-header">Post-Change Data</h5>
26
+ <div class="card-body">
27
+ {% if postchange_data %}
28
+ <pre class="change-data">{% for k, v in postchange_data.items %}{% spaceless %}
29
+ <span{% if k in diff_removed %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
30
+ {% endspaceless %}{% endfor %}{% endif %}
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="row">
36
+ <div class="col">
37
+ <div class="card">
38
+ <div class="card-header">
39
+ <h5 class="d-inline">Difference</h5>
40
+ <!-- <div class="btn-group btn-group-sm float-end noprint">
41
+ <a {% if prev_change %}href="{% url 'extras:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
42
+ <i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
43
+ </a>
44
+ <a {% if next_change %}href="{% url 'extras:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
45
+ Next <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
46
+ </a>
47
+ </div> -->
48
+ </div>
49
+ <div class="card-body">
50
+ {% if diff_added == diff_removed %}
51
+ <span class="text-muted" style="margin-left: 10px;">
52
+ {% if object.action == 'create' %}
53
+ Object Created
54
+ {% elif object.action == 'delete' %}
55
+ Object Deleted
56
+ {% else %}
57
+ No Changes
58
+ {% endif %}
59
+ </span>
60
+ {% else %}
61
+ <pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
62
+ <pre class="change-diff change-added">{{ diff_added|json }}</pre>
63
+ {% endif %}
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="modal-footer">
71
+ </div>
72
+ </div>
@@ -0,0 +1,20 @@
1
+ <div class="modal-header">
2
+ <h5 class="modal-title">Raw Data {{ object.pk }}</h5>
3
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
4
+ </div>
5
+ <div class="modal-body">
6
+ <div class="row mb-3">
7
+ <div class="col-12">
8
+ <div class="card">
9
+ <h5 class="card-header">JSON Output</h5>
10
+ <div class="card-body">
11
+ <pre>{{ object.data | json }}</pre>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="modal-footer">
19
+ </div>
20
+ </div>
@@ -0,0 +1,6 @@
1
+ {# Indicates that a job result is still pending; used for HTMX requests #}
2
+ <div class="spinner-border float-start me-1" id="spinner" role="status">
3
+ <span class="visually-hidden">Loading...</span>
4
+ </div>
5
+ <h3>Logs pending...</h3>
6
+ <small class="text-muted">Last updated {% now "H:i:s" %}</small>
@@ -0,0 +1,22 @@
1
+ {% load form_helpers %}
2
+
3
+ <form action="{{ form_url }}" method="post">
4
+ {% csrf_token %}
5
+
6
+ <div class="modal-header">
7
+ <h5 class="modal-title">Confirm Merge</h5>
8
+ </div>
9
+ <div class="modal-body">
10
+ <p>Are you sure you want to <strong class="text-danger">merge</strong> {{ object_type }} <strong>{{ object }}</strong>?</p>
11
+ {% render_form form %}
12
+ </div>
13
+ <div class="modal-footer">
14
+ {% if return_url %}
15
+ <a href="{{ return_url }}" class="btn btn-outline-secondary">Cancel</a>
16
+ {% else %}
17
+ <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
18
+ {% endif %}
19
+ <button type="submit" class="btn btn-danger">Merge</button>
20
+ </div>
21
+ </div>
22
+ </form>
@@ -0,0 +1,70 @@
1
+ {% if object.custom_field_data.ipfabric_source %}
2
+ <div class="btn-group" role="group" aria-label="Button group with nested dropdown">
3
+ <div class="btn-group" role="group">
4
+ <button id="btnGroupDrop1" type="button" class="btn btn-sm btn-success dropdown-toggle" data-bs-toggle="dropdown"
5
+ aria-expanded="false">
6
+ IP Fabric Topology
7
+ </button>
8
+ <ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
9
+ <li><a href="#"
10
+ hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=object.pk site=object.pk %}?snapshot=$last&source={{ object.custom_field_data.ipfabric_source }}"
11
+ hx-target="#topology-modal-content" data-bs-toggle="modal" data-bs-target="#topology-modal"
12
+ class="dropdown-item">
13
+ Last Snapshot
14
+ </a>
15
+ </li>
16
+ <li><a href="#"
17
+ hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=object.pk site=object.pk %}?snapshot=$prev&source={{ object.custom_field_data.ipfabric_source }}"
18
+ hx-target="#topology-modal-content" data-bs-toggle="modal" data-bs-target="#topology-modal"
19
+ class="dropdown-item">
20
+ Previous Snapshot
21
+ </a></li>
22
+ </ul>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="modal fade" id="topology-modal" tabindex="-1" aria-hidden="true">
27
+ <div class="modal-dialog modal-xl">
28
+ <div class="modal-content">
29
+ <div class="modal-header">
30
+ <h5 class="modal-title">{{ object.name }} Topology</h5>
31
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
32
+ </div>
33
+ <div id="topology-modal-content">
34
+ <div class="modal-body">
35
+ <div class="d-flex align-items-center">
36
+ <strong role="status">Loading...</strong>
37
+ <div class="spinner-border ms-auto" aria-hidden="true"></div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ {% endif %}
45
+
46
+ {% block javascript %}
47
+ <script>
48
+ document.addEventListener('DOMContentLoaded', function () {
49
+ var myModal = document.getElementById('topology-modal');
50
+ var modalBody = document.getElementById('topology-modal-content');
51
+ console.log(myModal);
52
+ myModal.addEventListener('hidden.bs.modal', function () {
53
+ // Clear modal content on close
54
+ console.log("Modal close");
55
+ modalBody.innerHTML = createLoadingContent();
56
+ });
57
+ });
58
+
59
+ function createLoadingContent() {
60
+ return `
61
+ <div class="modal-body">
62
+ <div class="d-flex align-items-center">
63
+ <strong role="status">Loading...</strong>
64
+ <div class="spinner-border ms-auto" aria-hidden="true"></div>
65
+ </div>
66
+ </div>
67
+ `;
68
+ }
69
+ </script>
70
+ {% endblock %}
@@ -0,0 +1,61 @@
1
+ {% load helpers %}
2
+
3
+ <div class="modal-body">
4
+ <div class="d-flex flex-row m-2"">
5
+ {% if svg %}
6
+ <div class=" d-flex flex-row flex-grow-1 align-items-center">
7
+ {{svg|safe}}
8
+ </div>
9
+ <div>
10
+ <div class="position-absolute top-0 end-0 m-2 border rounded p-2">
11
+ <ul class="list-unstyled">
12
+ <li><span>Fetched: {{ time|isodatetime }}</span></li>
13
+ <li>Snapshot Name: {{ snapshot.name }}</li>
14
+ <li>Snapshot ID: {{ snapshot.snapshot_id }}</li>
15
+ <li>Snapshot Date: {{ snapshot.end|isodatetime }}</li>
16
+ </ul>
17
+ </div>
18
+ </div>
19
+ {% else %}
20
+ <div class="d-flex flex-row flex-grow-1 align-items-center">
21
+ {{ error }}
22
+ </div>
23
+ {% if snapshot %}
24
+ <div>
25
+ <div class="border rounded p-2">
26
+ <ul class="list-unstyled">
27
+ <li><span>Fetched: {{ time|isodatetime }}</span></li>
28
+ <li>Snapshot Name: {{ snapshot.name }}</li>
29
+ <li>Snapshot ID: {{ snapshot.snapshot_id }}</li>
30
+ <li>Snapshot Date: {{ snapshot.end|isodatetime }}</li>
31
+ </ul>
32
+ </div>
33
+ </div>
34
+ {% endif %}
35
+ {% endif %}
36
+ </div>
37
+ </div>
38
+ {% if link %}
39
+ <div class="modal-footer">
40
+ <a href="#" hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=source.pk site=site.pk %}?snapshot={{ snapshot.snapshot_id }}"
41
+ hx-target="#topology-modal-content" class="btn btn-success" hx-indicator=".my-indicator"><span
42
+ class="spinner-border spinner-border-sm loader my-indicator" role="status" aria-hidden="true"></span>
43
+ Refresh</a>
44
+ <a href="{{ link }}" class="btn btn-primary" target="_blank">Open in IP Fabric</a>
45
+ </div>
46
+ {% endif %}
47
+
48
+ <style type="text/css">
49
+ .my-indicator {
50
+ display: none !important;
51
+ ;
52
+ }
53
+
54
+ .htmx-request .my-indicator {
55
+ display: inline-block !important;
56
+ }
57
+
58
+ .htmx-request.my-indicator {
59
+ display: inline-block !important;
60
+ }
61
+ </style>
@@ -0,0 +1,60 @@
1
+ {% extends base_template %}
2
+ {% load helpers %}
3
+ {% load i18n %}
4
+
5
+ {% block content %}
6
+ {% block table_controls %}
7
+ {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
8
+ {% endblock table_controls %}
9
+ <form method="post">
10
+ {% csrf_token %}
11
+ <div class="card">
12
+ <div class="card-body htmx-container table-responsive" id="object_list">
13
+ {% include 'htmx/table.html' %}
14
+ </div>
15
+ </div>
16
+ <div class="noprint bulk-buttons">
17
+ {% block bulk_controls %}
18
+ <div class="bulk-button-group">
19
+ <div class="btn-group" role="group">
20
+ {# Bulk edit buttons #}
21
+ {% block bulk_edit_controls %}
22
+ {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
23
+ {% if 'bulk_edit' in actions and bulk_edit_view %}
24
+ <button type="submit" name="_edit"
25
+ formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
26
+ class="btn btn-warning btn-sm">
27
+ <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
28
+ </button>
29
+ {% endif %}
30
+ {% endwith %}
31
+ {% endblock bulk_edit_controls %}
32
+ </div>
33
+ <div class="btn-group" role="group">
34
+ {# Bulk delete buttons #}
35
+ {% block bulk_delete_controls %}
36
+ {% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
37
+ {% if 'bulk_delete' in actions and bulk_delete_view %}
38
+ <button type="submit"
39
+ formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
40
+ class="btn btn-danger btn-sm">
41
+ <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
42
+ </button>
43
+ {% endif %}
44
+ {% endwith %}
45
+ {% endblock bulk_delete_controls %}
46
+ </div>
47
+ </div>
48
+ <div class="bulk-button-group">
49
+ {# Other bulk action buttons #}
50
+ {% block bulk_extra_controls %}{% endblock %}
51
+ </div>
52
+ {% endblock bulk_controls %}
53
+ </div>
54
+ </form>
55
+ {% endblock content %}
56
+
57
+ {% block modals %}
58
+ {{ block.super }}
59
+ {% table_config_form table %}
60
+ {% endblock modals %}