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 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('<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() {
|
||||
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';
|
||||
|
||||
@ -66,3 +66,31 @@ async def users_root(
|
||||
limit: int = Cookie(default=50, ge=1)
|
||||
):
|
||||
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-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') }}">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<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">
|
||||
|
||||
<!-- Mobile Filter Dropdown -->
|
||||
@ -117,11 +118,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -143,151 +139,13 @@
|
||||
<th class="d-md-none text-center">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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 + ((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 id="userTableBody">
|
||||
{% set offset = (current_page - 1) * limit %}
|
||||
{% include 'users_rows.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer clearfix">
|
||||
<div class="card-footer clearfix" id="paginationContainer">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-5 d-flex align-items-center">
|
||||
<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