This commit is contained in:
1 parent
9a1efb2c15
commit
c7a0ed93ff
4 files changed
+88
-49
No files matched your search
@@ -299,7 +299,7 @@ async def get_ups_ui_tiles(ups_name: str, user=Depends(require_session)):
|
||||
raw = r.get(f"ups:ui:tiles:{ups_name}")
|
||||
default = {
|
||||
"types": {}, "order": [], "hidden": [], "custom": [],
|
||||
"positions": {}, "card_size": None,
|
||||
"positions": {}, "card_size": None, "exists": False,
|
||||
}
|
||||
if not raw:
|
||||
return default
|
||||
@@ -312,6 +312,7 @@ async def get_ups_ui_tiles(ups_name: str, user=Depends(require_session)):
|
||||
"custom": data.get("custom", []),
|
||||
"positions": data.get("positions", {}),
|
||||
"card_size": data.get("card_size"),
|
||||
"exists": True,
|
||||
}
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
+51
-47
@@ -234,10 +234,14 @@ async function loadServerTileConfig(name) {
|
||||
scheduleServerTileSave(name);
|
||||
}
|
||||
|
||||
let pendingSave = null;
|
||||
const pendingSaves = {};
|
||||
function scheduleServerTileSave(name) {
|
||||
if (pendingSave) clearTimeout(pendingSave);
|
||||
pendingSave = setTimeout(() => saveServerTileConfig(name), 800);
|
||||
if (!name) return;
|
||||
if (pendingSaves[name]) clearTimeout(pendingSaves[name]);
|
||||
pendingSaves[name] = setTimeout(() => {
|
||||
delete pendingSaves[name];
|
||||
saveServerTileConfig(name);
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function autoArrangeTiles(name, grid) {
|
||||
@@ -314,14 +318,15 @@ function autoArrangeTiles(name, grid) {
|
||||
grid.style.height = `${cardHeight - 70}px`;
|
||||
|
||||
// Save card size
|
||||
persistCardSize(name, cardWidth, cardHeight);
|
||||
persistCardSize(name, cardWidth, cardHeight, false);
|
||||
debugLog(`Auto-arranged ${tiles.length} tiles for ${name}, card size: ${cardWidth}x${cardHeight}`);
|
||||
}
|
||||
}
|
||||
|
||||
function persistCardSize(name, width, height) {
|
||||
function persistCardSize(name, width, height, syncServer = true) {
|
||||
savedCardSizes[name] = { width, height };
|
||||
saveToStorage(CARD_SIZE_KEY, savedCardSizes);
|
||||
if (syncServer) scheduleServerTileSave(name);
|
||||
}
|
||||
|
||||
function restoreCardSize(name) {
|
||||
@@ -340,6 +345,20 @@ function restoreCardSize(name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasSavedPositions(name) {
|
||||
return Object.keys(savedTilePos[name] || {}).length > 0;
|
||||
}
|
||||
|
||||
function applyDefaultTileTypes(name) {
|
||||
savedTileTypes[name] = savedTileTypes[name] || {
|
||||
load_pct: 'gauge',
|
||||
volt_line: 'value',
|
||||
volt_output: 'value',
|
||||
watts_usage: 'value'
|
||||
};
|
||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
||||
}
|
||||
|
||||
async function saveServerTileConfig(name) {
|
||||
const types = savedTileTypes[name] || {};
|
||||
const order = (savedTileOrder[name] || []);
|
||||
@@ -407,7 +426,8 @@ function ensureUpsCard(name) {
|
||||
div.id = `card-${name}`;
|
||||
// Apply saved size if available
|
||||
if (savedSizes[name]) {
|
||||
const { w, h } = savedSizes[name];
|
||||
const w = savedSizes[name].width || savedSizes[name].w;
|
||||
const h = savedSizes[name].height || savedSizes[name].h;
|
||||
if (w) div.style.width = w + 'px';
|
||||
if (h) div.style.height = h + 'px';
|
||||
}
|
||||
@@ -465,13 +485,7 @@ function ensureUpsCard(name) {
|
||||
}
|
||||
// Set default tile types for new UPS (static layout)
|
||||
if (!savedTileTypes[name]) {
|
||||
savedTileTypes[name] = {
|
||||
load_pct: 'gauge',
|
||||
volt_line: 'value',
|
||||
volt_output: 'value',
|
||||
watts_usage: 'value'
|
||||
};
|
||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
||||
applyDefaultTileTypes(name);
|
||||
}
|
||||
// Initialize tiles
|
||||
initTilesFor(name, div.querySelector('[data-tile-grid]'));
|
||||
@@ -677,6 +691,9 @@ function createCustomTile(name, grid, tileCfg) {
|
||||
tile.setAttribute('draggable','true');
|
||||
const headerLabel = METRIC_LABELS[tileCfg.metric] ? METRIC_LABELS[tileCfg.metric] : tileCfg.metric;
|
||||
tile.innerHTML = `<div class="tile-controls"><button data-remove-tile title="Remove">✕</button></div><h4>${escapeHtml(headerLabel)}</h4><div class="tile-body"></div>`;
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.className = 'tile-resize';
|
||||
tile.appendChild(resizeHandle);
|
||||
grid.appendChild(tile);
|
||||
buildCustomTileVisualization(name, tile, tileCfg);
|
||||
const removeBtn = tile.querySelector('[data-remove-tile]');
|
||||
@@ -707,8 +724,9 @@ function rebuildAllTiles(name) {
|
||||
function removeCustomTile(name, id, tileEl) {
|
||||
savedCustomTiles[name] = (savedCustomTiles[name]||[]).filter(t => t.id !== id);
|
||||
try { localStorage.setItem(CUSTOM_TILES_KEY, JSON.stringify(savedCustomTiles)); } catch(_) {}
|
||||
const grid = tileEl?.parentElement;
|
||||
if (tileEl) tileEl.remove();
|
||||
persistTileOrder(name, tileEl.parentElement);
|
||||
if (grid) persistTileOrder(name, grid);
|
||||
scheduleServerTileSave(name);
|
||||
}
|
||||
|
||||
@@ -1508,22 +1526,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
savedTileTypes[name] = {
|
||||
load_pct: 'gauge',
|
||||
volt_line: 'value',
|
||||
volt_output: 'value',
|
||||
watts_usage: 'value'
|
||||
};
|
||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
||||
applyDefaultTileTypes(name);
|
||||
|
||||
const cfg = await loadServerTileConfig(name);
|
||||
if (cfg) {
|
||||
savedTileTypes[name] = cfg.types || savedTileTypes[name] || {};
|
||||
savedTileOrder[name] = cfg.order || savedTileOrder[name] || [];
|
||||
if (cfg && cfg.exists !== false) {
|
||||
savedTileTypes[name] = Object.keys(cfg.types || {}).length ? cfg.types : savedTileTypes[name] || {};
|
||||
savedTileOrder[name] = (cfg.order || []).length ? cfg.order : savedTileOrder[name] || [];
|
||||
const hiddenSet = {}; (cfg.hidden||[]).forEach(h => hiddenSet[h]=true);
|
||||
savedTileHidden[name] = hiddenSet;
|
||||
savedCustomTiles[name] = cfg.custom || [];
|
||||
savedTilePos[name] = cfg.positions || savedTilePos[name] || {};
|
||||
savedCustomTiles[name] = (cfg.custom || []).length ? cfg.custom : savedCustomTiles[name] || [];
|
||||
savedTilePos[name] = Object.keys(cfg.positions || {}).length ? cfg.positions : savedTilePos[name] || {};
|
||||
if (cfg.card_size && cfg.card_size.width && cfg.card_size.height) {
|
||||
savedCardSizes[name] = { width: cfg.card_size.width, height: cfg.card_size.height };
|
||||
saveToStorage(CARD_SIZE_KEY, savedCardSizes);
|
||||
@@ -1540,18 +1552,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Force rebuild tiles with correct types
|
||||
rebuildAllTiles(name);
|
||||
// If no stored positions existed, perform an auto layout
|
||||
if (!cfg.positions || Object.keys(cfg.positions).length === 0) {
|
||||
resetLayout(name);
|
||||
if (!hasSavedPositions(name)) {
|
||||
autoArrangeTiles(name, grid);
|
||||
persistTilePositions(name, grid);
|
||||
} else if (!Object.keys(cfg.positions || {}).length) {
|
||||
scheduleServerTileSave(name);
|
||||
}
|
||||
} else {
|
||||
// No server config: apply default static layout
|
||||
savedTileTypes[name] = {
|
||||
load_pct: 'gauge',
|
||||
volt_line: 'value',
|
||||
volt_output: 'value',
|
||||
watts_usage: 'value'
|
||||
};
|
||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
||||
// No server config: keep any local layout and backfill it to the server.
|
||||
applyDefaultTileTypes(name);
|
||||
const grid = card.querySelector('[data-tile-grid]');
|
||||
debugLog('Initializing default tiles for', name, 'grid found:', !!grid);
|
||||
grid.innerHTML='';
|
||||
@@ -1559,18 +1568,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
debugLog('Tiles created, count:', grid.children.length);
|
||||
// Force rebuild tiles with correct types
|
||||
rebuildAllTiles(name);
|
||||
// Apply static layout directly (same as in resetLayout)
|
||||
const loadTile = grid.querySelector('.tile[data-tile="load_pct"]');
|
||||
const lineTile = grid.querySelector('.tile[data-tile="volt_line"]');
|
||||
const outTile = grid.querySelector('.tile[data-tile="volt_output"]');
|
||||
const wattsTile = grid.querySelector('.tile[data-tile="watts_usage"]');
|
||||
if (loadTile) { loadTile.style.left='10px'; loadTile.style.top='10px'; loadTile.style.width='200px'; loadTile.style.height='160px'; }
|
||||
if (lineTile) { lineTile.style.left='230px'; lineTile.style.top='10px'; lineTile.style.width='180px'; lineTile.style.height='70px'; }
|
||||
if (outTile) { outTile.style.left='230px'; outTile.style.top='100px'; outTile.style.width='180px'; outTile.style.height='70px'; }
|
||||
if (wattsTile) { wattsTile.style.left='10px'; wattsTile.style.top='190px'; wattsTile.style.width='400px'; wattsTile.style.height='200px'; }
|
||||
if (!hasSavedPositions(name)) {
|
||||
autoArrangeTiles(name, grid);
|
||||
persistTilePositions(name, grid);
|
||||
} else {
|
||||
scheduleServerTileSave(name);
|
||||
}
|
||||
setTimeout(()=>{ Object.values(charts).forEach(ch=>{ try{ ch.resize(); }catch{} }); }, 50);
|
||||
persistTilePositions(name, grid);
|
||||
scheduleServerTileSave(name);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -61,5 +61,5 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/dashboard.js?v=5"></script>
|
||||
<script src="/static/js/dashboard.js?v=6"></script>
|
||||
{% endblock %}
|
||||
@@ -139,6 +139,40 @@ def test_ups_health_offline(authed_client, fake_redis):
|
||||
assert body["fail_count"] == 3
|
||||
|
||||
|
||||
def test_ui_tile_layout_preserves_tile_and_card_sizes(authed_client):
|
||||
payload = {
|
||||
"types": {"watts_usage": "line"},
|
||||
"order": ["load_pct", "watts_usage"],
|
||||
"hidden": [],
|
||||
"custom": [],
|
||||
"positions": {
|
||||
"load_pct": {"left": 20, "top": 40, "width": 260, "height": 180},
|
||||
"watts_usage": {"left": 320, "top": 40, "width": 520, "height": 260},
|
||||
},
|
||||
"card_size": {"width": 900, "height": 460},
|
||||
}
|
||||
|
||||
r = authed_client.post("/api/ups/u1/ui_tiles", json=payload)
|
||||
assert r.status_code == 200
|
||||
|
||||
r = authed_client.get("/api/ups/u1/ui_tiles")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["exists"] is True
|
||||
assert body["positions"]["load_pct"]["width"] == 260
|
||||
assert body["positions"]["load_pct"]["height"] == 180
|
||||
assert body["positions"]["watts_usage"]["width"] == 520
|
||||
assert body["positions"]["watts_usage"]["height"] == 260
|
||||
assert body["card_size"] == {"width": 900, "height": 460}
|
||||
|
||||
|
||||
def test_ui_tile_layout_reports_missing_server_config(authed_client):
|
||||
r = authed_client.get("/api/ups/u1/ui_tiles")
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json()["exists"] is False
|
||||
|
||||
|
||||
def test_battery_health_empty(authed_client):
|
||||
_add_ups(authed_client)
|
||||
r = authed_client.get("/api/ups/u1/battery_health")
|
||||
|
||||
Reference in new issue
Block a user