Repository: apache/airflow

Issue:
Pagination doesn't work with tags filter

**Apache Airflow version**:
2.0.1

**Environment**:

- **OS**: Linux Mint 19.2 
- **Kernel**: 5.5.0-050500-generic #202001262030 SMP Mon Jan 27 01:33:36 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

**What happened**:

Seems that pagination doesn't work. I filter DAGs by tags and get too many results to get them at one page. When I click second page I'm redirected to the first one (actually, it doesn't matter if I click second, last or any other - I'm always getting redirected to the first one).

**What you expected to happen**:

I expect to be redirected to the correct page when I click number on the bottom of the page.

**How to reproduce it**:
1. Create a lot of DAGs with the same tag
2. Filter by tag
3. Go to the next page in the pagination bar

**Implementation example**:



```
from airflow import DAG
from airflow.utils.dates import days_ago
for i in range(200):
    name = 'test_dag_' + str(i)
    dag = DAG(
        dag_id=name,
        schedule_interval=None,
        start_date=days_ago(2),
        tags=['example1'],
    )
    globals()[name] = dag
```



Discussion:
Thanks for opening your first issue here! Be sure to follow the issue template!


Resolving patch:
diff --git a/airflow/www/utils.py b/airflow/www/utils.py
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -69,10 +69,10 @@ def should_hide_value_for_key(key_name):
 
 def get_params(**kwargs):
     """Return URL-encoded params"""
-    return urlencode({d: v for d, v in kwargs.items() if v is not None})
+    return urlencode({d: v for d, v in kwargs.items() if v is not None}, True)
 
 
-def generate_pages(current_page, num_of_pages, search=None, status=None, window=7):
+def generate_pages(current_page, num_of_pages, search=None, status=None, tags=None, window=7):
     """
     Generates the HTML for a paging component using a similar logic to the paging
     auto-generated by Flask managed views. The paging component defines a number of
@@ -81,7 +81,7 @@ def generate_pages(current_page, num_of_pages, search=None, status=None, window=
     current one in the middle of the pager component. When in the last pages,
     the pages won't scroll and just keep moving until the last page. Pager also contains
     <first, previous, ..., next, last> pages.
-    This component takes into account custom parameters such as search and status,
+    This component takes into account custom parameters such as search, status, and tags
     which could be added to the pages link in order to maintain the state between
     client and server. It also allows to make a bookmark on a specific paging state.
 
@@ -89,6 +89,7 @@ def generate_pages(current_page, num_of_pages, search=None, status=None, window=
     :param num_of_pages: the total number of pages
     :param search: the search query string, if any
     :param status: 'all', 'active', or 'paused'
+    :param tags: array of strings of the current filtered tags
     :param window: the number of pages to be shown in the paging component (7 default)
     :return: the HTML string of the paging component
     """
@@ -127,7 +128,9 @@ def generate_pages(current_page, num_of_pages, search=None, status=None, window=
 
     is_disabled = 'disabled' if current_page <= 0 else ''
 
-    first_node_link = void_link if is_disabled else f'?{get_params(page=0, search=search, status=status)}'
+    first_node_link = (
+        void_link if is_disabled else f'?{get_params(page=0, search=search, status=status, tags=tags)}'
+    )
     output.append(
         first_node.format(
             href_link=first_node_link,
@@ -137,7 +140,7 @@ def generate_pages(current_page, num_of_pages, search=None, status=None, window=
 
     page_link = void_link
     if current_page > 0:
-        page_link = f'?{get_params(page=current_page - 1, search=search, status=status)}'
+        page_link = f'?{get_params(page=current_page - 1, search=search, status=status, tags=tags)}'
 
     output.append(previous_node.format(href_link=page_link, disabled=is_disabled))  # noqa
 
@@ -159,7 +162,7 @@ def is_current(current, page):  # noqa
             'is_active': 'active' if is_current(current_page, page) else '',
             'href_link': void_link
             if is_current(current_page, page)
-            else f'?{get_params(page=page, search=search, status=status)}',
+            else f'?{get_params(page=page, search=search, status=status, tags=tags)}',
             'page_num': page + 1,
         }
         output.append(page_node.format(**vals))  # noqa
@@ -169,13 +172,15 @@ def is_current(current, page):  # noqa
     page_link = (
         void_link
         if current_page >= num_of_pages - 1
-        else f'?{get_params(page=current_page + 1, search=search, status=status)}'
+        else f'?{get_params(page=current_page + 1, search=search, status=status, tags=tags)}'
     )
 
     output.append(next_node.format(href_link=page_link, disabled=is_disabled))  # noqa
 
     last_node_link = (
-        void_link if is_disabled else f'?{get_params(page=last_page, search=search, status=status)}'
+        void_link
+        if is_disabled
+        else f'?{get_params(page=last_page, search=search, status=status, tags=tags)}'
     )
     output.append(
         last_node.format(
diff --git a/airflow/www/views.py b/airflow/www/views.py
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -653,6 +653,7 @@ def index(self):
                 num_of_pages,
                 search=escape(arg_search_query) if arg_search_query else None,
                 status=arg_status_filter if arg_status_filter else None,
+                tags=arg_tags_filter if arg_tags_filter else None,
             ),
             num_runs=num_runs,
             tags=tags,


Test patch:
diff --git a/tests/www/test_utils.py b/tests/www/test_utils.py
--- a/tests/www/test_utils.py
+++ b/tests/www/test_utils.py
@@ -128,8 +128,13 @@ def test_params_none_and_zero(self):
         assert ['a=0', 'c=true'] == pairs
 
     def test_params_all(self):
-        query = utils.get_params(status='active', page=3, search='bash_')
-        assert {'page': ['3'], 'search': ['bash_'], 'status': ['active']} == parse_qs(query)
+        query = utils.get_params(tags=['tag1', 'tag2'], status='active', page=3, search='bash_')
+        assert {
+            'tags': ['tag1', 'tag2'],
+            'page': ['3'],
+            'search': ['bash_'],
+            'status': ['active'],
+        } == parse_qs(query)
 
     def test_params_escape(self):
         assert 'search=%27%3E%22%2F%3E%3Cimg+src%3Dx+onerror%3Dalert%281%29%3E' == utils.get_params(