avoiding triple reload in last job list
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# AssetManager 0.5.5.30
|
# AssetManager 0.5.5.31
|
||||||
|
|
||||||
AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. The project is released under the **Apache License 2.0** and may be used, modified, and redistributed for private and commercial purposes.
|
AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. The project is released under the **Apache License 2.0** and may be used, modified, and redistributed for private and commercial purposes.
|
||||||
|
|
||||||
|
|||||||
@@ -799,6 +799,10 @@ BASE_TRANSLATIONS.update({
|
|||||||
"software.version_count": ("Number of versions", "Anzahl Versionen"),
|
"software.version_count": ("Number of versions", "Anzahl Versionen"),
|
||||||
"software.asset_count": ("Assets", "Assets"),
|
"software.asset_count": ("Assets", "Assets"),
|
||||||
"software.entry_count": ("Entries", "Einträge"),
|
"software.entry_count": ("Entries", "Einträge"),
|
||||||
|
"software.unique_asset_count": ("Unique assets", "Eindeutige Assets"),
|
||||||
|
"software.unique_asset_count_help": ("Number of different assets on which this grouped software was found. An asset is counted only once, even if multiple versions or matching inventory records exist.", "Anzahl unterschiedlicher Assets, auf denen diese gruppierte Software gefunden wurde. Ein Asset wird nur einmal gezählt, auch wenn mehrere Versionen oder passende Inventareinträge vorhanden sind."),
|
||||||
|
"software.inventory_entry_count": ("Inventory entries", "Inventareinträge"),
|
||||||
|
"software.inventory_entry_count_help": ("Number of matching software inventory records. This value can be higher than the number of unique assets when multiple versions or records exist on the same asset.", "Anzahl der passenden Software-Inventardatensätze. Dieser Wert kann höher als die Anzahl eindeutiger Assets sein, wenn auf einem Asset mehrere Versionen oder Einträge vorhanden sind."),
|
||||||
"software.no_global_inventory": ("No central software inventory is available yet.", "Noch keine zentrale Softwareinventur vorhanden."),
|
"software.no_global_inventory": ("No central software inventory is available yet.", "Noch keine zentrale Softwareinventur vorhanden."),
|
||||||
"software.last_job": ("Last job", "Letzter Job"),
|
"software.last_job": ("Last job", "Letzter Job"),
|
||||||
"software.last_inventory": ("Last inventory", "Letzte Inventur")
|
"software.last_inventory": ("Last inventory", "Letzte Inventur")
|
||||||
|
|||||||
@@ -2823,3 +2823,6 @@ html[data-theme="dark"] .chart-total{
|
|||||||
|
|
||||||
/* Administrator editor for the protected MeshCentral node ID. */
|
/* Administrator editor for the protected MeshCentral node ID. */
|
||||||
.mesh-node-id-editor{display:grid;gap:.55rem}.mesh-node-id-input{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.mesh-node-id-actions{display:flex;flex-wrap:wrap;gap:.55rem;align-items:center}.mesh-node-id-actions .button{width:auto;margin:0}.mesh-node-id-editor small{font-weight:400;margin:0}
|
.mesh-node-id-editor{display:grid;gap:.55rem}.mesh-node-id-input{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.mesh-node-id-actions{display:flex;flex-wrap:wrap;gap:.55rem;align-items:center}.mesh-node-id-actions .button{width:auto;margin:0}.mesh-node-id-editor small{font-weight:400;margin:0}
|
||||||
|
|
||||||
|
/* Keep server-filter controls visible while only the table body is refreshed. */
|
||||||
|
table.server-filter-loading tbody{opacity:.55;transition:opacity .12s ease}
|
||||||
|
|||||||
@@ -63,6 +63,9 @@
|
|||||||
if (!event.detail?.table || event.detail.table === table) update();
|
if (!event.detail?.table || event.detail.table === table) update();
|
||||||
});
|
});
|
||||||
window.addEventListener('software-job-status-changed', update);
|
window.addEventListener('software-job-status-changed', update);
|
||||||
|
window.addEventListener('asset-table-content-reloaded', event => {
|
||||||
|
if (event.detail?.table === table) update();
|
||||||
|
});
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
let requestInProgress = false;
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
if (requestInProgress) return;
|
||||||
const active = activeRows();
|
const active = activeRows();
|
||||||
if (!active.length) return;
|
if (!active.length) return;
|
||||||
const ids = active.map(row => Number(row.dataset.jobId)).filter(Number.isInteger);
|
const ids = active.map(row => Number(row.dataset.jobId)).filter(Number.isInteger);
|
||||||
|
requestInProgress = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/software-jobs/statuses', {
|
const response = await fetch('/api/software-jobs/statuses', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -86,10 +89,17 @@
|
|||||||
if (activeRows().length) timer = window.setTimeout(refresh, 3000);
|
if (activeRows().length) timer = window.setTimeout(refresh, 3000);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
timer = window.setTimeout(refresh, 5000);
|
timer = window.setTimeout(refresh, 5000);
|
||||||
|
} finally {
|
||||||
|
requestInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeRows().length) timer = window.setTimeout(refresh, 1200);
|
if (activeRows().length) timer = window.setTimeout(refresh, 1200);
|
||||||
|
window.addEventListener('asset-table-content-reloaded', event => {
|
||||||
|
if (event.detail?.table !== table || !activeRows().length) return;
|
||||||
|
if (timer) window.clearTimeout(timer);
|
||||||
|
timer = window.setTimeout(refresh, 250);
|
||||||
|
});
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
if (timer) window.clearTimeout(timer);
|
if (timer) window.clearTimeout(timer);
|
||||||
}, {once: true});
|
}, {once: true});
|
||||||
|
|||||||
@@ -142,6 +142,8 @@
|
|||||||
const isAssetTable = table.id === 'asset-table';
|
const isAssetTable = table.id === 'asset-table';
|
||||||
const usesServerFilters = table.dataset.serverFilters === '1';
|
const usesServerFilters = table.dataset.serverFilters === '1';
|
||||||
let serverFilterTimer = null;
|
let serverFilterTimer = null;
|
||||||
|
let serverFilterController = null;
|
||||||
|
let serverFilterRequestId = 0;
|
||||||
|
|
||||||
// Normal tables get stable synthetic keys. The asset table already has
|
// Normal tables get stable synthetic keys. The asset table already has
|
||||||
// real data-field keys on its movable columns; selection and action
|
// real data-field keys on its movable columns; selection and action
|
||||||
@@ -316,7 +318,7 @@
|
|||||||
if (!usesServerFilters) return;
|
if (!usesServerFilters) return;
|
||||||
if (serverFilterTimer) window.clearTimeout(serverFilterTimer);
|
if (serverFilterTimer) window.clearTimeout(serverFilterTimer);
|
||||||
|
|
||||||
const navigate = () => {
|
const navigate = async () => {
|
||||||
const target = new URL(
|
const target = new URL(
|
||||||
table.dataset.serverFilterUrl || window.location.pathname,
|
table.dataset.serverFilterUrl || window.location.pathname,
|
||||||
window.location.origin
|
window.location.origin
|
||||||
@@ -331,13 +333,48 @@
|
|||||||
if (value) target.searchParams.set(`job_filter_${definition.serverName}`, value);
|
if (value) target.searchParams.set(`job_filter_${definition.serverName}`, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const requestId = ++serverFilterRequestId;
|
||||||
|
serverFilterController?.abort();
|
||||||
|
serverFilterController = new AbortController();
|
||||||
|
table.classList.add('server-filter-loading');
|
||||||
|
table.setAttribute('aria-busy', 'true');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.sessionStorage.setItem(
|
const response = await fetch(target.toString(), {
|
||||||
'assetmanager:software-jobs:last-server-filter',
|
cache: 'no-store',
|
||||||
document.activeElement?.name || ''
|
headers: {'X-Requested-With': 'AssetManagerTableFilter'},
|
||||||
|
signal: serverFilterController.signal,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const html = await response.text();
|
||||||
|
if (requestId !== serverFilterRequestId) return;
|
||||||
|
|
||||||
|
const documentCopy = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
const replacement = documentCopy.querySelector(
|
||||||
|
`table[data-storage-key="${CSS.escape(table.dataset.storageKey || '')}"]`
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
const replacementBody = replacement?.tBodies?.[0];
|
||||||
window.location.assign(target.toString());
|
if (!replacement || !replacementBody) throw new Error('Filtered table not found');
|
||||||
|
|
||||||
|
tbody.replaceChildren(...[...replacementBody.rows].map(row => row.cloneNode(true)));
|
||||||
|
table.dataset.serverFilterTotal = replacement.dataset.serverFilterTotal || String(tbody.rows.length);
|
||||||
|
window.history.replaceState({}, '', target.toString());
|
||||||
|
window.AssetManagerTime?.scan(tbody);
|
||||||
|
applyFilters();
|
||||||
|
window.dispatchEvent(new CustomEvent('asset-table-content-reloaded', {
|
||||||
|
detail: {table}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name !== 'AbortError') {
|
||||||
|
// Fall back to a normal navigation only when the partial reload fails.
|
||||||
|
window.location.assign(target.toString());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === serverFilterRequestId) {
|
||||||
|
table.classList.remove('server-filter-loading');
|
||||||
|
table.removeAttribute('aria-busy');
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (immediate) navigate();
|
if (immediate) navigate();
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
<th>{{ t('software.platform') }}</th>
|
<th>{{ t('software.platform') }}</th>
|
||||||
<th>{{ t('software.versions') }}</th>
|
<th>{{ t('software.versions') }}</th>
|
||||||
<th data-numeric-filter="1" data-filter-placeholder="{{ t('software.numeric_filter_placeholder') }}">{{ t('software.version_count') }}</th>
|
<th data-numeric-filter="1" data-filter-placeholder="{{ t('software.numeric_filter_placeholder') }}">{{ t('software.version_count') }}</th>
|
||||||
<th data-numeric-filter="1" data-filter-placeholder="{{ t('software.numeric_filter_placeholder') }}">{{ t('software.asset_count') }}</th>
|
<th data-numeric-filter="1" data-filter-placeholder="{{ t('software.numeric_filter_placeholder') }}"><span title="{{ t('software.unique_asset_count_help')|e }}">{{ t('software.unique_asset_count') }} ⓘ</span></th>
|
||||||
<th data-numeric-filter="1" data-filter-placeholder="{{ t('software.numeric_filter_placeholder') }}">{{ t('software.entry_count') }}</th>
|
<th data-numeric-filter="1" data-filter-placeholder="{{ t('software.numeric_filter_placeholder') }}"><span title="{{ t('software.inventory_entry_count_help')|e }}">{{ t('software.inventory_entry_count') }} ⓘ</span></th>
|
||||||
<th>{{ t('software.last_seen') }}</th>
|
<th>{{ t('software.last_seen') }}</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
APP_VERSION = "0.5.5.30"
|
APP_VERSION = "0.5.5.31"
|
||||||
__version__ = APP_VERSION
|
__version__ = APP_VERSION
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Version 0.5.5.31
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Renamed the aggregated software columns to **Unique assets** and **Inventory entries**.
|
||||||
|
- Added explanatory tooltips describing the difference between distinct assets and matching inventory records.
|
||||||
|
- Reworked server-side filters in the recent jobs table to update only the table body instead of reloading the complete page.
|
||||||
|
- Kept filter controls and the active-filter indicator visible during database queries.
|
||||||
|
- Preserved live job status updates and bulk restart controls after filtered rows are refreshed.
|
||||||
Reference in New Issue
Block a user