PC Detail: flow Details cards, drop the IP Configuration card
Details tab card layout ----------------------- The Details tab sat on .content-grid, a two-column CSS grid. Grid packs items into implicit rows, so every row is as tall as its tallest card and align-items:start leaves the shorter one with dead space beneath it: Warranty is short, Endpoint Status is tall, and Policy had to wait for the taller of the two rather than sliding up. That read as a staggered layout. Switched that one tab to multicol via a .details-flow modifier, which has no rows at all - each card starts where the previous one ended, so the holes close while cards still size to their content. Multicol is deliberately NOT applied to .content-grid globally. The note by that rule in style.css records why it was abandoned there: a column-span:all child (.pap-span, used by table panels) overlaps the shorter column when the two are unequal. That cannot happen on this tab - every panel in the details group is a keyvalue or list render, and spansFullWidth() only returns true for table (or a tabs panel holding one), so nothing in the subtree spans. Monitors and Service/Process DO carry table panels and stay on grid. The modifier is scoped to PCDetail, so the nine other detail views on .content-grid are untouched. Chosen over JS masonry on purpose. Masonry packs perfectly but has to measure every card and re-measure on resize, on panel collapse and on every async panel load - and these panels fetch after paint, so it would mean continuous forced reflow on a page that already polls. This is pure CSS: no measurement, no observer, no runtime cost. The tradeoff accepted is that column bottoms can end at slightly different heights. Cards carry break-inside: avoid so none fragments across the column break, and the 25px spacing returns as a margin because multicol has no gap between stacked items the way grid does. IP Configuration card --------------------- Removed: the data has no purpose on the page anymore. Taken out at the root rather than hidden - the panel declaration, the /asset/<id>/ipconfig route and the _IPCONFIG_COLUMNS constant all go, since a route with no consumer is dead surface that still answers requests. test_the_two_cards_do_not_duplicate_columns_beyond_the_address existed only to police the history/config column split; with one card gone it guards nothing, so it and its import go with it. The panel-count assertion in test_endpoints_asset_panels_are_computer_scoped moves 15 -> 14, which is the test doing its job and catching the removal. IP History is unchanged and still carries the address list. Not addressed here: three tzset() tests in test_endpoints_plugin.py fail on Windows because time.tzset() is Unix-only. Verified pre-existing against unmodified code; they pass in the Linux container.
This commit is contained in:
@@ -507,21 +507,26 @@ def panel_status(asset_id: int):
|
||||
if not state:
|
||||
return success_response({'fields': []})
|
||||
|
||||
def field(label, value, mono=False):
|
||||
return {'label': label, 'value': value, 'mono': mono}
|
||||
def field(label, value, mono=False, fmt=None):
|
||||
entry = {'label': label, 'value': value, 'mono': mono}
|
||||
if fmt:
|
||||
entry['format'] = fmt
|
||||
return entry
|
||||
|
||||
fields = [
|
||||
field('Logged in user', state.loggedinuser),
|
||||
field('Uptime', _format_uptime(state.uptimeseconds)),
|
||||
field('Idle', _format_uptime(state.idleseconds)),
|
||||
field('Last boot',
|
||||
state.lastboottime.isoformat() + 'Z' if state.lastboottime else None),
|
||||
state.lastboottime.isoformat() + 'Z' if state.lastboottime else None,
|
||||
fmt='datetime'),
|
||||
field('CPU %', state.cpupercent),
|
||||
field('Memory %', state.memorypercent),
|
||||
field('Agent', state.agentversion, mono=True),
|
||||
field('Tray', state.trayversion, mono=True),
|
||||
field('Last reported',
|
||||
state.lastreportedat.isoformat() + 'Z' if state.lastreportedat else None),
|
||||
state.lastreportedat.isoformat() + 'Z' if state.lastreportedat else None,
|
||||
fmt='datetime'),
|
||||
]
|
||||
fields = [f for f in fields if f['value'] not in (None, '')]
|
||||
|
||||
@@ -1424,15 +1429,6 @@ _IPHISTORY_COLUMNS = [
|
||||
{'key': 'lastchanged', 'label': 'Last changed'},
|
||||
]
|
||||
|
||||
_IPCONFIG_COLUMNS = [
|
||||
{'key': 'ipaddress', 'label': 'IP address'},
|
||||
{'key': 'subnetmask', 'label': 'Subnet mask'},
|
||||
{'key': 'gateway', 'label': 'Gateway'},
|
||||
{'key': 'dns1', 'label': 'DNS 1'},
|
||||
{'key': 'dns2', 'label': 'DNS 2'},
|
||||
{'key': 'assignment', 'label': 'Assignment'},
|
||||
]
|
||||
|
||||
|
||||
def _isostamp(value):
|
||||
"""ISO-8601 with a Z suffix, matching every other timestamp this file emits.
|
||||
@@ -1513,37 +1509,6 @@ def panel_iphistory(asset_id: int):
|
||||
return success_response(rows)
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/ipconfig', methods=['GET'])
|
||||
@require_permission('endpoints.view')
|
||||
def panel_ipconfig(asset_id: int):
|
||||
"""Table panel: subnet, gateway and DNS per recorded address.
|
||||
|
||||
Split from IP history rather than folded into it: history answers "what has
|
||||
this box been", configuration answers "how is this address set up", and one
|
||||
table trying to do both would be eleven columns wide. Rows with no
|
||||
configuration recorded are dropped - an address with no subnet, gateway or
|
||||
DNS contributes nothing here and already appears in the history card.
|
||||
"""
|
||||
rows = []
|
||||
for comm in _communication_rows(asset_id):
|
||||
address = (comm.ipaddress or '').strip()
|
||||
subnet = (comm.subnetmask or '').strip()
|
||||
gateway = (comm.gateway or '').strip()
|
||||
dns1 = (comm.dns1 or '').strip()
|
||||
dns2 = (comm.dns2 or '').strip()
|
||||
if not address or not (subnet or gateway or dns1 or dns2):
|
||||
continue
|
||||
rows.append({
|
||||
'ipaddress': address,
|
||||
'subnetmask': subnet or '-',
|
||||
'gateway': gateway or '-',
|
||||
'dns1': dns1 or '-',
|
||||
'dns2': dns2 or '-',
|
||||
'assignment': 'DHCP' if comm.isdhcp else 'Static',
|
||||
})
|
||||
return success_response(rows)
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/applications', methods=['GET'])
|
||||
@require_permission('endpoints.view')
|
||||
def panel_applications(asset_id: int):
|
||||
|
||||
Reference in New Issue
Block a user