netbox-atlassian 0.2.1__py3-none-any.whl → 0.2.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.
@@ -7,7 +7,7 @@ Searches by configurable fields (hostname, serial, role, etc.) with OR logic.
7
7
 
8
8
  from netbox.plugins import PluginConfig
9
9
 
10
- __version__ = "0.2.1"
10
+ __version__ = "0.2.2"
11
11
 
12
12
 
13
13
  class AtlassianConfig(PluginConfig):
@@ -2,177 +2,19 @@
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content %}
5
- <div class="row">
6
- <div class="col-12">
7
- <!-- Search Terms Info -->
8
- <div class="alert alert-info mb-3">
9
- <i class="mdi mdi-magnify"></i>
10
- <strong>Searching for:</strong>
11
- {% for term in search_terms %}
12
- <span class="badge bg-info text-white ms-1">{{ term }}</span>{% if not forloop.last %}<span class="text-muted ms-1">OR</span>{% endif %}
13
- {% empty %}
14
- <span class="text-muted">No search terms available</span>
15
- {% endfor %}
16
- <br>
17
- <small class="text-muted">
18
- Fields: {% for field in enabled_fields %}{{ field.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
19
- </small>
20
- </div>
21
-
22
- <div class="row">
23
- <!-- Jira Issues Column -->
24
- <div class="col-md-6 mb-4">
25
- <div class="card h-100">
26
- <div class="card-header d-flex justify-content-between align-items-center">
27
- <h5 class="card-title mb-0">
28
- <i class="mdi mdi-jira text-primary"></i> Jira Issues
29
- {% if jira_results.total > 0 %}
30
- <span class="badge bg-info text-white">{{ jira_results.total }} issue{{ jira_results.total|pluralize }}</span>
31
- {% endif %}
32
- </h5>
33
- {% if jira_url %}
34
- <a href="{{ jira_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
35
- <i class="mdi mdi-open-in-new"></i> Open Jira
36
- </a>
37
- {% endif %}
38
- </div>
39
- <div class="card-body">
40
- {% if not jira_configured %}
41
- <div class="alert alert-warning mb-0">
42
- <i class="mdi mdi-alert"></i> Jira not configured
43
- </div>
44
- {% elif jira_results.error %}
45
- <div class="alert alert-danger mb-0">
46
- <i class="mdi mdi-alert-circle"></i> {{ jira_results.error }}
47
- </div>
48
- {% elif jira_results.issues %}
49
- <div class="table-responsive">
50
- <table class="table table-sm table-hover mb-0">
51
- <thead>
52
- <tr>
53
- <th>Key</th>
54
- <th>Summary</th>
55
- <th>Status</th>
56
- <th>Type</th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- {% for issue in jira_results.issues %}
61
- <tr>
62
- <td>
63
- <a href="{{ issue.url }}" target="_blank" class="fw-bold">
64
- {{ issue.key }}
65
- </a>
66
- </td>
67
- <td>
68
- <span title="{{ issue.summary }}">
69
- {{ issue.summary|truncatechars:50 }}
70
- </span>
71
- </td>
72
- <td>
73
- {% if issue.status_category == 'done' %}
74
- <span class="badge bg-success text-white">{{ issue.status }}</span>
75
- {% elif issue.status_category == 'indeterminate' %}
76
- <span class="badge bg-primary text-white">{{ issue.status }}</span>
77
- {% else %}
78
- <span class="badge bg-secondary text-white">{{ issue.status }}</span>
79
- {% endif %}
80
- </td>
81
- <td>
82
- {% if issue.type_icon %}
83
- <img src="{{ issue.type_icon }}" alt="{{ issue.type }}" width="16" height="16" class="me-1">
84
- {% endif %}
85
- {{ issue.type }}
86
- </td>
87
- </tr>
88
- {% endfor %}
89
- </tbody>
90
- </table>
91
- </div>
92
- {% if jira_results.total > jira_results.issues|length %}
93
- <div class="text-muted small mt-2">
94
- Showing {{ jira_results.issues|length }} of {{ jira_results.total }} results
95
- </div>
96
- {% endif %}
97
- {% else %}
98
- <div class="text-muted text-center py-4">
99
- <i class="mdi mdi-information-outline fs-1"></i>
100
- <p class="mb-0">No Jira issues found</p>
101
- </div>
102
- {% endif %}
103
-
104
- {% if jira_results.cached %}
105
- <div class="text-muted small mt-2">
106
- <i class="mdi mdi-cached"></i> Results from cache
107
- </div>
108
- {% endif %}
109
- </div>
110
- </div>
111
- </div>
112
-
113
- <!-- Confluence Pages Column -->
114
- <div class="col-md-6 mb-4">
115
- <div class="card h-100">
116
- <div class="card-header d-flex justify-content-between align-items-center">
117
- <h5 class="card-title mb-0">
118
- <i class="mdi mdi-file-document text-info"></i> Confluence Pages
119
- {% if confluence_results.total > 0 %}
120
- <span class="badge bg-info text-white">{{ confluence_results.total }} page{{ confluence_results.total|pluralize }}</span>
121
- {% endif %}
122
- </h5>
123
- {% if confluence_url %}
124
- <a href="{{ confluence_url }}" target="_blank" class="btn btn-sm btn-outline-info">
125
- <i class="mdi mdi-open-in-new"></i> Open Confluence
126
- </a>
127
- {% endif %}
128
- </div>
129
- <div class="card-body">
130
- {% if not confluence_configured %}
131
- <div class="alert alert-warning mb-0">
132
- <i class="mdi mdi-alert"></i> Confluence not configured
133
- </div>
134
- {% elif confluence_results.error %}
135
- <div class="alert alert-danger mb-0">
136
- <i class="mdi mdi-alert-circle"></i> {{ confluence_results.error }}
137
- </div>
138
- {% elif confluence_results.pages %}
139
- <div class="list-group list-group-flush">
140
- {% for page in confluence_results.pages %}
141
- <a href="{{ page.url }}" target="_blank" class="list-group-item list-group-item-action">
142
- <div class="d-flex w-100 justify-content-between">
143
- <h6 class="mb-1">{{ page.title }}</h6>
144
- <small class="text-muted">{{ page.space_key }}</small>
145
- </div>
146
- {% if page.breadcrumb %}
147
- <small class="text-muted">{{ page.breadcrumb }}</small>
148
- {% endif %}
149
- <small class="text-muted d-block">
150
- Last modified by {{ page.last_modified_by }}
151
- </small>
152
- </a>
153
- {% endfor %}
154
- </div>
155
- {% if confluence_results.total > confluence_results.pages|length %}
156
- <div class="text-muted small mt-2">
157
- Showing {{ confluence_results.pages|length }} of {{ confluence_results.total }} results
158
- </div>
159
- {% endif %}
160
- {% else %}
161
- <div class="text-muted text-center py-4">
162
- <i class="mdi mdi-information-outline fs-1"></i>
163
- <p class="mb-0">No Confluence pages found</p>
164
- </div>
165
- {% endif %}
166
-
167
- {% if confluence_results.cached %}
168
- <div class="text-muted small mt-2">
169
- <i class="mdi mdi-cached"></i> Results from cache
170
- </div>
171
- {% endif %}
172
- </div>
173
- </div>
5
+ {% if loading %}
6
+ <div id="atlassian-content"
7
+ hx-get="{% url 'plugins:netbox_atlassian:device_content' pk=object.pk %}"
8
+ hx-trigger="load"
9
+ hx-swap="innerHTML">
10
+ <div class="d-flex justify-content-center align-items-center py-5">
11
+ <div class="text-center">
12
+ <div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
13
+ <span class="visually-hidden">Loading...</span>
174
14
  </div>
15
+ <p class="text-muted mb-0">Loading Atlassian data...</p>
175
16
  </div>
176
17
  </div>
177
18
  </div>
19
+ {% endif %}
178
20
  {% endblock %}
@@ -0,0 +1,175 @@
1
+ {% load helpers %}
2
+
3
+ <div class="row">
4
+ <div class="col-12">
5
+ <!-- Search Terms Info -->
6
+ <div class="alert alert-info mb-3">
7
+ <i class="mdi mdi-magnify"></i>
8
+ <strong>Searching for:</strong>
9
+ {% for term in search_terms %}
10
+ <span class="badge bg-info text-white ms-1">{{ term }}</span>{% if not forloop.last %}<span class="text-muted ms-1">OR</span>{% endif %}
11
+ {% empty %}
12
+ <span class="text-muted">No search terms available</span>
13
+ {% endfor %}
14
+ <br>
15
+ <small class="text-muted">
16
+ Fields: {% for field in enabled_fields %}{{ field.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
17
+ </small>
18
+ </div>
19
+
20
+ <div class="row">
21
+ <!-- Jira Issues Column -->
22
+ <div class="col-md-6 mb-4">
23
+ <div class="card h-100">
24
+ <div class="card-header d-flex justify-content-between align-items-center">
25
+ <h5 class="card-title mb-0">
26
+ <i class="mdi mdi-jira text-primary"></i> Jira Issues
27
+ {% if jira_results.total > 0 %}
28
+ <span class="badge bg-info text-white">{{ jira_results.total }} issue{{ jira_results.total|pluralize }}</span>
29
+ {% endif %}
30
+ </h5>
31
+ {% if jira_url %}
32
+ <a href="{{ jira_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
33
+ <i class="mdi mdi-open-in-new"></i> Open Jira
34
+ </a>
35
+ {% endif %}
36
+ </div>
37
+ <div class="card-body">
38
+ {% if not jira_configured %}
39
+ <div class="alert alert-warning mb-0">
40
+ <i class="mdi mdi-alert"></i> Jira not configured
41
+ </div>
42
+ {% elif jira_results.error %}
43
+ <div class="alert alert-danger mb-0">
44
+ <i class="mdi mdi-alert-circle"></i> {{ jira_results.error }}
45
+ </div>
46
+ {% elif jira_results.issues %}
47
+ <div class="table-responsive">
48
+ <table class="table table-sm table-hover mb-0">
49
+ <thead>
50
+ <tr>
51
+ <th>Key</th>
52
+ <th>Summary</th>
53
+ <th>Status</th>
54
+ <th>Type</th>
55
+ </tr>
56
+ </thead>
57
+ <tbody>
58
+ {% for issue in jira_results.issues %}
59
+ <tr>
60
+ <td>
61
+ <a href="{{ issue.url }}" target="_blank" class="fw-bold">
62
+ {{ issue.key }}
63
+ </a>
64
+ </td>
65
+ <td>
66
+ <span title="{{ issue.summary }}">
67
+ {{ issue.summary|truncatechars:50 }}
68
+ </span>
69
+ </td>
70
+ <td>
71
+ {% if issue.status_category == 'done' %}
72
+ <span class="badge bg-success text-white">{{ issue.status }}</span>
73
+ {% elif issue.status_category == 'indeterminate' %}
74
+ <span class="badge bg-primary text-white">{{ issue.status }}</span>
75
+ {% else %}
76
+ <span class="badge bg-secondary text-white">{{ issue.status }}</span>
77
+ {% endif %}
78
+ </td>
79
+ <td>
80
+ {% if issue.type_icon %}
81
+ <img src="{{ issue.type_icon }}" alt="{{ issue.type }}" width="16" height="16" class="me-1">
82
+ {% endif %}
83
+ {{ issue.type }}
84
+ </td>
85
+ </tr>
86
+ {% endfor %}
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ {% if jira_results.total > jira_results.issues|length %}
91
+ <div class="text-muted small mt-2">
92
+ Showing {{ jira_results.issues|length }} of {{ jira_results.total }} results
93
+ </div>
94
+ {% endif %}
95
+ {% else %}
96
+ <div class="text-muted text-center py-4">
97
+ <i class="mdi mdi-information-outline fs-1"></i>
98
+ <p class="mb-0">No Jira issues found</p>
99
+ </div>
100
+ {% endif %}
101
+
102
+ {% if jira_results.cached %}
103
+ <div class="text-muted small mt-2">
104
+ <i class="mdi mdi-cached"></i> Results from cache
105
+ </div>
106
+ {% endif %}
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Confluence Pages Column -->
112
+ <div class="col-md-6 mb-4">
113
+ <div class="card h-100">
114
+ <div class="card-header d-flex justify-content-between align-items-center">
115
+ <h5 class="card-title mb-0">
116
+ <i class="mdi mdi-file-document text-info"></i> Confluence Pages
117
+ {% if confluence_results.total > 0 %}
118
+ <span class="badge bg-info text-white">{{ confluence_results.total }} page{{ confluence_results.total|pluralize }}</span>
119
+ {% endif %}
120
+ </h5>
121
+ {% if confluence_url %}
122
+ <a href="{{ confluence_url }}" target="_blank" class="btn btn-sm btn-outline-info">
123
+ <i class="mdi mdi-open-in-new"></i> Open Confluence
124
+ </a>
125
+ {% endif %}
126
+ </div>
127
+ <div class="card-body">
128
+ {% if not confluence_configured %}
129
+ <div class="alert alert-warning mb-0">
130
+ <i class="mdi mdi-alert"></i> Confluence not configured
131
+ </div>
132
+ {% elif confluence_results.error %}
133
+ <div class="alert alert-danger mb-0">
134
+ <i class="mdi mdi-alert-circle"></i> {{ confluence_results.error }}
135
+ </div>
136
+ {% elif confluence_results.pages %}
137
+ <div class="list-group list-group-flush">
138
+ {% for page in confluence_results.pages %}
139
+ <a href="{{ page.url }}" target="_blank" class="list-group-item list-group-item-action">
140
+ <div class="d-flex w-100 justify-content-between">
141
+ <h6 class="mb-1">{{ page.title }}</h6>
142
+ <small class="text-muted">{{ page.space_key }}</small>
143
+ </div>
144
+ {% if page.breadcrumb %}
145
+ <small class="text-muted">{{ page.breadcrumb }}</small>
146
+ {% endif %}
147
+ <small class="text-muted d-block">
148
+ Last modified by {{ page.last_modified_by }}
149
+ </small>
150
+ </a>
151
+ {% endfor %}
152
+ </div>
153
+ {% if confluence_results.total > confluence_results.pages|length %}
154
+ <div class="text-muted small mt-2">
155
+ Showing {{ confluence_results.pages|length }} of {{ confluence_results.total }} results
156
+ </div>
157
+ {% endif %}
158
+ {% else %}
159
+ <div class="text-muted text-center py-4">
160
+ <i class="mdi mdi-information-outline fs-1"></i>
161
+ <p class="mb-0">No Confluence pages found</p>
162
+ </div>
163
+ {% endif %}
164
+
165
+ {% if confluence_results.cached %}
166
+ <div class="text-muted small mt-2">
167
+ <i class="mdi mdi-cached"></i> Results from cache
168
+ </div>
169
+ {% endif %}
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
@@ -2,177 +2,19 @@
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content %}
5
- <div class="row">
6
- <div class="col-12">
7
- <!-- Search Terms Info -->
8
- <div class="alert alert-info mb-3">
9
- <i class="mdi mdi-magnify"></i>
10
- <strong>Searching for:</strong>
11
- {% for term in search_terms %}
12
- <span class="badge bg-info text-white ms-1">{{ term }}</span>{% if not forloop.last %}<span class="text-muted ms-1">OR</span>{% endif %}
13
- {% empty %}
14
- <span class="text-muted">No search terms available</span>
15
- {% endfor %}
16
- <br>
17
- <small class="text-muted">
18
- Fields: {% for field in enabled_fields %}{{ field.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
19
- </small>
20
- </div>
21
-
22
- <div class="row">
23
- <!-- Jira Issues Column -->
24
- <div class="col-md-6 mb-4">
25
- <div class="card h-100">
26
- <div class="card-header d-flex justify-content-between align-items-center">
27
- <h5 class="card-title mb-0">
28
- <i class="mdi mdi-jira text-primary"></i> Jira Issues
29
- {% if jira_results.total > 0 %}
30
- <span class="badge bg-info text-white">{{ jira_results.total }} issue{{ jira_results.total|pluralize }}</span>
31
- {% endif %}
32
- </h5>
33
- {% if jira_url %}
34
- <a href="{{ jira_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
35
- <i class="mdi mdi-open-in-new"></i> Open Jira
36
- </a>
37
- {% endif %}
38
- </div>
39
- <div class="card-body">
40
- {% if not jira_configured %}
41
- <div class="alert alert-warning mb-0">
42
- <i class="mdi mdi-alert"></i> Jira not configured
43
- </div>
44
- {% elif jira_results.error %}
45
- <div class="alert alert-danger mb-0">
46
- <i class="mdi mdi-alert-circle"></i> {{ jira_results.error }}
47
- </div>
48
- {% elif jira_results.issues %}
49
- <div class="table-responsive">
50
- <table class="table table-sm table-hover mb-0">
51
- <thead>
52
- <tr>
53
- <th>Key</th>
54
- <th>Summary</th>
55
- <th>Status</th>
56
- <th>Type</th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- {% for issue in jira_results.issues %}
61
- <tr>
62
- <td>
63
- <a href="{{ issue.url }}" target="_blank" class="fw-bold">
64
- {{ issue.key }}
65
- </a>
66
- </td>
67
- <td>
68
- <span title="{{ issue.summary }}">
69
- {{ issue.summary|truncatechars:50 }}
70
- </span>
71
- </td>
72
- <td>
73
- {% if issue.status_category == 'done' %}
74
- <span class="badge bg-success text-white">{{ issue.status }}</span>
75
- {% elif issue.status_category == 'indeterminate' %}
76
- <span class="badge bg-primary text-white">{{ issue.status }}</span>
77
- {% else %}
78
- <span class="badge bg-secondary text-white">{{ issue.status }}</span>
79
- {% endif %}
80
- </td>
81
- <td>
82
- {% if issue.type_icon %}
83
- <img src="{{ issue.type_icon }}" alt="{{ issue.type }}" width="16" height="16" class="me-1">
84
- {% endif %}
85
- {{ issue.type }}
86
- </td>
87
- </tr>
88
- {% endfor %}
89
- </tbody>
90
- </table>
91
- </div>
92
- {% if jira_results.total > jira_results.issues|length %}
93
- <div class="text-muted small mt-2">
94
- Showing {{ jira_results.issues|length }} of {{ jira_results.total }} results
95
- </div>
96
- {% endif %}
97
- {% else %}
98
- <div class="text-muted text-center py-4">
99
- <i class="mdi mdi-information-outline fs-1"></i>
100
- <p class="mb-0">No Jira issues found</p>
101
- </div>
102
- {% endif %}
103
-
104
- {% if jira_results.cached %}
105
- <div class="text-muted small mt-2">
106
- <i class="mdi mdi-cached"></i> Results from cache
107
- </div>
108
- {% endif %}
109
- </div>
110
- </div>
111
- </div>
112
-
113
- <!-- Confluence Pages Column -->
114
- <div class="col-md-6 mb-4">
115
- <div class="card h-100">
116
- <div class="card-header d-flex justify-content-between align-items-center">
117
- <h5 class="card-title mb-0">
118
- <i class="mdi mdi-file-document text-info"></i> Confluence Pages
119
- {% if confluence_results.total > 0 %}
120
- <span class="badge bg-info text-white">{{ confluence_results.total }} page{{ confluence_results.total|pluralize }}</span>
121
- {% endif %}
122
- </h5>
123
- {% if confluence_url %}
124
- <a href="{{ confluence_url }}" target="_blank" class="btn btn-sm btn-outline-info">
125
- <i class="mdi mdi-open-in-new"></i> Open Confluence
126
- </a>
127
- {% endif %}
128
- </div>
129
- <div class="card-body">
130
- {% if not confluence_configured %}
131
- <div class="alert alert-warning mb-0">
132
- <i class="mdi mdi-alert"></i> Confluence not configured
133
- </div>
134
- {% elif confluence_results.error %}
135
- <div class="alert alert-danger mb-0">
136
- <i class="mdi mdi-alert-circle"></i> {{ confluence_results.error }}
137
- </div>
138
- {% elif confluence_results.pages %}
139
- <div class="list-group list-group-flush">
140
- {% for page in confluence_results.pages %}
141
- <a href="{{ page.url }}" target="_blank" class="list-group-item list-group-item-action">
142
- <div class="d-flex w-100 justify-content-between">
143
- <h6 class="mb-1">{{ page.title }}</h6>
144
- <small class="text-muted">{{ page.space_key }}</small>
145
- </div>
146
- {% if page.breadcrumb %}
147
- <small class="text-muted">{{ page.breadcrumb }}</small>
148
- {% endif %}
149
- <small class="text-muted d-block">
150
- Last modified by {{ page.last_modified_by }}
151
- </small>
152
- </a>
153
- {% endfor %}
154
- </div>
155
- {% if confluence_results.total > confluence_results.pages|length %}
156
- <div class="text-muted small mt-2">
157
- Showing {{ confluence_results.pages|length }} of {{ confluence_results.total }} results
158
- </div>
159
- {% endif %}
160
- {% else %}
161
- <div class="text-muted text-center py-4">
162
- <i class="mdi mdi-information-outline fs-1"></i>
163
- <p class="mb-0">No Confluence pages found</p>
164
- </div>
165
- {% endif %}
166
-
167
- {% if confluence_results.cached %}
168
- <div class="text-muted small mt-2">
169
- <i class="mdi mdi-cached"></i> Results from cache
170
- </div>
171
- {% endif %}
172
- </div>
173
- </div>
5
+ {% if loading %}
6
+ <div id="atlassian-content"
7
+ hx-get="{% url 'plugins:netbox_atlassian:vm_content' pk=object.pk %}"
8
+ hx-trigger="load"
9
+ hx-swap="innerHTML">
10
+ <div class="d-flex justify-content-center align-items-center py-5">
11
+ <div class="text-center">
12
+ <div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
13
+ <span class="visually-hidden">Loading...</span>
174
14
  </div>
15
+ <p class="text-muted mb-0">Loading Atlassian data...</p>
175
16
  </div>
176
17
  </div>
177
18
  </div>
19
+ {% endif %}
178
20
  {% endblock %}
netbox_atlassian/urls.py CHANGED
@@ -6,12 +6,16 @@ from django.urls import path
6
6
 
7
7
  from .views import (
8
8
  AtlassianSettingsView,
9
+ DeviceAtlassianContentView,
9
10
  TestConfluenceConnectionView,
10
11
  TestJiraConnectionView,
12
+ VMAtlassianContentView,
11
13
  )
12
14
 
13
15
  urlpatterns = [
14
16
  path("settings/", AtlassianSettingsView.as_view(), name="settings"),
15
17
  path("test-jira/", TestJiraConnectionView.as_view(), name="test_jira"),
16
18
  path("test-confluence/", TestConfluenceConnectionView.as_view(), name="test_confluence"),
19
+ path("device/<int:pk>/content/", DeviceAtlassianContentView.as_view(), name="device_content"),
20
+ path("vm/<int:pk>/content/", VMAtlassianContentView.as_view(), name="vm_content"),
17
21
  ]
netbox_atlassian/views.py CHANGED
@@ -10,8 +10,10 @@ import re
10
10
  from dcim.models import Device
11
11
  from django.conf import settings
12
12
  from django.contrib import messages
13
- from django.http import JsonResponse
13
+ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
14
+ from django.http import HttpResponse, JsonResponse
14
15
  from django.shortcuts import render
16
+ from django.template.loader import render_to_string
15
17
  from django.views import View
16
18
  from netbox.views import generic
17
19
  from utilities.views import ViewTab, register_model_view
@@ -51,6 +53,7 @@ def get_search_terms(device) -> list[str]:
51
53
  Get search terms from device based on configured search fields.
52
54
 
53
55
  Returns list of non-empty values from enabled search fields.
56
+ For comma-separated values (like multiple serial numbers), splits into individual terms.
54
57
  """
55
58
  config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
56
59
  search_fields = config.get("search_fields", [])
@@ -66,7 +69,15 @@ def get_search_terms(device) -> list[str]:
66
69
 
67
70
  value = get_device_attribute(device, attribute)
68
71
  if value and value.strip():
69
- terms.append(value.strip())
72
+ # Split comma-separated values (e.g., multiple serial numbers)
73
+ if "," in value:
74
+ for part in value.split(","):
75
+ part = part.strip()
76
+ if part and part not in terms:
77
+ terms.append(part)
78
+ else:
79
+ if value.strip() not in terms:
80
+ terms.append(value.strip())
70
81
 
71
82
  return terms
72
83
 
@@ -108,7 +119,7 @@ def should_show_atlassian_tab(device) -> bool:
108
119
 
109
120
  @register_model_view(Device, name="atlassian", path="atlassian")
110
121
  class DeviceAtlassianView(generic.ObjectView):
111
- """Display Jira issues and Confluence pages for a Device."""
122
+ """Display Jira issues and Confluence pages for a Device with async loading."""
112
123
 
113
124
  queryset = Device.objects.all()
114
125
  template_name = "netbox_atlassian/device_tab.html"
@@ -121,7 +132,26 @@ class DeviceAtlassianView(generic.ObjectView):
121
132
  )
122
133
 
123
134
  def get(self, request, pk):
124
- """Handle GET request for the Atlassian tab."""
135
+ """Render initial tab with loading spinner - content loads via htmx."""
136
+ device = Device.objects.get(pk=pk)
137
+ return render(
138
+ request,
139
+ self.template_name,
140
+ {
141
+ "object": device,
142
+ "tab": self.tab,
143
+ "loading": True,
144
+ },
145
+ )
146
+
147
+
148
+ class DeviceAtlassianContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
149
+ """HTMX endpoint that returns Atlassian content for async loading."""
150
+
151
+ permission_required = "dcim.view_device"
152
+
153
+ def get(self, request, pk):
154
+ """Fetch Atlassian data and return HTML content."""
125
155
  device = Device.objects.get(pk=pk)
126
156
 
127
157
  config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
@@ -149,27 +179,28 @@ class DeviceAtlassianView(generic.ObjectView):
149
179
  jira_url = config.get("jira_url", "").rstrip("/")
150
180
  confluence_url = config.get("confluence_url", "").rstrip("/")
151
181
 
152
- return render(
153
- request,
154
- self.template_name,
155
- {
156
- "object": device,
157
- "tab": self.tab,
158
- "search_terms": search_terms,
159
- "enabled_fields": enabled_fields,
160
- "jira_results": jira_results,
161
- "confluence_results": confluence_results,
162
- "jira_url": jira_url,
163
- "confluence_url": confluence_url,
164
- "jira_configured": bool(jira_url),
165
- "confluence_configured": bool(confluence_url),
166
- },
182
+ return HttpResponse(
183
+ render_to_string(
184
+ "netbox_atlassian/tab_content.html",
185
+ {
186
+ "object": device,
187
+ "search_terms": search_terms,
188
+ "enabled_fields": enabled_fields,
189
+ "jira_results": jira_results,
190
+ "confluence_results": confluence_results,
191
+ "jira_url": jira_url,
192
+ "confluence_url": confluence_url,
193
+ "jira_configured": bool(jira_url),
194
+ "confluence_configured": bool(confluence_url),
195
+ },
196
+ request=request,
197
+ )
167
198
  )
168
199
 
169
200
 
170
201
  @register_model_view(VirtualMachine, name="atlassian", path="atlassian")
171
202
  class VirtualMachineAtlassianView(generic.ObjectView):
172
- """Display Jira issues and Confluence pages for a VirtualMachine."""
203
+ """Display Jira issues and Confluence pages for a VirtualMachine with async loading."""
173
204
 
174
205
  queryset = VirtualMachine.objects.all()
175
206
  template_name = "netbox_atlassian/vm_tab.html"
@@ -182,7 +213,26 @@ class VirtualMachineAtlassianView(generic.ObjectView):
182
213
  )
183
214
 
184
215
  def get(self, request, pk):
185
- """Handle GET request for the Atlassian tab."""
216
+ """Render initial tab with loading spinner - content loads via htmx."""
217
+ vm = VirtualMachine.objects.get(pk=pk)
218
+ return render(
219
+ request,
220
+ self.template_name,
221
+ {
222
+ "object": vm,
223
+ "tab": self.tab,
224
+ "loading": True,
225
+ },
226
+ )
227
+
228
+
229
+ class VMAtlassianContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
230
+ """HTMX endpoint that returns Atlassian content for VM async loading."""
231
+
232
+ permission_required = "virtualization.view_virtualmachine"
233
+
234
+ def get(self, request, pk):
235
+ """Fetch Atlassian data and return HTML content."""
186
236
  vm = VirtualMachine.objects.get(pk=pk)
187
237
 
188
238
  config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
@@ -196,7 +246,6 @@ class VirtualMachineAtlassianView(generic.ObjectView):
196
246
  search_terms.append(str(vm.primary_ip4.address.ip))
197
247
 
198
248
  # Get configured search fields for display
199
- search_fields = config.get("search_fields", [])
200
249
  enabled_fields = [{"name": "Name", "attribute": "name", "enabled": True}]
201
250
  if vm.primary_ip4:
202
251
  enabled_fields.append({"name": "Primary IP", "attribute": "primary_ip4", "enabled": True})
@@ -216,21 +265,22 @@ class VirtualMachineAtlassianView(generic.ObjectView):
216
265
  jira_url = config.get("jira_url", "").rstrip("/")
217
266
  confluence_url = config.get("confluence_url", "").rstrip("/")
218
267
 
219
- return render(
220
- request,
221
- self.template_name,
222
- {
223
- "object": vm,
224
- "tab": self.tab,
225
- "search_terms": search_terms,
226
- "enabled_fields": enabled_fields,
227
- "jira_results": jira_results,
228
- "confluence_results": confluence_results,
229
- "jira_url": jira_url,
230
- "confluence_url": confluence_url,
231
- "jira_configured": bool(jira_url),
232
- "confluence_configured": bool(confluence_url),
233
- },
268
+ return HttpResponse(
269
+ render_to_string(
270
+ "netbox_atlassian/tab_content.html",
271
+ {
272
+ "object": vm,
273
+ "search_terms": search_terms,
274
+ "enabled_fields": enabled_fields,
275
+ "jira_results": jira_results,
276
+ "confluence_results": confluence_results,
277
+ "jira_url": jira_url,
278
+ "confluence_url": confluence_url,
279
+ "jira_configured": bool(jira_url),
280
+ "confluence_configured": bool(confluence_url),
281
+ },
282
+ request=request,
283
+ )
234
284
  )
235
285
 
236
286
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-atlassian
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: NetBox plugin to display Jira issues and Confluence pages related to devices
5
5
  Author-email: sieteunoseis <jeremy.worden@gmail.com>
6
6
  License: Apache-2.0
@@ -201,6 +201,12 @@ flake8 netbox_atlassian/
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for release history.
203
203
 
204
+ ## Support
205
+
206
+ If you find this plugin helpful, consider supporting development:
207
+
208
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/automatebldrs)
209
+
204
210
  ## License
205
211
 
206
212
  Apache 2.0
@@ -0,0 +1,14 @@
1
+ netbox_atlassian/__init__.py,sha256=cCI1AqClDapNesA6g-eoEvmXKGoqweNZFiODqrnQ7eE,2835
2
+ netbox_atlassian/atlassian_client.py,sha256=NaCwsHOx5kB9bz_g6rtfbjXFsE3J3KdrVY-wewAUIzg,13567
3
+ netbox_atlassian/forms.py,sha256=b8MKsU3Ou-c0s7Cga_72m7E3UCCliZF-17woybN66dk,3385
4
+ netbox_atlassian/navigation.py,sha256=tpRgS8qESmxm5eEuCOd1UjEVXEDTMM12H4lEaZk1BRw,497
5
+ netbox_atlassian/urls.py,sha256=3eLcwzSE6kG-9iDnrgxp6hFmbgRuUYRm196MIieuSF0,700
6
+ netbox_atlassian/views.py,sha256=3-5snH3vIGVFdY-Ejdb1oDXJHcwgkZ4re_KueYLKVkE,12068
7
+ netbox_atlassian/templates/netbox_atlassian/device_tab.html,sha256=yYNhzZHvVSFGa3liRYiHL0KosJv1offXS5v7L0vVGm0,677
8
+ netbox_atlassian/templates/netbox_atlassian/settings.html,sha256=bmiIzzJ6m1vwT5AOcOrdJ7uD3V31GbKUpT4M6TIP3wE,11709
9
+ netbox_atlassian/templates/netbox_atlassian/tab_content.html,sha256=vLH1glj16Pb_JarGJ9LP7FF1ijAt8B-MyZ-FlzITOjI,9473
10
+ netbox_atlassian/templates/netbox_atlassian/vm_tab.html,sha256=NG2Rk019tRLZtVLbrYmq8vakLyt-Gzj3WhGjxjfyn9Q,691
11
+ netbox_atlassian-0.2.2.dist-info/METADATA,sha256=Us88eAq8_jYfZY7YJ30R_na7jppaX8JJia76Rlr3shs,6881
12
+ netbox_atlassian-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
13
+ netbox_atlassian-0.2.2.dist-info/top_level.txt,sha256=VaKVLTW0o2SE7896PzRTWUaEeYt8CWsryBXI6Ij4ECg,17
14
+ netbox_atlassian-0.2.2.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- netbox_atlassian/__init__.py,sha256=L53QJQQEwrbcHmtMfZt68zq0OC5bgPbLqXN6phEDg8o,2835
2
- netbox_atlassian/atlassian_client.py,sha256=NaCwsHOx5kB9bz_g6rtfbjXFsE3J3KdrVY-wewAUIzg,13567
3
- netbox_atlassian/forms.py,sha256=b8MKsU3Ou-c0s7Cga_72m7E3UCCliZF-17woybN66dk,3385
4
- netbox_atlassian/navigation.py,sha256=tpRgS8qESmxm5eEuCOd1UjEVXEDTMM12H4lEaZk1BRw,497
5
- netbox_atlassian/urls.py,sha256=kIiL-G8wfMd1tdSrnWeWcTuHZMx6CCc5mLr_WjKn8Rs,454
6
- netbox_atlassian/views.py,sha256=EQlU2hcHNcRfwsD5C73L1X5u82WeB26XqSPcMVxl9S4,10118
7
- netbox_atlassian/templates/netbox_atlassian/device_tab.html,sha256=9HD8mx43vBogO6Yq3brHZghm9gmJQwbWfqLk9YHroMs,9546
8
- netbox_atlassian/templates/netbox_atlassian/settings.html,sha256=bmiIzzJ6m1vwT5AOcOrdJ7uD3V31GbKUpT4M6TIP3wE,11709
9
- netbox_atlassian/templates/netbox_atlassian/vm_tab.html,sha256=H6QL0sRTXo8O-0Xlha-U46NQ0UWNA8L3KzKe50tQeFE,9564
10
- netbox_atlassian-0.2.1.dist-info/METADATA,sha256=9cHCqt65varpaFPgYBWztA9Rpnp2OE1oikHu_yJ33ks,6641
11
- netbox_atlassian-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- netbox_atlassian-0.2.1.dist-info/top_level.txt,sha256=VaKVLTW0o2SE7896PzRTWUaEeYt8CWsryBXI6Ij4ECg,17
13
- netbox_atlassian-0.2.1.dist-info/RECORD,,