forked from mirrors/thatmattlove-hyperglass
clean up ui error handling
This commit is contained in:
parent
a5bb782d3e
commit
bf80acadc6
8 changed files with 120 additions and 302 deletions
|
|
@ -18,6 +18,7 @@ from sanic import response
|
|||
from sanic.exceptions import NotFound
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.exceptions import ServiceUnavailable
|
||||
from sanic_limiter import Limiter
|
||||
from sanic_limiter import RateLimitExceeded
|
||||
from sanic_limiter import get_remote_address
|
||||
|
|
@ -29,7 +30,6 @@ from hyperglass.configuration import devices
|
|||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.constants import Supported
|
||||
from hyperglass.constants import code
|
||||
from hyperglass.exceptions import (
|
||||
HyperglassError,
|
||||
AuthError,
|
||||
|
|
@ -97,7 +97,7 @@ count_data = Counter(
|
|||
count_errors = Counter(
|
||||
"count_errors",
|
||||
"Error Counter",
|
||||
["code", "reason", "source", "query_type", "loc_id", "target"],
|
||||
["reason", "source", "query_type", "loc_id", "target"],
|
||||
)
|
||||
|
||||
count_ratelimit = Counter(
|
||||
|
|
@ -126,32 +126,45 @@ async def metrics(request):
|
|||
|
||||
|
||||
@app.exception(InvalidUsage)
|
||||
async def handle_ui_errors(request, exception):
|
||||
"""Renders full error page for invalid URI"""
|
||||
async def handle_frontend_errors(request, exception):
|
||||
"""Handles user-facing feedback related to frontend/input errors"""
|
||||
client_addr = get_remote_address(request)
|
||||
error = exception.args[0]
|
||||
status = error["status"]
|
||||
alert = error["alert"]
|
||||
logger.info(error)
|
||||
count_errors.labels(
|
||||
status,
|
||||
code.get_reason(status),
|
||||
"Front End Error",
|
||||
client_addr,
|
||||
request.json["query_type"],
|
||||
request.json["location"],
|
||||
request.json["target"],
|
||||
request.json.get("query_type"),
|
||||
request.json.get("location"),
|
||||
request.json.get("target"),
|
||||
).inc()
|
||||
logger.error(f'Error: {error["message"]}, Source: {client_addr}')
|
||||
return response.json(
|
||||
{"output": error["message"], "status": status, "keywords": error["keywords"]},
|
||||
status=status,
|
||||
{"output": error["message"], "alert": alert, "keywords": error["keywords"]},
|
||||
status=400,
|
||||
)
|
||||
|
||||
|
||||
@app.exception(ServerError)
|
||||
async def handle_missing(request, exception):
|
||||
"""Renders full error page for invalid URI"""
|
||||
logger.error(f"Error: {exception}")
|
||||
return response.json(exception, status=code.invalid)
|
||||
@app.exception(ServiceUnavailable)
|
||||
async def handle_backend_errors(request, exception):
|
||||
"""Handles user-facing feedback related to backend errors"""
|
||||
client_addr = get_remote_address(request)
|
||||
error = exception.args[0]
|
||||
alert = error["alert"]
|
||||
logger.info(error)
|
||||
count_errors.labels(
|
||||
"Back End Error",
|
||||
client_addr,
|
||||
request.json.get("query_type"),
|
||||
request.json.get("location"),
|
||||
request.json.get("target"),
|
||||
).inc()
|
||||
logger.error(f'Error: {error["message"]}, Source: {client_addr}')
|
||||
return response.json(
|
||||
{"output": error["message"], "alert": alert, "keywords": error["keywords"]},
|
||||
status=503,
|
||||
)
|
||||
|
||||
|
||||
@app.exception(NotFound)
|
||||
|
|
@ -205,12 +218,19 @@ async def site(request):
|
|||
@app.route("/test", methods=["GET"])
|
||||
async def test_route(request):
|
||||
"""Test route for various tests"""
|
||||
html = render_html("results")
|
||||
html = render_html("500")
|
||||
return response.html(html, status=500)
|
||||
|
||||
|
||||
@app.route("/query", methods=["POST"])
|
||||
@limiter.limit(rate_limit_query, error_message="Query")
|
||||
@limiter.limit(
|
||||
rate_limit_query,
|
||||
error_message={
|
||||
"output": params.features.rate_limit.query.message,
|
||||
"alert": "danger",
|
||||
"keywords": [],
|
||||
},
|
||||
)
|
||||
async def hyperglass_main(request):
|
||||
"""
|
||||
Main backend application initiator. Ingests Ajax POST data from
|
||||
|
|
@ -221,27 +241,58 @@ async def hyperglass_main(request):
|
|||
lg_data = request.json
|
||||
logger.debug(f"Unvalidated input: {lg_data}")
|
||||
|
||||
query_location = lg_data.get("location")
|
||||
query_type = lg_data.get("query_type")
|
||||
query_target = lg_data.get("target")
|
||||
|
||||
# Return error if no target is specified
|
||||
if not lg_data["target"]:
|
||||
if not query_target:
|
||||
logger.debug("No input specified")
|
||||
raise handle_missing(request, params.messages.no_input)
|
||||
raise InvalidUsage(
|
||||
{
|
||||
"message": params.messages.no_input.format(
|
||||
query_type=params.branding.text.query_target
|
||||
),
|
||||
"alert": "warning",
|
||||
"keywords": [params.branding.text.query_target],
|
||||
}
|
||||
)
|
||||
|
||||
# Return error if no location is selected
|
||||
if lg_data["location"] not in devices.hostnames:
|
||||
if query_location not in devices.hostnames:
|
||||
logger.debug("No selection specified")
|
||||
raise handle_missing(request, params.messages.no_input)
|
||||
raise InvalidUsage(
|
||||
{
|
||||
"message": params.messages.no_input.format(
|
||||
query_type=params.branding.text.query_location
|
||||
),
|
||||
"alert": "warning",
|
||||
"keywords": [params.branding.text.query_location],
|
||||
}
|
||||
)
|
||||
|
||||
# Return error if no query type is selected
|
||||
if not Supported.is_supported_query(lg_data["query_type"]):
|
||||
if not Supported.is_supported_query(query_type):
|
||||
logger.debug("No query specified")
|
||||
raise handle_missing(request, params.messages.no_input)
|
||||
raise InvalidUsage(
|
||||
{
|
||||
"message": params.messages.no_input.format(
|
||||
query_type=params.branding.text.query_type
|
||||
),
|
||||
"alert": "warning",
|
||||
"keywords": [params.branding.text.query_location],
|
||||
}
|
||||
)
|
||||
|
||||
# Get client IP address for Prometheus logging & rate limiting
|
||||
client_addr = get_remote_address(request)
|
||||
|
||||
# Increment Prometheus counter
|
||||
count_data.labels(
|
||||
client_addr, lg_data["query_type"], lg_data["location"], lg_data["target"]
|
||||
client_addr,
|
||||
lg_data.get("query_type"),
|
||||
lg_data.get("location"),
|
||||
lg_data.get("target"),
|
||||
).inc()
|
||||
|
||||
logger.debug(f"Client Address: {client_addr}")
|
||||
|
|
@ -268,18 +319,15 @@ async def hyperglass_main(request):
|
|||
endtime = time.time()
|
||||
elapsedtime = round(endtime - starttime, 4)
|
||||
logger.debug(f"Query {cache_key} took {elapsedtime} seconds to run.")
|
||||
except (
|
||||
AuthError,
|
||||
RestError,
|
||||
ScrapeError,
|
||||
InputInvalid,
|
||||
InputNotAllowed,
|
||||
DeviceTimeout,
|
||||
) as backend_error:
|
||||
raise InvalidUsage(backend_error.__dict__())
|
||||
except (InputInvalid, InputNotAllowed) as frontend_error:
|
||||
raise InvalidUsage(frontend_error.__dict__())
|
||||
except (AuthError, RestError, ScrapeError, DeviceTimeout) as backend_error:
|
||||
raise ServiceUnavailable(backend_error.__dict__())
|
||||
|
||||
if not cache_value:
|
||||
raise handle_ui_errors(request, params.messages.request_timeout)
|
||||
raise ServerError(
|
||||
{"message": params.messages.general, "alert": "danger", "keywords": []}
|
||||
)
|
||||
|
||||
# Create a cache entry
|
||||
await r_cache.set(cache_key, str(cache_value))
|
||||
|
|
|
|||
|
|
@ -4,221 +4,3 @@ Renders Jinja2 & Sass templates for use by the front end application
|
|||
|
||||
from hyperglass.render.html import render_html
|
||||
from hyperglass.render.webassets import render_assets
|
||||
|
||||
'''
|
||||
# Standard Library Imports
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party Imports
|
||||
import jinja2
|
||||
import sass
|
||||
import yaml
|
||||
from logzero import logger
|
||||
from markdown2 import Markdown
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration import devices
|
||||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import params, networks
|
||||
from hyperglass.exceptions import HyperglassError
|
||||
|
||||
# Module Directories
|
||||
working_directory = Path(__file__).resolve().parent
|
||||
hyperglass_root = working_directory.parent
|
||||
file_loader = jinja2.FileSystemLoader(str(working_directory))
|
||||
env = jinja2.Environment(
|
||||
loader=file_loader, autoescape=True, extensions=["jinja2.ext.autoescape"]
|
||||
)
|
||||
|
||||
default_details = {
|
||||
"footer": """
|
||||
---
|
||||
template: footer
|
||||
---
|
||||
By using {{ branding.site_name }}, you agree to be bound by the following terms of \
|
||||
use: All queries executed on this page are logged for analysis and troubleshooting. \
|
||||
Users are prohibited from automating queries, or attempting to process queries in \
|
||||
bulk. This service is provided on a best effort basis, and {{ general.org_name }} \
|
||||
makes no availability or performance warranties or guarantees whatsoever.
|
||||
""",
|
||||
"bgp_aspath": r"""
|
||||
---
|
||||
template: bgp_aspath
|
||||
title: Supported AS Path Patterns
|
||||
---
|
||||
{{ branding.site_name }} accepts the following `AS_PATH` regular expression patterns:
|
||||
|
||||
| Expression | Match |
|
||||
| :------------------- | :-------------------------------------------- |
|
||||
| `_65000$` | Originated by 65000 |
|
||||
| `^65000_` | Received from 65000 |
|
||||
| `_65000_` | Via 65000 |
|
||||
| `_65000_65001_` | Via 65000 and 65001 |
|
||||
| `_65000(_.+_)65001$` | Anything from 65001 that passed through 65000 |
|
||||
""",
|
||||
"bgp_community": """
|
||||
---
|
||||
template: bgp_community
|
||||
title: BGP Communities
|
||||
---
|
||||
{{ branding.site_name }} makes use of the following BGP communities:
|
||||
|
||||
| Community | Description |
|
||||
| :-------- | :---------- |
|
||||
| `65000:1` | Example 1 |
|
||||
| `65000:2` | Example 2 |
|
||||
| `65000:3` | Example 3 |
|
||||
""",
|
||||
}
|
||||
|
||||
default_info = {
|
||||
"bgp_route": """
|
||||
---
|
||||
template: bgp_route
|
||||
---
|
||||
Performs BGP table lookup based on IPv4/IPv6 prefix.
|
||||
""",
|
||||
"bgp_community": """
|
||||
---
|
||||
template: bgp_community
|
||||
link: <a href="#" id="helplink_bgpc">{{ general.org_name }} BGP Communities</a>
|
||||
---
|
||||
Performs BGP table lookup based on <a href="https://tools.ietf.org/html/rfc4360" target\
|
||||
="_blank">Extended</a> or <a href="https://tools.ietf.org/html/rfc8195" target=\
|
||||
"_blank">Large</a> community value.
|
||||
|
||||
<!-- {{ info["link"] | safe }} -->
|
||||
""",
|
||||
"bgp_aspath": """
|
||||
---
|
||||
template: bgp_aspath
|
||||
link: <a href="#" id="helplink_bgpa">Supported BGP AS Path Expressions</a>
|
||||
---
|
||||
Performs BGP table lookup based on `AS_PATH` regular expression.
|
||||
|
||||
<!-- {{ info["link"] | safe }} -->
|
||||
""",
|
||||
"ping": """
|
||||
---
|
||||
template: ping
|
||||
---
|
||||
Sends 5 ICMP echo requests to the target.
|
||||
""",
|
||||
"traceroute": """
|
||||
---
|
||||
template: traceroute
|
||||
---
|
||||
Performs UDP Based traceroute to the target.<br>For information about how to \
|
||||
interpret traceroute results, <a href="https://hyperglass.readthedocs.io/en/latest/ass\
|
||||
ets/traceroute_nanog.pdf" target="_blank">click here</a>.
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
def generate_markdown(section, file_name):
|
||||
"""
|
||||
Renders markdown as HTML. If file_name exists in appropriate
|
||||
directory, it will be imported and used. If not, the default values
|
||||
will be used. Also renders the Front Matter values within each
|
||||
template.
|
||||
"""
|
||||
if section == "info":
|
||||
file = working_directory.joinpath(f"templates/info/{file_name}.md")
|
||||
defaults = default_info
|
||||
elif section == "details":
|
||||
file = working_directory.joinpath(f"templates/info/details/{file_name}.md")
|
||||
defaults = default_details
|
||||
if file.exists():
|
||||
with file.open(mode="r") as file_raw:
|
||||
yaml_raw = file_raw.read()
|
||||
else:
|
||||
yaml_raw = defaults[file_name]
|
||||
_, frontmatter, content = yaml_raw.split("---", 2)
|
||||
html_classes = {"table": "ui compact table"}
|
||||
markdown = Markdown(
|
||||
extras={
|
||||
"break-on-newline": True,
|
||||
"code-friendly": True,
|
||||
"tables": True,
|
||||
"html-classes": html_classes,
|
||||
}
|
||||
)
|
||||
frontmatter_rendered = (
|
||||
jinja2.Environment(
|
||||
loader=jinja2.BaseLoader,
|
||||
autoescape=True,
|
||||
extensions=["jinja2.ext.autoescape"],
|
||||
)
|
||||
.from_string(frontmatter)
|
||||
.render(params)
|
||||
)
|
||||
if frontmatter_rendered:
|
||||
frontmatter_loaded = yaml.safe_load(frontmatter_rendered)
|
||||
elif not frontmatter_rendered:
|
||||
frontmatter_loaded = {"frontmatter": None}
|
||||
content_rendered = (
|
||||
jinja2.Environment(
|
||||
loader=jinja2.BaseLoader,
|
||||
autoescape=True,
|
||||
extensions=["jinja2.ext.autoescape"],
|
||||
)
|
||||
.from_string(content)
|
||||
.render(params, info=frontmatter_loaded)
|
||||
)
|
||||
help_dict = dict(content=markdown.convert(content_rendered), **frontmatter_loaded)
|
||||
if not help_dict:
|
||||
raise HyperglassError(f"Error reading YAML frontmatter for {file_name}")
|
||||
return help_dict
|
||||
|
||||
|
||||
def html(template_name, **kwargs):
|
||||
"""Renders Jinja2 HTML templates"""
|
||||
details_name_list = ["footer", "bgp_aspath", "bgp_community"]
|
||||
details_dict = {}
|
||||
for details_name in details_name_list:
|
||||
details_data = generate_markdown("details", details_name)
|
||||
details_dict.update({details_name: details_data})
|
||||
info_list = ["bgp_route", "bgp_aspath", "bgp_community", "ping", "traceroute"]
|
||||
info_dict = {}
|
||||
for info_name in info_list:
|
||||
info_data = generate_markdown("info", info_name)
|
||||
info_dict.update({info_name: info_data})
|
||||
try:
|
||||
template_file = f"templates/{template_name}.html.j2"
|
||||
template = env.get_template(template_file)
|
||||
return template.render(
|
||||
params, info=info_dict, details=details_dict, networks=networks, **kwargs
|
||||
)
|
||||
except jinja2.TemplateNotFound as template_error:
|
||||
logger.error(
|
||||
f"Error rendering Jinja2 template {Path(template_file).resolve()}."
|
||||
)
|
||||
raise HyperglassError(template_error)
|
||||
|
||||
|
||||
def css():
|
||||
"""Renders Jinja2 template to Sass file, then compiles Sass as CSS"""
|
||||
scss_file = hyperglass_root.joinpath("static/sass/hyperglass.scss")
|
||||
css_file = hyperglass_root.joinpath("static/css/hyperglass.css")
|
||||
# Renders Jinja2 template as Sass file
|
||||
try:
|
||||
template_file = "templates/hyperglass.scss.j2"
|
||||
template = env.get_template(template_file)
|
||||
rendered_output = template.render(params)
|
||||
with scss_file.open(mode="w") as scss_output:
|
||||
scss_output.write(rendered_output)
|
||||
except jinja2.TemplateNotFound as template_error:
|
||||
logger.error(
|
||||
f"Error rendering Jinja2 template {Path(template_file).resolve()}."
|
||||
)
|
||||
raise HyperglassError(template_error)
|
||||
# Compiles Sass to CSS
|
||||
try:
|
||||
generated_sass = sass.compile(filename=str(scss_file))
|
||||
with css_file.open(mode="w") as css_output:
|
||||
css_output.write(generated_sass)
|
||||
logger.debug(f"Compiled Sass file {scss_file} to CSS file {css_file}.")
|
||||
except sass.CompileError as sassy:
|
||||
logger.error(f"Error compiling Sass in file {scss_file}.")
|
||||
raise HyperglassError(sassy)
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% macro errortext(title, subtitle, button) -%}
|
||||
|
||||
<div class="container d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||
<div class="container-fluid d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||
<div class="jumbotron bg-danger">
|
||||
<h1 class="display-4">{{ title }}</h1>
|
||||
<p class="lead">{{ subtitle }}</p>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<div class="form-row mb-4">
|
||||
<div class="col-md col-sm-12 mb-4 mb-md-0">
|
||||
<select multiple class="form-control form-control-lg hg-select" id="location" data-live-search="true"
|
||||
title="{{ branding.text.location }}">
|
||||
title="{{ branding.text.query_location }}">
|
||||
{% for (netname, loc_params) in networks.items() %}
|
||||
<optgroup label="{{ netname }}">
|
||||
{% for param in loc_params %}
|
||||
|
|
@ -55,8 +55,8 @@
|
|||
<div class="form-row mb-4">
|
||||
<div class="col" id="hg-target-container">
|
||||
<div class="input-group input-group-lg">
|
||||
<input class="form-control" type="text" placeholder="{{ branding.text.query_placeholder }}"
|
||||
aria-label="{{ branding.text.query_placeholder }}" aria-describedby="query_target" id="query_target"
|
||||
<input class="form-control" type="text" placeholder="{{ branding.text.query_target }}"
|
||||
aria-label="{{ branding.text.query_target }}" aria-describedby="query_target" id="query_target"
|
||||
required>
|
||||
<div class="input-group-append" id="hg-target-append">
|
||||
<button class="btn btn-primary" id="hg-submit-button" type="submit">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
<div class="modal fade" tabindex="-1" role="dialog" id="hg-ratelimit-query">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content bg-danger">
|
||||
{# <div class="modal-header">
|
||||
<h4 class="modal-title">{{ features.rate_limit.query.title }}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div> #}
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Jinja2-rendered theme elements
|
||||
$hg-primary: {{ colors.primary }}
|
||||
$hg-secondary: {{ colors.secondary }}
|
||||
$hg-danger: {{ colors.danger }}
|
||||
$hg-warning: {{ colors.warning }}
|
||||
$hg-background: {{ colors.background }}
|
||||
|
||||
$hg-primary: {{ colors.primary }}
|
||||
$hg-secondary: {{ colors.secondary }}
|
||||
$hg-danger: {{ colors.danger }}
|
||||
$hg-warning: {{ colors.warning }}
|
||||
$hg-background: {{ colors.background }}
|
||||
|
||||
@function findTextColor($color)
|
||||
$inverted: invert($color)
|
||||
@if (lightness($color) > 55%)
|
||||
|
|
@ -91,9 +91,9 @@ $font-family-monospace: "{{ font.mono }}", SFMono-Regular, Menlo, Monaco, Co
|
|||
$headings-font-weight: 400
|
||||
|
||||
//// Borders
|
||||
$border-radius: .44rem
|
||||
$border-radius-lg: .4rem
|
||||
$border-radius-sm: .35rem
|
||||
$border-radius: .5rem
|
||||
$border-radius-lg: .7rem
|
||||
$border-radius-sm: .4rem
|
||||
|
||||
//// Popovers
|
||||
$popover-bg: $hg-field-bg
|
||||
|
|
|
|||
|
|
@ -146,14 +146,16 @@ $(document).ready(() => {
|
|||
reloadPage();
|
||||
resultsContainer.hide();
|
||||
$('#hg-ratelimit-query').modal('hide');
|
||||
$('.animsition').animsition({
|
||||
inClass: 'fade-in',
|
||||
outClass: 'fade-out',
|
||||
inDuration: 400,
|
||||
outDuration: 400,
|
||||
transition: (url) => { window.location.href = url; },
|
||||
});
|
||||
formContainer.animsition('in');
|
||||
if (location.pathname == '/') {
|
||||
$('.animsition').animsition({
|
||||
inClass: 'fade-in',
|
||||
outClass: 'fade-out',
|
||||
inDuration: 400,
|
||||
outDuration: 400,
|
||||
transition: (url) => { window.location.href = url; },
|
||||
});
|
||||
formContainer.animsition('in');
|
||||
}
|
||||
});
|
||||
|
||||
const supportedBtn = qt => `<button class="btn btn-secondary hg-info-btn" id="hg-info-btn-${qt}" data-hg-type="${qt}" type="button"><div id="hg-info-icon-${qt}"><i class="remixicon-information-line"></i></div></button>`;
|
||||
|
|
@ -274,19 +276,16 @@ const queryApp = (queryType, queryTypeName, locationList, queryTarget) => {
|
|||
$(`#${loc}-text`).empty().html(displayHtml);
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
const codesDanger = [401, 415, 501, 503, 504];
|
||||
const codesWarning = [405];
|
||||
const statusCode = jqXHR.status;
|
||||
if (textStatus === 'timeout') {
|
||||
timeoutError(loc, inputMessages.request_timeout);
|
||||
} else if (jqXHR.status === 500 && textStatus !== 'timeout') {
|
||||
timeoutError(loc, inputMessages.request_timeout);
|
||||
} else if (codesDanger.includes(jqXHR.status)) {
|
||||
generateError('danger', loc, jqXHR.responseJSON.output);
|
||||
} else if (codesWarning.includes(jqXHR.status)) {
|
||||
generateError('warning', loc, jqXHR.responseJSON.output);
|
||||
} else if (jqXHR.status === 429) {
|
||||
resetResults();
|
||||
$('#hg-ratelimit-query').modal('show');
|
||||
} else if (statusCode === 500 && textStatus !== 'timeout') {
|
||||
timeoutError(loc, inputMessages.request_timeout);
|
||||
} else if ((jqXHR.responseJSON.alert === 'danger') || (jqXHR.responseJSON.alert === 'warning')) {
|
||||
generateError(jqXHR.responseJSON.alertype, loc, jqXHR.responseJSON.output);
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
|
|
@ -396,12 +395,3 @@ $('#hg-ratelimit-query').on('shown.bs.modal', () => {
|
|||
$('#hg-ratelimit-query').find('btn').on('click', () => {
|
||||
$('#hg-ratelimit-query').modal('hide');
|
||||
});
|
||||
|
||||
// Cheap hack for mobile keyboard popping up on a multiple select with live search - see bootstrap-select #1511
|
||||
// $('.bs-searchbox.form-control').on('focus', () => {
|
||||
// if (!bsBlurState) {
|
||||
// console.log('matched cheap hack');
|
||||
// $(this).blur();
|
||||
// bsBlurState = true;
|
||||
// }
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Custom Utility Classes
|
||||
|
||||
@media (min-width: 576px)
|
||||
.mw-sm-25
|
||||
max-width: 25% !important
|
||||
.mw-sm-25
|
||||
max-width: 25% !important
|
||||
|
||||
.mw-sm-50
|
||||
max-width: 50% !important
|
||||
|
|
@ -434,7 +434,7 @@
|
|||
|
||||
.bg-danger
|
||||
hr
|
||||
background-color: darken($hg-danger, 10%)
|
||||
background-color: darken($hg-danger, 10%)
|
||||
|
||||
.modal-body > p
|
||||
padding-left: 0.3rem !important
|
||||
|
|
@ -444,4 +444,8 @@
|
|||
|
||||
.popover-body
|
||||
max-height: 60vh !important
|
||||
overflow-y: auto !important
|
||||
overflow-y: auto !important
|
||||
|
||||
.jumbotron.bg-danger
|
||||
margin-top: 10% !important
|
||||
margin-bottom: 10% !important
|
||||
Loading…
Add table
Reference in a new issue