diff --git a/core/scripts/webpanel/assets/js/users.js b/core/scripts/webpanel/assets/js/users.js index d50dee3..7356090 100644 --- a/core/scripts/webpanel/assets/js/users.js +++ b/core/scripts/webpanel/assets/js/users.js @@ -11,10 +11,12 @@ $(function () { const BULK_URI_URL = contentSection.dataset.bulkUriUrl; const USERS_BASE_URL = contentSection.dataset.usersBaseUrl; const GET_USER_URL_TEMPLATE = contentSection.dataset.getUserUrlTemplate; + const SEARCH_USERS_URL = contentSection.dataset.searchUrl; const usernameRegex = /^[a-zA-Z0-9_]+$/; const passwordRegex = /^[a-zA-Z0-9]+$/; let cachedUserData = []; + let searchTimeout = null; function setCookie(name, value, days) { let expires = ""; @@ -65,12 +67,80 @@ $(function () { function validatePassword(inputElement, errorElement) { const password = $(inputElement).val(); - // The password is valid if it's empty (no change) OR it matches the alphanumeric regex. const isValid = password === '' || passwordRegex.test(password); $(errorElement).text(isValid ? "" : "Password can only contain letters and numbers."); $('#editSubmitButton').prop('disabled', !isValid); } + function refreshUserList() { + const query = $("#searchInput").val().trim(); + if (query !== "") { + performSearch(); + } else { + restoreInitialView(); + } + } + + function performSearch() { + const query = $("#searchInput").val().trim(); + const $userTableBody = $("#userTableBody"); + const $paginationContainer = $("#paginationContainer"); + const $userTotalCount = $("#user-total-count"); + + $paginationContainer.hide(); + $userTableBody.css('opacity', 0.5).html(' Searching...'); + + $.ajax({ + url: SEARCH_USERS_URL, + type: 'GET', + data: { q: query }, + success: function (data) { + $userTableBody.html(data); + checkIpLimitServiceStatus(); + const resultCount = $userTableBody.find('tr.user-main-row').length; + $userTotalCount.text(resultCount); + }, + error: function () { + Swal.fire("Error!", "An error occurred during search.", "error"); + $userTableBody.html('Search failed to load.'); + }, + complete: function () { + $userTableBody.css('opacity', 1); + } + }); + } + + function restoreInitialView() { + const $userTableBody = $("#userTableBody"); + const $paginationContainer = $("#paginationContainer"); + const $userTotalCount = $("#user-total-count"); + + $userTableBody.css('opacity', 0.5).html(' Loading users...'); + + $.ajax({ + url: USERS_BASE_URL, + type: 'GET', + success: function (data) { + const newBody = $(data).find('#userTableBody').html(); + const newPagination = $(data).find('#paginationContainer').html(); + const newTotalCount = $(data).find('#user-total-count').text(); + + $userTableBody.html(newBody); + $paginationContainer.html(newPagination).show(); + $userTotalCount.text(newTotalCount); + + checkIpLimitServiceStatus(); + }, + error: function () { + Swal.fire("Error!", "Could not restore the user list.", "error"); + $userTableBody.html('Failed to load users. Please refresh the page.'); + }, + complete: function () { + $userTableBody.css('opacity', 1); + } + }); + } + $('#editPassword').on('input', function() { validatePassword(this, '#editPasswordError'); }); @@ -118,6 +188,13 @@ $(function () { }).then((result) => { if (!result.isConfirmed) return; + Swal.fire({ + title: 'Deleting...', + text: 'Please wait', + allowOutsideClick: false, + didOpen: () => Swal.showLoading() + }); + if (selectedUsers.length > 1) { $.ajax({ url: BULK_REMOVE_URL, @@ -125,7 +202,7 @@ $(function () { contentType: "application/json", data: JSON.stringify({ usernames: selectedUsers }) }) - .done(() => Swal.fire("Success!", "Selected users have been deleted.", "success").then(() => location.reload())) + .done(() => Swal.fire("Success!", "Selected users have been deleted.", "success").then(() => refreshUserList())) .fail((err) => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred while deleting users.", "error")); } else { const singleUrl = REMOVE_USER_URL_TEMPLATE.replace('U', selectedUsers[0]); @@ -133,7 +210,7 @@ $(function () { url: singleUrl, method: "DELETE" }) - .done(() => Swal.fire("Success!", "The user has been deleted.", "success").then(() => location.reload())) + .done(() => Swal.fire("Success!", "The user has been deleted.", "success").then(() => refreshUserList())) .fail((err) => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred while deleting the user.", "error")); } }); @@ -151,13 +228,23 @@ $(function () { jsonData.unlimited = jsonData.unlimited === 'on'; + Swal.fire({ + title: 'Adding...', + text: 'Please wait', + allowOutsideClick: false, + didOpen: () => Swal.showLoading() + }); + $.ajax({ url: url, method: "POST", contentType: "application/json", data: JSON.stringify(jsonData), }) - .done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload())) + .done(res => { + $('#addUserModal').modal('hide'); + Swal.fire("Success!", res.detail, "success").then(() => refreshUserList()); + }) .fail(err => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred.", "error")) .always(() => button.prop('disabled', false)); }); @@ -213,13 +300,23 @@ $(function () { jsonData.blocked = jsonData.blocked === 'on'; jsonData.unlimited_ip = jsonData.unlimited_ip === 'on'; + Swal.fire({ + title: 'Updating...', + text: 'Please wait', + allowOutsideClick: false, + didOpen: () => Swal.showLoading() + }); + $.ajax({ url: url, method: "PATCH", contentType: "application/json", data: JSON.stringify(jsonData), }) - .done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload())) + .done(res => { + $('#editUserModal').modal('hide'); + Swal.fire("Success!", res.detail, "success").then(() => refreshUserList()); + }) .fail(err => Swal.fire("Error!", err.responseJSON?.detail, "error")) .always(() => button.prop('disabled', false)); }); @@ -240,11 +337,19 @@ $(function () { confirmButtonText: `Yes, ${action} it!`, }).then((result) => { if (!result.isConfirmed) return; + + Swal.fire({ + title: `${action.charAt(0).toUpperCase() + action.slice(1)}ing...`, + text: 'Please wait', + allowOutsideClick: false, + didOpen: () => Swal.showLoading() + }); + $.ajax({ url: urlTemplate.replace("U", encodeURIComponent(username)), method: isDelete ? "DELETE" : "GET", }) - .done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload())) + .done(res => Swal.fire("Success!", res.detail, "success").then(() => refreshUserList())) .fail(() => Swal.fire("Error!", `Failed to ${action} user.`, "error")); }); }); @@ -352,19 +457,6 @@ $(function () { .then(() => Swal.fire({ icon: "success", title: "Links copied!", showConfirmButton: false, timer: 1200 })); }); - function filterUsers() { - const searchText = $("#searchInput").val().toLowerCase(); - $("#userTable tbody tr.user-main-row").each(function () { - const username = $(this).find("td:eq(2)").text().toLowerCase(); - const note = $(this).data("note").toLowerCase(); - const isVisible = username.includes(searchText) || note.includes(searchText); - $(this).toggle(isVisible); - if (!isVisible) { - $(this).next('tr.user-details-row').hide(); - } - }); - } - $('#userTable').on('click', '.toggle-details-btn', function() { const $this = $(this); const icon = $this.find('i'); @@ -390,8 +482,23 @@ $(function () { $('#addUserModal a[data-toggle="tab"]').first().tab('show'); }); - $("#searchButton").on("click", filterUsers); - $("#searchInput").on("keyup", filterUsers); + $("#searchButton").on("click", performSearch); + $("#searchInput").on("keyup", function (e) { + clearTimeout(searchTimeout); + const query = $(this).val().trim(); + + if (e.key === 'Enter') { + performSearch(); + return; + } + + if (query === "") { + searchTimeout = setTimeout(restoreInitialView, 300); + return; + } + + searchTimeout = setTimeout(performSearch, 500); + }); function initializeLimitSelector() { const savedLimit = getCookie('limit') || '50'; diff --git a/core/scripts/webpanel/routers/user/user.py b/core/scripts/webpanel/routers/user/user.py index 3c9cd4c..ea2a2e1 100644 --- a/core/scripts/webpanel/routers/user/user.py +++ b/core/scripts/webpanel/routers/user/user.py @@ -65,4 +65,32 @@ async def users_root( templates: Jinja2Templates = Depends(get_templates), limit: int = Cookie(default=50, ge=1) ): - return await get_users_page(request, templates, 1, limit) \ No newline at end of file + return await get_users_page(request, templates, 1, limit) + +@router.get("/search/", name="search_users") +async def search_users( + request: Request, + q: str = Query(""), + templates: Jinja2Templates = Depends(get_templates) +): + try: + if not q: + all_users_data = [] + else: + all_users_data = cli_api.list_users() or [] + + query = q.lower() + + filtered_users_data = [ + user_data for user_data in all_users_data + if query in user_data.get('username', '').lower() or query in user_data.get('note', '').lower() + ] + + users: list[User] = [User.from_dict(user_data.get('username', ''), user_data) for user_data in filtered_users_data] + + return templates.TemplateResponse( + 'users_rows.html', + {'request': request, 'users': users} + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index d98a4e8..5e28b9a 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -49,11 +49,12 @@ data-user-uri-url-template="{{ url_for('show_user_uri_api', username='U') }}" data-bulk-uri-url="{{ url_for('show_multiple_user_uris_api') }}" data-users-base-url="{{ url_for('users') }}" - data-get-user-url-template="{{ url_for('get_user_api', username='U') }}"> + data-get-user-url-template="{{ url_for('get_user_api', username='U') }}" + data-search-url="{{ url_for('search_users') }}">
-

User List {% if total_users is defined %}({{ total_users }}){% endif %}

+

User List {% if total_users is defined %}({{ total_users }}){% endif %}

@@ -117,11 +118,6 @@
- {% if not users %} - - {% else %} @@ -143,151 +139,13 @@ - - {% for user in users|sort(attribute='username', case_sensitive=false) %} - - - - - - - - - - - - - - - - - - - - {% endfor %} + + {% set offset = (current_page - 1) * limit %} + {% include 'users_rows.html' %}
Details
- - {{ loop.index + ((current_page - 1) * limit) }}{{ user.username }} - {% if user.status == "Online" %} - Online - {% if user.online_count and user.online_count > 0 %} - ({{ user.online_count }}) - {% endif %} - {% elif user.status == "Offline" %} - Offline - {% elif user.status == "On-hold" %} - On-hold - {% elif user.status == "Conflict" %} - Conflict - {% else %} - {{ user.status }} - {% endif %} - {{ user.traffic_used }}{{ user.expiry_date }}{{ user.expiry_days }}{{ user.day_usage }} - {% if user.enable %} - - {% else %} - - {% endif %} - - {% if user.note %} - {{ user.note | truncate(20, True) }} - {% endif %} - - - - - - - - - - -
- {% endif %}
-