mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 12:06:53 -06:00
Add UI views for webhooks
This commit is contained in:
parent
10cbbee947
commit
4e0b795a3c
@ -1,60 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from utilities.forms import ContentTypeMultipleChoiceField, LaxURLField
|
||||
from .models import JobResult, Webhook
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
)
|
||||
payload_url = LaxURLField(
|
||||
label='URL'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
exclude = ()
|
||||
|
||||
|
||||
@admin.register(Webhook)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
||||
'ssl_verification',
|
||||
]
|
||||
list_filter = [
|
||||
'enabled', 'type_create', 'type_update', 'type_delete', 'content_types',
|
||||
]
|
||||
form = WebhookForm
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'content_types', 'enabled')
|
||||
}),
|
||||
('Events', {
|
||||
'fields': ('type_create', 'type_update', 'type_delete')
|
||||
}),
|
||||
('HTTP Request', {
|
||||
'fields': (
|
||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
),
|
||||
'classes': ('monospace',)
|
||||
}),
|
||||
('SSL', {
|
||||
'fields': ('ssl_verification', 'ca_file_path')
|
||||
})
|
||||
)
|
||||
|
||||
def models(self, obj):
|
||||
return ', '.join([ct.name for ct in obj.content_types.all()])
|
||||
from .models import JobResult
|
||||
|
||||
|
||||
#
|
||||
|
@ -268,6 +268,129 @@ class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Webhook', ('name', 'enabled')),
|
||||
('Assigned Models', ('content_types',)),
|
||||
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||
('HTTP Request', (
|
||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
)),
|
||||
('SSL', ('ssl_verification', 'ca_file_path')),
|
||||
)
|
||||
|
||||
|
||||
class WebhookCSVForm(CSVModelForm):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
help_text="One or more assigned object types"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = (
|
||||
'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||
'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification',
|
||||
'ca_file_path'
|
||||
)
|
||||
|
||||
|
||||
class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Webhook.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
type_create = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
type_update = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
type_delete = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
http_method = forms.ChoiceField(
|
||||
choices=WebhookHttpMethodChoices,
|
||||
required=False
|
||||
)
|
||||
payload_url = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
ssl_verification = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
secret = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
ca_file_path = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['secret', 'ca_file_path']
|
||||
|
||||
|
||||
class WebhookFilterForm(BootstrapMixin, forms.Form):
|
||||
field_groups = [
|
||||
['content_types', 'http_method'],
|
||||
['enabled', 'type_create', 'type_update', 'type_delete'],
|
||||
]
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
http_method = forms.MultipleChoiceField(
|
||||
choices=WebhookHttpMethodChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
type_create = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
type_update = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
type_delete = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Custom field models
|
||||
#
|
||||
|
@ -38,4 +38,14 @@ class Migration(migrations.Migration):
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
]
|
||||
|
@ -36,7 +36,8 @@ __all__ = (
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class Webhook(BigIDModel):
|
||||
@extras_features('webhooks')
|
||||
class Webhook(ChangeLoggedModel):
|
||||
"""
|
||||
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
||||
@ -129,6 +130,9 @@ class Webhook(BigIDModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:webhook', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@ -86,6 +86,27 @@ class ExportTemplateTable(BaseTable):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Webhook
|
||||
fields = (
|
||||
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
|
||||
'secret', 'ssl_validation', 'ca_file_path',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
@ -120,6 +120,49 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Webhook
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
webhooks = (
|
||||
Webhook(name='Webhook 1', payload_url='http://example.com/?1', type_create=True, http_method='POST'),
|
||||
Webhook(name='Webhook 2', payload_url='http://example.com/?2', type_create=True, http_method='POST'),
|
||||
Webhook(name='Webhook 3', payload_url='http://example.com/?3', type_create=True, http_method='POST'),
|
||||
)
|
||||
for webhook in webhooks:
|
||||
webhook.save()
|
||||
webhook.content_types.add(site_ct)
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Webhook X',
|
||||
'content_types': [site_ct.pk],
|
||||
'type_create': False,
|
||||
'type_update': True,
|
||||
'type_delete': True,
|
||||
'payload_url': 'http://example.com/?x',
|
||||
'http_method': 'GET',
|
||||
'http_content_type': 'application/foo',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,content_types,type_create,payload_url,http_method,http_content_type",
|
||||
"Webhook 4,dcim.site,True,http://example.com/?4,GET,application/json",
|
||||
"Webhook 5,dcim.site,True,http://example.com/?5,GET,application/json",
|
||||
"Webhook 6,dcim.site,True,http://example.com/?6,GET,application/json",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'enabled': False,
|
||||
'type_create': False,
|
||||
'type_update': True,
|
||||
'type_delete': True,
|
||||
'http_method': 'GET',
|
||||
}
|
||||
|
||||
|
||||
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Tag
|
||||
|
||||
|
@ -42,6 +42,18 @@ urlpatterns = [
|
||||
path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
|
||||
kwargs={'model': models.ExportTemplate}),
|
||||
|
||||
# Webhooks
|
||||
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
||||
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
||||
path('webhooks/import/', views.WebhookBulkImportView.as_view(), name='webhook_import'),
|
||||
path('webhooks/edit/', views.WebhookBulkEditView.as_view(), name='webhook_bulk_edit'),
|
||||
path('webhooks/delete/', views.WebhookBulkDeleteView.as_view(), name='webhook_bulk_delete'),
|
||||
path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'),
|
||||
path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
|
||||
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
|
||||
path('webhooks/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='webhook_changelog',
|
||||
kwargs={'model': models.Webhook}),
|
||||
|
||||
# Tags
|
||||
path('tags/', views.TagListView.as_view(), name='tag_list'),
|
||||
path('tags/add/', views.TagEditView.as_view(), name='tag_add'),
|
||||
|
@ -149,6 +149,49 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.ExportTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookListView(generic.ObjectListView):
|
||||
queryset = Webhook.objects.all()
|
||||
filterset = filtersets.WebhookFilterSet
|
||||
filterset_form = forms.WebhookFilterForm
|
||||
table = tables.WebhookTable
|
||||
|
||||
|
||||
class WebhookView(generic.ObjectView):
|
||||
queryset = Webhook.objects.all()
|
||||
|
||||
|
||||
class WebhookEditView(generic.ObjectEditView):
|
||||
queryset = Webhook.objects.all()
|
||||
model_form = forms.WebhookForm
|
||||
|
||||
|
||||
class WebhookDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Webhook.objects.all()
|
||||
|
||||
|
||||
class WebhookBulkImportView(generic.BulkImportView):
|
||||
queryset = Webhook.objects.all()
|
||||
model_form = forms.WebhookCSVForm
|
||||
table = tables.WebhookTable
|
||||
|
||||
|
||||
class WebhookBulkEditView(generic.BulkEditView):
|
||||
queryset = Webhook.objects.all()
|
||||
filterset = filtersets.WebhookFilterSet
|
||||
table = tables.WebhookTable
|
||||
form = forms.WebhookBulkEditForm
|
||||
|
||||
|
||||
class WebhookBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Webhook.objects.all()
|
||||
filterset = filtersets.WebhookFilterSet
|
||||
table = tables.WebhookTable
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% load plugins %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'extras:customfield_list' %}">Cusotm Fields</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'extras:customfield_list' %}">Custom Fields</a></li>
|
||||
<li class="breadcrumb-item">{{ object }}</li>
|
||||
{% endblock %}
|
||||
|
||||
|
165
netbox/templates/extras/webhook.html
Normal file
165
netbox/templates/extras/webhook.html
Normal file
@ -0,0 +1,165 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'extras:webhook_list' %}">Webhooks</a></li>
|
||||
<li class="breadcrumb-item">{{ object }}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Webhook
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Enabled</th>
|
||||
<td>
|
||||
{% if object.enabled %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Events
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Create</th>
|
||||
<td>
|
||||
{% if object.type_create %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Update</th>
|
||||
<td>
|
||||
{% if object.type_create %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Delete</th>
|
||||
<td>
|
||||
{% if object.type_create %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
HTTP Request
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">HTTP Method</th>
|
||||
<td>{{ object.get_http_method_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Payload URL</th>
|
||||
<td><code>{{ object.payload_url }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">HTTP Content Type</th>
|
||||
<td>{{ object.http_content_type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Secret</th>
|
||||
<td>{{ object.secret|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
SSL
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">SSL Verification</th>
|
||||
<td>
|
||||
{% if object.ssl_verification %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">CA File Path</th>
|
||||
<td>
|
||||
{% if object.ca_file_path %}
|
||||
<code>{{ object.ca_file_path }}</code>
|
||||
{% else %}
|
||||
&mdash
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Assigned Models
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
{% for ct in object.content_types.all %}
|
||||
<tr>
|
||||
<td>{{ ct }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Additional Headers
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.additional_headers }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Body Template
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.body_template }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -287,6 +287,8 @@ OTHER_MENU = Menu(
|
||||
add_url=None, import_url=None),
|
||||
MenuItem(label="Journal Entries",
|
||||
url="extras:journalentry_list", add_url=None, import_url=None),
|
||||
MenuItem(label="Webhooks", url="extras:webhook_list",
|
||||
add_url="extras:webhook_add", import_url="extras:webhook_import"),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
|
Loading…
Reference in New Issue
Block a user