feat: Implement server-side search for users
This commit is contained in:
@ -11,10 +11,12 @@ $(function () {
|
|||||||
const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
|
const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
|
||||||
const USERS_BASE_URL = contentSection.dataset.usersBaseUrl;
|
const USERS_BASE_URL = contentSection.dataset.usersBaseUrl;
|
||||||
const GET_USER_URL_TEMPLATE = contentSection.dataset.getUserUrlTemplate;
|
const GET_USER_URL_TEMPLATE = contentSection.dataset.getUserUrlTemplate;
|
||||||
|
const SEARCH_USERS_URL = contentSection.dataset.searchUrl;
|
||||||
|
|
||||||
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
||||||
const passwordRegex = /^[a-zA-Z0-9]+$/;
|
const passwordRegex = /^[a-zA-Z0-9]+$/;
|
||||||
let cachedUserData = [];
|
let cachedUserData = [];
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
function setCookie(name, value, days) {
|
function setCookie(name, value, days) {
|
||||||
let expires = "";
|
let expires = "";
|
||||||
@ -65,12 +67,80 @@ $(function () {
|
|||||||
|
|
||||||
function validatePassword(inputElement, errorElement) {
|
function validatePassword(inputElement, errorElement) {
|
||||||
const password = $(inputElement).val();
|
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);
|
const isValid = password === '' || passwordRegex.test(password);
|
||||||
$(errorElement).text(isValid ? "" : "Password can only contain letters and numbers.");
|
$(errorElement).text(isValid ? "" : "Password can only contain letters and numbers.");
|
||||||
$('#editSubmitButton').prop('disabled', !isValid);
|
$('#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('<tr><td colspan="14" class="text-center p-4"><i class="fas fa-spinner fa-spin"></i> Searching...</td></tr>');
|
||||||
|
|
||||||
|
$.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('<tr><td colspan="14" class="text-center p-4 text-danger">Search failed to load.</td></tr>');
|
||||||
|
},
|
||||||
|
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('<tr><td colspan="14" class="text-center p-4"><i class="fas fa-spinner fa-spin"></i> Loading users...</td></tr>');
|
||||||
|
|
||||||
|
$.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('<tr><td colspan="14" class="text-center p-4 text-danger">Failed to load users. Please refresh the page.</td></tr>');
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$userTableBody.css('opacity', 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$('#editPassword').on('input', function() {
|
$('#editPassword').on('input', function() {
|
||||||
validatePassword(this, '#editPasswordError');
|
validatePassword(this, '#editPasswordError');
|
||||||
});
|
});
|
||||||
@ -118,6 +188,13 @@ $(function () {
|
|||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (!result.isConfirmed) return;
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Deleting...',
|
||||||
|
text: 'Please wait',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
didOpen: () => Swal.showLoading()
|
||||||
|
});
|
||||||
|
|
||||||
if (selectedUsers.length > 1) {
|
if (selectedUsers.length > 1) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: BULK_REMOVE_URL,
|
url: BULK_REMOVE_URL,
|
||||||
@ -125,7 +202,7 @@ $(function () {
|
|||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
data: JSON.stringify({ usernames: selectedUsers })
|
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"));
|
.fail((err) => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred while deleting users.", "error"));
|
||||||
} else {
|
} else {
|
||||||
const singleUrl = REMOVE_USER_URL_TEMPLATE.replace('U', selectedUsers[0]);
|
const singleUrl = REMOVE_USER_URL_TEMPLATE.replace('U', selectedUsers[0]);
|
||||||
@ -133,7 +210,7 @@ $(function () {
|
|||||||
url: singleUrl,
|
url: singleUrl,
|
||||||
method: "DELETE"
|
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"));
|
.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';
|
jsonData.unlimited = jsonData.unlimited === 'on';
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Adding...',
|
||||||
|
text: 'Please wait',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
didOpen: () => Swal.showLoading()
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
data: JSON.stringify(jsonData),
|
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"))
|
.fail(err => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred.", "error"))
|
||||||
.always(() => button.prop('disabled', false));
|
.always(() => button.prop('disabled', false));
|
||||||
});
|
});
|
||||||
@ -213,13 +300,23 @@ $(function () {
|
|||||||
jsonData.blocked = jsonData.blocked === 'on';
|
jsonData.blocked = jsonData.blocked === 'on';
|
||||||
jsonData.unlimited_ip = jsonData.unlimited_ip === 'on';
|
jsonData.unlimited_ip = jsonData.unlimited_ip === 'on';
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Updating...',
|
||||||
|
text: 'Please wait',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
didOpen: () => Swal.showLoading()
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
data: JSON.stringify(jsonData),
|
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"))
|
.fail(err => Swal.fire("Error!", err.responseJSON?.detail, "error"))
|
||||||
.always(() => button.prop('disabled', false));
|
.always(() => button.prop('disabled', false));
|
||||||
});
|
});
|
||||||
@ -240,11 +337,19 @@ $(function () {
|
|||||||
confirmButtonText: `Yes, ${action} it!`,
|
confirmButtonText: `Yes, ${action} it!`,
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (!result.isConfirmed) return;
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: `${action.charAt(0).toUpperCase() + action.slice(1)}ing...`,
|
||||||
|
text: 'Please wait',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
didOpen: () => Swal.showLoading()
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: urlTemplate.replace("U", encodeURIComponent(username)),
|
url: urlTemplate.replace("U", encodeURIComponent(username)),
|
||||||
method: isDelete ? "DELETE" : "GET",
|
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"));
|
.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 }));
|
.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() {
|
$('#userTable').on('click', '.toggle-details-btn', function() {
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
const icon = $this.find('i');
|
const icon = $this.find('i');
|
||||||
@ -390,8 +482,23 @@ $(function () {
|
|||||||
$('#addUserModal a[data-toggle="tab"]').first().tab('show');
|
$('#addUserModal a[data-toggle="tab"]').first().tab('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#searchButton").on("click", filterUsers);
|
$("#searchButton").on("click", performSearch);
|
||||||
$("#searchInput").on("keyup", filterUsers);
|
$("#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() {
|
function initializeLimitSelector() {
|
||||||
const savedLimit = getCookie('limit') || '50';
|
const savedLimit = getCookie('limit') || '50';
|
||||||
|
|||||||
@ -66,3 +66,31 @@ async def users_root(
|
|||||||
limit: int = Cookie(default=50, ge=1)
|
limit: int = Cookie(default=50, ge=1)
|
||||||
):
|
):
|
||||||
return await get_users_page(request, templates, 1, limit)
|
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))
|
||||||
@ -49,11 +49,12 @@
|
|||||||
data-user-uri-url-template="{{ url_for('show_user_uri_api', username='U') }}"
|
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-bulk-uri-url="{{ url_for('show_multiple_user_uris_api') }}"
|
||||||
data-users-base-url="{{ url_for('users') }}"
|
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') }}">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">User List {% if total_users is defined %}({{ total_users }}){% endif %}</h3>
|
<h3 class="card-title">User List {% if total_users is defined %}(<span id="user-total-count">{{ total_users }}</span>){% endif %}</h3>
|
||||||
<div class="card-tools d-flex align-items-center flex-wrap">
|
<div class="card-tools d-flex align-items-center flex-wrap">
|
||||||
|
|
||||||
<!-- Mobile Filter Dropdown -->
|
<!-- Mobile Filter Dropdown -->
|
||||||
@ -117,11 +118,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body table-responsive p-0">
|
<div class="card-body table-responsive p-0">
|
||||||
{% if not users %}
|
|
||||||
<div class="alert alert-warning m-3" role="alert">
|
|
||||||
No users found.
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<table class="table table-bordered table-hover" id="userTable">
|
<table class="table table-bordered table-hover" id="userTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -143,151 +139,13 @@
|
|||||||
<th class="d-md-none text-center">Details</th>
|
<th class="d-md-none text-center">Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="userTableBody">
|
||||||
{% for user in users|sort(attribute='username', case_sensitive=false) %}
|
{% set offset = (current_page - 1) * limit %}
|
||||||
<tr class="user-main-row" data-note="{{ user.note or '' }}">
|
{% include 'users_rows.html' %}
|
||||||
<td>
|
|
||||||
<input type="checkbox" class="user-checkbox" value="{{ user.username }}">
|
|
||||||
</td>
|
|
||||||
<td>{{ loop.index + ((current_page - 1) * limit) }}</td>
|
|
||||||
<td data-username="{{ user.username }}">{{ user.username }}</td>
|
|
||||||
<td class="d-none d-md-table-cell">
|
|
||||||
{% if user.status == "Online" %}
|
|
||||||
<i class="fas fa-circle text-success"></i> Online
|
|
||||||
{% if user.online_count and user.online_count > 0 %}
|
|
||||||
({{ user.online_count }})
|
|
||||||
{% endif %}
|
|
||||||
{% elif user.status == "Offline" %}
|
|
||||||
<i class="fas fa-circle text-secondary"></i> Offline
|
|
||||||
{% elif user.status == "On-hold" %}
|
|
||||||
<i class="fas fa-circle text-warning"></i> On-hold
|
|
||||||
{% elif user.status == "Conflict" %}
|
|
||||||
<i class="fas fa-circle text-danger"></i> Conflict
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-circle text-danger"></i> {{ user.status }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell">{{ user.traffic_used }}</td>
|
|
||||||
<td class="d-none d-md-table-cell">{{ user.expiry_date }}</td>
|
|
||||||
<td class="d-none d-md-table-cell">{{ user.expiry_days }}</td>
|
|
||||||
<td class="d-none d-md-table-cell">{{ user.day_usage }}</td>
|
|
||||||
<td class="d-none d-md-table-cell">
|
|
||||||
{% if user.enable %}
|
|
||||||
<i class="fas fa-check-circle text-success"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-times-circle text-danger"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell note-cell">
|
|
||||||
{% if user.note %}
|
|
||||||
<span title="{{ user.note }}">{{ user.note | truncate(20, True) }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell unlimited-ip-cell requires-iplimit-service" style="display: none;">
|
|
||||||
{% if user.unlimited_ip %}
|
|
||||||
<i class="fas fa-shield-alt text-primary"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-times-circle text-muted"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell text-nowrap">
|
|
||||||
<a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal"
|
|
||||||
data-username="{{ user.username }}">
|
|
||||||
<i class="fas fa-qrcode"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell text-nowrap">
|
|
||||||
<button type="button" class="btn btn-sm btn-info edit-user"
|
|
||||||
data-user="{{ user.username }}" data-toggle="modal"
|
|
||||||
data-target="#editUserModal">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-warning reset-user"
|
|
||||||
data-user="{{ user.username }}">
|
|
||||||
<i class="fas fa-undo"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger delete-user"
|
|
||||||
data-user="{{ user.username }}">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td class="d-md-none text-center">
|
|
||||||
<button type="button" class="btn btn-sm btn-secondary toggle-details-btn">
|
|
||||||
<i class="fas fa-plus"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="user-details-row d-md-none" style="display: none;">
|
|
||||||
<td colspan="4">
|
|
||||||
<div class="user-details-content p-2">
|
|
||||||
<p><strong>Status:</strong>
|
|
||||||
<span>
|
|
||||||
{% if user.status == "Online" %}
|
|
||||||
<i class="fas fa-circle text-success"></i> Online
|
|
||||||
{% if user.online_count and user.online_count > 0 %}
|
|
||||||
({{ user.online_count }})
|
|
||||||
{% endif %}
|
|
||||||
{% elif user.status == "Offline" %}
|
|
||||||
<i class="fas fa-circle text-secondary"></i> Offline
|
|
||||||
{% elif user.status == "On-hold" %}
|
|
||||||
<i class="fas fa-circle text-warning"></i> On-hold
|
|
||||||
{% elif user.status == "Conflict" %}
|
|
||||||
<i class="fas fa-circle text-danger"></i> Conflict
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-circle text-danger"></i> {{ user.status }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p><strong>Traffic Usage:</strong> <span>{{ user.traffic_used }}</span></p>
|
|
||||||
<p><strong>Expiry Date:</strong> <span>{{ user.expiry_date }}</span></p>
|
|
||||||
<p><strong>Expiry Days:</strong> <span>{{ user.expiry_days }}</span></p>
|
|
||||||
<p><strong>Day Usage:</strong> <span>{{ user.day_usage }}</span></p>
|
|
||||||
<p><strong>Note:</strong> <span>{{ user.note or 'N/A' }}</span></p>
|
|
||||||
<p><strong>Enable:</strong>
|
|
||||||
<span>
|
|
||||||
{% if user.enable %}
|
|
||||||
<i class="fas fa-check-circle text-success"></i> Enabled
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-times-circle text-danger"></i> Disabled
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="requires-iplimit-service" style="display: none;"><strong>Unlimited IP:</strong>
|
|
||||||
<span>
|
|
||||||
{% if user.unlimited_ip %}
|
|
||||||
<i class="fas fa-shield-alt text-primary"></i> Yes
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-times-circle text-muted"></i> No
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div class="mt-2">
|
|
||||||
<strong>Configs:</strong>
|
|
||||||
<a href="#" class="btn btn-sm btn-outline-secondary config-link" data-toggle="modal" data-target="#qrcodeModal" data-username="{{ user.username }}">
|
|
||||||
<i class="fas fa-qrcode"></i> Show
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 user-details-actions">
|
|
||||||
<strong>Actions:</strong>
|
|
||||||
<button type="button" class="btn btn-sm btn-info edit-user" data-user="{{ user.username }}" data-toggle="modal" data-target="#editUserModal">
|
|
||||||
<i class="fas fa-edit"></i> Edit
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-warning reset-user" data-user="{{ user.username }}">
|
|
||||||
<i class="fas fa-undo"></i> Reset
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger delete-user" data-user="{{ user.username }}">
|
|
||||||
<i class="fas fa-trash"></i> Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer clearfix">
|
<div class="card-footer clearfix" id="paginationContainer">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-md-5 d-flex align-items-center">
|
<div class="col-sm-12 col-md-5 d-flex align-items-center">
|
||||||
<div class="dataTables_info" role="status" aria-live="polite">
|
<div class="dataTables_info" role="status" aria-live="polite">
|
||||||
|
|||||||
147
core/scripts/webpanel/templates/users_rows.html
Normal file
147
core/scripts/webpanel/templates/users_rows.html
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
{% if not users %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="14" class="text-center p-4">
|
||||||
|
No users found.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
{% for user in users|sort(attribute='username', case_sensitive=false) %}
|
||||||
|
<tr class="user-main-row" data-note="{{ user.note or '' }}">
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="user-checkbox" value="{{ user.username }}">
|
||||||
|
</td>
|
||||||
|
<td>{{ loop.index + (offset if offset is defined else 0) }}</td>
|
||||||
|
<td data-username="{{ user.username }}">{{ user.username }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">
|
||||||
|
{% if user.status == "Online" %}
|
||||||
|
<i class="fas fa-circle text-success"></i> Online
|
||||||
|
{% if user.online_count and user.online_count > 0 %}
|
||||||
|
({{ user.online_count }})
|
||||||
|
{% endif %}
|
||||||
|
{% elif user.status == "Offline" %}
|
||||||
|
<i class="fas fa-circle text-secondary"></i> Offline
|
||||||
|
{% elif user.status == "On-hold" %}
|
||||||
|
<i class="fas fa-circle text-warning"></i> On-hold
|
||||||
|
{% elif user.status == "Conflict" %}
|
||||||
|
<i class="fas fa-circle text-danger"></i> Conflict
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-circle text-danger"></i> {{ user.status }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ user.traffic_used }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ user.expiry_date }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ user.expiry_days }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ user.day_usage }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">
|
||||||
|
{% if user.enable %}
|
||||||
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times-circle text-danger"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-md-table-cell note-cell">
|
||||||
|
{% if user.note %}
|
||||||
|
<span title="{{ user.note }}">{{ user.note | truncate(20, True) }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-md-table-cell unlimited-ip-cell requires-iplimit-service" style="display: none;">
|
||||||
|
{% if user.unlimited_ip %}
|
||||||
|
<i class="fas fa-shield-alt text-primary"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times-circle text-muted"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-md-table-cell text-nowrap">
|
||||||
|
<a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal"
|
||||||
|
data-username="{{ user.username }}">
|
||||||
|
<i class="fas fa-qrcode"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-md-table-cell text-nowrap">
|
||||||
|
<button type="button" class="btn btn-sm btn-info edit-user"
|
||||||
|
data-user="{{ user.username }}" data-toggle="modal"
|
||||||
|
data-target="#editUserModal">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning reset-user"
|
||||||
|
data-user="{{ user.username }}">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger delete-user"
|
||||||
|
data-user="{{ user.username }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="d-md-none text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary toggle-details-btn">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="user-details-row d-md-none" style="display: none;">
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="user-details-content p-2">
|
||||||
|
<p><strong>Status:</strong>
|
||||||
|
<span>
|
||||||
|
{% if user.status == "Online" %}
|
||||||
|
<i class="fas fa-circle text-success"></i> Online
|
||||||
|
{% if user.online_count and user.online_count > 0 %}
|
||||||
|
({{ user.online_count }})
|
||||||
|
{% endif %}
|
||||||
|
{% elif user.status == "Offline" %}
|
||||||
|
<i class="fas fa-circle text-secondary"></i> Offline
|
||||||
|
{% elif user.status == "On-hold" %}
|
||||||
|
<i class="fas fa-circle text-warning"></i> On-hold
|
||||||
|
{% elif user.status == "Conflict" %}
|
||||||
|
<i class="fas fa-circle text-danger"></i> Conflict
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-circle text-danger"></i> {{ user.status }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p><strong>Traffic Usage:</strong> <span>{{ user.traffic_used }}</span></p>
|
||||||
|
<p><strong>Expiry Date:</strong> <span>{{ user.expiry_date }}</span></p>
|
||||||
|
<p><strong>Expiry Days:</strong> <span>{{ user.expiry_days }}</span></p>
|
||||||
|
<p><strong>Day Usage:</strong> <span>{{ user.day_usage }}</span></p>
|
||||||
|
<p><strong>Note:</strong> <span>{{ user.note or 'N/A' }}</span></p>
|
||||||
|
<p><strong>Enable:</strong>
|
||||||
|
<span>
|
||||||
|
{% if user.enable %}
|
||||||
|
<i class="fas fa-check-circle text-success"></i> Enabled
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times-circle text-danger"></i> Disabled
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="requires-iplimit-service" style="display: none;"><strong>Unlimited IP:</strong>
|
||||||
|
<span>
|
||||||
|
{% if user.unlimited_ip %}
|
||||||
|
<i class="fas fa-shield-alt text-primary"></i> Yes
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times-circle text-muted"></i> No
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Configs:</strong>
|
||||||
|
<a href="#" class="btn btn-sm btn-outline-secondary config-link" data-toggle="modal" data-target="#qrcodeModal" data-username="{{ user.username }}">
|
||||||
|
<i class="fas fa-qrcode"></i> Show
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 user-details-actions">
|
||||||
|
<strong>Actions:</strong>
|
||||||
|
<button type="button" class="btn btn-sm btn-info edit-user" data-user="{{ user.username }}" data-toggle="modal" data-target="#editUserModal">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning reset-user" data-user="{{ user.username }}">
|
||||||
|
<i class="fas fa-undo"></i> Reset
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger delete-user" data-user="{{ user.username }}">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
Reference in New Issue
Block a user