Improve web UI: sort/search/context menu, fix security and UX bugs
This commit is contained in:
@@ -2042,15 +2042,16 @@ def update_connection(connection_id: str):
|
|||||||
secret_key = request.form.get("secret_key", "").strip()
|
secret_key = request.form.get("secret_key", "").strip()
|
||||||
region = request.form.get("region", "us-east-1").strip()
|
region = request.form.get("region", "us-east-1").strip()
|
||||||
|
|
||||||
if not all([name, endpoint, access_key, secret_key]):
|
if not all([name, endpoint, access_key]):
|
||||||
if _wants_json():
|
if _wants_json():
|
||||||
return jsonify({"error": "All fields are required"}), 400
|
return jsonify({"error": "Name, endpoint, and access key are required"}), 400
|
||||||
flash("All fields are required", "danger")
|
flash("Name, endpoint, and access key are required", "danger")
|
||||||
return redirect(url_for("ui.connections_dashboard"))
|
return redirect(url_for("ui.connections_dashboard"))
|
||||||
|
|
||||||
conn.name = name
|
conn.name = name
|
||||||
conn.endpoint_url = endpoint
|
conn.endpoint_url = endpoint
|
||||||
conn.access_key = access_key
|
conn.access_key = access_key
|
||||||
|
if secret_key:
|
||||||
conn.secret_key = secret_key
|
conn.secret_key = secret_key
|
||||||
conn.region = region
|
conn.region = region
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
APP_VERSION = "0.2.9"
|
APP_VERSION = "0.3.0"
|
||||||
|
|
||||||
|
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
|
|||||||
@@ -2819,6 +2819,112 @@ body:has(.login-card) .main-wrapper {
|
|||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1060;
|
||||||
|
min-width: 180px;
|
||||||
|
background: var(--myfsio-card-bg);
|
||||||
|
border: 1px solid var(--myfsio-card-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .context-menu {
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.625rem;
|
||||||
|
padding: 0.5rem 0.875rem;
|
||||||
|
color: var(--myfsio-text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background-color: var(--myfsio-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.text-danger:hover {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--myfsio-card-border);
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-shortcut {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--myfsio-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd-shortcuts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd-shortcuts-list .shortcut-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd-shortcuts-list kbd {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.75rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--myfsio-preview-bg);
|
||||||
|
border: 1px solid var(--myfsio-card-border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
color: var(--myfsio-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .kbd-shortcuts-list kbd {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-dropdown .dropdown-item.active,
|
||||||
|
.sort-dropdown .dropdown-item:active {
|
||||||
|
background-color: var(--myfsio-hover-bg);
|
||||||
|
color: var(--myfsio-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-dropdown .dropdown-item {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.sidebar,
|
.sidebar,
|
||||||
.mobile-header {
|
.mobile-header {
|
||||||
|
|||||||
@@ -162,6 +162,8 @@
|
|||||||
let isLoadingObjects = false;
|
let isLoadingObjects = false;
|
||||||
let hasMoreObjects = false;
|
let hasMoreObjects = false;
|
||||||
let currentFilterTerm = '';
|
let currentFilterTerm = '';
|
||||||
|
let currentSortField = 'name';
|
||||||
|
let currentSortDir = 'asc';
|
||||||
let pageSize = 5000;
|
let pageSize = 5000;
|
||||||
let currentPrefix = '';
|
let currentPrefix = '';
|
||||||
let allObjects = [];
|
let allObjects = [];
|
||||||
@@ -348,14 +350,18 @@
|
|||||||
const currentInputs = {
|
const currentInputs = {
|
||||||
objectCount: allObjects.length,
|
objectCount: allObjects.length,
|
||||||
prefix: currentPrefix,
|
prefix: currentPrefix,
|
||||||
filterTerm: currentFilterTerm
|
filterTerm: currentFilterTerm,
|
||||||
|
sortField: currentSortField,
|
||||||
|
sortDir: currentSortDir
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!forceRecompute &&
|
if (!forceRecompute &&
|
||||||
memoizedVisibleItems !== null &&
|
memoizedVisibleItems !== null &&
|
||||||
memoizedInputs.objectCount === currentInputs.objectCount &&
|
memoizedInputs.objectCount === currentInputs.objectCount &&
|
||||||
memoizedInputs.prefix === currentInputs.prefix &&
|
memoizedInputs.prefix === currentInputs.prefix &&
|
||||||
memoizedInputs.filterTerm === currentInputs.filterTerm) {
|
memoizedInputs.filterTerm === currentInputs.filterTerm &&
|
||||||
|
memoizedInputs.sortField === currentInputs.sortField &&
|
||||||
|
memoizedInputs.sortDir === currentInputs.sortDir) {
|
||||||
return memoizedVisibleItems;
|
return memoizedVisibleItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +400,19 @@
|
|||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
if (a.type === 'folder' && b.type === 'file') return -1;
|
if (a.type === 'folder' && b.type === 'file') return -1;
|
||||||
if (a.type === 'file' && b.type === 'folder') return 1;
|
if (a.type === 'file' && b.type === 'folder') return 1;
|
||||||
const aKey = a.type === 'folder' ? a.path : a.data.key;
|
if (a.type === 'folder' && b.type === 'folder') {
|
||||||
const bKey = b.type === 'folder' ? b.path : b.data.key;
|
return a.path.localeCompare(b.path);
|
||||||
return aKey.localeCompare(bKey);
|
}
|
||||||
|
const dir = currentSortDir === 'asc' ? 1 : -1;
|
||||||
|
if (currentSortField === 'size') {
|
||||||
|
return (a.data.size - b.data.size) * dir;
|
||||||
|
}
|
||||||
|
if (currentSortField === 'date') {
|
||||||
|
const aTime = new Date(a.data.lastModified || a.data.last_modified || 0).getTime();
|
||||||
|
const bTime = new Date(b.data.lastModified || b.data.last_modified || 0).getTime();
|
||||||
|
return (aTime - bTime) * dir;
|
||||||
|
}
|
||||||
|
return a.data.key.localeCompare(b.data.key) * dir;
|
||||||
});
|
});
|
||||||
|
|
||||||
memoizedVisibleItems = items;
|
memoizedVisibleItems = items;
|
||||||
@@ -2034,6 +2050,128 @@
|
|||||||
refreshVirtualList();
|
refreshVirtualList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-sort-field]').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const field = el.dataset.sortField;
|
||||||
|
const dir = el.dataset.sortDir || 'asc';
|
||||||
|
currentSortField = field;
|
||||||
|
currentSortDir = dir;
|
||||||
|
document.querySelectorAll('[data-sort-field]').forEach(s => s.classList.remove('active'));
|
||||||
|
el.classList.add('active');
|
||||||
|
var label = document.getElementById('sort-dropdown-label');
|
||||||
|
if (label) label.textContent = el.textContent.trim();
|
||||||
|
refreshVirtualList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT' || e.target.isContentEditable) return;
|
||||||
|
|
||||||
|
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('object-search')?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
var kbModal = document.getElementById('keyboardShortcutsModal');
|
||||||
|
if (kbModal) {
|
||||||
|
var instance = bootstrap.Modal.getOrCreateInstance(kbModal);
|
||||||
|
instance.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
var searchInput = document.getElementById('object-search');
|
||||||
|
if (searchInput && document.activeElement === searchInput) {
|
||||||
|
searchInput.value = '';
|
||||||
|
currentFilterTerm = '';
|
||||||
|
refreshVirtualList();
|
||||||
|
searchInput.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Delete' && !e.ctrlKey && !e.metaKey) {
|
||||||
|
if (selectedRows.size > 0 && bulkDeleteButton && !bulkDeleteButton.disabled) {
|
||||||
|
bulkDeleteButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
if (visibleItems.length > 0 && selectAllCheckbox) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctxMenu = document.getElementById('objectContextMenu');
|
||||||
|
let ctxTargetRow = null;
|
||||||
|
|
||||||
|
const hideContextMenu = () => {
|
||||||
|
if (ctxMenu) ctxMenu.classList.add('d-none');
|
||||||
|
ctxTargetRow = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ctxMenu) {
|
||||||
|
document.addEventListener('click', hideContextMenu);
|
||||||
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
const row = e.target.closest('[data-object-row]');
|
||||||
|
if (!row) { hideContextMenu(); return; }
|
||||||
|
e.preventDefault();
|
||||||
|
ctxTargetRow = row;
|
||||||
|
|
||||||
|
const x = Math.min(e.clientX, window.innerWidth - 200);
|
||||||
|
const y = Math.min(e.clientY, window.innerHeight - 200);
|
||||||
|
ctxMenu.style.left = x + 'px';
|
||||||
|
ctxMenu.style.top = y + 'px';
|
||||||
|
ctxMenu.classList.remove('d-none');
|
||||||
|
});
|
||||||
|
|
||||||
|
ctxMenu.querySelectorAll('[data-ctx-action]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
if (!ctxTargetRow) return;
|
||||||
|
const action = btn.dataset.ctxAction;
|
||||||
|
const key = ctxTargetRow.dataset.key;
|
||||||
|
const bucket = objectsContainer?.dataset.bucket || '';
|
||||||
|
|
||||||
|
if (action === 'download') {
|
||||||
|
const url = ctxTargetRow.dataset.downloadUrl;
|
||||||
|
if (url) window.open(url, '_blank');
|
||||||
|
} else if (action === 'copy-path') {
|
||||||
|
const s3Path = 's3://' + bucket + '/' + key;
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(s3Path).then(() => {
|
||||||
|
if (window.showToast) window.showToast('Copied: ' + s3Path, 'Copied', 'success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === 'presign') {
|
||||||
|
selectRow(ctxTargetRow);
|
||||||
|
presignLink.value = '';
|
||||||
|
presignModal?.show();
|
||||||
|
requestPresignedUrl();
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
const deleteEndpoint = ctxTargetRow.dataset.deleteEndpoint;
|
||||||
|
if (deleteEndpoint) {
|
||||||
|
selectRow(ctxTargetRow);
|
||||||
|
const deleteModalEl = document.getElementById('deleteObjectModal');
|
||||||
|
const deleteModal = deleteModalEl ? bootstrap.Modal.getOrCreateInstance(deleteModalEl) : null;
|
||||||
|
const deleteObjectForm = document.getElementById('deleteObjectForm');
|
||||||
|
const deleteObjectKey = document.getElementById('deleteObjectKey');
|
||||||
|
if (deleteModal && deleteObjectForm) {
|
||||||
|
deleteObjectForm.setAttribute('action', deleteEndpoint);
|
||||||
|
if (deleteObjectKey) deleteObjectKey.textContent = key;
|
||||||
|
deleteModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
refreshVersionsButton?.addEventListener('click', () => {
|
refreshVersionsButton?.addEventListener('click', () => {
|
||||||
if (!activeRow) {
|
if (!activeRow) {
|
||||||
versionList.innerHTML = '<p class="text-muted small mb-0">Select an object to view versions.</p>';
|
versionList.innerHTML = '<p class="text-muted small mb-0">Select an object to view versions.</p>';
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ window.ConnectionsManagement = (function() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var controller = new AbortController();
|
var controller = new AbortController();
|
||||||
var timeoutId = setTimeout(function() { controller.abort(); }, 15000);
|
var timeoutId = setTimeout(function() { controller.abort(); }, 10000);
|
||||||
|
|
||||||
var response = await fetch(endpoints.healthTemplate.replace('CONNECTION_ID', connectionId), {
|
var response = await fetch(endpoints.healthTemplate.replace('CONNECTION_ID', connectionId), {
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
@@ -147,7 +147,7 @@ window.ConnectionsManagement = (function() {
|
|||||||
'<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editConnectionModal" ' +
|
'<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editConnectionModal" ' +
|
||||||
'data-id="' + window.UICore.escapeHtml(conn.id) + '" data-name="' + window.UICore.escapeHtml(conn.name) + '" ' +
|
'data-id="' + window.UICore.escapeHtml(conn.id) + '" data-name="' + window.UICore.escapeHtml(conn.name) + '" ' +
|
||||||
'data-endpoint="' + window.UICore.escapeHtml(conn.endpoint_url) + '" data-region="' + window.UICore.escapeHtml(conn.region) + '" ' +
|
'data-endpoint="' + window.UICore.escapeHtml(conn.endpoint_url) + '" data-region="' + window.UICore.escapeHtml(conn.region) + '" ' +
|
||||||
'data-access="' + window.UICore.escapeHtml(conn.access_key) + '" data-secret="' + window.UICore.escapeHtml(conn.secret_key || '') + '" title="Edit connection">' +
|
'data-access="' + window.UICore.escapeHtml(conn.access_key) + '" title="Edit connection">' +
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">' +
|
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">' +
|
||||||
'<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/></svg></button>' +
|
'<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/></svg></button>' +
|
||||||
'<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteConnectionModal" ' +
|
'<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteConnectionModal" ' +
|
||||||
@@ -185,7 +185,9 @@ window.ConnectionsManagement = (function() {
|
|||||||
document.getElementById('edit_endpoint_url').value = button.getAttribute('data-endpoint') || '';
|
document.getElementById('edit_endpoint_url').value = button.getAttribute('data-endpoint') || '';
|
||||||
document.getElementById('edit_region').value = button.getAttribute('data-region') || '';
|
document.getElementById('edit_region').value = button.getAttribute('data-region') || '';
|
||||||
document.getElementById('edit_access_key').value = button.getAttribute('data-access') || '';
|
document.getElementById('edit_access_key').value = button.getAttribute('data-access') || '';
|
||||||
document.getElementById('edit_secret_key').value = button.getAttribute('data-secret') || '';
|
document.getElementById('edit_secret_key').value = '';
|
||||||
|
document.getElementById('edit_secret_key').placeholder = '(unchanged — leave blank to keep current)';
|
||||||
|
document.getElementById('edit_secret_key').required = false;
|
||||||
document.getElementById('editTestResult').innerHTML = '';
|
document.getElementById('editTestResult').innerHTML = '';
|
||||||
|
|
||||||
var form = document.getElementById('editConnectionForm');
|
var form = document.getElementById('editConnectionForm');
|
||||||
@@ -288,9 +290,6 @@ window.ConnectionsManagement = (function() {
|
|||||||
editBtn.setAttribute('data-endpoint', data.connection.endpoint_url);
|
editBtn.setAttribute('data-endpoint', data.connection.endpoint_url);
|
||||||
editBtn.setAttribute('data-region', data.connection.region);
|
editBtn.setAttribute('data-region', data.connection.region);
|
||||||
editBtn.setAttribute('data-access', data.connection.access_key);
|
editBtn.setAttribute('data-access', data.connection.access_key);
|
||||||
if (data.connection.secret_key) {
|
|
||||||
editBtn.setAttribute('data-secret', data.connection.secret_key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var deleteBtn = row.querySelector('[data-bs-target="#deleteConnectionModal"]');
|
var deleteBtn = row.querySelector('[data-bs-target="#deleteConnectionModal"]');
|
||||||
|
|||||||
@@ -191,6 +191,10 @@ window.UICore = (function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
pollingManager.stopAll();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCsrfToken: getCsrfToken,
|
getCsrfToken: getCsrfToken,
|
||||||
formatBytes: formatBytes,
|
formatBytes: formatBytes,
|
||||||
|
|||||||
@@ -100,8 +100,26 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Upload
|
Upload
|
||||||
</button>
|
</button>
|
||||||
|
<div class="dropdown sort-dropdown">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" title="Sort objects">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293V2.5zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zM7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z"/>
|
||||||
|
</svg>
|
||||||
|
<span id="sort-dropdown-label">Name A-Z</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><button class="dropdown-item active" type="button" data-sort-field="name" data-sort-dir="asc">Name A-Z</button></li>
|
||||||
|
<li><button class="dropdown-item" type="button" data-sort-field="name" data-sort-dir="desc">Name Z-A</button></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><button class="dropdown-item" type="button" data-sort-field="size" data-sort-dir="desc">Size (largest)</button></li>
|
||||||
|
<li><button class="dropdown-item" type="button" data-sort-field="size" data-sort-dir="asc">Size (smallest)</button></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><button class="dropdown-item" type="button" data-sort-field="date" data-sort-dir="desc">Date (newest)</button></li>
|
||||||
|
<li><button class="dropdown-item" type="button" data-sort-field="date" data-sort-dir="asc">Date (oldest)</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div class="position-relative search-wrapper">
|
<div class="position-relative search-wrapper">
|
||||||
<input id="object-search" class="form-control form-control-sm" type="search" placeholder="Filter objects" style="max-width: 180px;" />
|
<input id="object-search" class="form-control form-control-sm" type="search" placeholder="Filter objects (press /)" style="max-width: 180px;" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bulk-actions d-none" id="bulk-actions-wrapper">
|
<div class="bulk-actions d-none" id="bulk-actions-wrapper">
|
||||||
<button class="btn btn-outline-danger btn-sm" type="button" data-bulk-delete-trigger disabled>
|
<button class="btn btn-outline-danger btn-sm" type="button" data-bulk-delete-trigger disabled>
|
||||||
@@ -2663,6 +2681,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="context-menu d-none" id="objectContextMenu">
|
||||||
|
<button class="context-menu-item" data-ctx-action="download">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
<button class="context-menu-item" data-ctx-action="copy-path">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2Zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6ZM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2Z"/>
|
||||||
|
</svg>
|
||||||
|
Copy S3 Path
|
||||||
|
</button>
|
||||||
|
<button class="context-menu-item" data-ctx-action="presign">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
|
||||||
|
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z"/>
|
||||||
|
</svg>
|
||||||
|
Share Link
|
||||||
|
</button>
|
||||||
|
<div class="context-menu-divider"></div>
|
||||||
|
<button class="context-menu-item text-danger" data-ctx-action="delete">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
|
</svg>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="keyboardShortcutsModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header border-0 pb-0">
|
||||||
|
<h5 class="modal-title fw-semibold">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-primary me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h12zM2 4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2z"/>
|
||||||
|
<path d="M13 10.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm0-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5 0A.25.25 0 0 1 8.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 8 8.75v-.5zm2 0a.25.25 0 0 1 .25-.25h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5a.25.25 0 0 1-.25-.25v-.5zm1 2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5-2A.25.25 0 0 1 6.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 6 8.75v-.5zm-2 0A.25.25 0 0 1 4.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 4 8.75v-.5zm-2 0A.25.25 0 0 1 2.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 2 8.75v-.5zm11-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0A.25.25 0 0 1 9.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 9 6.75v-.5zm-2 0A.25.25 0 0 1 7.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 7 6.75v-.5zm-2 0A.25.25 0 0 1 5.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 5 6.75v-.5zm-3 0A.25.25 0 0 1 2.25 6h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5A.25.25 0 0 1 2 6.75v-.5zm0 4a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm2 0a.25.25 0 0 1 .25-.25h5.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-5.5a.25.25 0 0 1-.25-.25v-.5z"/>
|
||||||
|
</svg>
|
||||||
|
Keyboard Shortcuts
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body pt-2">
|
||||||
|
<div class="kbd-shortcuts-list">
|
||||||
|
<div class="shortcut-row"><span class="text-muted">Search objects</span><kbd>/</kbd></div>
|
||||||
|
<div class="shortcut-row"><span class="text-muted">Select all</span><span><kbd>Ctrl</kbd> + <kbd>A</kbd></span></div>
|
||||||
|
<div class="shortcut-row"><span class="text-muted">Delete selected</span><kbd>Del</kbd></div>
|
||||||
|
<div class="shortcut-row"><span class="text-muted">Clear search</span><kbd>Esc</kbd></div>
|
||||||
|
<div class="shortcut-row"><span class="text-muted">Show shortcuts</span><kbd>?</kbd></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
|
|||||||
@@ -89,6 +89,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="col-12 d-none" id="bucket-no-results">
|
||||||
|
<div class="text-center py-5 text-muted">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="mb-3 opacity-50" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="mb-0 fw-medium">No buckets match your filter.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="createBucketModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="createBucketModal" tabindex="-1" aria-hidden="true">
|
||||||
@@ -149,6 +157,15 @@
|
|||||||
item.classList.add('d-none');
|
item.classList.add('d-none');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var noResults = document.getElementById('bucket-no-results');
|
||||||
|
if (noResults) {
|
||||||
|
if (term && visibleCount === 0) {
|
||||||
|
noResults.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
noResults.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,6 @@
|
|||||||
data-endpoint="{{ conn.endpoint_url }}"
|
data-endpoint="{{ conn.endpoint_url }}"
|
||||||
data-region="{{ conn.region }}"
|
data-region="{{ conn.region }}"
|
||||||
data-access="{{ conn.access_key }}"
|
data-access="{{ conn.access_key }}"
|
||||||
data-secret="{{ conn.secret_key }}"
|
|
||||||
title="Edit connection">
|
title="Edit connection">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/>
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<small class="text-muted">Need help? Check the <a href="#" class="text-decoration-none">documentation</a></small>
|
<small class="text-muted">Need help? Check the <a href="{{ url_for('ui.docs_page') }}" class="text-decoration-none">documentation</a></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="metrics-error-banner" class="alert alert-danger d-none d-flex align-items-center mb-4" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="flex-shrink-0 me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
|
</svg>
|
||||||
|
<div>Failed to fetch metrics. The API server may be unreachable.</div>
|
||||||
|
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert" aria-label="Close" style="font-size: 0.65rem;"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 mb-4">
|
<div class="row g-4 mb-4">
|
||||||
<div class="col-md-6 col-xl-3">
|
<div class="col-md-6 col-xl-3">
|
||||||
<div class="card shadow-sm h-100 border-0 metric-card">
|
<div class="card shadow-sm h-100 border-0 metric-card">
|
||||||
@@ -540,9 +548,15 @@
|
|||||||
if (el) el.textContent = data.app.buckets;
|
if (el) el.textContent = data.app.buckets;
|
||||||
|
|
||||||
countdown = 5;
|
countdown = 5;
|
||||||
|
var banner = document.getElementById('metrics-error-banner');
|
||||||
|
if (banner) banner.classList.add('d-none');
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error('Metrics fetch error:', err);
|
console.error('Metrics fetch error:', err);
|
||||||
|
var banner = document.getElementById('metrics-error-banner');
|
||||||
|
if (banner) {
|
||||||
|
banner.classList.remove('d-none');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user