Fixed web ui. Added deprecation feature

This commit is contained in:
Ultradesu
2025-07-19 12:56:25 +03:00
parent e33910a2db
commit 45ac3fca51
9 changed files with 1055 additions and 533 deletions

View File

@@ -36,6 +36,14 @@
<button id="addKeyBtn" class="btn btn-primary">Add SSH Key</button>
<button id="bulkDeleteBtn" class="btn btn-danger" disabled>Deprecate Selected</button>
<button id="bulkPermanentDeleteBtn" class="btn btn-danger" disabled style="display: none;">Delete Selected</button>
<div class="filter-controls">
<label class="filter-label">
<input type="checkbox" id="showDeprecatedOnly">
<span>Show only deprecated keys</span>
</label>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search servers or keys...">
</div>
@@ -48,9 +56,9 @@
<th>
<input type="checkbox" id="selectAll">
</th>
<th>Server</th>
<th>Key Type</th>
<th>Server/Type</th>
<th>Key Preview</th>
<th></th>
<th>Actions</th>
</tr>
</thead>

View File

@@ -3,9 +3,13 @@ class SSHKeyManager {
this.currentFlow = null;
this.keys = [];
this.filteredKeys = [];
this.groupedKeys = {};
this.expandedGroups = new Set();
this.currentPage = 1;
this.keysPerPage = 20;
this.serversPerPage = 10;
this.selectedKeys = new Set();
this.showDeprecatedOnly = false;
this.initializeEventListeners();
this.loadFlows();
@@ -50,6 +54,21 @@ class SSHKeyManager {
this.filterKeys(e.target.value);
});
// Deprecated filter checkbox
document.getElementById('showDeprecatedOnly').addEventListener('change', (e) => {
this.showDeprecatedOnly = e.target.checked;
// Update visual state
const filterLabel = e.target.closest('.filter-label');
if (e.target.checked) {
filterLabel.classList.add('active');
} else {
filterLabel.classList.remove('active');
}
this.filterKeys(document.getElementById('searchInput').value);
});
// Select all checkbox
document.getElementById('selectAll').addEventListener('change', (e) => {
this.toggleSelectAll(e.target.checked);
@@ -146,11 +165,12 @@ class SSHKeyManager {
try {
this.showLoading();
const response = await fetch(`/${this.currentFlow}/keys`);
const response = await fetch(`/${this.currentFlow}/keys?include_deprecated=true`);
if (!response.ok) throw new Error('Failed to load keys');
this.keys = await response.json();
this.filteredKeys = [...this.keys];
this.groupKeys();
this.filterKeys();
this.updateStats();
this.renderTable();
this.selectedKeys.clear();
@@ -163,16 +183,37 @@ class SSHKeyManager {
}
}
groupKeys() {
this.groupedKeys = {};
this.keys.forEach(key => {
if (!this.groupedKeys[key.server]) {
this.groupedKeys[key.server] = [];
}
this.groupedKeys[key.server].push(key);
});
// Groups are closed by default - no auto-expand
}
filterKeys(searchTerm) {
if (!searchTerm.trim()) {
this.filteredKeys = [...this.keys];
let keys = [...this.keys];
// Apply deprecated filter first
if (this.showDeprecatedOnly) {
keys = keys.filter(key => key.deprecated);
}
// Then apply search filter
if (!searchTerm || !searchTerm.trim()) {
this.filteredKeys = keys;
} else {
const term = searchTerm.toLowerCase();
this.filteredKeys = this.keys.filter(key =>
this.filteredKeys = keys.filter(key =>
key.server.toLowerCase().includes(term) ||
key.public_key.toLowerCase().includes(term)
);
}
this.currentPage = 1;
this.renderTable();
}
@@ -184,6 +225,17 @@ class SSHKeyManager {
document.getElementById('uniqueServers').textContent = uniqueServers.size;
}
getGroupedFilteredKeys() {
const groupedFilteredKeys = {};
this.filteredKeys.forEach(key => {
if (!groupedFilteredKeys[key.server]) {
groupedFilteredKeys[key.server] = [];
}
groupedFilteredKeys[key.server].push(key);
});
return groupedFilteredKeys;
}
renderTable() {
const tbody = document.getElementById('keysTableBody');
const noKeysMessage = document.getElementById('noKeysMessage');
@@ -197,37 +249,78 @@ class SSHKeyManager {
noKeysMessage.style.display = 'none';
const startIndex = (this.currentPage - 1) * this.keysPerPage;
const endIndex = startIndex + this.keysPerPage;
const pageKeys = this.filteredKeys.slice(startIndex, endIndex);
// Group filtered keys by server
const groupedFilteredKeys = this.getGroupedFilteredKeys();
tbody.innerHTML = pageKeys.map((key, index) => {
const keyType = this.getKeyType(key.public_key);
const keyPreview = this.getKeyPreview(key.public_key);
const keyId = `${key.server}-${key.public_key}`;
// Calculate pagination for grouped view
const servers = Object.keys(groupedFilteredKeys).sort();
// For pagination, we'll show a reasonable number of server groups per page
const startServerIndex = (this.currentPage - 1) * this.serversPerPage;
const endServerIndex = startServerIndex + this.serversPerPage;
const pageServers = servers.slice(startServerIndex, endServerIndex);
let html = '';
pageServers.forEach(server => {
const serverKeys = groupedFilteredKeys[server];
const activeCount = serverKeys.filter(k => !k.deprecated).length;
const deprecatedCount = serverKeys.filter(k => k.deprecated).length;
const isExpanded = this.expandedGroups.has(server);
return `
<tr${key.deprecated ? ' class="deprecated"' : ''}>
// Server group header
html += `
<tr class="host-group-header ${isExpanded ? '' : 'collapsed'}">
<td>
<input type="checkbox" data-key-id="${keyId}" ${this.selectedKeys.has(keyId) ? 'checked' : ''}>
<input type="checkbox"
data-group="${this.escapeHtml(server)}"
onchange="sshKeyManager.toggleGroupSelection('${this.escapeHtml(server)}', this.checked)"
onclick="event.stopPropagation()">
</td>
<td>
${this.escapeHtml(key.server)}
${key.deprecated ? '<span class="deprecated-badge">DEPRECATED</span>' : ''}
</td>
<td><span class="key-type ${keyType.toLowerCase()}">${keyType}</span></td>
<td><span class="key-preview">${keyPreview}</span></td>
<td class="table-actions">
<button class="btn btn-sm btn-secondary" onclick="sshKeyManager.viewKey('${keyId}')">View</button>
${key.deprecated ?
`<button class="btn btn-sm btn-success" onclick="sshKeyManager.restoreKey('${keyId}')">Restore</button>
<button class="btn btn-sm btn-danger" onclick="sshKeyManager.permanentlyDeleteKey('${keyId}')">Delete</button>` :
`<button class="btn btn-sm btn-danger" onclick="sshKeyManager.deleteKey('${keyId}')">Deprecate</button>`
}
<td colspan="4" onclick="sshKeyManager.toggleGroup('${this.escapeHtml(server)}')" style="cursor: pointer;">
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
<strong>${this.escapeHtml(server)}</strong>
<span class="host-summary">
<span class="key-count">${serverKeys.length} keys</span>
${deprecatedCount > 0 ? `<span class="deprecated-count">${deprecatedCount} deprecated</span>` : ''}
</span>
</td>
</tr>
`;
}).join('');
// Server keys (if expanded)
if (isExpanded) {
serverKeys.forEach(key => {
const keyType = this.getKeyType(key.public_key);
const keyPreview = this.getKeyPreview(key.public_key);
const keyId = `${key.server}-${key.public_key}`;
html += `
<tr class="key-row${key.deprecated ? ' deprecated' : ''}">
<td>
<input type="checkbox" data-key-id="${keyId}" ${this.selectedKeys.has(keyId) ? 'checked' : ''}>
</td>
<td style="padding-left: 2rem;">
<span class="key-type ${keyType.toLowerCase()}">${keyType}</span>
${key.deprecated ? '<span class="deprecated-badge">DEPRECATED</span>' : ''}
</td>
<td><span class="key-preview">${keyPreview}</span></td>
<td></td>
<td class="table-actions">
<button class="btn btn-sm btn-secondary" onclick="sshKeyManager.viewKey('${keyId}')">View</button>
${key.deprecated ?
`<button class="btn btn-sm btn-success" onclick="sshKeyManager.restoreKey('${keyId}')">Restore</button>
<button class="btn btn-sm btn-danger" onclick="sshKeyManager.permanentlyDeleteKey('${keyId}')">Delete</button>` :
`<button class="btn btn-sm btn-danger" onclick="sshKeyManager.deleteKey('${keyId}')">Deprecate</button>`
}
</td>
</tr>
`;
});
}
});
tbody.innerHTML = html;
// Add event listeners for checkboxes
tbody.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
@@ -240,14 +333,78 @@ class SSHKeyManager {
}
this.updateBulkDeleteButton();
this.updateSelectAllCheckbox();
this.updateGroupCheckboxes(); // Update group checkboxes when individual keys change
});
});
// Update group checkboxes to show correct indeterminate state
this.updateGroupCheckboxes();
this.updatePagination();
}
toggleGroup(server) {
if (this.expandedGroups.has(server)) {
this.expandedGroups.delete(server);
} else {
this.expandedGroups.add(server);
}
this.renderTable();
}
toggleGroupSelection(server, isChecked) {
const groupedFilteredKeys = this.getGroupedFilteredKeys();
const serverKeys = groupedFilteredKeys[server] || [];
serverKeys.forEach(key => {
const keyId = `${key.server}-${key.public_key}`;
if (isChecked) {
this.selectedKeys.add(keyId);
} else {
this.selectedKeys.delete(keyId);
}
});
this.updateBulkDeleteButton();
this.updateSelectAllCheckbox();
this.updateGroupCheckboxes();
// Update individual checkboxes without full re-render
const tbody = document.getElementById('keysTableBody');
serverKeys.forEach(key => {
const keyId = `${key.server}-${key.public_key}`;
const checkbox = tbody.querySelector(`input[data-key-id="${keyId}"]`);
if (checkbox) {
checkbox.checked = this.selectedKeys.has(keyId);
}
});
}
updateGroupCheckboxes() {
const groupedFilteredKeys = this.getGroupedFilteredKeys();
const tbody = document.getElementById('keysTableBody');
Object.keys(groupedFilteredKeys).forEach(server => {
const serverKeys = groupedFilteredKeys[server];
const groupCheckbox = tbody.querySelector(`input[data-group="${server}"]`);
if (groupCheckbox) {
const allSelected = serverKeys.every(key =>
this.selectedKeys.has(`${key.server}-${key.public_key}`)
);
const someSelected = serverKeys.some(key =>
this.selectedKeys.has(`${key.server}-${key.public_key}`)
);
groupCheckbox.checked = allSelected;
groupCheckbox.indeterminate = someSelected && !allSelected;
}
});
}
updatePagination() {
const totalPages = Math.ceil(this.filteredKeys.length / this.keysPerPage);
const groupedFilteredKeys = this.getGroupedFilteredKeys();
const totalServers = Object.keys(groupedFilteredKeys).length;
const totalPages = Math.ceil(totalServers / this.serversPerPage);
document.getElementById('pageInfo').textContent = `Page ${this.currentPage} of ${totalPages}`;
document.getElementById('prevPage').disabled = this.currentPage <= 1;
@@ -255,7 +412,10 @@ class SSHKeyManager {
}
changePage(newPage) {
const totalPages = Math.ceil(this.filteredKeys.length / this.keysPerPage);
const groupedFilteredKeys = this.getGroupedFilteredKeys();
const totalServers = Object.keys(groupedFilteredKeys).length;
const totalPages = Math.ceil(totalServers / this.serversPerPage);
if (newPage >= 1 && newPage <= totalPages) {
this.currentPage = newPage;
this.renderTable();

View File

@@ -170,6 +170,46 @@ header h1 {
flex-wrap: wrap;
}
.filter-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.filter-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-primary);
cursor: pointer;
user-select: none;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius);
transition: background-color 0.2s ease;
}
.filter-label:hover {
background-color: var(--background);
}
.filter-label.active {
background-color: var(--primary-color);
color: white;
}
.filter-label.active input[type="checkbox"] {
accent-color: white;
}
.filter-label input[type="checkbox"] {
margin: 0;
}
.filter-label span {
white-space: nowrap;
}
.search-box input {
padding: 0.5rem 1rem;
border: 1px solid var(--border);
@@ -225,6 +265,79 @@ header h1 {
color: var(--text-secondary);
}
.host-group-header {
background-color: #f1f5f9;
font-weight: 600;
transition: background-color 0.2s ease;
border-left: 4px solid var(--primary-color);
}
.host-group-header:hover {
background-color: #e2e8f0;
}
.host-group-header.collapsed {
border-left-color: var(--secondary-color);
}
.host-group-header .expand-icon {
transition: transform 0.2s ease;
display: inline-block;
margin-right: 0.5rem;
user-select: none;
}
.host-group-header.collapsed .expand-icon {
transform: rotate(-90deg);
}
.host-group-header input[type="checkbox"] {
margin: 0;
}
.host-group-header td:first-child {
width: 50px;
text-align: center;
}
.host-group-header td:nth-child(2) {
cursor: pointer;
user-select: none;
}
.key-row {
border-left: 4px solid transparent;
}
.key-row.hidden {
display: none;
}
.host-summary {
font-size: 0.875rem;
color: var(--text-secondary);
}
.key-count {
background-color: var(--primary-color);
color: white;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
margin-left: 0.5rem;
}
.deprecated-count {
background-color: var(--danger-color);
color: white;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
margin-left: 0.25rem;
}
.key-preview {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
@@ -480,6 +593,11 @@ header h1 {
.actions-panel {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.filter-controls {
justify-content: center;
}
.search-box input {
@@ -515,6 +633,13 @@ input[type="checkbox"] {
accent-color: var(--primary-color);
}
/* Indeterminate checkbox styling */
input[type="checkbox"]:indeterminate {
background-color: var(--primary-color);
background-image: linear-gradient(90deg, transparent 40%, white 40%, white 60%, transparent 60%);
border-color: var(--primary-color);
}
/* Action buttons in table */
.table-actions {
display: flex;