Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

49 add select all for armis sites and universal search for device names #54

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/helper/armis.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,18 @@ def get_devices(acloud, sites):
sites = ','.join(f'"{site}"' for site in sites)
deviceList = acloud.get_devices(
asq=f'in:devices site:{sites} timeFrame:"7 Days" {vlan_bl}',
fields_wanted=['id', 'ipAddress', 'macAddress', 'name', 'boundaries']
fields_wanted=['id', 'ipAddress', 'macAddress', 'name', 'boundaries', 'site']
)
return _remove_existing_devices(deviceList)
# flake8: qa

@armiscloud
def get_single_device(acloud, deviceName):
device = acloud.get_devices(
asq=f'in:devices name:{deviceName.strip()} timeFrame:"7 Days"',
fields_wanted=['id', 'ipAddress', 'macAddress', 'name', 'boundaries', 'site']
)
return device

def get_boundaries(deviceList):
unique_boundaries = set()
Expand Down
14 changes: 8 additions & 6 deletions src/nac/subviews/armis.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
from django.core.cache import cache
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin


from helper.armis import get_armis_sites, get_devices, get_tenant_url, get_boundaries, map_ids_to_names, get_vlan_blacklist
from helper.armis import get_armis_sites, get_devices, get_tenant_url, get_boundaries, map_ids_to_names, get_single_device, get_vlan_blacklist


class ArmisView(LoginRequiredMixin, View):
Expand All @@ -41,13 +39,17 @@ def get(self, request, *args, **kwargs): # rendering the html base with site-co
context['display'] = True
return render(request, self.template_name, context)

def post(self, request, *args, **kwargs): # gets site-id chosen in html-dropdown, gets Devices based on site-id, shows them via device-context
def post(self, request, *args, **kwargs): # gets site-id chosen in checkbox, gets Devices based on site-id, shows them via device-context
context = self._get_context()
selected_sites = request.POST.getlist('site-ids[]')
context['display'] = False if selected_sites else True
search_device = request.POST.get('deviceName')
context['display'] = False if selected_sites or search_device else True
context['selected_sites'] = selected_sites if selected_sites else ''
if selected_sites:
if selected_sites and not search_device:
context['devices'] = get_devices(map_ids_to_names(selected_sites, context['armis_sites']))
context['boundaries'] = get_boundaries(context['devices'])
else:
context['devices'] = get_single_device(search_device)
context['boundaries'] = get_boundaries(context['devices'])

return render(request, self.template_name, context)
10 changes: 9 additions & 1 deletion src/static/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,15 @@ td {
height: 20px;
cursor: pointer;
}

.search-container {
display: flex;
align-items: center;
}

.search-container input {
margin-right: 8px;
}

.split-content {
display: flex;
justify-content: space-between;
Expand Down
69 changes: 55 additions & 14 deletions src/templates/armis_import.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
{% block content %}
<div class="container">
<h1>Armis Import</h1>
<div id="searchContainer" style="display: {% if display or not devices %}block{% else %}none{% endif %}">
<form id="deviceSubmitForm" method="POST" action="{% url 'armis_import' %}">
{% csrf_token %}
<div class="search-container">
<input type="text" id="deviceSearch" name="deviceName" placeholder="Search for Device..." class="form-control" style="width: 300px;">
<button type="submit" id="searchButton" class="btn btn-secondary btn-sm">Search</button>
</div>
</form>
<br></br>
</div>
<p>
<button id="toggleSiteForm" class="btn btn-secondary btn-sm">
{% if display %}Hide Sites{% else %}Show Sites{% endif %}
Expand All @@ -22,20 +32,25 @@ <h1>Armis Import</h1>
<div id="formContainer" style="display: {% if display %}block{% else %}none{% endif %}">
<form id="siteSubmitForm" method="POST" action="{% url 'armis_import' %}">
{% csrf_token %}
<div class="split-content">
<div class="checkbox-container">
{% for site_id, site_info in armis_sites.items %}
<div class="checkbox-item">
<input type="checkbox" id="site-{{ site_id }}" name="site-ids[]" value="{{ site_id }}" class="custom-checkbox">
<label for="site-{{ site_id }}" class="checkbox-label">{{ site_info.name }}</label>
</div>
{% endfor %}
<div class="split-content">
<div class="checkbox-container">
<div class="checkbox-item">
<input type="checkbox" id="select-all" class="custom-checkbox">
<label for="select-all" class="checkbox-label">Select all</label>
</div>
<div
{% for site_id, site_info in armis_sites.items %}
<div class="checkbox-item">
<input type="checkbox" id="site-{{ site_id }}" name="site-ids[]" value="{{ site_id }}" class="custom-checkbox">
<label for="site-{{ site_id }}" class="checkbox-label">{{ site_info.name }}</label>
</div>
{% endfor %}
</div>
</div>
</div>
<button type="submit" class="btn btn-secondary btn-sm">Select</button>
</form>
</div>
{%if devices%}
{%if devices %}
<div id="pagination">
<button id="prevPage" class="link-button">
<span class="icon-desc" desc="previous page"><img src="{% static 'icons/caret-left-square.svg' %}" alt="previous page" height="16" width="16" class="icon-zoom"/> </span>
Expand Down Expand Up @@ -90,7 +105,7 @@ <h1>Armis Import</h1>
</style>
<tr>
<th scope="col">
<input type="text" id="searchInput" placeholder="Search Name, IP, or MAC">
<input type="text" id="searchInput" placeholder="Search for Name, IP, Mac or Site" style="width: 300px;">
</th>
<th scope="col"></th>
<th scope="col"></th>
Expand All @@ -108,6 +123,7 @@ <h1>Armis Import</h1>
<th scope="col">Device Name <span class="sort-icon" data-column="name">▲</span></th>
<th scope="col">MAC Address <span class="sort-icon" data-column="macAddress">▲</span></th>
<th scope="col">IP Address <span class="sort-icon" data-column="ipAddress">▲</span></th>
<th scope="col">Site <span class="sort-icon" data-column="site">▲</span></th>
<th scope="col">Boundaries</th>
<th scope="col"></th>
</tr>
Expand All @@ -123,6 +139,10 @@ <h2 id="modalTitle"></h2>
<ul id="modalList"></ul>
</div>
</div>
{% elif not display %}
<div class="alert alert-info" role="alert">
No Devices found...
</div>
{%endif%}

</div>
Expand All @@ -132,18 +152,38 @@ <h2 id="modalTitle"></h2>
let data = [
{% for device in devices %}
{
name: "{{ device.name }}",
name: "{{ device.name }}",
macAddress: "{{ device.macAddress}}",
ipAddress: "{{ device.ipAddress }}",
boundaries: "{{ device.boundaries }}",
url: "{{tenant_url}}/inventory/devices/{{ device.id }}"
site: "{{ device.site.name}}",
url: "{{tenant_url}}/inventory/devices/{{ device.id }}"
},
{% endfor %}
];
let filteredData = [...data]; // copies array to prevent changes on original data
let sortOrder = 'asc';
let searchTerm = '';

document.addEventListener('DOMContentLoaded', function() {
var selectAll = document.getElementById('select-all');
var siteCheckboxes = document.querySelectorAll('.custom-checkbox'); // gets all elements with given CSS-Class

selectAll.addEventListener('change', function() {
siteCheckboxes.forEach(function(checkbox) {
checkbox.checked = selectAll.checked;
});
});

siteCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
selectAll.checked = Array.from(siteCheckboxes).every(function(checkbox) { // checks select-all checkbox only if every checkbox is selected
return checkbox.checked;
});
});
});
});

document.getElementById('toggleSiteForm').addEventListener('click', function() {
var formContainer = document.getElementById('formContainer');
if (formContainer.style.display === 'none') {
Expand Down Expand Up @@ -228,7 +268,7 @@ <h2 id="modalTitle"></h2>

filteredData = data.filter(function(device) { // filters for devices where the function returns true
const matchesBoundary = !selectedBoundary || device.boundaries.split(',').map(b => b.trim()).includes(selectedBoundary); // sets selectedBoundary to True if boundary isnt set,
const matchesSearch = device.name.toLowerCase().startsWith(searchTerm) || device.name.toLowerCase().includes(searchTerm) ||
const matchesSearch = device.name.toLowerCase().startsWith(searchTerm) || device.name.toLowerCase().includes(searchTerm) || device.site.toLowerCase().replace(" ","").startsWith(searchTerm) ||
device.ipAddress.toLowerCase().replace(/[^0-9]/g, '').startsWith(searchTerm.replace(/[^a-z0-9]/g, '') || false) || // replace everything that is not in 0-9 range
device.macAddress.toLowerCase().replace(/[^a-f0-9]/g, '').startsWith(searchTerm.replace(/[^a-z0-9]/g, '') || false); // replace everything that is not in hex (a-f,0-9) range
return matchesBoundary && matchesSearch; // and-Operator so devices can get filtered by boundary AND searchTerm, otherwise it would filter for either one
Expand Down Expand Up @@ -271,6 +311,7 @@ <h2 id="modalTitle"></h2>
} else {
ipCell.querySelector('span').addEventListener("click", () => showDetails("IP-Adresse", device.ipAddress));
}
row.insertCell().textContent = device.site
row.insertCell().textContent = device.boundaries;

const importCell = row.insertCell();
Expand Down
19 changes: 18 additions & 1 deletion src/tests/test_helper/test_armis.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
get_armis_sites,
_remove_existing_devices,
get_devices, get_tenant_url,
get_single_device,
)
from nac.context_processors import armis_context
from django.core.cache import cache
Expand Down Expand Up @@ -97,7 +98,7 @@ def test_get_devices(mock_remove_existing_devices, mock_config):
assert result == mock_devices
mock_armis_cloud.get_devices.assert_called_once_with(
asq='in:devices site:"TestSite" timeFrame:"7 Days" !networkInterface:(vlans:100,200)',
fields_wanted=['id', 'ipAddress', 'macAddress', 'name', 'boundaries']
fields_wanted=['id', 'ipAddress', 'macAddress', 'name', 'boundaries', 'site']
)
mock_remove_existing_devices.assert_called_once_with(mock_devices)

Expand All @@ -107,6 +108,22 @@ def test_get_tenant_url(mock_config):
assert get_tenant_url() == "https://test_host"


def test_get_single_device(mock_config):
mock_devices = [{'id': '1', 'name': 'Device1'}]
mock_armis_cloud = MagicMock()
mock_armis_cloud.get_devices.return_value = mock_devices

# manually call original function without decorator to prevent argument error
with patch('helper.armis.armis_config', mock_config):
result = get_single_device.__wrapped__(mock_armis_cloud, 'Device1')

assert result == mock_devices
mock_armis_cloud.get_devices.assert_called_once_with(
asq='in:devices name:Device1 timeFrame:"7 Days"',
fields_wanted=['id', 'ipAddress', 'macAddress', 'name', 'boundaries', 'site']
)


@pytest.mark.parametrize("tenant_hostname, validity", [
("", False),
("test_host", True),
Expand Down
11 changes: 8 additions & 3 deletions src/tests/test_views/test_armis_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,22 @@ def test_post_with_site(self, mock_render, mock_get_devices, mock_get_context, a
mock_get_devices.assert_called_once_with(['Site1'])

@patch('nac.subviews.armis.ArmisView._get_context')
@patch('nac.subviews.armis.get_single_device')
@patch('nac.subviews.armis.get_boundaries')
@patch('nac.subviews.armis.render')
def test_post_without_site(self, mock_render, mock_get_context, armis_view, rf):
def test_post_without_site(self, mock_render, mock_get_boundaries, mock_get_single_device, mock_get_context, armis_view, rf):
mock_context = {'armis_sites': {'1': {'name': 'Site1'}}}
mock_get_context.return_value = mock_context

mock_get_single_device.return_value = {'device': 'Default'}
mock_get_boundaries.return_value = {'boundary'}
request = rf.post('/armis/')
armis_view.post(request)

expected_context = {
'armis_sites': {'1': {'name': 'Site1'}},
'display': True,
'selected_sites': ''
'selected_sites': '',
'devices': {'device': 'Default'},
'boundaries': {'boundary'}
}
mock_render.assert_called_once_with(request, armis_view.template_name, expected_context)
Loading