Merge pull request #370 from GQingL/main

Enhance RBAC UI with scrolling, spacing, and layout fixes
This commit is contained in:
公明 authored and GitHub committed 2026-10-28 09:45:04 +08:00
commit 5affb4df1b
15 files changed
+212 -27

No files matched your search

+83 -9
View File
@@ -10604,6 +10604,43 @@ html[data-theme="dark"] .robot-binding-service-hint-icon {
font-size: 0.875rem;
}
.pagination-custom-select {
display: inline-flex;
flex: 0 0 auto;
width: 76px;
vertical-align: middle;
}
.pagination-custom-select .settings-custom-select-trigger {
min-height: 34px;
padding: 6px 10px;
border-radius: 8px;
font-size: 0.8125rem;
}
.pagination-custom-select .settings-custom-select-caret {
width: 8px;
height: 8px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
font-size: 0;
transform: translateY(-2px) rotate(45deg);
}
.settings-custom-select.pagination-custom-select.open .settings-custom-select-caret {
transform: translateY(2px) rotate(225deg);
}
.sidebar-list-pagination .pagination-custom-select {
width: 64px;
}
.sidebar-list-pagination .pagination-custom-select .settings-custom-select-trigger {
min-height: 28px;
padding: 4px 8px;
font-size: 0.75rem;
}
.settings-custom-select-value {
flex: 1;
min-width: 0;
@@ -14008,6 +14045,10 @@ html[data-theme="dark"] .robot-binding-service-hint-icon {
display: grid;
grid-template-columns: minmax(110px, 0.65fr) minmax(150px, 1fr) minmax(190px, 1.25fr);
gap: 8px;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
scrollbar-width: thin;
}
.rbac-resource-filterbar :is(.form-select, .form-input),
@@ -16117,6 +16158,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.external-mcp-actions {
display: flex;
flex: 0 0 auto;
min-width: 0;
align-items: center;
gap: 12px;
flex-wrap: wrap;
@@ -16124,9 +16167,12 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.external-mcp-stats {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 20px;
margin-left: auto;
gap: 8px 20px;
width: 100%;
min-width: 0;
margin-left: 0;
padding: 10px 20px;
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
border: 1px solid var(--border-color);
@@ -16228,21 +16274,24 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.external-mcp-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.external-mcp-item-info {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
flex: 1;
width: 100%;
min-width: 0;
}
.external-mcp-item-info h4 {
flex: 1 1 180px;
min-width: 0;
overflow-wrap: anywhere;
margin: 0;
font-size: 1.125rem;
font-weight: 600;
@@ -16269,6 +16318,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
}
.external-mcp-status {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
@@ -16347,6 +16397,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.external-mcp-item-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
@@ -33734,6 +33785,14 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error {
font-size: 0.75rem;
border-radius: 6px;
}
.sidebar-list-pagination .projects-filter-select-dropdown {
top: auto;
bottom: calc(100% + 4px);
left: auto;
min-width: 88px;
}
.projects-fact-filter-field--search input {
padding-left: 34px;
background: #fff;
@@ -42301,6 +42360,12 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr
max-height: none;
}
.rbac-panel-users > .rbac-panel-head,
.rbac-panel-users > .rbac-search {
/* Keep the fixed controls intact when a long member list needs to shrink. */
flex: 0 0 auto;
}
.rbac-panel-users .rbac-list {
flex: 1 1 auto;
min-height: 0;
@@ -42694,7 +42759,7 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr
gap: 0;
min-height: 0;
max-height: none;
padding: 0 12px;
padding: 12px;
}
.rbac-assignments-list .rbac-assignment-row {
@@ -42814,19 +42879,23 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr
}
.rbac-role-management {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 18px;
border-radius: 10px;
}
.rbac-role-management-head {
flex: 0 0 auto;
align-items: flex-end;
flex-wrap: wrap;
row-gap: 12px;
}
.rbac-role-management-head > div:first-child {
flex: 1 1 320px;
flex: 1 1 auto;
min-width: 0;
}
@@ -42861,7 +42930,12 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr
}
.rbac-role-catalog {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
align-content: start;
align-items: stretch;
}
+1
View File
@@ -402,6 +402,7 @@ function renderAuditLogsPagination() {
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + totalPages + ')"' + (disabledLast ? ' disabled' : '') + '>' + esc(lastPageLabel) + '</button>';
html += '</div></div>';
container.innerHTML = html;
if (typeof initPaginationSelects === 'function') initPaginationSelects(container);
}
function goAuditLogsPage(p) {
+4
View File
@@ -3256,6 +3256,7 @@
const totalPages = Math.max(1, Math.ceil(total / pageSize));
if (total === 0) {
paginationContainer.innerHTML = '';
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
return;
}
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
@@ -3284,6 +3285,7 @@
`;
html += '</div>';
paginationContainer.innerHTML = html;
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
if (typeof applyTranslations === 'function') applyTranslations(paginationContainer);
};
@@ -4194,6 +4196,7 @@
if (total === 0) {
paginationContainer.innerHTML = '';
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
return;
}
@@ -4224,6 +4227,7 @@
`;
html += '</div>';
paginationContainer.innerHTML = html;
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
if (typeof applyTranslations === 'function') applyTranslations(paginationContainer);
};
+2
View File
@@ -509,6 +509,7 @@ function renderChatFilesPagination() {
if (groupMode === 'folder' || chatFilesTotal <= 0) {
pager.hidden = true;
pager.innerHTML = '';
if (typeof initPaginationSelects === 'function') initPaginationSelects(pager);
return;
}
const totalPages = chatFilesTotalPages();
@@ -542,6 +543,7 @@ function renderChatFilesPagination() {
<button type="button" class="btn-secondary" ${chatFilesPage >= totalPages ? 'disabled' : ''} onclick="changeChatFilesPage(${chatFilesPage + 1})">${escapeHtml(nextLabel)}</button>
</div>`;
pager.hidden = false;
if (typeof initPaginationSelects === 'function') initPaginationSelects(pager);
}
function changeChatFilesPage(page) {
+2
View File
@@ -9923,6 +9923,7 @@ function renderConversationsPagination(visibleCount) {
if (!total) {
el.innerHTML = '';
el.hidden = true;
if (typeof initPaginationSelects === 'function') initPaginationSelects(el);
return;
}
@@ -9963,6 +9964,7 @@ function renderConversationsPagination(visibleCount) {
</select>
</label>
</div>`;
if (typeof initPaginationSelects === 'function') initPaginationSelects(el);
} finally {
conversationsPaginationRenderLock = false;
}
+1
View File
@@ -174,6 +174,7 @@ function renderHitlPagination(containerId, state, goPageFnName, pageSizeChangeFn
html += '<button type="button" class="btn-secondary" onclick="' + esc(goPageFnName) + '(' + totalPages + ')"' + (disabledLast ? ' disabled' : '') + '>' + esc(lastPageLabel) + '</button>';
html += '</div></div>';
container.innerHTML = html;
if (typeof initPaginationSelects === 'function') initPaginationSelects(container);
}
function hitlEffectiveEnabled(cfg) {
+1
View File
@@ -9322,6 +9322,7 @@ function renderMonitorPagination() {
`;
container.appendChild(pagination);
if (typeof initPaginationSelects === 'function') initPaginationSelects(pagination);
// 初始化每页显示数量选择器
initializeMonitorPageSize();
+59
View File
@@ -0,0 +1,59 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
function setup() {
const source = fs.readFileSync(path.join(__dirname, 'settings.js'), 'utf8');
const context = vm.createContext({ window: { innerWidth: 800, innerHeight: 600 } });
vm.runInContext(source.slice(0, source.indexOf('function getRobotStatus(')), context);
vm.runInContext('globalThis.selects = settingsCustomSelects;', context);
return context;
}
function menuAt(rect) {
return {
wrapper: { classList: { contains: () => true } },
trigger: { getBoundingClientRect: () => rect },
menu: { style: {} },
};
}
test('a pagination menu near the viewport bottom opens above its trigger', () => {
const context = setup();
const reg = menuAt({ left: 700, top: 550, bottom: 584, width: 76 });
context.positionSettingsCustomSelectMenu(reg);
assert.equal(reg.menu.style.top, 'auto');
assert.equal(reg.menu.style.bottom, '56px');
assert.ok(parseInt(reg.menu.style.left) + parseInt(reg.menu.style.width) <= 788);
});
test('a menu near the viewport top opens below instead', () => {
const context = setup();
const reg = menuAt({ left: 20, top: 20, bottom: 54, width: 76 });
context.positionSettingsCustomSelectMenu(reg);
assert.equal(reg.menu.style.top, '60px');
assert.equal(reg.menu.style.bottom, 'auto');
});
test('replacing pagination DOM removes its detached floating menu', () => {
const context = setup();
let removed = false;
let synced = null;
const stale = { isConnected: false };
const current = { isConnected: true };
const reg = {
wrapper: { classList: { remove() {} }, appendChild(menu) { menu.parentNode = this; } },
trigger: { setAttribute() {} },
menu: { style: {}, classList: { remove() {} }, remove() { removed = true; } },
};
context.selects.set(stale, reg);
context.selects.set(current, {});
context.syncSettingsCustomSelect = select => { synced = select; };
context.refreshSettingsCustomSelects();
assert.equal(removed, true);
assert.equal(context.selects.has(stale), false);
assert.equal(context.selects.has(current), true);
assert.equal(synced, current);
});
+1
View File
@@ -1135,6 +1135,7 @@ function renderProjectAssetsPagination() {
<button type="button" class="btn-secondary" onclick="loadProjectAssets(${totalPages})" ${atLast ? 'disabled' : ''}>${escapeHtml(tpFmt('skillsPage.lastPage', '尾页'))}</button>
</div>
</div>`;
if (typeof initPaginationSelects === 'function') initPaginationSelects(root);
}
function changeProjectAssetsPageSize(value) {
+1
View File
@@ -1032,6 +1032,7 @@ function renderRoleToolsPagination() {
`;
toolsList.appendChild(pagination);
if (typeof initPaginationSelects === 'function') initPaginationSelects(pagination);
}
function syncRoleToolsFilterButtons() {
+39 -5
View File
@@ -172,7 +172,15 @@ function syncSettingsCustomSelect(select) {
}
function refreshSettingsCustomSelects() {
settingsCustomSelects.forEach((_reg, select) => syncSettingsCustomSelect(select));
settingsCustomSelects.forEach((reg, select) => {
if (!select.isConnected) {
closeSettingsCustomSelect(select);
reg.menu.remove();
settingsCustomSelects.delete(select);
return;
}
syncSettingsCustomSelect(select);
});
}
function enhanceSettingsSelect(select) {
@@ -277,10 +285,7 @@ function enhanceSettingsSelect(select) {
syncSettingsCustomSelect(select);
}
function initSettingsCustomSelects(root) {
const scope = root || document.getElementById('page-settings');
if (!scope) return;
scope.querySelectorAll('select').forEach(enhanceSettingsSelect);
function bindSettingsCustomSelectEvents() {
if (!settingsCustomSelectsDocBound) {
document.addEventListener('click', closeAllSettingsCustomSelects);
document.addEventListener('keydown', (event) => {
@@ -288,8 +293,35 @@ function initSettingsCustomSelects(root) {
});
document.addEventListener('scroll', repositionOpenSettingsCustomSelects, true);
window.addEventListener('resize', repositionOpenSettingsCustomSelects);
window.addEventListener('hashchange', closeAllSettingsCustomSelects);
settingsCustomSelectsDocBound = true;
}
}
function initSettingsCustomSelects(root) {
const scope = root || document.getElementById('page-settings');
if (!scope) return;
scope.querySelectorAll('select').forEach(enhanceSettingsSelect);
bindSettingsCustomSelectEvents();
refreshSettingsCustomSelects();
}
// Pagination menus use the floating select so clipped panels cannot hide options.
function initPaginationSelects(root) {
if (!root) return;
root.querySelectorAll('.pagination-page-size select').forEach(select => {
if (select.dataset.projectsCustomSelect === '1') return;
enhanceSettingsSelect(select);
const reg = settingsCustomSelects.get(select);
if (!reg) return;
reg.wrapper.classList.add('pagination-custom-select');
const label = select.labels?.[0] || select.closest('.pagination-page-size');
const labelText = label && Array.from(label.childNodes)
.filter(node => node.nodeType === 3).map(node => node.textContent).join(' ').trim();
const accessibleName = select.getAttribute('aria-label') || labelText;
if (accessibleName) reg.trigger.setAttribute('aria-label', accessibleName);
});
bindSettingsCustomSelectEvents();
refreshSettingsCustomSelects();
}
@@ -1675,6 +1707,7 @@ function renderToolsPagination() {
// 如果只有一页或没有数据,不显示分页
if (toolsPagination.totalPages <= 1) {
initPaginationSelects(toolsList);
return;
}
@@ -1716,6 +1749,7 @@ function renderToolsPagination() {
`;
toolsList.appendChild(pagination);
initPaginationSelects(pagination);
}
// 处理工具checkbox状态变化
+2
View File
@@ -231,6 +231,7 @@ function renderSkillsPagination() {
// 即使只有一页也显示分页信息(参考MCP样式)
if (total === 0) {
paginationContainer.innerHTML = '';
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
return;
}
@@ -277,6 +278,7 @@ function renderSkillsPagination() {
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
// 确保分页组件与列表内容区域对齐(不包括滚动条)
function alignPaginationWidth() {
+2
View File
@@ -1687,6 +1687,7 @@ function renderBatchQueuesPagination() {
// 即使只有一页也显示分页信息(与 MCP 监控一致)
if (total === 0) {
paginationContainer.innerHTML = '';
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
return;
}
@@ -1726,6 +1727,7 @@ function renderBatchQueuesPagination() {
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
}
// 跳转到指定页面
+1
View File
@@ -1522,6 +1522,7 @@ function renderVulnerabilityPagination() {
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
if (typeof initPaginationSelects === 'function') initPaginationSelects(paginationContainer);
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(paginationContainer);
}
+13 -13
View File
@@ -39,7 +39,7 @@
}
})();
</script>
<link rel="stylesheet" href="/static/css/style.css?v=20261027-settings-sidebar-scroll">
<link rel="stylesheet" href="/static/css/style.css?v=20261027-pagination-custom-select">
<link rel="stylesheet" href="/static/css/tool-guard.css?v=20260907-7">
<link rel="stylesheet" href="/static/css/chat-plan-progress.css?v=20260813-4">
<link rel="stylesheet" href="/static/css/c2.css">
@@ -7024,35 +7024,35 @@
<script src="/static/js/agents.js"></script>
<script src="/static/js/dashboard.js?v=20261011-token-settings-cache-only"></script>
<script src="/static/js/chat-scroll.js?v=20260815-1"></script>
<script src="/static/js/monitor.js?v=20261011-token-usage"></script>
<script src="/static/js/monitor.js?v=20261011-token-usage-pagination-20261027"></script>
<script src="/static/js/attack-chain-context.js?v=20261012-summary-2"></script>
<script src="/static/js/chat.js?v=20261016-context-memory-token-usage"></script>
<script src="/static/js/chat.js?v=20261016-context-memory-token-usage-pagination-20261027"></script>
<script src="/static/js/chat-plan-progress.js?v=20260815-1"></script>
<script src="/static/js/hitl.js?v=20260819-1"></script>
<script src="/static/js/hitl.js?v=20260819-1-pagination-20261027"></script>
<script src="/static/js/tool-guard.js?v=20261010-conflict"></script>
<script src="/static/js/token-usage.js?v=20261012-token-analytics"></script>
<script src="/static/js/settings.js?v=20261011-token-settings"></script>
<script src="/static/js/settings.js?v=20261011-token-settings-pagination-20261027"></script>
<script src="/static/js/audit-datetime-picker.js"></script>
<script src="/static/js/audit.js"></script>
<script src="/static/js/audit.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/storage.js"></script>
<script src="/static/js/wechat-robot.js"></script>
<script src="/static/vendor/xterm.js"></script>
<script src="/static/vendor/xterm-addon-fit.js"></script>
<script src="/static/js/terminal.js"></script>
<script src="/static/js/knowledge.js"></script>
<script src="/static/js/skills.js"></script>
<script src="/static/js/skills.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/fact-graph.js"></script>
<script src="/static/js/projects.js?v=20260819-1"></script>
<script src="/static/js/vulnerability.js?v=20261010-retest-2"></script>
<script src="/static/js/projects.js?v=20260819-1-pagination-20261027"></script>
<script src="/static/js/vulnerability.js?v=20261010-retest-2-pagination-20261027"></script>
<script src="/static/js/webshell.js?v=20261011-token-usage"></script>
<script src="/static/js/chat-files.js"></script>
<script src="/static/js/tasks.js"></script>
<script src="/static/js/chat-files.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/tasks.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/workflow-package-client.js"></script>
<script src="/static/js/workflows.js"></script>
<script src="/static/js/update.js?v={{.Version}}"></script>
<script src="/static/js/roles.js"></script>
<script src="/static/js/roles.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/rbac.js"></script>
<script src="/static/js/c2.js"></script>
<script src="/static/js/c2.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/rbac-guards.js?v=20260907-1"></script>
<!-- 移动端交互层:必须最后加载,它包装 window.switchPage 并注入移动端 DOM -->
<script src="/static/js/mobile-ui.js?v=20261006-9"></script>