Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • andersharrisson/csentry
  • ics-infrastructure/csentry
2 results
Show changes
Showing
with 1462 additions and 600 deletions
......@@ -46,7 +46,7 @@ def list_items():
@login_required
def _generate_excel_file():
task = current_user.launch_task(
"generate_items_excel_file", func="generate_items_excel_file", timeout=180
"generate_items_excel_file", func="generate_items_excel_file", job_timeout=180
)
db.session.commit()
return utils.redirect_to_job_status(task.id)
......@@ -210,10 +210,6 @@ def attributes_favorites():
@bp.route("/_retrieve_attributes_favorites")
@login_required
def retrieve_attributes_favorites():
if current_user not in db.session:
# If the current user is cached, it won't be in the sqlalchemy session
# Add it to access the user favorite attributes relationship
db.session.add(current_user)
data = [
(
favorite.base64_image(),
......@@ -238,7 +234,7 @@ def attributes(kind):
db.session.add(new_model)
try:
db.session.commit()
except sa.exc.IntegrityError as e:
except sa.exc.IntegrityError:
db.session.rollback()
flash(f"{form.name.data} already exists! {kind} not created.", "error")
else:
......@@ -275,10 +271,6 @@ def update_favorites(kind):
Add or remove the attribute from the favorites when the
checkbox is checked/unchecked in the attributes table
"""
if current_user not in db.session:
# If the current user is cached, it won't be in the sqlalchemy session
# Add it to access the user favorite attributes relationship
db.session.add(current_user)
try:
model = getattr(models, kind)
except AttributeError:
......
......@@ -16,7 +16,7 @@ import rq_dashboard
from flask import Blueprint, render_template, jsonify, g, current_app, abort, request
from flask_login import login_required, current_user
from rq import push_connection, pop_connection
from ..extensions import sentry
from sentry_sdk import last_event_id
from .. import utils
bp = Blueprint("main", __name__)
......@@ -50,13 +50,7 @@ def not_found_error(error):
@bp.app_errorhandler(500)
def internal_error(error):
if current_app.config["SENTRY_DSN"]:
event_id = g.sentry_event_id
public_dsn = sentry.client.get_public_dsn("https")
else:
event_id = ""
public_dsn = ""
return render_template("500.html", event_id=event_id, public_dsn=public_dsn), 500
return (render_template("500.html", sentry_event_id=last_event_id()), 500)
@bp.app_errorhandler(utils.CSEntryError)
......@@ -87,7 +81,7 @@ def modified_static_file(endpoint, values):
def get_redis_connection():
redis_connection = getattr(g, "_redis_connection", None)
if redis_connection is None:
redis_url = current_app.config["REDIS_URL"]
redis_url = current_app.config["RQ_REDIS_URL"]
redis_connection = g._redis_connection = redis.from_url(redis_url)
return redis_connection
......@@ -105,6 +99,7 @@ def pop_rq_connection(exception=None):
@bp.route("/")
@login_required
def index():
"""Return the application index"""
return render_template("index.html")
......
This diff is collapsed.
......@@ -9,7 +9,6 @@ This module defines the network blueprint forms.
:license: BSD 2-Clause, see LICENSE for more details.
"""
import ipaddress
from flask import current_app
from flask_login import current_user
from wtforms import (
......@@ -28,6 +27,7 @@ from ..validators import (
RegexpList,
IPNetwork,
HOST_NAME_RE,
INTERFACE_NAME_RE,
VLAN_NAME_RE,
MAC_ADDRESS_RE,
NoValidateSelectField,
......@@ -56,18 +56,12 @@ def starts_with_hostname(form, field):
def ip_in_network(form, field):
"""Check that the IP is in the network"""
network_id_field = form["network_id"]
network = models.Network.query.get(network_id_field.data)
ip = ipaddress.ip_address(field.data)
if ip not in network.network_ip:
if not network_id_field.data:
raise validators.ValidationError(
f"IP address {ip} is not in network {network.address}"
"Can't validate the IP. No network was selected."
)
# Admin user can create IP outside the defined range
if current_user.is_authenticated and not current_user.is_admin:
if ip < network.first or ip > network.last:
raise validators.ValidationError(
f"IP address {ip} is not in range {network.first} - {network.last}"
)
network = models.Network.query.get(network_id_field.data)
utils.validate_ip(field.data, network)
class DomainForm(CSEntryForm):
......@@ -88,8 +82,8 @@ class NetworkScopeForm(CSEntryForm):
],
)
description = TextAreaField("Description")
first_vlan = IntegerField("First vlan")
last_vlan = IntegerField("Last vlan")
first_vlan = IntegerField("First vlan", validators=[validators.optional()])
last_vlan = IntegerField("Last vlan", validators=[validators.optional()])
supernet = StringField(
"Supernet", validators=[validators.InputRequired(), IPNetwork()]
)
......@@ -111,7 +105,9 @@ class NetworkForm(CSEntryForm):
Unique(models.Network, column="vlan_name"),
],
)
vlan_id = NoValidateSelectField("Vlan id", choices=[])
vlan_id = NoValidateSelectField(
"Vlan id", choices=[], coerce=utils.coerce_to_str_or_none
)
description = TextAreaField("Description")
prefix = NoValidateSelectField("Prefix", choices=[])
address = NoValidateSelectField("Address", choices=[])
......@@ -120,6 +116,7 @@ class NetworkForm(CSEntryForm):
gateway = NoValidateSelectField("Gateway IP", choices=[])
domain_id = SelectField("Domain")
admin_only = BooleanField("Admin only")
sensitive = BooleanField("Sensitive")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
......@@ -129,10 +126,35 @@ class NetworkForm(CSEntryForm):
self.domain_id.choices = utils.get_model_choices(models.Domain, attr="name")
class EditNetworkForm(CSEntryForm):
vlan_name = StringField(
"Vlan name",
description="vlan name must be 3-25 characters long and contain only letters, numbers and dash",
validators=[
validators.InputRequired(),
validators.Regexp(VLAN_NAME_RE),
Unique(models.Network, column="vlan_name"),
],
)
vlan_id = IntegerField("Vlan id")
description = TextAreaField("Description")
address = StringField("Address")
first_ip = StringField("First IP")
last_ip = StringField("Last IP")
gateway = StringField("Gateway IP")
domain_id = SelectField("Domain")
admin_only = BooleanField("Admin only")
sensitive = BooleanField("Sensitive")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.domain_id.choices = utils.get_model_choices(models.Domain, attr="name")
class HostForm(CSEntryForm):
name = StringField(
"Hostname",
description="hostname must be 2-20 characters long and contain only letters, numbers and dash",
description="hostname must be 2-24 characters long and contain only letters, numbers and dash",
validators=[
validators.InputRequired(),
validators.Regexp(HOST_NAME_RE),
......@@ -143,7 +165,11 @@ class HostForm(CSEntryForm):
)
description = TextAreaField("Description")
device_type_id = SelectField("Device Type")
is_ioc = BooleanField("IOC", default=False)
is_ioc = BooleanField(
"IOC",
default=False,
description="This host will be used to run IOCs",
)
ansible_vars = YAMLField(
"Ansible vars",
description="Enter variables in YAML format. See https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html",
......@@ -177,16 +203,17 @@ class InterfaceForm(CSEntryForm):
)
interface_name = StringField(
"Interface name",
description="name must be 2-20 characters long and contain only letters, numbers and dash",
description="name must be 2-29 characters long and contain only letters, numbers and dash",
validators=[
validators.InputRequired(),
validators.Regexp(HOST_NAME_RE),
validators.Regexp(INTERFACE_NAME_RE),
Unique(models.Interface),
starts_with_hostname,
UniqueAccrossModels([models.Cname]),
],
filters=[utils.lowercase_field],
)
interface_description = TextAreaField("Description")
random_mac = BooleanField("Random MAC", default=False)
mac = StringField(
"MAC",
......@@ -198,7 +225,7 @@ class InterfaceForm(CSEntryForm):
)
cnames_string = StringField(
"Cnames",
description="space separated list of cnames (must be 2-20 characters long and contain only letters, numbers and dash)",
description="space separated list of cnames (must be 2-24 characters long and contain only letters, numbers and dash)",
validators=[
validators.Optional(),
RegexpList(HOST_NAME_RE),
......@@ -238,8 +265,14 @@ class CreateVMForm(CSEntryForm):
)
class GenerateZTPConfigForm(CSEntryForm):
pass
class BootProfileForm(CSEntryForm):
boot_profile = SelectField("Boot profile")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.boot_profile.choices = utils.get_choices(
current_app.config["AUTOINSTALL_BOOT_PROFILES"]
)
class AnsibleGroupForm(CSEntryForm):
......@@ -263,9 +296,13 @@ class AnsibleGroupForm(CSEntryForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.children.choices = utils.get_model_choices(
models.AnsibleGroup, attr="name"
)
self.children.choices = [
(str(group.id), group.name)
for group in models.AnsibleGroup.query.order_by(
models.AnsibleGroup.name
).all()
if group.name != "all"
]
self.hosts.choices = utils.get_model_choices(
models.Host, attr="fqdn", order_by="name"
)
This diff is collapsed.
......@@ -23,7 +23,7 @@ def create_index(index, mapping, **kwargs):
"""Create an index with the given mapping"""
if not current_app.elasticsearch:
return
body = {"mappings": {"_doc": {"dynamic": "strict", "properties": mapping}}}
body = {"mappings": {"dynamic": "strict", "properties": mapping}}
current_app.elasticsearch.indices.create(index=index, body=body, **kwargs)
......@@ -37,7 +37,6 @@ def add_to_index(index, body):
id = body.pop("id")
current_app.elasticsearch.index(
index=index,
doc_type="_doc",
id=id,
body=body,
refresh=current_app.config["ELASTICSEARCH_REFRESH"],
......@@ -49,10 +48,7 @@ def remove_from_index(index, id):
if not current_app.elasticsearch:
return
current_app.elasticsearch.delete(
index=index,
doc_type="_doc",
id=id,
refresh=current_app.config["ELASTICSEARCH_REFRESH"],
index=index, id=id, refresh=current_app.config["ELASTICSEARCH_REFRESH"]
)
......@@ -65,13 +61,30 @@ def query_index(index, query, page=1, per_page=20, sort=None):
return [], 0
kwargs = {
"index": index,
"doc_type": "_doc",
"q": query,
"from_": (page - 1) * per_page,
"size": per_page,
}
if sort is not None:
kwargs["sort"] = sort
current_app.logger.debug(f"Search: {kwargs}")
search = current_app.elasticsearch.search(**kwargs)
ids = [int(hit["_id"]) for hit in search["hits"]["hits"]]
return ids, search["hits"]["total"]
return ids, search["hits"]["total"]["value"]
def update_document(index, id, partial_doc):
"""Update partially a document
:param index: elasticsearch index
:param id: document id
:param dict partial_doc: fields to update
"""
if not current_app.elasticsearch:
return
current_app.elasticsearch.update(
index=index,
id=id,
body={"doc": partial_doc},
refresh=current_app.config["ELASTICSEARCH_REFRESH"],
)
......@@ -11,7 +11,6 @@ This module implements the app default settings.
"""
import base64
import os
import raven
from pathlib import Path
from datetime import timedelta
......@@ -33,7 +32,7 @@ SESSION_TYPE = "redis"
SESSION_REDIS_URL = "redis://redis:6379/0"
CACHE_TYPE = "redis"
CACHE_REDIS_URL = "redis://redis:6379/1"
REDIS_URL = "redis://redis:6379/2"
RQ_REDIS_URL = "redis://redis:6379/2"
ELASTICSEARCH_URL = "http://elasticsearch:9200"
ELASTICSEARCH_INDEX_SUFFIX = "-dev"
......@@ -42,38 +41,56 @@ ELASTICSEARCH_INDEX_SUFFIX = "-dev"
# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html
ELASTICSEARCH_REFRESH = "false"
LDAP_HOST = "esss.lu.se"
LDAP_BASE_DN = "DC=esss,DC=lu,DC=se"
LDAP_USER_DN = "OU=ESS Users"
LDAP_GROUP_DN = ""
LDAP_HOST = os.environ.get("LDAP_HOST", "esss.lu.se")
LDAP_PORT = int(os.environ.get("LDAP_PORT", 636))
LDAP_USE_SSL = os.environ.get("LDAP_USE_SSL", "true").lower() == "true"
LDAP_BASE_DN = os.environ.get("LDAP_BASE_DN", "DC=esss,DC=lu,DC=se")
LDAP_USER_DN = os.environ.get("LDAP_USER_DN", "")
LDAP_GROUP_DN = os.environ.get("LDAP_GROUP_DN", "")
LDAP_BIND_USER_DN = os.environ.get("LDAP_BIND_USER_DN", "ldapuser")
LDAP_BIND_USER_PASSWORD = os.environ.get("LDAP_BIND_USER_PASSWORD", "secret")
LDAP_USER_RDN_ATTR = "cn"
LDAP_USER_LOGIN_ATTR = "sAMAccountName"
LDAP_ALWAYS_SEARCH_BIND = True
LDAP_USER_OBJECT_FILTER = "(samAccountType=805306368)"
LDAP_GROUP_OBJECT_FILTER = ""
LDAP_USER_SEARCH_SCOPE = "SUBTREE"
LDAP_GROUP_SEARCH_SCOPE = "SUBTREE"
LDAP_GROUP_MEMBERS_ATTR = "member"
LDAP_GET_USER_ATTRIBUTES = ["cn", "sAMAccountName", "mail"]
LDAP_GET_GROUP_ATTRIBUTES = ["cn"]
LDAP_USER_RDN_ATTR = os.environ.get("LDAP_USER_RDN_ATTR", "cn")
LDAP_USER_LOGIN_ATTR = os.environ.get("LDAP_USER_LOGIN_ATTR", "sAMAccountName")
LDAP_ALWAYS_SEARCH_BIND = (
os.environ.get("LDAP_ALWAYS_SEARCH_BIND", "true").lower() == "true"
)
LDAP_USER_OBJECT_FILTER = os.environ.get(
"LDAP_USER_OBJECT_FILTER", "(samAccountType=805306368)"
)
LDAP_GROUP_OBJECT_FILTER = os.environ.get("LDAP_GROUP_OBJECT_FILTER", "")
LDAP_USER_SEARCH_SCOPE = os.environ.get("LDAP_USER_SEARCH_SCOPE", "SUBTREE")
LDAP_GROUP_SEARCH_SCOPE = os.environ.get("LDAP_GROUP_SEARCH_SCOPE", "SUBTREE")
LDAP_GROUP_MEMBERS_ATTR = os.environ.get("LDAP_GROUP_MEMBERS_ATTR", "member")
# The following variables should be a list
# Can be passed as space separated string
LDAP_GET_USER_ATTRIBUTES = os.environ.get(
"LDAP_GET_USER_ATTRIBUTES", "cn sAMAccountName mail"
).split()
LDAP_GET_GROUP_ATTRIBUTES = os.environ.get("LDAP_GET_GROUP_ATTRIBUTES", "cn").split()
# Mapping between CSEntry groups and LDAP groups
# The generic "network" group is automatically added based
# on all CSENTRY_DOMAINS_LDAP_GROUPS
# on all CSENTRY_NETWORK_SCOPES_LDAP_GROUPS
CSENTRY_LDAP_GROUPS = {
"admin": ["ICS Control System Infrastructure group"],
"auditor": ["ICS Control System Infrastructure group"],
"inventory": ["ICS Employees", "ICS Consultants"],
}
# Domains the user has access to based on LDAP groups
CSENTRY_DOMAINS_LDAP_GROUPS = {
"esss.lu.se": ["ICS Control System Infrastructure group"],
"tn.esss.lu.se": ["ICS Employees", "ICS Consultants"],
"cslab.esss.lu.se": ["ICS Employees", "ICS Consultants"],
# Network scopes the user has access to based on LDAP groups
# Admin users have access to all network scopes (even if not defined here)
CSENTRY_NETWORK_SCOPES_LDAP_GROUPS = {
"TechnicalNetwork": ["ICS Employees", "ICS Consultants"],
"LabNetworks": ["ICS Employees", "ICS Consultants"],
}
# List of domains where users can create a VM
ALLOWED_VM_CREATION_DOMAINS = ["cslab.esss.lu.se"]
# List of network scopes where users can create a VM
ALLOWED_VM_CREATION_NETWORK_SCOPES = ["LabNetworks"]
# List of network scopes where users can set the boot profile for physical machines
ALLOWED_SET_BOOT_PROFILE_NETWORK_SCOPES = ["LabNetworks"]
# List of device types for which the boot profile can be set
ALLOWED_SET_BOOT_PROFILE_DEVICE_TYPES = ["PhysicalMachine", "MTCA-AMC"]
# List of existing boot profiles
# Shall be kept in sync with the ics-ans-role-autoinstall Ansible role
AUTOINSTALL_BOOT_PROFILES = ["localboot", "default", "cct", "LCR", "thinclient"]
NETWORK_DEFAULT_PREFIX = 24
# ICS Ids starting with this prefix are considered temporary and can be changed
......@@ -89,6 +106,9 @@ DOCUMENTATION_URL = "http://ics-infrastructure.pages.esss.lu.se/csentry/index.ht
# Shall be set to staging|production|development
CSENTRY_ENVIRONMENT = "staging"
# Maximum number of elements returned per page by an API call
MAX_PER_PAGE = 100
AWX_URL = "https://torn.tn.esss.lu.se"
# AWX dynamic inventory source to update
# Use the id because resource.update requires a number
......@@ -100,7 +120,6 @@ AWX_CORE_SERVICES_UPDATE = "ics-ans-core @ DHCP test"
AWX_CORE_SERVICES_UPDATE_RESOURCE = "job"
AWX_CREATE_VM = "deploy-vm-in-proxmox"
AWX_CREATE_VIOC = "deploy-vm-in-proxmox"
AWX_ZTP_CONFIGURATION = "ics-ans-ztp"
AWX_POST_INSTALL = {
"VIOC": {"esss.lu.se": "", "tn.esss.lu.se": "", "cslab.esss.lu.se": ""},
"VM": {
......@@ -109,6 +128,7 @@ AWX_POST_INSTALL = {
"cslab.esss.lu.se": "customize-LabVM",
},
}
AWX_SET_NETWORK_BOOT_PROFILE = "deploy-autoinstall_server@setboot"
AWX_JOB_ENABLED = False
AWX_VM_CREATION_ENABLED = False
......@@ -123,12 +143,11 @@ VIOC_DISK_CHOICES = [15, 50, 100, 250]
VIOC_OSVERSION_CHOICES = ["centos7"]
# Sentry integration
CSENTRY_RELEASE = raven.fetch_git_sha(Path(__file__).parents[1])
# Leave to empty string to disable sentry integration
SENTRY_DSN = os.environ.get("SENTRY_DSN", "")
SENTRY_USER_ATTRS = ["username"]
SENTRY_CONFIG = {"release": CSENTRY_RELEASE}
# Static local files
CSENTRY_STATIC_DIR = Path(__file__).parent / "static"
CSENTRY_STATIC_FILES = CSENTRY_STATIC_DIR / "files"
RQ_DEFAULT_TIMEOUT = 1800
source diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
......@@ -46,6 +46,13 @@ a.nav-item.nav-link.active, .navbar-nav .nav-item:focus,
color: #ffffff;
}
/* version in navbar */
.navbar-right .navbar-text {
color: #b4ebff;
font-family: 'Titillium Web';
font-weight: 400;
}
/* username */
.navbar-right .dropdown-toggle {
color: #b4ebff;
......
This diff is collapsed.
app/static/favicon.ico

14.7 KiB

$(document).ready(function() {
var attributes_table = $("#attributes_table").DataTable({
"ajax": function(data, callback, settings) {
var kind = $('li a.nav-link.active').text();
$(document).ready(function () {
var attributes_table = $("#attributes_table").DataTable({
ajax: function (data, callback, settings) {
var kind = $("li a.nav-link.active").text();
$.getJSON(
$SCRIPT_ROOT + "/inventory/_retrieve_attributes/" + kind,
function(json) {
function (json) {
callback(json);
});
}
);
},
"order": [[2, 'asc']],
"columnDefs": [
order: [[2, "asc"]],
columnDefs: [
{
"targets": [0],
"orderable": false,
'className': 'text-center align-middle',
"render": function(data, type, row) {
targets: [0],
orderable: false,
className: "text-center align-middle",
render: function (data, type, row) {
// render a checkbox to add/remove the attribute to the user's favorites
var checked = data.favorite ? "checked" : ""
return '<input type="checkbox" value="' + data.id + '" ' + checked + '>'
var checked = data.favorite ? "checked" : "";
return (
'<input type="checkbox" value="' + data.id + '" ' + checked + ">"
);
},
"width": "5%",
width: "5%",
},
{
"targets": [1],
"orderable": false,
"render": function(data, type, row) {
targets: [1],
orderable: false,
render: function (data, type, row) {
// render QR code from base64 string
return '<img class="img-fluid" src="data:image/png;base64,' + data + '">';
return (
'<img class="img-fluid" src="data:image/png;base64,' + data + '">'
);
},
"width": "10%",
}
width: "10%",
},
],
"paging": false
paging: false,
});
// update the user favorites
$("#attributes_table").on('change', 'input[type="checkbox"]', function() {
var kind = $('li a.nav-link.active').text();
$("#attributes_table").on("change", 'input[type="checkbox"]', function () {
var kind = $("li a.nav-link.active").text();
$.ajax({
type: "POST",
url: $SCRIPT_ROOT + "/inventory/_update_favorites/" + kind ,
url: $SCRIPT_ROOT + "/inventory/_update_favorites/" + kind,
data: JSON.stringify({
id: $(this).val(),
checked: this.checked
checked: this.checked,
}),
contentType : 'application/json'
contentType: "application/json",
});
});
var attributes_favorites_table = $("#attributes_favorites_table").DataTable({
"ajax": function(data, callback, settings) {
var attributes_favorites_table = $("#attributes_favorites_table").DataTable({
ajax: function (data, callback, settings) {
$.getJSON(
$SCRIPT_ROOT + "/inventory/_retrieve_attributes_favorites",
function(json) {
function (json) {
callback(json);
});
}
);
},
"order": [[1, 'asc']],
"columnDefs": [
order: [[1, "asc"]],
columnDefs: [
{
"targets": [0],
"orderable": false,
"render": function(data, type, row) {
targets: [0],
orderable: false,
render: function (data, type, row) {
// render QR code from base64 string
return '<img class="img-fluid" src="data:image/png;base64,' + data + '">';
return (
'<img class="img-fluid" src="data:image/png;base64,' + data + '">'
);
},
"width": "10%",
}
width: "10%",
},
],
"paging": false
paging: false,
});
});
This diff is collapsed.
$(document).ready(function() {
$.fn.focusWithoutScrolling = function(){
var x = window.scrollX, y = window.scrollY;
$(document).ready(function () {
$.fn.focusWithoutScrolling = function () {
var x = window.scrollX,
y = window.scrollY;
this.focus();
window.scrollTo(x, y);
return this; //chainability
......@@ -15,11 +15,11 @@ $(document).ready(function() {
// Prevent enter key to submit the form when scanning a label
// and remove the ICS:ics_id: prefix
$("#ics_id").keydown(function(event) {
if(event.keyCode == 13) {
$("#ics_id").keydown(function (event) {
if (event.keyCode == 13) {
event.preventDefault();
var value = $(this).val();
$(this).val(value.replace('CSE:ics_id:', ''));
$(this).val(value.replace("CSE:ics_id:", ""));
$("#serial_number").focus();
return false;
}
......@@ -28,22 +28,21 @@ $(document).ready(function() {
// Prevent enter key to submit the form when scanning serial number
// Focus on the submit button so that scanning the Submit action
// will submit the form (due to the CR sent by the scanner)
$("#serial_number").keydown(function(event) {
if(event.keyCode == 13) {
$("#serial_number").keydown(function (event) {
if (event.keyCode == 13) {
event.preventDefault();
$("#submit").focusWithoutScrolling();
return false;
}
});
$("#clear").click(function() {
$("#clear").click(function () {
// clear all select fields
$("select").val('');
$("select").val("");
});
// Update the stack member field linked to the host when changing it
$("#host_id").on('change', function() {
$("#host_id").on("change", function () {
update_stack_member();
});
});
function flash_alert(message, category) {
var htmlString = '<div class="alert alert-' + category + ' alert-dismissible" role="alert">'
htmlString += '<button type="button" class="close" data-dismiss="alert" aria-label="Close">'
htmlString += '<span aria-hidden="true">&times;</span></button>' + message + '</div>'
var htmlString =
'<div class="alert alert-' + category + ' alert-dismissible" role="alert">';
htmlString +=
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">';
htmlString +=
'<span aria-hidden="true">&times;</span></button>' + message + "</div>";
$(htmlString).prependTo("#mainContent").hide().slideDown();
}
function remove_alerts() {
$(".alert").slideUp("normal", function() {
$(".alert").slideUp("normal", function () {
$(this).remove();
});
}
......@@ -27,35 +30,36 @@ function check_job_status(status_url, $modal) {
dataType: "json",
url: status_url,
cache: false,
success: function(data, status, request) {
success: function (data, status, request) {
switch (data.status) {
case "unknown":
$modal.modal('hide');
$modal.modal("hide");
flash_alert("Unknown job id", "danger");
break;
case "finished":
$modal.modal('hide');
$modal.modal("hide");
switch (data.func_name) {
case "generate_items_excel_file":
window.location.href = $SCRIPT_ROOT + "/static/files/" + data.result;
window.location.href =
$SCRIPT_ROOT + "/static/files/" + data.result;
break;
}
break;
case "failed":
$modal.modal('hide');
$modal.modal("hide");
flash_alert(data.message, "danger");
break;
case "started":
if( data.progress !== null ) {
if (data.progress !== null) {
update_progress_bar(data.progress);
}
default:
// queued/started/deferred
setTimeout(function() {
setTimeout(function () {
check_job_status(status_url, $modal);
}, 500);
}
}
},
});
}
......@@ -63,15 +67,20 @@ function check_job_status(status_url, $modal) {
function update_selectfield(field_id, data, selected_value) {
var $field = $(field_id);
$field.empty();
$.map(data, function(option, index) {
if( option == "None" ) {
$.map(data, function (option, index) {
if (option == "None") {
var text = "";
} else {
var text = option;
}
$field.append($("<option></option>").attr("value", option).text(text));
});
$field.val(selected_value);
if (selected_value == "") {
$field.prop("disabled", true);
} else {
$field.val(selected_value);
$field.prop("disabled", false);
}
}
// Function to populate dynamically the stack_member field
......@@ -82,23 +91,40 @@ function update_stack_member() {
$.getJSON(
$SCRIPT_ROOT + "/inventory/_retrieve_free_stack_members/" + host_id,
{
ics_id: $("#ics_id").val()
ics_id: $("#ics_id").val(),
},
function(json) {
update_selectfield("#stack_member", json.data.stack_members, json.data.selected_member);
function (json) {
update_selectfield(
"#stack_member",
json.data.stack_members,
json.data.selected_member
);
$("#stack_member").prop("disabled", json.data.disabled);
}
);
}
$(document).ready(function() {
// Function to initialize tooltip on DataTables search box
// Only used when using server side processing
function configure_search_tooltip() {
$(".dataTables_filter").tooltip({
title:
"Enter keyword(s) or use wildcard character '*' for partial match. See the help for more information.",
});
// Hide the tooltip when clicking in the search box
$("input[type='search'").focus(function () {
$(".dataTables_filter").tooltip("hide");
});
}
$(document).ready(function () {
// When an invalid input was submitted, the server
// adds the "is-invalid" class to form fields to display
// them in red with an error message
// When starting to type again, we want to remove the error
// message to not confuse the user
$('input[type="text"]').keyup(function(event) {
$('input[type="text"]').keyup(function (event) {
$(this).removeClass("is-invalid");
});
......@@ -106,5 +132,4 @@ $(document).ready(function() {
// for all Select and MultiSelect fields with the
// selectize-default class
$(".selectize-default").selectize();
});
This diff is collapsed.
$(document).ready(function() {
var domains_table = $("#domains_table").DataTable({
"paging": false
$(document).ready(function () {
var domains_table = $("#domains_table").DataTable({
paging: false,
});
});
$(document).ready(function() {
$(document).ready(function () {
// Populate the stack member field linked to the host on first page load
update_stack_member();
// Update the stack member field linked to the host when changing it
$("#host_id").on('change', function() {
$("#host_id").on("change", function () {
update_stack_member();
});
});