Fix UI: versioning modals and object browser panel showing 'null'
This commit is contained in:
@@ -377,9 +377,18 @@ class ObjectStorage:
|
|||||||
raise StorageError("Bucket contains archived object versions")
|
raise StorageError("Bucket contains archived object versions")
|
||||||
if has_multipart:
|
if has_multipart:
|
||||||
raise StorageError("Bucket has active multipart uploads")
|
raise StorageError("Bucket has active multipart uploads")
|
||||||
|
bucket_id = bucket_path.name
|
||||||
self._remove_tree(bucket_path)
|
self._remove_tree(bucket_path)
|
||||||
self._remove_tree(self._system_bucket_root(bucket_path.name))
|
self._remove_tree(self._system_bucket_root(bucket_id))
|
||||||
self._remove_tree(self._multipart_bucket_root(bucket_path.name))
|
self._remove_tree(self._multipart_bucket_root(bucket_id))
|
||||||
|
self._bucket_config_cache.pop(bucket_id, None)
|
||||||
|
with self._cache_lock:
|
||||||
|
self._object_cache.pop(bucket_id, None)
|
||||||
|
self._cache_version.pop(bucket_id, None)
|
||||||
|
self._sorted_key_cache.pop(bucket_id, None)
|
||||||
|
stale = [k for k in self._meta_read_cache if k[0] == bucket_id]
|
||||||
|
for k in stale:
|
||||||
|
del self._meta_read_cache[k]
|
||||||
|
|
||||||
def list_objects(
|
def list_objects(
|
||||||
self,
|
self,
|
||||||
@@ -1832,30 +1841,40 @@ class ObjectStorage:
|
|||||||
|
|
||||||
def _read_bucket_config(self, bucket_name: str) -> dict[str, Any]:
|
def _read_bucket_config(self, bucket_name: str) -> dict[str, Any]:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
config_path = self._bucket_config_path(bucket_name)
|
||||||
cached = self._bucket_config_cache.get(bucket_name)
|
cached = self._bucket_config_cache.get(bucket_name)
|
||||||
if cached:
|
if cached:
|
||||||
config, cached_time = cached
|
config, cached_time, cached_mtime = cached
|
||||||
if now - cached_time < self._bucket_config_cache_ttl:
|
if now - cached_time < self._bucket_config_cache_ttl:
|
||||||
|
try:
|
||||||
|
current_mtime = config_path.stat().st_mtime if config_path.exists() else 0.0
|
||||||
|
except OSError:
|
||||||
|
current_mtime = 0.0
|
||||||
|
if current_mtime == cached_mtime:
|
||||||
return config.copy()
|
return config.copy()
|
||||||
|
|
||||||
config_path = self._bucket_config_path(bucket_name)
|
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
self._bucket_config_cache[bucket_name] = ({}, now)
|
self._bucket_config_cache[bucket_name] = ({}, now, 0.0)
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
data = json.loads(config_path.read_text(encoding="utf-8"))
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
config = data if isinstance(data, dict) else {}
|
config = data if isinstance(data, dict) else {}
|
||||||
self._bucket_config_cache[bucket_name] = (config, now)
|
mtime = config_path.stat().st_mtime
|
||||||
|
self._bucket_config_cache[bucket_name] = (config, now, mtime)
|
||||||
return config.copy()
|
return config.copy()
|
||||||
except (OSError, json.JSONDecodeError):
|
except (OSError, json.JSONDecodeError):
|
||||||
self._bucket_config_cache[bucket_name] = ({}, now)
|
self._bucket_config_cache[bucket_name] = ({}, now, 0.0)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _write_bucket_config(self, bucket_name: str, payload: dict[str, Any]) -> None:
|
def _write_bucket_config(self, bucket_name: str, payload: dict[str, Any]) -> None:
|
||||||
config_path = self._bucket_config_path(bucket_name)
|
config_path = self._bucket_config_path(bucket_name)
|
||||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||||
self._bucket_config_cache[bucket_name] = (payload.copy(), time.time())
|
try:
|
||||||
|
mtime = config_path.stat().st_mtime
|
||||||
|
except OSError:
|
||||||
|
mtime = 0.0
|
||||||
|
self._bucket_config_cache[bucket_name] = (payload.copy(), time.time(), mtime)
|
||||||
|
|
||||||
def _set_bucket_config_entry(self, bucket_name: str, key: str, value: Any | None) -> None:
|
def _set_bucket_config_entry(self, bucket_name: str, key: str, value: Any | None) -> None:
|
||||||
config = self._read_bucket_config(bucket_name)
|
config = self._read_bucket_config(bucket_name)
|
||||||
|
|||||||
@@ -137,11 +137,11 @@
|
|||||||
const versionPanel = document.getElementById('version-panel');
|
const versionPanel = document.getElementById('version-panel');
|
||||||
const versionList = document.getElementById('version-list');
|
const versionList = document.getElementById('version-list');
|
||||||
const refreshVersionsButton = document.getElementById('refreshVersionsButton');
|
const refreshVersionsButton = document.getElementById('refreshVersionsButton');
|
||||||
const archivedCard = document.getElementById('archived-objects-card');
|
let archivedCard = document.getElementById('archived-objects-card');
|
||||||
const archivedBody = archivedCard?.querySelector('[data-archived-body]');
|
let archivedBody = archivedCard?.querySelector('[data-archived-body]');
|
||||||
const archivedCountBadge = archivedCard?.querySelector('[data-archived-count]');
|
let archivedCountBadge = archivedCard?.querySelector('[data-archived-count]');
|
||||||
const archivedRefreshButton = archivedCard?.querySelector('[data-archived-refresh]');
|
let archivedRefreshButton = archivedCard?.querySelector('[data-archived-refresh]');
|
||||||
const archivedEndpoint = archivedCard?.dataset.archivedEndpoint;
|
let archivedEndpoint = archivedCard?.dataset.archivedEndpoint;
|
||||||
let versioningEnabled = objectsContainer?.dataset.versioning === 'true';
|
let versioningEnabled = objectsContainer?.dataset.versioning === 'true';
|
||||||
const versionsCache = new Map();
|
const versionsCache = new Map();
|
||||||
let activeRow = null;
|
let activeRow = null;
|
||||||
@@ -1737,6 +1737,15 @@
|
|||||||
loadArchivedObjects();
|
loadArchivedObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const propertiesTab = document.getElementById('properties-tab');
|
||||||
|
if (propertiesTab) {
|
||||||
|
propertiesTab.addEventListener('shown.bs.tab', () => {
|
||||||
|
if (archivedCard && archivedEndpoint) {
|
||||||
|
loadArchivedObjects();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function restoreVersion(row, version) {
|
async function restoreVersion(row, version) {
|
||||||
if (!row || !version?.version_id) return;
|
if (!row || !version?.version_id) return;
|
||||||
const template = row.dataset.restoreTemplate;
|
const template = row.dataset.restoreTemplate;
|
||||||
@@ -4163,6 +4172,47 @@
|
|||||||
var archivedCardEl = document.getElementById('archived-objects-card');
|
var archivedCardEl = document.getElementById('archived-objects-card');
|
||||||
if (archivedCardEl) {
|
if (archivedCardEl) {
|
||||||
archivedCardEl.style.display = enabled ? '' : 'none';
|
archivedCardEl.style.display = enabled ? '' : 'none';
|
||||||
|
} else if (enabled) {
|
||||||
|
var endpoint = window.BucketDetailConfig?.endpoints?.archivedObjects || '';
|
||||||
|
if (endpoint) {
|
||||||
|
var html = '<div class="card shadow-sm mt-4" id="archived-objects-card" data-archived-endpoint="' + endpoint + '">' +
|
||||||
|
'<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">' +
|
||||||
|
'<div class="d-flex align-items-center">' +
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-warning me-2" viewBox="0 0 16 16">' +
|
||||||
|
'<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>' +
|
||||||
|
'</svg><span class="fw-semibold">Archived Objects</span></div>' +
|
||||||
|
'<div class="d-flex align-items-center gap-2">' +
|
||||||
|
'<span class="badge text-bg-secondary" data-archived-count>0 items</span>' +
|
||||||
|
'<button class="btn btn-outline-secondary btn-sm" type="button" data-archived-refresh>' +
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">' +
|
||||||
|
'<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>' +
|
||||||
|
'<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>' +
|
||||||
|
'</svg>Refresh</button></div></div>' +
|
||||||
|
'<div class="card-body">' +
|
||||||
|
'<p class="text-muted small mb-3">Objects that have been deleted while versioning is enabled. Their previous versions remain available until you restore or purge them.</p>' +
|
||||||
|
'<div class="table-responsive"><table class="table table-sm table-hover align-middle mb-0">' +
|
||||||
|
'<thead class="table-light"><tr>' +
|
||||||
|
'<th scope="col"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1 text-muted" viewBox="0 0 16 16">' +
|
||||||
|
'<path d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3z"/>' +
|
||||||
|
'</svg>Key</th>' +
|
||||||
|
'<th scope="col">Latest Version</th>' +
|
||||||
|
'<th scope="col" class="text-center">Versions</th>' +
|
||||||
|
'<th scope="col" class="text-end">Actions</th>' +
|
||||||
|
'</tr></thead>' +
|
||||||
|
'<tbody data-archived-body><tr><td colspan="4" class="text-center text-muted py-4">' +
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="mb-2 d-block mx-auto" viewBox="0 0 16 16">' +
|
||||||
|
'<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>' +
|
||||||
|
'</svg>No archived objects</td></tr></tbody>' +
|
||||||
|
'</table></div></div></div>';
|
||||||
|
card.insertAdjacentHTML('afterend', html);
|
||||||
|
archivedCard = document.getElementById('archived-objects-card');
|
||||||
|
archivedBody = archivedCard.querySelector('[data-archived-body]');
|
||||||
|
archivedCountBadge = archivedCard.querySelector('[data-archived-count]');
|
||||||
|
archivedRefreshButton = archivedCard.querySelector('[data-archived-refresh]');
|
||||||
|
archivedEndpoint = endpoint;
|
||||||
|
archivedRefreshButton.addEventListener('click', function() { loadArchivedObjects(); });
|
||||||
|
loadArchivedObjects();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dropZone = document.getElementById('objects-drop-zone');
|
var dropZone = document.getElementById('objects-drop-zone');
|
||||||
@@ -4170,6 +4220,15 @@
|
|||||||
dropZone.setAttribute('data-versioning', enabled ? 'true' : 'false');
|
dropZone.setAttribute('data-versioning', enabled ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bulkPurgeWrap = document.getElementById('bulkDeletePurgeWrap');
|
||||||
|
if (bulkPurgeWrap) {
|
||||||
|
bulkPurgeWrap.classList.toggle('d-none', !enabled);
|
||||||
|
}
|
||||||
|
var singleDeleteVerWrap = document.getElementById('deleteObjectVersioningWrap');
|
||||||
|
if (singleDeleteVerWrap) {
|
||||||
|
singleDeleteVerWrap.classList.toggle('d-none', !enabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
var newForm = document.getElementById('enableVersioningForm');
|
var newForm = document.getElementById('enableVersioningForm');
|
||||||
if (newForm) {
|
if (newForm) {
|
||||||
|
|||||||
@@ -2272,13 +2272,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul class="list-group mb-3" id="bulkDeleteList" style="max-height: 200px; overflow-y: auto;"></ul>
|
<ul class="list-group mb-3" id="bulkDeleteList" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||||
<div class="text-muted small" id="bulkDeleteStatus"></div>
|
<div class="text-muted small" id="bulkDeleteStatus"></div>
|
||||||
{% if versioning_enabled %}
|
<div class="form-check mt-3 p-3 bg-body-tertiary rounded-3 {% if not versioning_enabled %}d-none{% endif %}" id="bulkDeletePurgeWrap">
|
||||||
<div class="form-check mt-3 p-3 bg-body-tertiary rounded-3">
|
|
||||||
<input class="form-check-input" type="checkbox" id="bulkDeletePurge" />
|
<input class="form-check-input" type="checkbox" id="bulkDeletePurge" />
|
||||||
<label class="form-check-label" for="bulkDeletePurge">Also delete archived versions</label>
|
<label class="form-check-label" for="bulkDeletePurge">Also delete archived versions</label>
|
||||||
<div class="form-text">Removes any archived versions stored in the archive.</div>
|
<div class="form-text">Removes any archived versions stored in the archive.</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
@@ -2316,7 +2314,7 @@
|
|||||||
<div class="p-3 bg-body-tertiary rounded-3 mb-3">
|
<div class="p-3 bg-body-tertiary rounded-3 mb-3">
|
||||||
<code id="deleteObjectKey" class="d-block text-break"></code>
|
<code id="deleteObjectKey" class="d-block text-break"></code>
|
||||||
</div>
|
</div>
|
||||||
{% if versioning_enabled %}
|
<div id="deleteObjectVersioningWrap" class="{% if not versioning_enabled %}d-none{% endif %}">
|
||||||
<div class="alert alert-warning d-flex align-items-start small mb-3" role="alert">
|
<div class="alert alert-warning d-flex align-items-start small mb-3" role="alert">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="flex-shrink-0 me-2 mt-0" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="flex-shrink-0 me-2 mt-0" viewBox="0 0 16 16">
|
||||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
|
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
|
||||||
@@ -2328,7 +2326,7 @@
|
|||||||
<label class="form-check-label" for="deletePurgeVersions">Also delete all archived versions</label>
|
<label class="form-check-label" for="deletePurgeVersions">Also delete all archived versions</label>
|
||||||
<div class="form-text mb-0">Removes the live object and every stored version.</div>
|
<div class="form-text mb-0">Removes the live object and every stored version.</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
@@ -2771,7 +2769,8 @@
|
|||||||
window.BucketDetailConfig = {
|
window.BucketDetailConfig = {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
versioning: "{{ url_for('ui.update_bucket_versioning', bucket_name=bucket_name) }}",
|
versioning: "{{ url_for('ui.update_bucket_versioning', bucket_name=bucket_name) }}",
|
||||||
bucketsOverview: "{{ url_for('ui.buckets_overview') }}"
|
bucketsOverview: "{{ url_for('ui.buckets_overview') }}",
|
||||||
|
archivedObjects: "{{ url_for('ui.archived_objects', bucket_name=bucket_name) }}"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user