mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 16:46:03 +00:00
v2.5.0: replace keys table with responsive cards
This commit is contained in:
@@ -1138,12 +1138,12 @@ function renderUserTraffic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderUsers() {
|
function renderUsers() {
|
||||||
const tbody = $("#usersTable");
|
const container = $("#usersTable");
|
||||||
if (!state.users.length) {
|
if (!state.users.length) {
|
||||||
tbody.innerHTML = `<tr><td colspan="6" class="empty-cell">${escapeHtml(t("noKeys"))}</td></tr>`;
|
container.innerHTML = `<div class="empty empty-cell">${escapeHtml(t("noKeys"))}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = state.users.map((user) => {
|
container.innerHTML = state.users.map((user) => {
|
||||||
const pending = state.pendingUsers.has(user.name);
|
const pending = state.pendingUsers.has(user.name);
|
||||||
const selected = user.name === state.userTrafficUser;
|
const selected = user.name === state.userTrafficUser;
|
||||||
const traffic = user.traffic || {};
|
const traffic = user.traffic || {};
|
||||||
@@ -1151,13 +1151,12 @@ function renderUsers() {
|
|||||||
const activeIps = Number(traffic.active_unique_ips) || 0;
|
const activeIps = Number(traffic.active_unique_ips) || 0;
|
||||||
const maxUniqueIps = Number.isFinite(Number(user.max_unique_ips)) ? Math.max(0, Number(user.max_unique_ips)) : 0;
|
const maxUniqueIps = Number.isFinite(Number(user.max_unique_ips)) ? Math.max(0, Number(user.max_unique_ips)) : 0;
|
||||||
return `
|
return `
|
||||||
<tr class="${user.enabled ? "" : "disabled-row"} ${pending ? "pending-row" : ""} ${selected ? "selected-row" : ""}" data-select-user-traffic="${escapeAttr(user.name)}" aria-selected="${selected ? "true" : "false"}">
|
<article class="key-card ${user.enabled ? "" : "disabled-row"} ${pending ? "pending-row" : ""} ${selected ? "selected-row" : ""}" data-select-user-traffic="${escapeAttr(user.name)}" aria-selected="${selected ? "true" : "false"}">
|
||||||
<td data-label="${escapeAttr(t("tableUser"))}">
|
<div class="key-card-user">
|
||||||
|
<span class="field-label">${escapeHtml(t("tableUser"))}</span>
|
||||||
<button class="key-name-button" type="button" data-user-traffic="${escapeAttr(user.name)}">
|
<button class="key-name-button" type="button" data-user-traffic="${escapeAttr(user.name)}">
|
||||||
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
|
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
|
||||||
<td data-label="${escapeAttr(t("tableStatus"))}">
|
|
||||||
<div class="status-control">
|
<div class="status-control">
|
||||||
<label class="switch" title="${escapeAttr(user.main ? t("main") : (user.enabled ? t("disableKey") : t("enableKey")))}">
|
<label class="switch" title="${escapeAttr(user.main ? t("main") : (user.enabled ? t("disableKey") : t("enableKey")))}">
|
||||||
<input type="checkbox" data-toggle-user="${escapeAttr(user.name)}" ${user.enabled ? "checked" : ""} ${user.main || pending ? "disabled" : ""}>
|
<input type="checkbox" data-toggle-user="${escapeAttr(user.name)}" ${user.enabled ? "checked" : ""} ${user.main || pending ? "disabled" : ""}>
|
||||||
@@ -1165,15 +1164,20 @@ function renderUsers() {
|
|||||||
</label>
|
</label>
|
||||||
<strong class="${user.enabled ? "state-on" : "state-off"}">${escapeHtml(pending ? t("applying") : (user.enabled ? t("enabled") : t("disabled")))}</strong>
|
<strong class="${user.enabled ? "state-on" : "state-off"}">${escapeHtml(pending ? t("applying") : (user.enabled ? t("enabled") : t("disabled")))}</strong>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td data-label="${escapeAttr(t("tableSecret"))}"><code title="${escapeAttr(user.secret)}">${escapeHtml(user.secret)}</code></td>
|
<div class="key-card-secret">
|
||||||
<td data-label="${escapeAttr(t("tableLink"))}">
|
<span class="field-label">${escapeHtml(t("tableSecret"))}</span>
|
||||||
|
<code title="${escapeAttr(user.secret)}">${escapeHtml(user.secret)}</code>
|
||||||
|
</div>
|
||||||
|
<div class="key-card-links">
|
||||||
|
<span class="field-label">${escapeHtml(t("tableLink"))}</span>
|
||||||
<div class="mini-actions">
|
<div class="mini-actions">
|
||||||
<button class="soft" data-copy="${escapeAttr(user.link)}" ${user.enabled ? "" : "disabled"}>${escapeHtml(t("copyLink"))}</button>
|
<button class="soft" data-copy="${escapeAttr(user.link)}" ${user.enabled ? "" : "disabled"}>${escapeHtml(t("copyLink"))}</button>
|
||||||
<button class="soft" data-user-qr="${escapeAttr(user.name)}">${escapeHtml(t("showQr"))}</button>
|
<button class="soft" data-user-qr="${escapeAttr(user.name)}">${escapeHtml(t("showQr"))}</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td data-label="${escapeAttr(t("tableTraffic"))}">
|
<div class="key-card-traffic">
|
||||||
|
<span class="field-label">${escapeHtml(t("tableTraffic"))}</span>
|
||||||
<div class="traffic-cell">
|
<div class="traffic-cell">
|
||||||
<div class="traffic-main">
|
<div class="traffic-main">
|
||||||
<span>
|
<span>
|
||||||
@@ -1188,14 +1192,15 @@ function renderUsers() {
|
|||||||
<button class="soft" type="submit" data-ip-limit-save="${escapeAttr(user.name)}">${escapeHtml(t("saveIpLimit"))}</button>
|
<button class="soft" type="submit" data-ip-limit-save="${escapeAttr(user.name)}">${escapeHtml(t("saveIpLimit"))}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td data-label="${escapeAttr(t("tableActions"))}">
|
<div class="key-card-actions">
|
||||||
|
<span class="field-label">${escapeHtml(t("tableActions"))}</span>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="soft" data-copy="${escapeAttr(user.secret)}">${escapeHtml(t("copySecret"))}</button>
|
<button class="soft" data-copy="${escapeAttr(user.secret)}">${escapeHtml(t("copySecret"))}</button>
|
||||||
<button class="danger" data-delete="${escapeAttr(user.name)}" ${user.main ? "disabled" : ""}>${escapeHtml(t("delete"))}</button>
|
<button class="danger" data-delete="${escapeAttr(user.name)}" ${user.main ? "disabled" : ""}>${escapeHtml(t("delete"))}</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</article>
|
||||||
`; }).join("");
|
`; }).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,20 +194,8 @@
|
|||||||
<button type="submit" data-i18n="addKey">Add key</button>
|
<button type="submit" data-i18n="addKey">Add key</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="keys-wrap">
|
||||||
<table class="keys-table">
|
<div class="keys-list" id="usersTable" aria-live="polite"></div>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-i18n="tableUser">User</th>
|
|
||||||
<th data-i18n="tableStatus">Status</th>
|
|
||||||
<th data-i18n="tableSecret">Secret</th>
|
|
||||||
<th data-i18n="tableLink">Link</th>
|
|
||||||
<th data-i18n="tableTraffic">Traffic</th>
|
|
||||||
<th data-i18n="tableActions">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="usersTable"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel user-traffic-panel" id="userTrafficPanel">
|
<div class="panel user-traffic-panel" id="userTrafficPanel">
|
||||||
@@ -400,6 +388,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/app.js?v=2.5.0-admin16" type="module"></script>
|
<script src="/app.js?v=2.5.0-admin17" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -783,6 +783,79 @@ h2 {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keys-wrap {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns:
|
||||||
|
minmax(150px, .85fr)
|
||||||
|
minmax(260px, 1.35fr)
|
||||||
|
minmax(200px, 1fr)
|
||||||
|
minmax(260px, 1.1fr)
|
||||||
|
minmax(210px, .9fr);
|
||||||
|
grid-template-areas: "user secret links traffic actions";
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .16s ease, box-shadow .16s ease, border-color .16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card:hover {
|
||||||
|
background: color-mix(in srgb, var(--blue) 7%, var(--panel));
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card.selected-row {
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 34%, var(--line));
|
||||||
|
background: color-mix(in srgb, var(--blue) 10%, var(--panel));
|
||||||
|
box-shadow: inset 4px 0 0 var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card-user,
|
||||||
|
.key-card-secret,
|
||||||
|
.key-card-links,
|
||||||
|
.key-card-traffic,
|
||||||
|
.key-card-actions {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card-user { grid-area: user; }
|
||||||
|
.key-card-secret { grid-area: secret; }
|
||||||
|
.key-card-links { grid-area: links; }
|
||||||
|
.key-card-traffic { grid-area: traffic; }
|
||||||
|
.key-card-actions { grid-area: actions; }
|
||||||
|
|
||||||
|
.key-card-secret code {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 720px;
|
min-width: 720px;
|
||||||
@@ -863,6 +936,10 @@ td small {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-name-button small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -899,6 +976,24 @@ td small {
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-card .mini-actions,
|
||||||
|
.key-card .action-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card .mini-actions button,
|
||||||
|
.key-card .action-buttons button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.traffic-cell {
|
.traffic-cell {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1310,6 +1405,14 @@ td small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
@media (max-width: 1280px) {
|
||||||
|
.key-card {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
grid-template-areas:
|
||||||
|
"user traffic"
|
||||||
|
"secret links"
|
||||||
|
"actions actions";
|
||||||
|
}
|
||||||
|
|
||||||
.service-grid {
|
.service-grid {
|
||||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||||
}
|
}
|
||||||
@@ -1488,6 +1591,16 @@ td small {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"user"
|
||||||
|
"secret"
|
||||||
|
"links"
|
||||||
|
"traffic"
|
||||||
|
"actions";
|
||||||
|
}
|
||||||
|
|
||||||
.ip-limit-control {
|
.ip-limit-control {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,19 +129,22 @@ class AdminFeatureTests(unittest.TestCase):
|
|||||||
self.assertNotIn("client = 3", text)
|
self.assertNotIn("client = 3", text)
|
||||||
self.assertNotIn("old = 4", text)
|
self.assertNotIn("old = 4", text)
|
||||||
|
|
||||||
def test_keys_table_keeps_actions_inside_table_cell_wrapper(self):
|
def test_keys_view_uses_card_layout_without_horizontal_table(self):
|
||||||
app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8")
|
app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8")
|
||||||
styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8")
|
styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8")
|
||||||
index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8")
|
index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
self.assertNotIn('class="actions"', app_js)
|
self.assertNotIn('class="actions"', app_js)
|
||||||
|
self.assertIn('class="key-card', app_js)
|
||||||
|
self.assertIn('class="key-card-actions"', app_js)
|
||||||
self.assertIn('class="action-buttons"', app_js)
|
self.assertIn('class="action-buttons"', app_js)
|
||||||
self.assertIn('class="key-name-button"', app_js)
|
self.assertIn('class="key-name-button"', app_js)
|
||||||
self.assertIn(".keys-table .mini-actions,\n.keys-table .action-buttons", styles)
|
self.assertIn(".key-card .mini-actions,\n.key-card .action-buttons", styles)
|
||||||
self.assertIn("grid-template-columns: 1fr", styles)
|
self.assertIn("grid-template-columns: 1fr", styles)
|
||||||
self.assertIn("min-height: 44px", styles)
|
self.assertIn("min-height: 44px", styles)
|
||||||
self.assertNotIn("td.actions", styles)
|
self.assertNotIn("td.actions", styles)
|
||||||
self.assertRegex(index, r'<table class="keys-table">[\s\S]*?<tbody id="usersTable">')
|
self.assertIn('class="keys-list" id="usersTable"', index)
|
||||||
|
self.assertNotIn('class="keys-table"', index)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user