From 1fe8c2e9c582220a732025d60c515202912f1de3 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Wed, 21 Feb 2024 10:45:01 +0000
Subject: [PATCH 01/16] Change admin users search filter

New admin users search is supposed to have better UX to filter users

Changelog: added
---
 .../components/admin_users_filter_app.vue     | 141 ++++++++++++++++++
 app/assets/javascripts/admin/users/index.js   |  13 ++
 app/assets/javascripts/admin/users/router.js  |   6 +
 .../javascripts/pages/admin/users/index.js    |   8 +-
 .../stylesheets/page_bundles/search.scss      |   5 +
 app/views/admin/users/_users.html.haml        |  70 ++-------
 locale/gitlab.pot                             |  18 +--
 7 files changed, 186 insertions(+), 75 deletions(-)
 create mode 100644 app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
 create mode 100644 app/assets/javascripts/admin/users/router.js

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
new file mode 100644
index 0000000000000000..f92c1b136ec40e09
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -0,0 +1,141 @@
+<script>
+import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+
+export const ADMIN_FILTER_TYPES = {
+  Admins: 'admins',
+  Enabled2FA: 'two_factor_enabled',
+  Disabled2FA: 'two_factor_disabled',
+  External: 'external',
+  Blocked: 'blocked',
+  Banned: 'banned',
+  BlockedPendingApproval: 'blocked_pending_approval',
+  Deactivated: 'deactivated',
+  Wop: 'wop',
+  Trusted: 'trusted',
+};
+
+export default {
+  name: 'AdminUsersFilterApp',
+  components: {
+    GlFilteredSearch,
+  },
+  data() {
+    return {
+      filterValue: [],
+      availableTokens: [
+        {
+          title: s__('AdminUsers|Access level'),
+          type: 'admins',
+          token: GlFilteredSearchToken,
+          operators: OPERATORS_IS,
+          unique: true,
+          options: [{ value: ADMIN_FILTER_TYPES.Admins, title: s__('AdminUsers|Administrator') }],
+        },
+        {
+          title: s__('AdminUsers|Two-factor Authentication'),
+          type: '2fa',
+          token: GlFilteredSearchToken,
+          operators: OPERATORS_IS,
+          unique: true,
+          options: [
+            { value: ADMIN_FILTER_TYPES.Enabled2FA, title: __('On') },
+            { value: ADMIN_FILTER_TYPES.Disabled2FA, title: __('Off') },
+          ],
+        },
+        {
+          title: __('Status'),
+          type: 'status',
+          token: GlFilteredSearchToken,
+          operators: OPERATORS_IS,
+          unique: true,
+          options: [
+            { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
+            { value: ADMIN_FILTER_TYPES.Blocked, title: s__('AdminUsers|Blocked') },
+            { value: ADMIN_FILTER_TYPES.Banned, title: s__('AdminUsers|Banned') },
+            {
+              value: ADMIN_FILTER_TYPES.BlockedPendingApproval,
+              title: s__('AdminUsers|Pending approval'),
+            },
+            { value: ADMIN_FILTER_TYPES.Deactivated, title: s__('AdminUsers|Deactivated') },
+            { value: ADMIN_FILTER_TYPES.Wop, title: s__('AdminUsers|Without projects') },
+            { value: ADMIN_FILTER_TYPES.Trusted, title: s__('AdminUsers|Trusted') },
+          ],
+        },
+      ],
+    };
+  },
+  computed: {
+    /**
+     * Currently BE support only one filter at the time
+     * https://gitlab.com/gitlab-org/gitlab/-/issues/254377
+     */
+    filteredAvailableTokens() {
+      const anySelectedFilter = this.filterValue.find((selectedFilter) => {
+        return Object.values(ADMIN_FILTER_TYPES).includes(selectedFilter.value?.data);
+      });
+
+      if (anySelectedFilter) {
+        return this.availableTokens.filter((token) => {
+          return token.options.find((option) => option.value === anySelectedFilter.value?.data);
+        });
+      }
+
+      return this.availableTokens;
+    },
+    isAdminTab() {
+      return this.$route.query.filter === ADMIN_FILTER_TYPES.Admins;
+    },
+  },
+  created() {
+    if (this.$route.query.filter) {
+      const filter = this.availableTokens.find((token) => {
+        return token.options.find((option) => option.value === this.$route.query.filter);
+      });
+
+      if (filter) {
+        this.filterValue.push({
+          type: filter.type,
+          value: {
+            data: this.$route.query.filter,
+            operator: filter.operators[0].value,
+          },
+        });
+      }
+    }
+
+    if (this.$route.query.search_query) {
+      this.filterValue.push(this.$route.query.search_query);
+    }
+  },
+  methods: {
+    handleSearch(filters) {
+      const newUrl = new URL(window.location);
+      newUrl.searchParams.delete('page');
+      newUrl.searchParams.delete('filter');
+      newUrl.searchParams.delete('search_query');
+
+      filters?.forEach((filter) => {
+        if (typeof filter === 'string') {
+          newUrl.searchParams.set('search_query', filter);
+        } else {
+          newUrl.searchParams.set('filter', filter.value.data);
+        }
+      });
+
+      window.location = newUrl;
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-filtered-search
+    v-model="filterValue"
+    class="gl-mb-4"
+    :placeholder="s__('AdminUsers|Search by name, email, or username')"
+    :available-tokens="filteredAvailableTokens"
+    @submit="handleSearch"
+  />
+</template>
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index 2bd37d3fffe6f4de..ec773622796e33b7 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -1,13 +1,18 @@
 import Vue from 'vue';
+import VueRouter from 'vue-router';
 import VueApollo from 'vue-apollo';
 import createDefaultClient from '~/lib/graphql';
 import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 import csrf from '~/lib/utils/csrf';
+import { createRouter } from './router';
 import AdminUsersApp from './components/app.vue';
+import AdminUsersFilterApp from './components/admin_users_filter_app.vue';
 import DeleteUserModal from './components/modals/delete_user_modal.vue';
 import UserActions from './components/user_actions.vue';
 
 Vue.use(VueApollo);
+Vue.use(VueRouter);
+const router = createRouter();
 
 const apolloProvider = new VueApollo({
   defaultClient: createDefaultClient(),
@@ -37,6 +42,14 @@ const initApp = (el, component, userPropKey, props = {}) => {
 export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) =>
   initApp(el, AdminUsersApp, 'users');
 
+export const initAdminUsersFilterApp = () => {
+  return new Vue({
+    el: document.querySelector('#js-admin-users-filter-app'),
+    router,
+    render: (createElement) => createElement(AdminUsersFilterApp),
+  }).$mount();
+};
+
 export const initAdminUserActions = (el = document.querySelector('#js-admin-user-actions')) =>
   initApp(el, UserActions, 'user', { showButtonLabels: true });
 
diff --git a/app/assets/javascripts/admin/users/router.js b/app/assets/javascripts/admin/users/router.js
new file mode 100644
index 0000000000000000..5b1621d98bf78784
--- /dev/null
+++ b/app/assets/javascripts/admin/users/router.js
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+Vue.use(VueRouter);
+
+export const createRouter = () => new VueRouter({ mode: 'history' });
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index 41e99a3baf5415f7..1192cd7cb55a2ddb 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -1,7 +1,13 @@
-import { initAdminUsersApp, initDeleteUserModals, initAdminUserActions } from '~/admin/users';
+import {
+  initAdminUsersApp,
+  initAdminUsersFilterApp,
+  initDeleteUserModals,
+  initAdminUserActions,
+} from '~/admin/users';
 import initConfirmModal from '~/confirm_modal';
 
 initAdminUsersApp();
+initAdminUsersFilterApp();
 initAdminUserActions();
 initDeleteUserModals();
 initConfirmModal();
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 0bbb4d05c2de3a45..17c2a98d494d2c8b 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -387,3 +387,8 @@ input[type='search'] {
   }
 }
 /* stylelint-enable property-no-vendor-prefix */
+
+.admin-users-search {
+  display: grid;
+  grid-template-columns: 1fr auto;
+}
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 3daf3fe19227663e..fed8196fc65972fd 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,66 +7,18 @@
     - c.with_body do
       = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
 
-.top-area
-  .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
-    %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
-      = sprite_icon('chevron-lg-left', size: 12)
-    %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
-      = sprite_icon('chevron-lg-right', size: 12)
-    = gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full' }) do
-      = gl_tab_link_to admin_users_path, { item_active: active_when(params[:filter].nil?), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Active')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.active_without_ghosts))
-      = gl_tab_link_to admin_users_path(filter: "admins"), { item_active: active_when(params[:filter] == 'admins'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Admins')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.admins))
-      = gl_tab_link_to admin_users_path(filter: 'two_factor_enabled'), { item_active: active_when(params[:filter] == 'two_factor_enabled'), class: 'filter-two-factor-enabled gl-border-0!' } do
-        = s_('AdminUsers|2FA Enabled')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.with_two_factor))
-      = gl_tab_link_to admin_users_path(filter: 'two_factor_disabled'), { item_active: active_when(params[:filter] == 'two_factor_disabled'), class: 'filter-two-factor-disabled gl-border-0!' } do
-        = s_('AdminUsers|2FA Disabled')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.without_two_factor))
-      = gl_tab_link_to admin_users_path(filter: 'external'), { item_active: active_when(params[:filter] == 'external'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|External')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.external))
-      = gl_tab_link_to admin_users_path(filter: "blocked"), { item_active: active_when(params[:filter] == 'blocked'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Blocked')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.blocked))
-      = gl_tab_link_to admin_users_path(filter: "banned"), { item_active: active_when(params[:filter] == 'banned'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Banned')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.banned))
-      = gl_tab_link_to admin_users_path(filter: "blocked_pending_approval"), { item_active: active_when(params[:filter] == 'blocked_pending_approval'), class: 'filter-blocked-pending-approval gl-border-0!', data: { testid: 'pending-approval-tab' } } do
-        = s_('AdminUsers|Pending approval')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.blocked_pending_approval))
-      = gl_tab_link_to admin_users_path(filter: "deactivated"), { item_active: active_when(params[:filter] == 'deactivated'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Deactivated')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.deactivated))
-      = gl_tab_link_to admin_users_path(filter: "wop"), { item_active: active_when(params[:filter] == 'wop'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Without projects')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.without_projects))
-      = gl_tab_link_to admin_users_path(filter: "trusted"), { item_active: active_when(params[:filter] == 'trusted'), class: 'gl-border-0!' } do
-        = s_('AdminUsers|Trusted')
-        = gl_tab_counter_badge(limited_counter_with_delimiter(User.trusted))
-  .nav-controls
-    = render_if_exists 'admin/users/admin_email_users'
-    = render_if_exists 'admin/users/admin_export_user_permissions'
-    = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
-      = s_('AdminUsers|New user')
+.top-area.gl-justify-content-end.gl-pr-5.gl-py-3
+  = render_if_exists 'admin/users/admin_email_users'
+  = render_if_exists 'admin/users/admin_export_user_permissions'
+  = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
+    = s_('AdminUsers|New user')
 
-.row-content-block.gl-border-0{ data: { testid: "filtered-search-block" } }
-  = form_tag admin_users_path, method: :get do
-    - if params[:filter].present?
-      = hidden_field_tag "filter", h(params[:filter])
-    .search-holder
-      .search-field-holder
-        = search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email, or username'), class: 'form-control search-text-input js-search-input', spellcheck: false, data: { testid: 'user-search-field' }
-        - if @sort.present?
-          = hidden_field_tag :sort, @sort
-        = sprite_icon('search', css_class: 'search-icon')
-        = button_tag s_('AdminUsers|Search users') if Rails.env.test?
-      .dropdown.gl-sm-ml-3
-        = label_tag s_('AdminUsers|Sort by')
-        = gl_redirect_listbox_tag admin_users_sort_options(filter: params[:filter], search_query: params[:search_query]), @sort, data: { placement: 'right' }
+
+.admin-users-search.row-content-block.border-top-0
+  #js-admin-users-filter-app.gl-mb-4
+  .dropdown.gl-sm-ml-3
+    = label_tag s_('AdminUsers|Sort by')
+    = gl_redirect_listbox_tag admin_users_sort_options(filter: params[:filter], search_query: params[:search_query]), @sort, data: { placement: 'right' }
 
 #js-admin-users-app{ data: admin_users_data_attributes(@users) }
   = render Pajamas::SpinnerComponent.new(size: :lg, class: 'gl-my-7')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c5345c5f4f7e1f91..d11c9f5f62c49bb3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3950,12 +3950,6 @@ msgstr ""
 msgid "AdminUsers|(Pending approval)"
 msgstr ""
 
-msgid "AdminUsers|2FA Disabled"
-msgstr ""
-
-msgid "AdminUsers|2FA Enabled"
-msgstr ""
-
 msgid "AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user. Validated users can use free CI minutes on instance runners."
 msgstr ""
 
@@ -3977,9 +3971,6 @@ msgstr ""
 msgid "AdminUsers|Activate user %{username}?"
 msgstr ""
 
-msgid "AdminUsers|Active"
-msgstr ""
-
 msgid "AdminUsers|Adjust the user cap setting on your instance"
 msgstr ""
 
@@ -3989,9 +3980,6 @@ msgstr ""
 msgid "AdminUsers|Administrator"
 msgstr ""
 
-msgid "AdminUsers|Admins"
-msgstr ""
-
 msgid "AdminUsers|An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted."
 msgstr ""
 
@@ -4196,9 +4184,6 @@ msgstr ""
 msgid "AdminUsers|Search by name, email, or username"
 msgstr ""
 
-msgid "AdminUsers|Search users"
-msgstr ""
-
 msgid "AdminUsers|Send email to users"
 msgstr ""
 
@@ -4253,6 +4238,9 @@ msgstr ""
 msgid "AdminUsers|Trusted"
 msgstr ""
 
+msgid "AdminUsers|Two-factor Authentication"
+msgstr ""
+
 msgid "AdminUsers|Unban user"
 msgstr ""
 
-- 
GitLab


From c033f3a02bef57e06d05651183e8653e17acd3d8 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 5 Mar 2024 17:49:18 +0000
Subject: [PATCH 02/16] Tests for admin_users_filter_app

---
 .../components/admin_users_filter_app.vue     |  33 ++--
 app/views/admin/users/_users.html.haml        |   7 -
 app/views/admin/users/index.html.haml         |   7 +-
 locale/gitlab.pot                             |  12 +-
 spec/features/admin/users/users_spec.rb       |   7 -
 .../components/admin_users_filter_app_spec.js | 143 ++++++++++++++++++
 6 files changed, 174 insertions(+), 35 deletions(-)
 create mode 100644 spec/frontend/admin/users/components/admin_users_filter_app_spec.js

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index f92c1b136ec40e09..247d880351a59c4b 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -1,6 +1,7 @@
 <script>
 import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
 import { s__, __ } from '~/locale';
+import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
 import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
 
 export const ADMIN_FILTER_TYPES = {
@@ -26,15 +27,18 @@ export default {
       filterValue: [],
       availableTokens: [
         {
-          title: s__('AdminUsers|Access level'),
+          title: s__('AdminUsers|access level'),
           type: 'admins',
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
           unique: true,
-          options: [{ value: ADMIN_FILTER_TYPES.Admins, title: s__('AdminUsers|Administrator') }],
+          options: [
+            { value: ADMIN_FILTER_TYPES.Admins, title: s__('AdminUsers|Administrator') },
+            { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
+          ],
         },
         {
-          title: s__('AdminUsers|Two-factor Authentication'),
+          title: s__('AdminUsers|two-factor authentication'),
           type: '2fa',
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
@@ -45,13 +49,12 @@ export default {
           ],
         },
         {
-          title: __('Status'),
-          type: 'status',
+          title: __('state'),
+          type: 'state',
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
           unique: true,
           options: [
-            { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
             { value: ADMIN_FILTER_TYPES.Blocked, title: s__('AdminUsers|Blocked') },
             { value: ADMIN_FILTER_TYPES.Banned, title: s__('AdminUsers|Banned') },
             {
@@ -84,9 +87,6 @@ export default {
 
       return this.availableTokens;
     },
-    isAdminTab() {
-      return this.$route.query.filter === ADMIN_FILTER_TYPES.Admins;
-    },
   },
   created() {
     if (this.$route.query.filter) {
@@ -111,20 +111,18 @@ export default {
   },
   methods: {
     handleSearch(filters) {
-      const newUrl = new URL(window.location);
-      newUrl.searchParams.delete('page');
-      newUrl.searchParams.delete('filter');
-      newUrl.searchParams.delete('search_query');
+      const newParams = {};
 
       filters?.forEach((filter) => {
         if (typeof filter === 'string') {
-          newUrl.searchParams.set('search_query', filter);
+          newParams.search_query = filter;
         } else {
-          newUrl.searchParams.set('filter', filter.value.data);
+          newParams.filter = filter.value.data;
         }
       });
 
-      window.location = newUrl;
+      const newUrl = setUrlParams(newParams, window.location.href, true);
+      visitUrl(newUrl);
     },
   },
 };
@@ -133,9 +131,10 @@ export default {
 <template>
   <gl-filtered-search
     v-model="filterValue"
-    class="gl-mb-4"
     :placeholder="s__('AdminUsers|Search by name, email, or username')"
     :available-tokens="filteredAvailableTokens"
+    class="gl-mb-4"
+    terms-as-tokens
     @submit="handleSearch"
   />
 </template>
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index fed8196fc65972fd..1ab26f0ae9c6f786 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,13 +7,6 @@
     - c.with_body do
       = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
 
-.top-area.gl-justify-content-end.gl-pr-5.gl-py-3
-  = render_if_exists 'admin/users/admin_email_users'
-  = render_if_exists 'admin/users/admin_export_user_permissions'
-  = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
-    = s_('AdminUsers|New user')
-
-
 .admin-users-search.row-content-block.border-top-0
   #js-admin-users-filter-app.gl-mb-4
   .dropdown.gl-sm-ml-3
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 86b777d8458bbf01..3cd4e274accff396 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,6 +1,11 @@
 - page_title _("Users")
 
-= render 'tabs'
+.gl-display-flex.justify-content-between.gl-align-items-center
+  = render 'tabs'
+  = render_if_exists 'admin/users/admin_email_users'
+  = render_if_exists 'admin/users/admin_export_user_permissions'
+  = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
+    = s_('AdminUsers|New user')
 
 .tab-content
   .tab-pane.active
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d11c9f5f62c49bb3..6edadc72d183e0da 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4238,9 +4238,6 @@ msgstr ""
 msgid "AdminUsers|Trusted"
 msgstr ""
 
-msgid "AdminUsers|Two-factor Authentication"
-msgstr ""
-
 msgid "AdminUsers|Unban user"
 msgstr ""
 
@@ -4343,6 +4340,9 @@ msgstr ""
 msgid "AdminUsers|Your GitLab instance has reached the maximum allowed %{user_doc_link} set by an instance admin."
 msgstr ""
 
+msgid "AdminUsers|access level"
+msgstr ""
+
 msgid "AdminUsers|approve them"
 msgstr ""
 
@@ -4352,6 +4352,9 @@ msgstr ""
 msgid "AdminUsers|docs"
 msgstr ""
 
+msgid "AdminUsers|two-factor authentication"
+msgstr ""
+
 msgid "AdminUsers|user cap"
 msgstr ""
 
@@ -61287,6 +61290,9 @@ msgstr ""
 msgid "starts on %{timebox_start_date}"
 msgstr ""
 
+msgid "state"
+msgstr ""
+
 msgid "structure is too large. Maximum size is %{max_size} characters"
 msgstr ""
 
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index d3fe47605174353c..004c0df4ed362a5a 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -78,14 +78,7 @@
 
     describe 'tabs' do
       it 'has multiple tabs to filter users' do
-        expect(page).to have_link('Active', href: admin_users_path)
         expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins'))
-        expect(page).to have_link('2FA Enabled', href: admin_users_path(filter: 'two_factor_enabled'))
-        expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled'))
-        expect(page).to have_link('External', href: admin_users_path(filter: 'external'))
-        expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked'))
-        expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated'))
-        expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop'))
       end
 
       context '`Pending approval` tab' do
diff --git a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
new file mode 100644
index 0000000000000000..1e5a6f2746ae9214
--- /dev/null
+++ b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
@@ -0,0 +1,143 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import { createRouter } from '~/admin/users/router';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
+import AdminUsersFilterApp, {
+  ADMIN_FILTER_TYPES,
+} from '~/admin/users/components/admin_users_filter_app.vue';
+
+const mockFilters = [
+  {
+    type: 'admins',
+    value: { data: 'admins', operator: '=' },
+    id: 1,
+  },
+];
+
+const adminToken = {
+  title: s__('AdminUsers|access level'),
+  type: 'admins',
+  token: GlFilteredSearchToken,
+  operators: OPERATORS_IS,
+  unique: true,
+  options: [
+    { value: ADMIN_FILTER_TYPES.Admins, title: s__('AdminUsers|Administrator') },
+    { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
+  ],
+};
+
+jest.mock('~/lib/utils/url_utility', () => {
+  return {
+    ...jest.requireActual('~/lib/utils/url_utility'),
+    visitUrl: jest.fn(),
+  };
+});
+
+describe('AdminUsersFilterApp', () => {
+  let wrapper;
+
+  const createComponent = ({ router = undefined } = {}) => {
+    wrapper = shallowMount(AdminUsersFilterApp, {
+      router: router || createRouter(),
+    });
+  };
+
+  const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+
+  describe('Mounts GlFilteredSearch with corresponding  filters', () => {
+    it.each`
+      filter
+      ${ADMIN_FILTER_TYPES.Admins}
+      ${ADMIN_FILTER_TYPES.Enabled2FA}
+      ${ADMIN_FILTER_TYPES.Disabled2FA}
+      ${ADMIN_FILTER_TYPES.External}
+      ${ADMIN_FILTER_TYPES.Blocked}
+      ${ADMIN_FILTER_TYPES.Banned}
+      ${ADMIN_FILTER_TYPES.BlockedPendingApproval}
+      ${ADMIN_FILTER_TYPES.Deactivated}
+      ${ADMIN_FILTER_TYPES.Wop}
+      ${ADMIN_FILTER_TYPES.Trusted}
+    `(`includes token with $filter as option`, ({ filter }) => {
+      createComponent();
+      const availableTokens = findFilteredSearch().props('availableTokens');
+      const tokenExists = availableTokens.find((token) => {
+        return token.options?.find((option) => {
+          return option.value === filter;
+        });
+      });
+
+      expect(Boolean(tokenExists)).toBe(true);
+    });
+
+    /**
+     * Currently BE support only one filter at the time
+     * https://gitlab.com/gitlab-org/gitlab/-/issues/254377
+     */
+    it('filters available tokens to one that is chosen', async () => {
+      createComponent();
+      const filteredSearch = findFilteredSearch();
+      filteredSearch.vm.$emit('input', mockFilters);
+      await nextTick();
+      expect(filteredSearch.props('availableTokens')).toEqual([adminToken]);
+    });
+  });
+
+  describe('URL search params', () => {
+    /**
+     * Currently BE support only one filter at the time
+     * https://gitlab.com/gitlab-org/gitlab/-/issues/254377
+     */
+    it('includes the only filter if query param `filter` equals one of available filters', async () => {
+      const router = createRouter();
+      await router.replace({ query: { filter: ADMIN_FILTER_TYPES.Admins } });
+
+      createComponent({ router });
+      const availableTokens = findFilteredSearch().props('availableTokens');
+      expect(availableTokens).toEqual([adminToken]);
+    });
+
+    it('includes all available filters if query param `filter` is not from ADMIN_FILTER_TYPES', async () => {
+      const router = createRouter();
+      await router.replace({ query: { filter: 'filter-that-does-not-exist' } });
+      createComponent({ router });
+      const availableTokens = findFilteredSearch().props('availableTokens');
+
+      // by default we have 3 filters [admin, 2da, state]
+      expect(availableTokens.length).toEqual(3);
+    });
+
+    it('triggers location changes having emitted `submit` event', async () => {
+      createComponent();
+      const filteredSearch = findFilteredSearch();
+      filteredSearch.vm.$emit('submit', mockFilters);
+      await nextTick();
+      expect(visitUrl).toHaveBeenCalledWith(`${getBaseURL()}/?filter=admins`);
+    });
+
+    it('Removes all query param except filter if filter has been changed', async () => {
+      const router = createRouter();
+      await router.replace({
+        query: {
+          page: 2,
+          filter: 'filter-that-does-not-exist',
+        },
+      });
+      createComponent({ router });
+      const filteredSearch = findFilteredSearch();
+      filteredSearch.vm.$emit('submit', mockFilters);
+      await nextTick();
+      expect(visitUrl).toHaveBeenCalledWith(`${getBaseURL()}/?filter=admins`);
+    });
+    it('adds `search_query` if raw text filter was submitted', async () => {
+      createComponent();
+      const filteredSearch = findFilteredSearch();
+      filteredSearch.vm.$emit('submit', ['administrator']);
+      await nextTick();
+      expect(visitUrl).toHaveBeenCalledWith(`${getBaseURL()}/?search_query=administrator`);
+    });
+  });
+});
-- 
GitLab


From 2146514fb97f400ad33074aa1cc63144b6549bc8 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Wed, 6 Mar 2024 13:15:28 +0000
Subject: [PATCH 03/16] fix tests spec/features/admin/users/users_spec.rb

---
 app/views/admin/users/_users.html.haml  |  2 +-
 spec/features/admin/users/users_spec.rb | 70 +++++--------------------
 2 files changed, 14 insertions(+), 58 deletions(-)

diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 1ab26f0ae9c6f786..a119700b16767fc9 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,7 +7,7 @@
     - c.with_body do
       = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
 
-.admin-users-search.row-content-block.border-top-0
+.admin-users-search.row-content-block.border-top-0{ data: { testid: "filtered-search-block" } }
   #js-admin-users-filter-app.gl-mb-4
   .dropdown.gl-sm-ml-3
     = label_tag s_('AdminUsers|Sort by')
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 004c0df4ed362a5a..28ccf40c38a13fc2 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -76,22 +76,6 @@
       end
     end
 
-    describe 'tabs' do
-      it 'has multiple tabs to filter users' do
-        expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins'))
-      end
-
-      context '`Pending approval` tab' do
-        before do
-          visit admin_users_path
-        end
-
-        it 'shows the `Pending approval` tab' do
-          expect(page).to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval'))
-        end
-      end
-    end
-
     describe 'search and sort' do
       before_all do
         create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago)
@@ -126,10 +110,7 @@
       end
 
       it 'searches with respect of sorting' do
-        visit admin_users_path(sort: 'name_asc')
-
-        fill_in :search_query, with: 'Foo'
-        click_button('Search users')
+        visit admin_users_path(sort: 'name_asc', search_query: 'Foo')
 
         expect(first_row.text).to include('Foo Bar')
         expect(second_row.text).to include('Foo Baz')
@@ -155,38 +136,19 @@
     end
 
     describe 'Two-factor Authentication filters' do
-      it 'counts users who have enabled 2FA' do
-        create(:user, :two_factor)
-
-        visit admin_users_path
-
-        page.within('.filter-two-factor-enabled .gl-tab-counter-badge') do
-          expect(page).to have_content('1')
-        end
-      end
-
       it 'filters by users who have enabled 2FA' do
         user = create(:user, :two_factor)
 
-        visit admin_users_path
-        click_link '2FA Enabled'
+        visit admin_users_path(filter: 'two_factor_enabled')
 
         expect(page).to have_content(user.email)
+        expect(page.all('[role="row"]').length).to be(2)
       end
 
-      it 'counts users who have not enabled 2FA' do
-        visit admin_users_path
-
-        page.within('.filter-two-factor-disabled .gl-tab-counter-badge') do
-          expect(page).to have_content('2') # Including admin
-        end
-      end
-
-      it 'filters by users who have not enabled 2FA' do
-        visit admin_users_path
-        click_link '2FA Disabled'
+      it 'filters users who have not enabled 2FA' do
+        visit admin_users_path(filter: 'two_factor_disabled')
 
-        expect(page).to have_content(user.email)
+        expect(page.all('[role="row"]').length).to be(3) # Including admin
       end
     end
 
@@ -194,18 +156,15 @@
       it 'counts users who are pending approval' do
         create_list(:user, 2, :blocked_pending_approval)
 
-        visit admin_users_path
+        visit admin_users_path(filter: 'blocked_pending_approval')
 
-        page.within('.filter-blocked-pending-approval .gl-tab-counter-badge') do
-          expect(page).to have_content('2')
-        end
+        expect(page.all('[role="row"]').length).to be(3)
       end
 
       it 'filters by users who are pending approval' do
         user = create(:user, :blocked_pending_approval)
 
-        visit admin_users_path
-        click_link 'Pending approval'
+        visit admin_users_path(filter: 'blocked_pending_approval')
 
         expect(page).to have_content(user.email)
       end
@@ -231,7 +190,7 @@
         expect(page).to have_content('Successfully blocked')
         expect(page).not_to have_content(user.email)
 
-        click_link 'Blocked'
+        visit admin_users_path(filter: 'blocked')
 
         wait_for_requests
 
@@ -269,7 +228,7 @@
         expect(page).to have_content('Successfully deactivated')
         expect(page).not_to have_content(user.email)
 
-        click_link 'Deactivated'
+        visit admin_users_path(filter: 'deactivated')
 
         wait_for_requests
 
@@ -309,9 +268,7 @@
       it 'sends a welcome email and a password reset email to the user upon admin approval', :sidekiq_inline do
         user = create(:user, :blocked_pending_approval, created_by_id: current_user.id)
 
-        visit admin_users_path
-
-        click_link 'Pending approval'
+        visit admin_users_path(filter: 'blocked_pending_approval')
 
         click_user_dropdown_toggle(user.id)
 
@@ -382,8 +339,7 @@
       it 'shows user info', :aggregate_failures do
         user = create(:user, :blocked_pending_approval)
 
-        visit admin_users_path
-        click_link 'Pending approval'
+        visit admin_users_path(filter: 'blocked_pending_approval')
         click_link user.name
 
         expect(page).to have_content(user.name)
-- 
GitLab


From 1ac4f4b0ecc0d64a9c0655ab97d93b6e0c289bfa Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Wed, 6 Mar 2024 16:10:08 +0000
Subject: [PATCH 04/16] fix qa tests

---
 qa/qa/page/admin/overview/users/index.rb              | 11 +++--------
 .../browser_ui/10_govern/login/register_spec.rb       |  4 +++-
 2 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/qa/qa/page/admin/overview/users/index.rb b/qa/qa/page/admin/overview/users/index.rb
index fb1a7c29008fbbe6..81f70e507f92d748 100644
--- a/qa/qa/page/admin/overview/users/index.rb
+++ b/qa/qa/page/admin/overview/users/index.rb
@@ -6,21 +6,16 @@ module Admin
       module Overview
         module Users
           class Index < QA::Page::Base
-            view 'app/views/admin/users/_users.html.haml' do
-              element 'user-search-field'
-              element 'pending-approval-tab'
-            end
-
             view 'app/assets/javascripts/vue_shared/components/users_table/users_table.vue' do
               element 'user-row-content'
             end
 
             def search_user(username)
-              find_element('user-search-field').set(username).send_keys(:return)
+              submit_search_term(username)
             end
 
-            def click_pending_approval_tab
-              click_element 'pending-approval-tab'
+            def choose_pending_approval_filter
+              select_tokens('state', '=', 'Pending approval', submit: true)
             end
 
             def click_user(username)
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb
index c9a7d0a914f4a55c..a12e7e058d1ce8df 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require 'spec_helper'
+
 module QA
   RSpec.shared_examples 'registration and login' do
     it 'allows the user to register and login' do
@@ -157,7 +159,7 @@ def approve_user(user)
         Page::Main::Menu.perform(&:go_to_admin_area)
         Page::Admin::Menu.perform(&:go_to_users_overview)
         Page::Admin::Overview::Users::Index.perform do |index|
-          index.click_pending_approval_tab
+          index.choose_pending_approval_filter
           index.search_user(user.username)
           index.click_user(user.name)
         end
-- 
GitLab


From c97f961e99d720f0279bee005d4f88e338a17607 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Fri, 8 Mar 2024 14:22:29 +0000
Subject: [PATCH 05/16] Fix wording

---
 .../admin/users/components/admin_users_filter_app.vue       | 6 +++---
 .../admin/users/components/admin_users_filter_app_spec.js   | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index 247d880351a59c4b..b5fca8519b08549a 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -27,7 +27,7 @@ export default {
       filterValue: [],
       availableTokens: [
         {
-          title: s__('AdminUsers|access level'),
+          title: s__('AdminUsers|Access Level'),
           type: 'admins',
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
@@ -38,7 +38,7 @@ export default {
           ],
         },
         {
-          title: s__('AdminUsers|two-factor authentication'),
+          title: s__('AdminUsers|Two-factor authentication'),
           type: '2fa',
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
@@ -49,7 +49,7 @@ export default {
           ],
         },
         {
-          title: __('state'),
+          title: __('State'),
           type: 'state',
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
diff --git a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
index 1e5a6f2746ae9214..9265470e5900b7c2 100644
--- a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
+++ b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
@@ -19,7 +19,7 @@ const mockFilters = [
 ];
 
 const adminToken = {
-  title: s__('AdminUsers|access level'),
+  title: s__('AdminUsers|Access Level'),
   type: 'admins',
   token: GlFilteredSearchToken,
   operators: OPERATORS_IS,
-- 
GitLab


From 310a119d4c2b7650b0caea19fff2cd001ff5c4b2 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Fri, 8 Mar 2024 14:52:10 +0000
Subject: [PATCH 06/16] fix blocks borders

---
 .../stylesheets/page_bundles/search.scss      | 25 ++++++++++++++++++-
 app/views/admin/users/_tabs.html.haml         |  2 +-
 app/views/admin/users/_users.html.haml        |  4 +--
 app/views/admin/users/index.html.haml         | 11 ++++----
 locale/gitlab.pot                             | 18 ++++++-------
 5 files changed, 42 insertions(+), 18 deletions(-)

diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 17c2a98d494d2c8b..24378a519a9a3c1b 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -389,6 +389,29 @@ input[type='search'] {
 /* stylelint-enable property-no-vendor-prefix */
 
 .admin-users-search {
-  display: grid;
+  display: block;
   grid-template-columns: 1fr auto;
+
+  @include media-breakpoint-up(lg) {
+    display: grid;
+  }
+
+  .gl-filtered-search-term-input {
+    width: 100%;
+  }
+}
+
+.top-area {
+  &.admin-users-top-area {
+    @include media-breakpoint-down(md) {
+      flex-flow: initial; // override default .top-area behavior
+      flex-wrap: wrap;
+    }
+  }
+
+  .admin-users-nav {
+    @include media-breakpoint-up(lg) {
+      border: 0;
+    }
+  }
 }
diff --git a/app/views/admin/users/_tabs.html.haml b/app/views/admin/users/_tabs.html.haml
index 6c14e1189fe91e61..4b70661609a08502 100644
--- a/app/views/admin/users/_tabs.html.haml
+++ b/app/views/admin/users/_tabs.html.haml
@@ -1,3 +1,3 @@
-= gl_tabs_nav({ class: 'js-users-tabs' }) do
+= gl_tabs_nav({ class: ['js-users-tabs', 'gl-w-full', request.path == admin_users_path ? 'admin-users-nav' : ''].join(' ') }) do
   = gl_tab_link_to s_('AdminUsers|Users'), admin_users_path
   = gl_tab_link_to s_('AdminUsers|Cohorts'), admin_cohorts_path
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index a119700b16767fc9..ae5d371b363d1c1c 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,9 +7,9 @@
     - c.with_body do
       = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
 
-.admin-users-search.row-content-block.border-top-0{ data: { testid: "filtered-search-block" } }
+.admin-users-search.row-content-block.gl-border-bottom-0.gl-border-top-0{ data: { testid: "filtered-search-block" } }
   #js-admin-users-filter-app.gl-mb-4
-  .dropdown.gl-sm-ml-3
+  .dropdown.gl-lg-ml-3
     = label_tag s_('AdminUsers|Sort by')
     = gl_redirect_listbox_tag admin_users_sort_options(filter: params[:filter], search_query: params[:search_query]), @sort, data: { placement: 'right' }
 
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 3cd4e274accff396..66bb361e9c1e4ba4 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,11 +1,12 @@
 - page_title _("Users")
 
-.gl-display-flex.justify-content-between.gl-align-items-center
+.top-area.admin-users-top-area.gl-display-flex.justify-content-between.gl-align-items-center
   = render 'tabs'
-  = render_if_exists 'admin/users/admin_email_users'
-  = render_if_exists 'admin/users/admin_export_user_permissions'
-  = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
-    = s_('AdminUsers|New user')
+  .nav-controls.gl-my-3
+    = render_if_exists 'admin/users/admin_email_users'
+    = render_if_exists 'admin/users/admin_export_user_permissions'
+    = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
+      = s_('AdminUsers|New user')
 
 .tab-content
   .tab-pane.active
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6edadc72d183e0da..b867017f18a1221f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3959,6 +3959,9 @@ msgstr ""
 msgid "AdminUsers|Access Git repositories"
 msgstr ""
 
+msgid "AdminUsers|Access Level"
+msgstr ""
+
 msgid "AdminUsers|Access level"
 msgstr ""
 
@@ -4238,6 +4241,9 @@ msgstr ""
 msgid "AdminUsers|Trusted"
 msgstr ""
 
+msgid "AdminUsers|Two-factor authentication"
+msgstr ""
+
 msgid "AdminUsers|Unban user"
 msgstr ""
 
@@ -4340,9 +4346,6 @@ msgstr ""
 msgid "AdminUsers|Your GitLab instance has reached the maximum allowed %{user_doc_link} set by an instance admin."
 msgstr ""
 
-msgid "AdminUsers|access level"
-msgstr ""
-
 msgid "AdminUsers|approve them"
 msgstr ""
 
@@ -4352,9 +4355,6 @@ msgstr ""
 msgid "AdminUsers|docs"
 msgstr ""
 
-msgid "AdminUsers|two-factor authentication"
-msgstr ""
-
 msgid "AdminUsers|user cap"
 msgstr ""
 
@@ -49083,6 +49083,9 @@ msgstr ""
 msgid "Starts: %{startsAt}"
 msgstr ""
 
+msgid "State"
+msgstr ""
+
 msgid "State your message to activate"
 msgstr ""
 
@@ -61290,9 +61293,6 @@ msgstr ""
 msgid "starts on %{timebox_start_date}"
 msgstr ""
 
-msgid "state"
-msgstr ""
-
 msgid "structure is too large. Maximum size is %{max_size} characters"
 msgstr ""
 
-- 
GitLab


From 52d9464a1925848629dccc904f0bee4be209582b Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 12 Mar 2024 11:51:08 +0100
Subject: [PATCH 07/16] 448885 Reorder filters

---
 .../components/admin_users_filter_app.vue     | 28 +++++++++----------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index b5fca8519b08549a..f94b6833c90d9347 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -37,17 +37,6 @@ export default {
             { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
           ],
         },
-        {
-          title: s__('AdminUsers|Two-factor authentication'),
-          type: '2fa',
-          token: GlFilteredSearchToken,
-          operators: OPERATORS_IS,
-          unique: true,
-          options: [
-            { value: ADMIN_FILTER_TYPES.Enabled2FA, title: __('On') },
-            { value: ADMIN_FILTER_TYPES.Disabled2FA, title: __('Off') },
-          ],
-        },
         {
           title: __('State'),
           type: 'state',
@@ -55,15 +44,26 @@ export default {
           operators: OPERATORS_IS,
           unique: true,
           options: [
-            { value: ADMIN_FILTER_TYPES.Blocked, title: s__('AdminUsers|Blocked') },
             { value: ADMIN_FILTER_TYPES.Banned, title: s__('AdminUsers|Banned') },
+            { value: ADMIN_FILTER_TYPES.Blocked, title: s__('AdminUsers|Blocked') },
+            { value: ADMIN_FILTER_TYPES.Deactivated, title: s__('AdminUsers|Deactivated') },
             {
               value: ADMIN_FILTER_TYPES.BlockedPendingApproval,
               title: s__('AdminUsers|Pending approval'),
             },
-            { value: ADMIN_FILTER_TYPES.Deactivated, title: s__('AdminUsers|Deactivated') },
-            { value: ADMIN_FILTER_TYPES.Wop, title: s__('AdminUsers|Without projects') },
             { value: ADMIN_FILTER_TYPES.Trusted, title: s__('AdminUsers|Trusted') },
+            { value: ADMIN_FILTER_TYPES.Wop, title: s__('AdminUsers|Without projects') },
+          ],
+        },
+        {
+          title: s__('AdminUsers|Two-factor authentication'),
+          type: '2fa',
+          token: GlFilteredSearchToken,
+          operators: OPERATORS_IS,
+          unique: true,
+          options: [
+            { value: ADMIN_FILTER_TYPES.Enabled2FA, title: __('On') },
+            { value: ADMIN_FILTER_TYPES.Disabled2FA, title: __('Off') },
           ],
         },
       ],
-- 
GitLab


From d8e43f413b85e602fbbd6feb43678db633e4e2a1 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Thu, 21 Mar 2024 18:16:42 +0100
Subject: [PATCH 08/16] Refactor based on MR comments

---
 .../components/admin_users_filter_app.vue     | 85 ++++++++++---------
 .../admin/users/components/filter_types.js    | 27 ++++++
 .../stylesheets/page_bundles/search.scss      |  1 -
 app/helpers/users_helper.rb                   |  6 ++
 app/views/admin/users/_tabs.html.haml         |  2 +-
 app/views/admin/users/_users.html.haml        |  2 +-
 .../components/admin_users_filter_app_spec.js | 50 ++++++-----
 7 files changed, 104 insertions(+), 69 deletions(-)
 create mode 100644 app/assets/javascripts/admin/users/components/filter_types.js

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index f94b6833c90d9347..a03ee7f3f755665a 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -3,19 +3,22 @@ import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
 import { s__, __ } from '~/locale';
 import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
 import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
-
-export const ADMIN_FILTER_TYPES = {
-  Admins: 'admins',
-  Enabled2FA: 'two_factor_enabled',
-  Disabled2FA: 'two_factor_disabled',
-  External: 'external',
-  Blocked: 'blocked',
-  Banned: 'banned',
-  BlockedPendingApproval: 'blocked_pending_approval',
-  Deactivated: 'deactivated',
-  Wop: 'wop',
-  Trusted: 'trusted',
-};
+import {
+  ALL_FILTER_TYPES,
+  FILTER_TYPE_ADMINS,
+  FILTER_TYPE_ENABLED2FA,
+  FILTER_TYPE_DISABLED2FA,
+  FILTER_TYPE_EXTERNAL,
+  FILTER_TYPE_BLOCKED,
+  FILTER_TYPE_BANNED,
+  FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
+  FILTER_TYPE_DEACTIVATED,
+  FILTER_TYPE_WOP,
+  FILTER_TYPE_TRUSTED,
+  TOKEN_ACCESS_LEVEL,
+  TOKEN_STATE,
+  TOKEN_2FA,
+} from './filter_types';
 
 export default {
   name: 'AdminUsersFilterApp',
@@ -28,77 +31,79 @@ export default {
       availableTokens: [
         {
           title: s__('AdminUsers|Access Level'),
-          type: 'admins',
+          type: TOKEN_ACCESS_LEVEL,
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
           unique: true,
           options: [
-            { value: ADMIN_FILTER_TYPES.Admins, title: s__('AdminUsers|Administrator') },
-            { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
+            { value: FILTER_TYPE_ADMINS, title: s__('AdminUsers|Administrator') },
+            { value: FILTER_TYPE_EXTERNAL, title: s__('AdminUsers|External') },
           ],
         },
         {
           title: __('State'),
-          type: 'state',
+          type: TOKEN_STATE,
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
           unique: true,
           options: [
-            { value: ADMIN_FILTER_TYPES.Banned, title: s__('AdminUsers|Banned') },
-            { value: ADMIN_FILTER_TYPES.Blocked, title: s__('AdminUsers|Blocked') },
-            { value: ADMIN_FILTER_TYPES.Deactivated, title: s__('AdminUsers|Deactivated') },
+            { value: FILTER_TYPE_BANNED, title: s__('AdminUsers|Banned') },
+            { value: FILTER_TYPE_BLOCKED, title: s__('AdminUsers|Blocked') },
+            { value: FILTER_TYPE_DEACTIVATED, title: s__('AdminUsers|Deactivated') },
             {
-              value: ADMIN_FILTER_TYPES.BlockedPendingApproval,
+              value: FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
               title: s__('AdminUsers|Pending approval'),
             },
-            { value: ADMIN_FILTER_TYPES.Trusted, title: s__('AdminUsers|Trusted') },
-            { value: ADMIN_FILTER_TYPES.Wop, title: s__('AdminUsers|Without projects') },
+            { value: FILTER_TYPE_TRUSTED, title: s__('AdminUsers|Trusted') },
+            { value: FILTER_TYPE_WOP, title: s__('AdminUsers|Without projects') },
           ],
         },
         {
           title: s__('AdminUsers|Two-factor authentication'),
-          type: '2fa',
+          type: TOKEN_2FA,
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
           unique: true,
           options: [
-            { value: ADMIN_FILTER_TYPES.Enabled2FA, title: __('On') },
-            { value: ADMIN_FILTER_TYPES.Disabled2FA, title: __('Off') },
+            { value: FILTER_TYPE_ENABLED2FA, title: __('On') },
+            { value: FILTER_TYPE_DISABLED2FA, title: __('Off') },
           ],
         },
       ],
     };
   },
   computed: {
-    /**
-     * Currently BE support only one filter at the time
-     * https://gitlab.com/gitlab-org/gitlab/-/issues/254377
-     */
-    filteredAvailableTokens() {
-      const anySelectedFilter = this.filterValue.find((selectedFilter) => {
-        return Object.values(ADMIN_FILTER_TYPES).includes(selectedFilter.value?.data);
+    queryFilter() {
+      return this.$route.query.filter;
+    },
+    selectedFilter() {
+      return this.filterValue.find((selectedFilter) => {
+        return ALL_FILTER_TYPES.includes(selectedFilter.value?.data);
       });
-
-      if (anySelectedFilter) {
+    },
+    selectedFilterData() {
+      return this.selectedFilter?.value?.data;
+    },
+    filteredAvailableTokens() {
+      if (this.selectedFilterData) {
         return this.availableTokens.filter((token) => {
-          return token.options.find((option) => option.value === anySelectedFilter.value?.data);
+          return token.options.find((option) => option.value === this.selectedFilterData);
         });
       }
-
       return this.availableTokens;
     },
   },
   created() {
-    if (this.$route.query.filter) {
+    if (this.queryFilter) {
       const filter = this.availableTokens.find((token) => {
-        return token.options.find((option) => option.value === this.$route.query.filter);
+        return token.options.find((option) => option.value === this.queryFilter);
       });
 
       if (filter) {
         this.filterValue.push({
           type: filter.type,
           value: {
-            data: this.$route.query.filter,
+            data: this.queryFilter,
             operator: filter.operators[0].value,
           },
         });
diff --git a/app/assets/javascripts/admin/users/components/filter_types.js b/app/assets/javascripts/admin/users/components/filter_types.js
new file mode 100644
index 0000000000000000..9f5630d6378d1021
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/filter_types.js
@@ -0,0 +1,27 @@
+export const FILTER_TYPE_ADMINS = 'admins';
+export const FILTER_TYPE_ENABLED2FA = 'two_factor_enabled';
+export const FILTER_TYPE_DISABLED2FA = 'two_factor_disabled';
+export const FILTER_TYPE_EXTERNAL = 'external';
+export const FILTER_TYPE_BLOCKED = 'blocked';
+export const FILTER_TYPE_BANNED = 'banned';
+export const FILTER_TYPE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
+export const FILTER_TYPE_DEACTIVATED = 'deactivated';
+export const FILTER_TYPE_WOP = 'wop';
+export const FILTER_TYPE_TRUSTED = 'trusted';
+
+export const ALL_FILTER_TYPES = [
+  FILTER_TYPE_ADMINS,
+  FILTER_TYPE_ENABLED2FA,
+  FILTER_TYPE_DISABLED2FA,
+  FILTER_TYPE_EXTERNAL,
+  FILTER_TYPE_BLOCKED,
+  FILTER_TYPE_BANNED,
+  FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
+  FILTER_TYPE_DEACTIVATED,
+  FILTER_TYPE_WOP,
+  FILTER_TYPE_TRUSTED,
+];
+
+export const TOKEN_ACCESS_LEVEL = 'access_level';
+export const TOKEN_STATE = 'state';
+export const TOKEN_2FA = '2fa';
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 24378a519a9a3c1b..74b6a563226704e2 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -389,7 +389,6 @@ input[type='search'] {
 /* stylelint-enable property-no-vendor-prefix */
 
 .admin-users-search {
-  display: block;
   grid-template-columns: 1fr auto;
 
   @include media-breakpoint-up(lg) {
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index c0658859cc1557e2..c364d41554d13cd3 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -350,6 +350,12 @@ def localized_user_roles
   def preload_project_associations(_)
     # Overridden in EE
   end
+
+  def admin_user_tab_classes
+    return 'js-users-tabs gl-w-full' unless request.path == admin_users_path
+
+    'js-users-tabs gl-w-full admin-users-nav'
+  end
 end
 
 UsersHelper.prepend_mod_with('UsersHelper')
diff --git a/app/views/admin/users/_tabs.html.haml b/app/views/admin/users/_tabs.html.haml
index 4b70661609a08502..bbbd05d79f8af981 100644
--- a/app/views/admin/users/_tabs.html.haml
+++ b/app/views/admin/users/_tabs.html.haml
@@ -1,3 +1,3 @@
-= gl_tabs_nav({ class: ['js-users-tabs', 'gl-w-full', request.path == admin_users_path ? 'admin-users-nav' : ''].join(' ') }) do
+= gl_tabs_nav({ class: admin_user_tab_classes }) do
   = gl_tab_link_to s_('AdminUsers|Users'), admin_users_path
   = gl_tab_link_to s_('AdminUsers|Cohorts'), admin_cohorts_path
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index ae5d371b363d1c1c..3aa4cbd9d0d4a88f 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,7 +7,7 @@
     - c.with_body do
       = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
 
-.admin-users-search.row-content-block.gl-border-bottom-0.gl-border-top-0{ data: { testid: "filtered-search-block" } }
+.admin-users-search.row-content-block.gl-display-block.gl-border-bottom-0.gl-border-top-0{ data: { testid: "filtered-search-block" } }
   #js-admin-users-filter-app.gl-mb-4
   .dropdown.gl-lg-ml-3
     = label_tag s_('AdminUsers|Sort by')
diff --git a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
index 9265470e5900b7c2..0213d9f8f7a123c5 100644
--- a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
+++ b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
@@ -6,27 +6,25 @@ import { s__ } from '~/locale';
 import { createRouter } from '~/admin/users/router';
 import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
 import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
-import AdminUsersFilterApp, {
-  ADMIN_FILTER_TYPES,
-} from '~/admin/users/components/admin_users_filter_app.vue';
+import AdminUsersFilterApp from '~/admin/users/components/admin_users_filter_app.vue';
 
 const mockFilters = [
   {
-    type: 'admins',
+    type: 'access_level',
     value: { data: 'admins', operator: '=' },
     id: 1,
   },
 ];
 
-const adminToken = {
+const accessLevelToken = {
   title: s__('AdminUsers|Access Level'),
-  type: 'admins',
+  type: 'access_level',
   token: GlFilteredSearchToken,
   operators: OPERATORS_IS,
   unique: true,
   options: [
-    { value: ADMIN_FILTER_TYPES.Admins, title: s__('AdminUsers|Administrator') },
-    { value: ADMIN_FILTER_TYPES.External, title: s__('AdminUsers|External') },
+    { value: 'admins', title: s__('AdminUsers|Administrator') },
+    { value: 'external', title: s__('AdminUsers|External') },
   ],
 };
 
@@ -47,23 +45,24 @@ describe('AdminUsersFilterApp', () => {
   };
 
   const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+  const findAvailableTokens = () => findFilteredSearch().props('availableTokens');
 
   describe('Mounts GlFilteredSearch with corresponding  filters', () => {
     it.each`
       filter
-      ${ADMIN_FILTER_TYPES.Admins}
-      ${ADMIN_FILTER_TYPES.Enabled2FA}
-      ${ADMIN_FILTER_TYPES.Disabled2FA}
-      ${ADMIN_FILTER_TYPES.External}
-      ${ADMIN_FILTER_TYPES.Blocked}
-      ${ADMIN_FILTER_TYPES.Banned}
-      ${ADMIN_FILTER_TYPES.BlockedPendingApproval}
-      ${ADMIN_FILTER_TYPES.Deactivated}
-      ${ADMIN_FILTER_TYPES.Wop}
-      ${ADMIN_FILTER_TYPES.Trusted}
+      ${'admins'}
+      ${'two_factor_enabled'}
+      ${'two_factor_disabled'}
+      ${'external'}
+      ${'blocked'}
+      ${'banned'}
+      ${'blocked_pending_approval'}
+      ${'deactivated'}
+      ${'wop'}
+      ${'trusted'}
     `(`includes token with $filter as option`, ({ filter }) => {
       createComponent();
-      const availableTokens = findFilteredSearch().props('availableTokens');
+      const availableTokens = findAvailableTokens();
       const tokenExists = availableTokens.find((token) => {
         return token.options?.find((option) => {
           return option.value === filter;
@@ -82,7 +81,7 @@ describe('AdminUsersFilterApp', () => {
       const filteredSearch = findFilteredSearch();
       filteredSearch.vm.$emit('input', mockFilters);
       await nextTick();
-      expect(filteredSearch.props('availableTokens')).toEqual([adminToken]);
+      expect(findAvailableTokens()).toEqual([accessLevelToken]);
     });
   });
 
@@ -93,21 +92,20 @@ describe('AdminUsersFilterApp', () => {
      */
     it('includes the only filter if query param `filter` equals one of available filters', async () => {
       const router = createRouter();
-      await router.replace({ query: { filter: ADMIN_FILTER_TYPES.Admins } });
+      await router.replace({ query: { filter: 'admins' } });
 
       createComponent({ router });
-      const availableTokens = findFilteredSearch().props('availableTokens');
-      expect(availableTokens).toEqual([adminToken]);
+      expect(findAvailableTokens()).toEqual([accessLevelToken]);
     });
 
-    it('includes all available filters if query param `filter` is not from ADMIN_FILTER_TYPES', async () => {
+    // all possible filters are listed here app/assets/javascripts/admin/users/components/filter_types.js
+    it('includes all available filters if query param `filter` is not acceptable filter', async () => {
       const router = createRouter();
       await router.replace({ query: { filter: 'filter-that-does-not-exist' } });
       createComponent({ router });
-      const availableTokens = findFilteredSearch().props('availableTokens');
 
       // by default we have 3 filters [admin, 2da, state]
-      expect(availableTokens.length).toEqual(3);
+      expect(findAvailableTokens().length).toEqual(3);
     });
 
     it('triggers location changes having emitted `submit` event', async () => {
-- 
GitLab


From a304c68ed83582ed1e02f27a244b03d67eb2ead1 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 26 Mar 2024 12:17:48 +0100
Subject: [PATCH 09/16] 451605 Replace css class with gitlab-ui util

---
 app/assets/stylesheets/page_bundles/search.scss | 4 ----
 app/views/admin/users/_users.html.haml          | 2 +-
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 74b6a563226704e2..e047ab0ddb9fbff8 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -391,10 +391,6 @@ input[type='search'] {
 .admin-users-search {
   grid-template-columns: 1fr auto;
 
-  @include media-breakpoint-up(lg) {
-    display: grid;
-  }
-
   .gl-filtered-search-term-input {
     width: 100%;
   }
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 3aa4cbd9d0d4a88f..7eda3a8709034e8b 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,7 +7,7 @@
     - c.with_body do
       = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
 
-.admin-users-search.row-content-block.gl-display-block.gl-border-bottom-0.gl-border-top-0{ data: { testid: "filtered-search-block" } }
+.admin-users-search.row-content-block.gl-lg-display-grid.gl-border-bottom-0.gl-border-top-0{ data: { testid: "filtered-search-block" } }
   #js-admin-users-filter-app.gl-mb-4
   .dropdown.gl-lg-ml-3
     = label_tag s_('AdminUsers|Sort by')
-- 
GitLab


From 660e94a5a7603d9838b1cb9bfd66dca6e3883dbe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= <esanz-garcia@gitlab.com>
Date: Tue, 2 Apr 2024 09:23:08 +0000
Subject: [PATCH 10/16] Update wording

---
 .../admin/users/components/admin_users_filter_app.vue           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index a03ee7f3f755665a..ddd2ff6d7a150d14 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -30,7 +30,7 @@ export default {
       filterValue: [],
       availableTokens: [
         {
-          title: s__('AdminUsers|Access Level'),
+          title: s__('AdminUsers|Access level'),
           type: TOKEN_ACCESS_LEVEL,
           token: GlFilteredSearchToken,
           operators: OPERATORS_IS,
-- 
GitLab


From cd74ada9916d912f7958b2334b792e4cf6ec599e Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 2 Apr 2024 11:29:26 +0200
Subject: [PATCH 11/16] Refactor const names

---
 .../admin/users/components/admin_users_filter_app.vue     | 8 ++++----
 .../javascripts/admin/users/components/filter_types.js    | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index ddd2ff6d7a150d14..c06d1a9a05679639 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -6,8 +6,8 @@ import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/consta
 import {
   ALL_FILTER_TYPES,
   FILTER_TYPE_ADMINS,
-  FILTER_TYPE_ENABLED2FA,
-  FILTER_TYPE_DISABLED2FA,
+  FILTER_TYPE_ENABLED_2FA,
+  FILTER_TYPE_DISABLED_2FA,
   FILTER_TYPE_EXTERNAL,
   FILTER_TYPE_BLOCKED,
   FILTER_TYPE_BANNED,
@@ -65,8 +65,8 @@ export default {
           operators: OPERATORS_IS,
           unique: true,
           options: [
-            { value: FILTER_TYPE_ENABLED2FA, title: __('On') },
-            { value: FILTER_TYPE_DISABLED2FA, title: __('Off') },
+            { value: FILTER_TYPE_ENABLED_2FA, title: __('On') },
+            { value: FILTER_TYPE_DISABLED_2FA, title: __('Off') },
           ],
         },
       ],
diff --git a/app/assets/javascripts/admin/users/components/filter_types.js b/app/assets/javascripts/admin/users/components/filter_types.js
index 9f5630d6378d1021..4c8b06a6f451828c 100644
--- a/app/assets/javascripts/admin/users/components/filter_types.js
+++ b/app/assets/javascripts/admin/users/components/filter_types.js
@@ -1,6 +1,6 @@
 export const FILTER_TYPE_ADMINS = 'admins';
-export const FILTER_TYPE_ENABLED2FA = 'two_factor_enabled';
-export const FILTER_TYPE_DISABLED2FA = 'two_factor_disabled';
+export const FILTER_TYPE_ENABLED_2FA = 'two_factor_enabled';
+export const FILTER_TYPE_DISABLED_2FA = 'two_factor_disabled';
 export const FILTER_TYPE_EXTERNAL = 'external';
 export const FILTER_TYPE_BLOCKED = 'blocked';
 export const FILTER_TYPE_BANNED = 'banned';
@@ -11,8 +11,8 @@ export const FILTER_TYPE_TRUSTED = 'trusted';
 
 export const ALL_FILTER_TYPES = [
   FILTER_TYPE_ADMINS,
-  FILTER_TYPE_ENABLED2FA,
-  FILTER_TYPE_DISABLED2FA,
+  FILTER_TYPE_ENABLED_2FA,
+  FILTER_TYPE_DISABLED_2FA,
   FILTER_TYPE_EXTERNAL,
   FILTER_TYPE_BLOCKED,
   FILTER_TYPE_BANNED,
-- 
GitLab


From 73c03d23ff209b20e0093ca1ccfab3f1e8b7d372 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 2 Apr 2024 11:32:31 +0200
Subject: [PATCH 12/16] Move degining available tokens outside of component

---
 .../components/admin_users_filter_app.vue     | 86 ++++++++++---------
 1 file changed, 44 insertions(+), 42 deletions(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index c06d1a9a05679639..faa8cf7a3685452a 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -20,6 +20,49 @@ import {
   TOKEN_2FA,
 } from './filter_types';
 
+const availableTokens = [
+  {
+    title: s__('AdminUsers|Access level'),
+    type: TOKEN_ACCESS_LEVEL,
+    token: GlFilteredSearchToken,
+    operators: OPERATORS_IS,
+    unique: true,
+    options: [
+      { value: FILTER_TYPE_ADMINS, title: s__('AdminUsers|Administrator') },
+      { value: FILTER_TYPE_EXTERNAL, title: s__('AdminUsers|External') },
+    ],
+  },
+  {
+    title: __('State'),
+    type: TOKEN_STATE,
+    token: GlFilteredSearchToken,
+    operators: OPERATORS_IS,
+    unique: true,
+    options: [
+      { value: FILTER_TYPE_BANNED, title: s__('AdminUsers|Banned') },
+      { value: FILTER_TYPE_BLOCKED, title: s__('AdminUsers|Blocked') },
+      { value: FILTER_TYPE_DEACTIVATED, title: s__('AdminUsers|Deactivated') },
+      {
+        value: FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
+        title: s__('AdminUsers|Pending approval'),
+      },
+      { value: FILTER_TYPE_TRUSTED, title: s__('AdminUsers|Trusted') },
+      { value: FILTER_TYPE_WOP, title: s__('AdminUsers|Without projects') },
+    ],
+  },
+  {
+    title: s__('AdminUsers|Two-factor authentication'),
+    type: TOKEN_2FA,
+    token: GlFilteredSearchToken,
+    operators: OPERATORS_IS,
+    unique: true,
+    options: [
+      { value: FILTER_TYPE_ENABLED_2FA, title: __('On') },
+      { value: FILTER_TYPE_DISABLED_2FA, title: __('Off') },
+    ],
+  },
+];
+
 export default {
   name: 'AdminUsersFilterApp',
   components: {
@@ -28,48 +71,7 @@ export default {
   data() {
     return {
       filterValue: [],
-      availableTokens: [
-        {
-          title: s__('AdminUsers|Access level'),
-          type: TOKEN_ACCESS_LEVEL,
-          token: GlFilteredSearchToken,
-          operators: OPERATORS_IS,
-          unique: true,
-          options: [
-            { value: FILTER_TYPE_ADMINS, title: s__('AdminUsers|Administrator') },
-            { value: FILTER_TYPE_EXTERNAL, title: s__('AdminUsers|External') },
-          ],
-        },
-        {
-          title: __('State'),
-          type: TOKEN_STATE,
-          token: GlFilteredSearchToken,
-          operators: OPERATORS_IS,
-          unique: true,
-          options: [
-            { value: FILTER_TYPE_BANNED, title: s__('AdminUsers|Banned') },
-            { value: FILTER_TYPE_BLOCKED, title: s__('AdminUsers|Blocked') },
-            { value: FILTER_TYPE_DEACTIVATED, title: s__('AdminUsers|Deactivated') },
-            {
-              value: FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
-              title: s__('AdminUsers|Pending approval'),
-            },
-            { value: FILTER_TYPE_TRUSTED, title: s__('AdminUsers|Trusted') },
-            { value: FILTER_TYPE_WOP, title: s__('AdminUsers|Without projects') },
-          ],
-        },
-        {
-          title: s__('AdminUsers|Two-factor authentication'),
-          type: TOKEN_2FA,
-          token: GlFilteredSearchToken,
-          operators: OPERATORS_IS,
-          unique: true,
-          options: [
-            { value: FILTER_TYPE_ENABLED_2FA, title: __('On') },
-            { value: FILTER_TYPE_DISABLED_2FA, title: __('Off') },
-          ],
-        },
-      ],
+      availableTokens,
     };
   },
   computed: {
-- 
GitLab


From 24c23999fbb037feb64829d1eb41fe240e010b79 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 2 Apr 2024 11:38:24 +0200
Subject: [PATCH 13/16] Update po files

---
 locale/gitlab.pot                                              | 3 ---
 .../admin/users/components/admin_users_filter_app_spec.js      | 2 +-
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b867017f18a1221f..c64311224dd3d8f8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3959,9 +3959,6 @@ msgstr ""
 msgid "AdminUsers|Access Git repositories"
 msgstr ""
 
-msgid "AdminUsers|Access Level"
-msgstr ""
-
 msgid "AdminUsers|Access level"
 msgstr ""
 
diff --git a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
index 0213d9f8f7a123c5..60b81f6e1e9f247f 100644
--- a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
+++ b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
@@ -17,7 +17,7 @@ const mockFilters = [
 ];
 
 const accessLevelToken = {
-  title: s__('AdminUsers|Access Level'),
+  title: s__('AdminUsers|Access level'),
   type: 'access_level',
   token: GlFilteredSearchToken,
   operators: OPERATORS_IS,
-- 
GitLab


From 48cf798bc10733208dafe46c0a53f5dab240c6ab Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Tue, 2 Apr 2024 11:55:12 +0200
Subject: [PATCH 14/16] Remove availableTokens from components data

---
 .../admin/users/components/admin_users_filter_app.vue      | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index faa8cf7a3685452a..c77d1f59c0144c44 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -71,7 +71,6 @@ export default {
   data() {
     return {
       filterValue: [],
-      availableTokens,
     };
   },
   computed: {
@@ -88,16 +87,16 @@ export default {
     },
     filteredAvailableTokens() {
       if (this.selectedFilterData) {
-        return this.availableTokens.filter((token) => {
+        return availableTokens.filter((token) => {
           return token.options.find((option) => option.value === this.selectedFilterData);
         });
       }
-      return this.availableTokens;
+      return availableTokens;
     },
   },
   created() {
     if (this.queryFilter) {
-      const filter = this.availableTokens.find((token) => {
+      const filter = availableTokens.find((token) => {
         return token.options.find((option) => option.value === this.queryFilter);
       });
 
-- 
GitLab


From d6b4d016fd26f24340fbeaa2edf01b947736ccd8 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Wed, 3 Apr 2024 10:58:35 +0200
Subject: [PATCH 15/16] Remove VueRouter from the app

---
 .../admin/users/components/admin_users_filter_app.vue    | 9 +++++----
 app/assets/javascripts/admin/users/index.js              | 2 --
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index c77d1f59c0144c44..7c9f1fe6d9ab4913 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -1,7 +1,7 @@
 <script>
 import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
 import { s__, __ } from '~/locale';
-import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { setUrlParams, visitUrl, getParameterValues } from '~/lib/utils/url_utility';
 import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
 import {
   ALL_FILTER_TYPES,
@@ -75,7 +75,7 @@ export default {
   },
   computed: {
     queryFilter() {
-      return this.$route.query.filter;
+      return getParameterValues('filter')[0];
     },
     selectedFilter() {
       return this.filterValue.find((selectedFilter) => {
@@ -111,8 +111,9 @@ export default {
       }
     }
 
-    if (this.$route.query.search_query) {
-      this.filterValue.push(this.$route.query.search_query);
+    const [searchQuery] = getParameterValues('search_query');
+    if (searchQuery) {
+      this.filterValue.push(searchQuery);
     }
   },
   methods: {
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index ec773622796e33b7..67f4b825c9ff8ea8 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -1,5 +1,4 @@
 import Vue from 'vue';
-import VueRouter from 'vue-router';
 import VueApollo from 'vue-apollo';
 import createDefaultClient from '~/lib/graphql';
 import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -11,7 +10,6 @@ import DeleteUserModal from './components/modals/delete_user_modal.vue';
 import UserActions from './components/user_actions.vue';
 
 Vue.use(VueApollo);
-Vue.use(VueRouter);
 const router = createRouter();
 
 const apolloProvider = new VueApollo({
-- 
GitLab


From ec46878cb541042fadb9d7cfdc93a8e83c378193 Mon Sep 17 00:00:00 2001
From: Ivan Shtyrliaiev <ee923925@gmail.com>
Date: Thu, 11 Apr 2024 20:08:44 +0200
Subject: [PATCH 16/16] Refactor according to mr comments

---
 .../components/admin_users_filter_app.vue     | 118 +++---------------
 .../admin/users/components/filter_types.js    |  27 ----
 .../javascripts/admin/users/constants.js      |  63 ++++++++++
 app/assets/javascripts/admin/users/index.js   |  11 +-
 app/assets/javascripts/admin/users/router.js  |   6 -
 app/assets/javascripts/admin/users/utils.js   |  33 +++++
 .../stylesheets/page_bundles/search.scss      |  15 ---
 app/helpers/users_helper.rb                   |   2 +-
 app/views/admin/users/index.html.haml         |   4 +-
 .../components/admin_users_filter_app_spec.js |  39 +++---
 10 files changed, 133 insertions(+), 185 deletions(-)
 delete mode 100644 app/assets/javascripts/admin/users/components/filter_types.js
 delete mode 100644 app/assets/javascripts/admin/users/router.js

diff --git a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
index 7c9f1fe6d9ab4913..06dab9f9bdf7edc8 100644
--- a/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
+++ b/app/assets/javascripts/admin/users/components/admin_users_filter_app.vue
@@ -1,67 +1,8 @@
 <script>
-import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
-import { setUrlParams, visitUrl, getParameterValues } from '~/lib/utils/url_utility';
-import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
-import {
-  ALL_FILTER_TYPES,
-  FILTER_TYPE_ADMINS,
-  FILTER_TYPE_ENABLED_2FA,
-  FILTER_TYPE_DISABLED_2FA,
-  FILTER_TYPE_EXTERNAL,
-  FILTER_TYPE_BLOCKED,
-  FILTER_TYPE_BANNED,
-  FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
-  FILTER_TYPE_DEACTIVATED,
-  FILTER_TYPE_WOP,
-  FILTER_TYPE_TRUSTED,
-  TOKEN_ACCESS_LEVEL,
-  TOKEN_STATE,
-  TOKEN_2FA,
-} from './filter_types';
-
-const availableTokens = [
-  {
-    title: s__('AdminUsers|Access level'),
-    type: TOKEN_ACCESS_LEVEL,
-    token: GlFilteredSearchToken,
-    operators: OPERATORS_IS,
-    unique: true,
-    options: [
-      { value: FILTER_TYPE_ADMINS, title: s__('AdminUsers|Administrator') },
-      { value: FILTER_TYPE_EXTERNAL, title: s__('AdminUsers|External') },
-    ],
-  },
-  {
-    title: __('State'),
-    type: TOKEN_STATE,
-    token: GlFilteredSearchToken,
-    operators: OPERATORS_IS,
-    unique: true,
-    options: [
-      { value: FILTER_TYPE_BANNED, title: s__('AdminUsers|Banned') },
-      { value: FILTER_TYPE_BLOCKED, title: s__('AdminUsers|Blocked') },
-      { value: FILTER_TYPE_DEACTIVATED, title: s__('AdminUsers|Deactivated') },
-      {
-        value: FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
-        title: s__('AdminUsers|Pending approval'),
-      },
-      { value: FILTER_TYPE_TRUSTED, title: s__('AdminUsers|Trusted') },
-      { value: FILTER_TYPE_WOP, title: s__('AdminUsers|Without projects') },
-    ],
-  },
-  {
-    title: s__('AdminUsers|Two-factor authentication'),
-    type: TOKEN_2FA,
-    token: GlFilteredSearchToken,
-    operators: OPERATORS_IS,
-    unique: true,
-    options: [
-      { value: FILTER_TYPE_ENABLED_2FA, title: __('On') },
-      { value: FILTER_TYPE_DISABLED_2FA, title: __('Off') },
-    ],
-  },
-];
+import { GlFilteredSearch } from '@gitlab/ui';
+import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { TOKENS, TOKEN_TYPES } from '../constants';
+import { initializeValues } from '../utils';
 
 export default {
   name: 'AdminUsersFilterApp',
@@ -70,51 +11,20 @@ export default {
   },
   data() {
     return {
-      filterValue: [],
+      values: initializeValues(),
     };
   },
   computed: {
-    queryFilter() {
-      return getParameterValues('filter')[0];
-    },
-    selectedFilter() {
-      return this.filterValue.find((selectedFilter) => {
-        return ALL_FILTER_TYPES.includes(selectedFilter.value?.data);
-      });
-    },
-    selectedFilterData() {
-      return this.selectedFilter?.value?.data;
-    },
-    filteredAvailableTokens() {
-      if (this.selectedFilterData) {
-        return availableTokens.filter((token) => {
-          return token.options.find((option) => option.value === this.selectedFilterData);
-        });
-      }
-      return availableTokens;
-    },
-  },
-  created() {
-    if (this.queryFilter) {
-      const filter = availableTokens.find((token) => {
-        return token.options.find((option) => option.value === this.queryFilter);
-      });
+    availableTokens() {
+      // Once a token is selected, discard the rest
+      const tokenType = this.values.find(({ type }) => TOKEN_TYPES.includes(type))?.type;
 
-      if (filter) {
-        this.filterValue.push({
-          type: filter.type,
-          value: {
-            data: this.queryFilter,
-            operator: filter.operators[0].value,
-          },
-        });
+      if (tokenType) {
+        return TOKENS.filter(({ type }) => type === tokenType);
       }
-    }
 
-    const [searchQuery] = getParameterValues('search_query');
-    if (searchQuery) {
-      this.filterValue.push(searchQuery);
-    }
+      return TOKENS;
+    },
   },
   methods: {
     handleSearch(filters) {
@@ -137,9 +47,9 @@ export default {
 
 <template>
   <gl-filtered-search
-    v-model="filterValue"
+    v-model="values"
     :placeholder="s__('AdminUsers|Search by name, email, or username')"
-    :available-tokens="filteredAvailableTokens"
+    :available-tokens="availableTokens"
     class="gl-mb-4"
     terms-as-tokens
     @submit="handleSearch"
diff --git a/app/assets/javascripts/admin/users/components/filter_types.js b/app/assets/javascripts/admin/users/components/filter_types.js
deleted file mode 100644
index 4c8b06a6f451828c..0000000000000000
--- a/app/assets/javascripts/admin/users/components/filter_types.js
+++ /dev/null
@@ -1,27 +0,0 @@
-export const FILTER_TYPE_ADMINS = 'admins';
-export const FILTER_TYPE_ENABLED_2FA = 'two_factor_enabled';
-export const FILTER_TYPE_DISABLED_2FA = 'two_factor_disabled';
-export const FILTER_TYPE_EXTERNAL = 'external';
-export const FILTER_TYPE_BLOCKED = 'blocked';
-export const FILTER_TYPE_BANNED = 'banned';
-export const FILTER_TYPE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
-export const FILTER_TYPE_DEACTIVATED = 'deactivated';
-export const FILTER_TYPE_WOP = 'wop';
-export const FILTER_TYPE_TRUSTED = 'trusted';
-
-export const ALL_FILTER_TYPES = [
-  FILTER_TYPE_ADMINS,
-  FILTER_TYPE_ENABLED_2FA,
-  FILTER_TYPE_DISABLED_2FA,
-  FILTER_TYPE_EXTERNAL,
-  FILTER_TYPE_BLOCKED,
-  FILTER_TYPE_BANNED,
-  FILTER_TYPE_BLOCKED_PENDING_APPROVAL,
-  FILTER_TYPE_DEACTIVATED,
-  FILTER_TYPE_WOP,
-  FILTER_TYPE_TRUSTED,
-];
-
-export const TOKEN_ACCESS_LEVEL = 'access_level';
-export const TOKEN_STATE = 'state';
-export const TOKEN_2FA = '2fa';
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 73383623aa2d310a..060ca0cbf99065cb 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1,3 +1,6 @@
+import { GlFilteredSearchToken } from '@gitlab/ui';
+
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
 import { s__, __ } from '~/locale';
 
 export const I18N_USER_ACTIONS = {
@@ -18,3 +21,63 @@ export const I18N_USER_ACTIONS = {
   trust: s__('AdminUsers|Trust user'),
   untrust: s__('AdminUsers|Untrust user'),
 };
+
+const OPTION_ADMINS = 'admins';
+const OPTION_2FA_ENABLED = 'two_factor_enabled';
+const OPTION_2FA_DISABLED = 'two_factor_disabled';
+const OPTION_EXTERNAL = 'external';
+const OPTION_BLOCKED = 'blocked';
+const OPTION_BANNED = 'banned';
+const OPTION_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
+const OPTION_DEACTIVATED = 'deactivated';
+const OPTION_WOP = 'wop';
+const OPTION_TRUSTED = 'trusted';
+
+export const TOKEN_ACCESS_LEVEL = 'access_level';
+export const TOKEN_STATE = 'state';
+export const TOKEN_2FA = '2fa';
+
+export const TOKEN_TYPES = [TOKEN_ACCESS_LEVEL, TOKEN_STATE, TOKEN_2FA];
+
+export const TOKENS = [
+  {
+    title: s__('AdminUsers|Access level'),
+    type: TOKEN_ACCESS_LEVEL,
+    token: GlFilteredSearchToken,
+    operators: OPERATORS_IS,
+    unique: true,
+    options: [
+      { value: OPTION_ADMINS, title: s__('AdminUsers|Administrator') },
+      { value: OPTION_EXTERNAL, title: s__('AdminUsers|External') },
+    ],
+  },
+  {
+    title: __('State'),
+    type: TOKEN_STATE,
+    token: GlFilteredSearchToken,
+    operators: OPERATORS_IS,
+    unique: true,
+    options: [
+      { value: OPTION_BANNED, title: s__('AdminUsers|Banned') },
+      { value: OPTION_BLOCKED, title: s__('AdminUsers|Blocked') },
+      { value: OPTION_DEACTIVATED, title: s__('AdminUsers|Deactivated') },
+      {
+        value: OPTION_BLOCKED_PENDING_APPROVAL,
+        title: s__('AdminUsers|Pending approval'),
+      },
+      { value: OPTION_TRUSTED, title: s__('AdminUsers|Trusted') },
+      { value: OPTION_WOP, title: s__('AdminUsers|Without projects') },
+    ],
+  },
+  {
+    title: s__('AdminUsers|Two-factor authentication'),
+    type: TOKEN_2FA,
+    token: GlFilteredSearchToken,
+    operators: OPERATORS_IS,
+    unique: true,
+    options: [
+      { value: OPTION_2FA_ENABLED, title: __('On') },
+      { value: OPTION_2FA_DISABLED, title: __('Off') },
+    ],
+  },
+];
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index 67f4b825c9ff8ea8..ae9fe207eff95a54 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -3,14 +3,12 @@ import VueApollo from 'vue-apollo';
 import createDefaultClient from '~/lib/graphql';
 import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 import csrf from '~/lib/utils/csrf';
-import { createRouter } from './router';
 import AdminUsersApp from './components/app.vue';
 import AdminUsersFilterApp from './components/admin_users_filter_app.vue';
 import DeleteUserModal from './components/modals/delete_user_modal.vue';
 import UserActions from './components/user_actions.vue';
 
 Vue.use(VueApollo);
-const router = createRouter();
 
 const apolloProvider = new VueApollo({
   defaultClient: createDefaultClient(),
@@ -37,20 +35,19 @@ const initApp = (el, component, userPropKey, props = {}) => {
   });
 };
 
-export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) =>
-  initApp(el, AdminUsersApp, 'users');
-
 export const initAdminUsersFilterApp = () => {
   return new Vue({
     el: document.querySelector('#js-admin-users-filter-app'),
-    router,
     render: (createElement) => createElement(AdminUsersFilterApp),
-  }).$mount();
+  });
 };
 
 export const initAdminUserActions = (el = document.querySelector('#js-admin-user-actions')) =>
   initApp(el, UserActions, 'user', { showButtonLabels: true });
 
+export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) =>
+  initApp(el, AdminUsersApp, 'users');
+
 export const initDeleteUserModals = () => {
   return new Vue({
     functional: true,
diff --git a/app/assets/javascripts/admin/users/router.js b/app/assets/javascripts/admin/users/router.js
deleted file mode 100644
index 5b1621d98bf78784..0000000000000000
--- a/app/assets/javascripts/admin/users/router.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-
-Vue.use(VueRouter);
-
-export const createRouter = () => new VueRouter({ mode: 'history' });
diff --git a/app/assets/javascripts/admin/users/utils.js b/app/assets/javascripts/admin/users/utils.js
index f6c1091ba27640b7..7bfb35a3b76ecf13 100644
--- a/app/assets/javascripts/admin/users/utils.js
+++ b/app/assets/javascripts/admin/users/utils.js
@@ -1,3 +1,6 @@
+import { queryToObject } from '~/lib/utils/url_utility';
+import { TOKENS } from './constants';
+
 export const generateUserPaths = (paths, id) => {
   return Object.fromEntries(
     Object.entries(paths).map(([action, genericPath]) => {
@@ -5,3 +8,33 @@ export const generateUserPaths = (paths, id) => {
     }),
   );
 };
+
+/**
+ * Initialize values based on the URL parameters
+ * @param {string} query
+ */
+export function initializeValues(query = document.location.search) {
+  const values = [];
+
+  const { filter, search_query: searchQuery } = queryToObject(query);
+
+  if (filter) {
+    const token = TOKENS.find(({ options }) => options.some(({ value }) => value === filter));
+
+    if (token) {
+      values.push({
+        type: token.type,
+        value: {
+          data: filter,
+          operator: token.operators[0].value,
+        },
+      });
+    }
+  }
+
+  if (searchQuery) {
+    values.push(searchQuery);
+  }
+
+  return values;
+}
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index e047ab0ddb9fbff8..7933b233acc46319 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -395,18 +395,3 @@ input[type='search'] {
     width: 100%;
   }
 }
-
-.top-area {
-  &.admin-users-top-area {
-    @include media-breakpoint-down(md) {
-      flex-flow: initial; // override default .top-area behavior
-      flex-wrap: wrap;
-    }
-  }
-
-  .admin-users-nav {
-    @include media-breakpoint-up(lg) {
-      border: 0;
-    }
-  }
-}
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index c364d41554d13cd3..346a5fd8e693d0e8 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -354,7 +354,7 @@ def preload_project_associations(_)
   def admin_user_tab_classes
     return 'js-users-tabs gl-w-full' unless request.path == admin_users_path
 
-    'js-users-tabs gl-w-full admin-users-nav'
+    'js-users-tabs gl-w-full gl-border-0'
   end
 end
 
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 66bb361e9c1e4ba4..1da6dc3d2d9eab41 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,8 +1,8 @@
 - page_title _("Users")
 
-.top-area.admin-users-top-area.gl-display-flex.justify-content-between.gl-align-items-center
+.top-area.gl-display-flex.justify-content-between
   = render 'tabs'
-  .nav-controls.gl-my-3
+  .nav-controls.gl-mt-3
     = render_if_exists 'admin/users/admin_email_users'
     = render_if_exists 'admin/users/admin_export_user_permissions'
     = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
diff --git a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
index 60b81f6e1e9f247f..389aeb924bb2ee9f 100644
--- a/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
+++ b/spec/frontend/admin/users/components/admin_users_filter_app_spec.js
@@ -3,7 +3,6 @@ import { nextTick } from 'vue';
 import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
 
 import { s__ } from '~/locale';
-import { createRouter } from '~/admin/users/router';
 import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
 import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
 import AdminUsersFilterApp from '~/admin/users/components/admin_users_filter_app.vue';
@@ -38,10 +37,8 @@ jest.mock('~/lib/utils/url_utility', () => {
 describe('AdminUsersFilterApp', () => {
   let wrapper;
 
-  const createComponent = ({ router = undefined } = {}) => {
-    wrapper = shallowMount(AdminUsersFilterApp, {
-      router: router || createRouter(),
-    });
+  const createComponent = () => {
+    wrapper = shallowMount(AdminUsersFilterApp);
   };
 
   const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
@@ -86,23 +83,24 @@ describe('AdminUsersFilterApp', () => {
   });
 
   describe('URL search params', () => {
+    afterEach(() => {
+      window.history.pushState({}, null, '');
+    });
+
     /**
      * Currently BE support only one filter at the time
      * https://gitlab.com/gitlab-org/gitlab/-/issues/254377
      */
-    it('includes the only filter if query param `filter` equals one of available filters', async () => {
-      const router = createRouter();
-      await router.replace({ query: { filter: 'admins' } });
-
-      createComponent({ router });
+    it('includes the only filter if query param `filter` equals one of available filters', () => {
+      window.history.replaceState({}, '', '/?filter=admins');
+      createComponent();
       expect(findAvailableTokens()).toEqual([accessLevelToken]);
     });
 
-    // all possible filters are listed here app/assets/javascripts/admin/users/components/filter_types.js
-    it('includes all available filters if query param `filter` is not acceptable filter', async () => {
-      const router = createRouter();
-      await router.replace({ query: { filter: 'filter-that-does-not-exist' } });
-      createComponent({ router });
+    // all possible filters are listed here app/assets/javascripts/admin/users/constants.js
+    it('includes all available filters if query param `filter` is not acceptable filter', () => {
+      window.history.replaceState({}, '', '/?filter=filter-that-does-not-exist');
+      createComponent();
 
       // by default we have 3 filters [admin, 2da, state]
       expect(findAvailableTokens().length).toEqual(3);
@@ -117,19 +115,14 @@ describe('AdminUsersFilterApp', () => {
     });
 
     it('Removes all query param except filter if filter has been changed', async () => {
-      const router = createRouter();
-      await router.replace({
-        query: {
-          page: 2,
-          filter: 'filter-that-does-not-exist',
-        },
-      });
-      createComponent({ router });
+      window.history.replaceState({}, '', '/?page=2&filter=filter-that-does-not-exist');
+      createComponent();
       const filteredSearch = findFilteredSearch();
       filteredSearch.vm.$emit('submit', mockFilters);
       await nextTick();
       expect(visitUrl).toHaveBeenCalledWith(`${getBaseURL()}/?filter=admins`);
     });
+
     it('adds `search_query` if raw text filter was submitted', async () => {
       createComponent();
       const filteredSearch = findFilteredSearch();
-- 
GitLab