mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 20:22:53 -06:00
* MarkdownWidget * Change border and color of active markdown tab * Fix template name typo * Add render markdown endpoint * Static assets for markdown widget * widget style fix and unique ids based on name * Replace SmallTextArea with SmallMarkdownWidget * Clear innerHTML before swapping * render markdown directly in template * change render markdown view path * remove small markdown widget * Simplify rendering logic * Use a form to clean input Markdown data --------- Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
This commit is contained in:
parent
33286aad39
commit
fa60f9d2a8
@ -7,7 +7,7 @@ from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea,
|
||||
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
StaticSelect,
|
||||
)
|
||||
|
||||
@ -35,7 +35,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
@ -63,7 +62,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
@ -125,7 +123,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
|
@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget,
|
||||
DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@ -138,7 +138,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -309,7 +308,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -345,7 +343,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -406,7 +403,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -441,7 +437,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -551,7 +546,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -594,7 +588,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -644,7 +637,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -668,7 +660,6 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -714,7 +705,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -776,7 +766,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
|
@ -2,6 +2,7 @@ from .model_forms import *
|
||||
from .filtersets import *
|
||||
from .bulk_edit import *
|
||||
from .bulk_import import *
|
||||
from .misc import *
|
||||
from .mixins import *
|
||||
from .config import *
|
||||
from .scripts import *
|
||||
|
14
netbox/extras/forms/misc.py
Normal file
14
netbox/extras/forms/misc.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django import forms
|
||||
|
||||
__all__ = (
|
||||
'RenderMarkdownForm',
|
||||
)
|
||||
|
||||
|
||||
class RenderMarkdownForm(forms.Form):
|
||||
"""
|
||||
Provides basic validation for markup to be rendered.
|
||||
"""
|
||||
text = forms.CharField(
|
||||
required=False
|
||||
)
|
@ -92,4 +92,6 @@ urlpatterns = [
|
||||
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
|
||||
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
|
||||
|
||||
# Markdown
|
||||
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown")
|
||||
]
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count, Q
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import View
|
||||
@ -10,6 +10,7 @@ from rq import Worker
|
||||
|
||||
from netbox.views import generic
|
||||
from utilities.htmx import is_htmx
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
@ -885,3 +886,18 @@ class JobResultBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = JobResult.objects.all()
|
||||
filterset = filtersets.JobResultFilterSet
|
||||
table = tables.JobResultTable
|
||||
|
||||
|
||||
#
|
||||
# Markdown
|
||||
#
|
||||
|
||||
class RenderMarkdownView(View):
|
||||
|
||||
def post(self, request):
|
||||
form = forms.RenderMarkdownForm(request.POST)
|
||||
if not form.is_valid():
|
||||
HttpResponseBadRequest()
|
||||
rendered = render_markdown(form.cleaned_data['text'])
|
||||
|
||||
return HttpResponse(rendered)
|
||||
|
@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField,
|
||||
SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField,
|
||||
StaticSelect, DynamicModelMultipleChoiceField
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@ -48,7 +48,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -69,7 +68,6 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -116,7 +114,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -145,7 +142,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -227,7 +223,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -266,7 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -314,7 +308,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -359,7 +352,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -442,7 +434,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -474,7 +465,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -504,7 +494,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -4,6 +4,7 @@ import { initMoveButtons } from './moveOptions';
|
||||
import { initReslug } from './reslug';
|
||||
import { initSelectAll } from './selectAll';
|
||||
import { initSelectMultiple } from './selectMultiple';
|
||||
import { initMarkdownPreviews } from './markdownPreview';
|
||||
|
||||
export function initButtons(): void {
|
||||
for (const func of [
|
||||
@ -13,6 +14,7 @@ export function initButtons(): void {
|
||||
initSelectAll,
|
||||
initSelectMultiple,
|
||||
initMoveButtons,
|
||||
initMarkdownPreviews,
|
||||
]) {
|
||||
func();
|
||||
}
|
||||
|
45
netbox/project-static/src/buttons/markdownPreview.ts
Normal file
45
netbox/project-static/src/buttons/markdownPreview.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { isTruthy } from 'src/util';
|
||||
|
||||
/**
|
||||
* interface for htmx configRequest event
|
||||
*/
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
'htmx:configRequest': CustomEvent<{
|
||||
parameters: Record<string, string>;
|
||||
headers: Record<string, string>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
function initMarkdownPreview(markdownWidget: HTMLDivElement) {
|
||||
const previewButton = markdownWidget.querySelector('button.preview-button') as HTMLButtonElement;
|
||||
const textarea = markdownWidget.querySelector('textarea') as HTMLTextAreaElement;
|
||||
const preview = markdownWidget.querySelector('div.preview') as HTMLDivElement;
|
||||
|
||||
/**
|
||||
* Make sure the textarea has style attribute height
|
||||
* So that it can be copied over to preview div.
|
||||
*/
|
||||
if (!isTruthy(textarea.style.height)) {
|
||||
const { height } = textarea.getBoundingClientRect();
|
||||
textarea.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the value of the textarea to the body of the htmx request
|
||||
* and copy the height of text are to the preview div
|
||||
*/
|
||||
previewButton.addEventListener('htmx:configRequest', e => {
|
||||
e.detail.parameters = { text: textarea.value || '' };
|
||||
e.detail.headers['X-CSRFToken'] = window.CSRF_TOKEN;
|
||||
preview.style.minHeight = textarea.style.height;
|
||||
preview.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
export function initMarkdownPreviews(): void {
|
||||
for (const markdownWidget of document.querySelectorAll<HTMLDivElement>('.markdown-widget')) {
|
||||
initMarkdownPreview(markdownWidget);
|
||||
}
|
||||
}
|
@ -236,12 +236,12 @@ table {
|
||||
}
|
||||
|
||||
th.asc > a::after {
|
||||
content: "\f0140";
|
||||
content: '\f0140';
|
||||
font-family: 'Material Design Icons';
|
||||
}
|
||||
|
||||
th.desc > a::after {
|
||||
content: "\f0143";
|
||||
content: '\f0143';
|
||||
font-family: 'Material Design Icons';
|
||||
}
|
||||
|
||||
@ -419,7 +419,7 @@ nav.search {
|
||||
// Styles for the quicksearch and its clear button;
|
||||
// Overrides input-group styles and adds transition effects
|
||||
.quicksearch {
|
||||
input[type="search"] {
|
||||
input[type='search'] {
|
||||
border-radius: $border-radius !important;
|
||||
}
|
||||
|
||||
@ -998,9 +998,24 @@ div.card-overlay {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Markdown widget */
|
||||
.markdown-widget {
|
||||
.nav-link {
|
||||
border-bottom: 0;
|
||||
|
||||
&.active {
|
||||
background-color: var(--nbx-body-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
background-color: var(--nbx-pre-bg);
|
||||
}
|
||||
}
|
||||
|
||||
// Preformatted text blocks
|
||||
td pre {
|
||||
margin-bottom: 0
|
||||
margin-bottom: 0;
|
||||
}
|
||||
pre.block {
|
||||
padding: $spacer;
|
||||
|
@ -2,7 +2,7 @@ from django import forms
|
||||
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import *
|
||||
from utilities.forms import CommentField, DynamicModelChoiceField, SmallTextarea
|
||||
from utilities.forms import CommentField, DynamicModelChoiceField
|
||||
|
||||
__all__ = (
|
||||
'ContactBulkEditForm',
|
||||
@ -106,7 +106,6 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
|
@ -27,7 +27,7 @@ class CommentField(forms.CharField):
|
||||
"""
|
||||
A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
|
||||
"""
|
||||
widget = forms.Textarea
|
||||
widget = widgets.MarkdownWidget
|
||||
help_text = f"""
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
<a href="{static('docs/reference/markdown/')}" target="_blank" tabindex="-1">
|
||||
|
@ -16,6 +16,7 @@ __all__ = (
|
||||
'ColorSelect',
|
||||
'DatePicker',
|
||||
'DateTimePicker',
|
||||
'MarkdownWidget',
|
||||
'NumericArrayField',
|
||||
'SelectDurationWidget',
|
||||
'SelectSpeedWidget',
|
||||
@ -116,6 +117,10 @@ class SelectDurationWidget(forms.NumberInput):
|
||||
template_name = 'widgets/select_duration.html'
|
||||
|
||||
|
||||
class MarkdownWidget(forms.Textarea):
|
||||
template_name = 'widgets/markdown_input.html'
|
||||
|
||||
|
||||
class NumericArrayField(SimpleArrayField):
|
||||
|
||||
def clean(self, value):
|
||||
|
@ -6,7 +6,7 @@
|
||||
{# Render the field label, except for: #}
|
||||
{# 1. Checkboxes (label appears to the right of the field #}
|
||||
{# 2. Textareas with no label set (will expand across entire row) #}
|
||||
{% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' and not label %}
|
||||
{% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' or field|widget_type == 'markdownwidget' and not label %}
|
||||
{% else %}
|
||||
<label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
|
||||
{{ label }}
|
||||
|
22
netbox/utilities/templates/widgets/markdown_input.html
Normal file
22
netbox/utilities/templates/widgets/markdown_input.html
Normal file
@ -0,0 +1,22 @@
|
||||
<div class="border rounded markdown-widget">
|
||||
<ul class="nav nav-tabs px-3 pt-2 rounded-top border-0">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active " id="{{ widget.name }}-input-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-input" type="button" role="tab" aria-controls="{{ widget.name }}-input" aria-selected="true">
|
||||
Write
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button hx-target="#{{ widget.name }}-preview" hx-swap="innerHTML" hx-post="{% url 'extras:render_markdown' %}" class="nav-link preview-button" id="{{ widget.name }}-markdown-preview-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-markdown-preview" type="button" role="tab" aria-controls="{{ widget.name }}-markdown-preview" aria-selected="false">
|
||||
Preview
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content bg-body rounded-bottom border-top">
|
||||
<div class="tab-pane show active" id="{{ widget.name }}-input" role="tabpanel" aria-labelledby="{{ widget.name }}-input-tab">
|
||||
{% include "django/forms/widgets/textarea.html" %}
|
||||
</div>
|
||||
<div class="tab-pane show" id="{{ widget.name }}-markdown-preview" role="tabpanel" aria-labelledby="{{ widget.name }}-markdown-preview-tab">
|
||||
<div id="{{ widget.name }}-preview" class="preview px-3 py-2">Testing</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -9,7 +9,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect
|
||||
DynamicModelMultipleChoiceField, StaticSelect
|
||||
)
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
@ -90,7 +90,6 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
@ -163,7 +162,6 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@ from dcim.choices import LinkStatusChoices
|
||||
from ipam.models import VLAN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea
|
||||
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField
|
||||
from wireless.choices import *
|
||||
from wireless.constants import SSID_MAX_LENGTH
|
||||
from wireless.models import *
|
||||
@ -74,7 +74,6 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
@ -119,7 +118,6 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user