diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 8257b78..ba505b0 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99565bc..3c5aac0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@ All notable changes to ExaFS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.1.8] - 2025-10-20
+
+### Fixed
+- check all for group edit / delete
+
+### Added
+- pagination for Expired and All Dashboard cards
+- search form quick clear by button or ESC key
+- improved search to work with paginated data
+
## [1.1.7] - 2025-10-16
### Fixed
diff --git a/config.example.py b/config.example.py
index d5994e2..6831062 100644
--- a/config.example.py
+++ b/config.example.py
@@ -3,6 +3,8 @@ class Config:
Default config options
"""
+ # Locale for Babel
+ BABEL_DEFAULT_LOCALE = "en_US_POSIX"
# Limits
FLOWSPEC4_MAX_RULES = 9000
FLOWSPEC6_MAX_RULES = 9000
diff --git a/flowapp/__about__.py b/flowapp/__about__.py
index b12417c..23581c2 100755
--- a/flowapp/__about__.py
+++ b/flowapp/__about__.py
@@ -1,4 +1,4 @@
-__version__ = "1.1.7"
+__version__ = "1.1.8"
__title__ = "ExaFS"
__description__ = "Tool for creation, validation, and execution of ExaBGP messages."
__author__ = "CESNET / Jiri Vrany, Petr Adamec, Josef Verich, Jakub Man"
diff --git a/flowapp/models/utils.py b/flowapp/models/utils.py
index a5e38aa..8270173 100644
--- a/flowapp/models/utils.py
+++ b/flowapp/models/utils.py
@@ -290,69 +290,135 @@ def get_existing_community(name=None):
return community.id if hasattr(community, "id") else None
-def get_ip_rules(rule_type, rule_state, sort="expires", order="desc"):
- """
- Returns list of rules sorted by sort column ordered asc or desc
- :param sort: sorting column
- :param order: asc or desc
- :return: list
- """
+def _get_flowspec4_rules(rule_state, sort="expires", order="desc", page=1, per_page=50, paginate=False):
+ """Get Flowspec4 rules with optional pagination"""
today = datetime.now()
comp_func = utils.get_comp_func(rule_state)
- if rule_type == "ipv4":
- sorter_ip4 = getattr(Flowspec4, sort, Flowspec4.id)
- sorting_ip4 = getattr(sorter_ip4, order)
- if comp_func:
- rules4 = (
- db.session.query(Flowspec4).filter(comp_func(Flowspec4.expires, today)).order_by(sorting_ip4()).all()
- )
- else:
- rules4 = db.session.query(Flowspec4).order_by(sorting_ip4()).all()
+ sorter = getattr(Flowspec4, sort, Flowspec4.id)
+ sorting = getattr(sorter, order)
- return rules4
+ query = db.session.query(Flowspec4)
- if rule_type == "ipv6":
- sorter_ip6 = getattr(Flowspec6, sort, Flowspec6.id)
- sorting_ip6 = getattr(sorter_ip6, order)
- if comp_func:
- rules6 = (
- db.session.query(Flowspec6).filter(comp_func(Flowspec6.expires, today)).order_by(sorting_ip6()).all()
- )
- else:
- rules6 = db.session.query(Flowspec6).order_by(sorting_ip6()).all()
+ if comp_func:
+ query = query.filter(comp_func(Flowspec4.expires, today))
- return rules6
+ query = query.order_by(sorting())
- if rule_type == "rtbh":
- sorter_rtbh = getattr(RTBH, sort, RTBH.id)
- sorting_rtbh = getattr(sorter_rtbh, order)
+ if paginate:
+ pagination = query.paginate(page=page, per_page=per_page, error_out=False, max_per_page=500)
+ return pagination.items, pagination
+ else:
+ return query.all()
- if comp_func:
- rules_rtbh = db.session.query(RTBH).filter(comp_func(RTBH.expires, today)).order_by(sorting_rtbh()).all()
- else:
- rules_rtbh = db.session.query(RTBH).order_by(sorting_rtbh()).all()
+def _get_flowspec6_rules(rule_state, sort="expires", order="desc", page=1, per_page=50, paginate=False):
+ """Get Flowspec6 rules with optional pagination"""
+
+ today = datetime.now()
+ comp_func = utils.get_comp_func(rule_state)
- return rules_rtbh
+ sorter = getattr(Flowspec6, sort, Flowspec6.id)
+ sorting = getattr(sorter, order)
- if rule_type == "whitelist":
- sorter_whitelist = getattr(Whitelist, sort, Whitelist.id)
- sorting_whitelist = getattr(sorter_whitelist, order)
+ query = db.session.query(Flowspec6)
- if comp_func:
- rules_whitelist = (
- db.session.query(Whitelist)
- .filter(comp_func(Whitelist.expires, today))
- .order_by(sorting_whitelist())
- .all()
- )
+ if comp_func:
+ query = query.filter(comp_func(Flowspec6.expires, today))
- else:
- rules_whitelist = db.session.query(Whitelist).order_by(sorting_whitelist()).all()
+ query = query.order_by(sorting())
+
+ if paginate:
+ pagination = query.paginate(page=page, per_page=per_page, error_out=False, max_per_page=500)
+ return pagination.items, pagination
+ else:
+ return query.all()
+
+
+def _get_rtbh_rules(rule_state, sort="expires", order="desc", page=1, per_page=50, paginate=False):
+ """Get RTBH rules with optional pagination"""
+
+ today = datetime.now()
+ comp_func = utils.get_comp_func(rule_state)
+
+ sorter = getattr(RTBH, sort, RTBH.id)
+ sorting = getattr(sorter, order)
- return rules_whitelist
+ query = db.session.query(RTBH)
+
+ if comp_func:
+ query = query.filter(comp_func(RTBH.expires, today))
+
+ query = query.order_by(sorting())
+
+ if paginate:
+ pagination = query.paginate(page=page, per_page=per_page, error_out=False, max_per_page=500)
+ return pagination.items, pagination
+ else:
+ return query.all()
+
+
+def _get_whitelist_rules(rule_state, sort="expires", order="desc", page=1, per_page=50, paginate=False):
+ """Get Whitelist rules with optional pagination"""
+
+ today = datetime.now()
+ comp_func = utils.get_comp_func(rule_state)
+
+ sorter = getattr(Whitelist, sort, Whitelist.id)
+ sorting = getattr(sorter, order)
+
+ query = db.session.query(Whitelist)
+
+ if comp_func:
+ query = query.filter(comp_func(Whitelist.expires, today))
+
+ query = query.order_by(sorting())
+
+ if paginate:
+ pagination = query.paginate(page=page, per_page=per_page, error_out=False, max_per_page=500)
+ return pagination.items, pagination
+ else:
+ return query.all()
+
+
+# Facade function - keeps backward compatibility and config-based routing
+def get_ip_rules(rule_type, rule_state, sort="expires", order="desc", page=1, per_page=50, paginate=False):
+ """
+ Returns list of rules sorted by sort column ordered asc or desc, with optional pagination.
+ This is a facade function that delegates to type-specific handlers.
+
+ Args:
+ rule_type: Type of rule ('ipv4', 'ipv6', 'rtbh', 'whitelist')
+ rule_state: State filter ('active', 'expired', 'all')
+ sort: Column to sort by (default: 'expires')
+ order: Sort order 'asc' or 'desc' (default: 'desc')
+ page: Page number (1-indexed, default: 1)
+ per_page: Number of items per page (default: 50)
+ paginate: If True, return (items, pagination) tuple; if False, return all items
+
+ Returns:
+ If paginate=True: tuple of (list of rules, pagination object)
+ If paginate=False: list of all rules
+ """
+ # Dispatch to appropriate handler
+ handlers = {
+ "ipv4": _get_flowspec4_rules,
+ "ipv6": _get_flowspec6_rules,
+ "rtbh": _get_rtbh_rules,
+ "whitelist": _get_whitelist_rules,
+ }
+
+ handler = handlers.get(rule_type)
+
+ if handler:
+ return handler(rule_state, sort, order, page, per_page, paginate)
+ else:
+ # Unknown rule type
+ if paginate:
+ return [], None
+ else:
+ return []
def get_user_rules_ids(user_id, rule_type):
diff --git a/flowapp/static/js/check_all.js b/flowapp/static/js/check_all.js
new file mode 100644
index 0000000..5df58fc
--- /dev/null
+++ b/flowapp/static/js/check_all.js
@@ -0,0 +1,15 @@
+document.getElementById("check-all").addEventListener("click", function(event){
+ /**
+ * find all checkboxes in current dashboard and toggle checked all / none
+ */
+ const inputs = document.querySelectorAll("input[type='checkbox']");
+ if (this.checked) {
+ for(let minput of inputs) {
+ minput.checked = true;
+ }
+ } else {
+ for(let minput of inputs) {
+ minput.checked = false;
+ }
+ }
+});
\ No newline at end of file
diff --git a/flowapp/static/js/search_helpers.js b/flowapp/static/js/search_helpers.js
new file mode 100644
index 0000000..0712db3
--- /dev/null
+++ b/flowapp/static/js/search_helpers.js
@@ -0,0 +1,18 @@
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelectorAll('.btn-close[data-clear-url]').forEach(btn => {
+ btn.addEventListener('click', function() {
+ window.location.href = this.dataset.clearUrl;
+ });
+ });
+
+ const searchInput = document.querySelector('input[name="squery"]');
+ if (searchInput) {
+ const clearUrl = searchInput.dataset.clearUrl;
+
+ searchInput.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' && this.value && clearUrl) {
+ window.location.href = clearUrl;
+ }
+ });
+ }
+});
diff --git a/flowapp/templates/layouts/default.html b/flowapp/templates/layouts/default.html
index 0463591..4ecbecd 100644
--- a/flowapp/templates/layouts/default.html
+++ b/flowapp/templates/layouts/default.html
@@ -113,5 +113,7 @@
+
+