Merge pull request #299 from digitalocean/develop

Release v1.2.2
This commit is contained in:
Jeremy Stretch 2016-07-14 15:21:40 -04:00 committed by GitHub
commit 4e64e1ea95
25 changed files with 227 additions and 19 deletions

View File

@ -112,6 +112,9 @@ Generate a random secret key of at least 50 alphanumeric characters. This key mu
You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key. You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key.
!!! note
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
# Run Database Migrations # Run Database Migrations
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example): Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):

View File

@ -1,9 +1,40 @@
import django_filters import django_filters
from django.db.models import Q
from dcim.models import Site from dcim.models import Site
from .models import Provider, Circuit, CircuitType from .models import Provider, Circuit, CircuitType
class ProviderFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = Provider
fields = ['q', 'name', 'account', 'asn']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value)
)
class CircuitFilter(django_filters.FilterSet): class CircuitFilter(django_filters.FilterSet):
q = django_filters.MethodFilter( q = django_filters.MethodFilter(
action='search', action='search',

View File

@ -59,6 +59,16 @@ class ProviderBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
def provider_site_choices():
site_choices = Site.objects.all()
return [(s.slug, s.name) for s in site_choices]
class ProviderFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
# Circuit types # Circuit types
# #

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-13 19:24
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0002_auto_20160622_1821'),
]
operations = [
migrations.AlterField(
model_name='provider',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
]

View File

@ -1,6 +1,7 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from dcim.fields import ASNField
from dcim.models import Site, Interface from dcim.models import Site, Interface
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
@ -12,7 +13,7 @@ class Provider(CreatedUpdatedModel):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN') asn = ASNField(blank=True, null=True, verbose_name='ASN')
account = models.CharField(max_length=30, blank=True, verbose_name='Account number') account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
portal_url = models.URLField(blank=True, verbose_name='Portal') portal_url = models.URLField(blank=True, verbose_name='Portal')
noc_contact = models.TextField(blank=True, verbose_name='NOC contact') noc_contact = models.TextField(blank=True, verbose_name='NOC contact')

View File

@ -16,6 +16,8 @@ from .models import Circuit, CircuitType, Provider
class ProviderListView(ObjectListView): class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits')) queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
table = tables.ProviderTable table = tables.ProviderTable
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider'] edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
template_name = 'circuits/provider_list.html' template_name = 'circuits/provider_list.html'

View File

@ -1,11 +1,20 @@
from netaddr import EUI, mac_unix_expanded from netaddr import EUI, mac_unix_expanded
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
class ASNField(models.BigIntegerField):
description = "32-bit ASN field"
default_validators = [
MinValueValidator(1),
MaxValueValidator(4294967295),
]
class mac_unix_expanded_uppercase(mac_unix_expanded): class mac_unix_expanded_uppercase(mac_unix_expanded):
word_fmt = '%.2X' word_fmt = '%.2X'

View File

@ -122,6 +122,11 @@ class DeviceFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = django_filters.ModelMultipleChoiceFilter(
name='rack__group',
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack', name='rack',
queryset=Rack.objects.all(), queryset=Rack.objects.all(),

View File

@ -186,7 +186,7 @@ def rack_group_choices():
class RackFilterForm(forms.Form, BootstrapMixin): class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices, site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
@ -502,6 +502,11 @@ def device_site_choices():
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices] return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
def device_rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
return [(g.pk, '{} ({})'.format(g, g.device_count)) for g in group_choices]
def device_role_choices(): def device_role_choices():
role_choices = DeviceRole.objects.annotate(device_count=Count('devices')) role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices] return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
@ -520,6 +525,8 @@ def device_platform_choices():
class DeviceFilterForm(forms.Form, BootstrapMixin): class DeviceFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=device_site_choices, site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=device_role_choices, role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type', device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-13 19:24
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0008_device_remove_primary_ip'),
]
operations = [
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
]

View File

@ -11,7 +11,7 @@ from extras.rpc import RPC_CLIENTS
from utilities.fields import NullableCharField from utilities.fields import NullableCharField
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from .fields import MACAddressField from .fields import ASNField, MACAddressField
RACK_FACE_FRONT = 0 RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1 RACK_FACE_REAR = 1
@ -145,7 +145,7 @@ class Site(CreatedUpdatedModel):
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
facility = models.CharField(max_length=50, blank=True) facility = models.CharField(max_length=50, blank=True)
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN') asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True) physical_address = models.CharField(max_length=200, blank=True)
shipping_address = models.CharField(max_length=200, blank=True) shipping_address = models.CharField(max_length=200, blank=True)
comments = models.TextField(blank=True) comments = models.TextField(blank=True)

View File

@ -273,7 +273,10 @@ def devicetype(request, pk):
poweroutlet_table = tables.PowerOutletTemplateTable( poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
) )
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype)) mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
mgmt_only=True))
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
mgmt_only=False))
devicebay_table = tables.DeviceBayTemplateTable( devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
) )
@ -282,6 +285,7 @@ def devicetype(request, pk):
consoleserverport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True poweroutlet_table.base_columns['pk'].visible = True
mgmt_interface_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True devicebay_table.base_columns['pk'].visible = True
@ -291,6 +295,7 @@ def devicetype(request, pk):
'consoleserverport_table': consoleserverport_table, 'consoleserverport_table': consoleserverport_table,
'powerport_table': powerport_table, 'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table, 'poweroutlet_table': poweroutlet_table,
'mgmt_interface_table': mgmt_interface_table,
'interface_table': interface_table, 'interface_table': interface_table,
'devicebay_table': devicebay_table, 'devicebay_table': devicebay_table,
}) })
@ -348,7 +353,7 @@ class ComponentTemplateCreateView(View):
return render(request, 'dcim/component_template_add.html', { return render(request, 'dcim/component_template_add.html', {
'devicetype': devicetype, 'devicetype': devicetype,
'component_type': self.model._meta.verbose_name, 'component_type': self.model._meta.verbose_name,
'form': self.form(), 'form': self.form(initial=request.GET),
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}), 'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
}) })

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.2.1' VERSION = '1.2.2'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@ -138,7 +138,6 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'utilities.context_processors.settings', 'utilities.context_processors.settings',
'django.core.context_processors.request',
], ],
}, },
}, },

View File

@ -2,6 +2,9 @@
* { * {
margin: 0; margin: 0;
} }
html {
overflow-y: scroll;
}
html, body { html, body {
height: 100%; height: 100%;
} }

View File

@ -1,9 +1,15 @@
$(document).ready(function() { $(document).ready(function() {
// "Select all" checkbox in a table header // "Select all" checkbox in a table header
$('th input:checkbox').click(function (event) { $('th input:checkbox[name=_all]').click(function (event) {
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked')); $(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
}); });
// Uncheck the "select all" checkbox if an item is unchecked
$('input:checkbox[name=pk]').click(function (event) {
if (!$(this).attr('checked')) {
$(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
}
});
// Slugify // Slugify
function slugify(s, num_chars) { function slugify(s, num_chars) {

View File

@ -14,8 +14,28 @@
</div> </div>
<h1>Providers</h1> <h1>Providers</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div> </div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'circuits:provider_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/filter_panel.html' %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -25,6 +25,7 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong> <strong>Search</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View File

@ -42,7 +42,7 @@
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
<tr> <tr>
<td>Manufacturer</td> <td>Manufacturer</td>
<td>{{ devicetype.manufacturer }}</td> <td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
</tr> </tr>
<tr> <tr>
<td>Model Name</td> <td>Model Name</td>
@ -54,7 +54,13 @@
</tr> </tr>
<tr> <tr>
<td>Full Depth</td> <td>Full Depth</td>
<td>{{ devicetype.is_full_depth|yesno|capfirst }}</td> <td>
{% if devicetype.is_full_depth %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
</tr> </tr>
</table> </table>
</div> </div>
@ -64,21 +70,70 @@
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
<tr> <tr>
<td>Is a Console Server</td> <td class="text-right">
<td>{{ devicetype.is_console_server|yesno|capfirst }}</td> {% if devicetype.is_console_server %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>Console Server</strong><br />
<small class="text-muted">This device {% if devicetype.is_console_server %}has{% else %}does not have{% endif %} console server ports</small>
</td>
</tr> </tr>
<tr> <tr>
<td>Is a PDU</td> <td class="text-right">
<td>{{ devicetype.is_pdu|yesno|capfirst }}</td> {% if devicetype.is_pdu %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>PDU</strong><br />
<small class="text-muted">This device {% if devicetype.is_pdu %}has{% else %}does not have{% endif %} power outlets</small>
</td>
</tr> </tr>
<tr> <tr>
<td>Is a Network Device</td> <td class="text-right">
<td>{{ devicetype.is_network_device|yesno|capfirst }}</td> {% if devicetype.is_network_device %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>Network Device</strong><br />
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small>
</td>
</tr>
<tr>
<td class="text-right">
{% if devicetype.subdevice_role == True %}
<label class="label label-primary">Parent</label>
{% elif devicetype.subdevice_role == False %}
<label class="label label-info">Child</label>
{% else %}
<label class="label label-default">None</label>
{% endif %}
</td>
<td>
<strong>Parent/Child</strong><br />
{% if devicetype.subdevice_role == True %}
<small class="text-muted">This device has device bays for mounting child devices</small>
{% elif devicetype.subdevice_role == False %}
<small class="text-muted">This device can only be mounted in a parent device</small>
{% else %}
<small class="text-muted">This device does not have device bays</small>
{% endif %}
</td>
</tr> </tr>
</table> </table>
</div> </div>
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% if devicetype.is_parent_device %} {% if devicetype.is_parent_device %}

View File

@ -4,7 +4,10 @@
{% csrf_token %} {% csrf_token %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a href="{% url add_url pk=devicetype.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add {{ title }}</a> <a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs pull-right">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add {{ title }}
</a>
<strong>{{ title }}</strong> <strong>{{ title }}</strong>
</div> </div>
{% render_table table 'table.html' %} {% render_table table 'table.html' %}

View File

@ -25,6 +25,7 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong> <strong>Search</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View File

@ -21,6 +21,7 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong> <strong>Search</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View File

@ -2,6 +2,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span>
<strong>Filter</strong> <strong>Filter</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View File

@ -26,6 +26,7 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong> <strong>Search</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View File

@ -26,6 +26,7 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong> <strong>Search</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View File

@ -26,6 +26,7 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search by ID</strong> <strong>Search by ID</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">