add mapped drives card
Mapped drives existed only as a comma-joined line in the user context card, which is fine for three drives and unreadable for a dozen. They now get their own list card in Monitors: one row per letter, remote path as meta, health as a badge. Reuses the statecolor/levels vocabulary the tracked-services cards already declare rather than new CSS. Only unhealthy drives are coloured. Green on every working drive makes the broken one just another badge in a row of badges, and the card exists to make the exception findable at a glance. Unhealthy rows sort first for the same reason - a user with a dozen mappings should not hunt for the broken one - and the sort is on health alone, so drive-letter order survives within each group and the list does not reshuffle between refreshes. The merge logic is untouched. _mapped_drive_rows now returns structured rows and the user-context summary line is built from them, so the card and the line cannot drift apart about a drive's status. The two silent-failure paths are mutation-tested: forcing every badge green failed 7 tests, and the first sort test passed a mutation that added a secondary key because every broken drive in it said the same word - it now uses statuses that would reorder under a text sort. Also corrects a stale warning claiming the merge is untested; it has been covered since Phase 1.
This commit is contained in:
@@ -864,8 +864,8 @@ def _worse_drive_status(left, right):
|
||||
)
|
||||
|
||||
|
||||
def _mapped_drives(context):
|
||||
"""Merge the two mapped-drive views into one row per drive letter.
|
||||
def _mapped_drive_rows(context):
|
||||
"""Merge the two mapped-drive views into one structured row per letter.
|
||||
|
||||
Windows exposes mapped drives through Win32_NetworkConnection (the 'net use'
|
||||
table) and through Get-SmbMapping, and they do not always agree - a share can
|
||||
@@ -875,8 +875,9 @@ def _mapped_drives(context):
|
||||
A drive present in only one source is still a drive: it is reported with the
|
||||
status that source gave it, rather than dropped for lack of a second opinion.
|
||||
|
||||
WARNING - this function is not covered by tests. Three behaviours here are
|
||||
load-bearing and easy to break while "simplifying":
|
||||
WARNING - three behaviours here are load-bearing and easy to break while
|
||||
"simplifying". They are covered by tests/test_plugins/test_endpoints_mapped
|
||||
_drives.py; if you touch this, read those first:
|
||||
|
||||
1. Worse-status-wins. Never report the healthier of the two sources. A
|
||||
drive that either source calls Unavailable IS unavailable to the
|
||||
@@ -888,8 +889,7 @@ def _mapped_drives(context):
|
||||
string must never be optimistically treated as healthy.
|
||||
|
||||
Changing any of these makes a broken drive look fine, which is a silent
|
||||
failure - the panel stays green and nobody is told. If you touch this,
|
||||
add the tests first.
|
||||
failure - the card stays green and nobody is told.
|
||||
"""
|
||||
drives = {}
|
||||
|
||||
@@ -928,20 +928,64 @@ def _mapped_drives(context):
|
||||
rows = []
|
||||
for letter in sorted(drives):
|
||||
drive = drives[letter]
|
||||
status = (drive.get('status') or '').strip()
|
||||
remote = (drive.get('remote') or '').strip()
|
||||
label = '{} {}'.format(letter, remote).strip()
|
||||
# A drive neither source gave a status for is Unknown, never blank. An
|
||||
# unannotated row reads as healthy, so a failed check would render as a
|
||||
# working drive - the one outcome this card must never produce.
|
||||
if not status:
|
||||
status = 'Unknown'
|
||||
status = (drive.get('status') or '').strip() or 'Unknown'
|
||||
healthy = status.lower() in ('ok', 'connected')
|
||||
rows.append({
|
||||
'drive': letter,
|
||||
'remote': (drive.get('remote') or '').strip(),
|
||||
'statuslabel': status,
|
||||
# Only unhealthy drives are coloured. Green on every working drive
|
||||
# makes the broken one just another badge in a row of badges; the
|
||||
# card's job is to make the exception findable at a glance.
|
||||
'statecolor': 'green' if healthy else 'red',
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def _mapped_drives(context):
|
||||
"""The mapped drives as one-line labels, for the user context card.
|
||||
|
||||
Built on _mapped_drive_rows so the summary line and the card can never
|
||||
disagree about a drive's status.
|
||||
"""
|
||||
labels = []
|
||||
for row in _mapped_drive_rows(context):
|
||||
label = '{} {}'.format(row['drive'], row['remote']).strip()
|
||||
# Only the problem states are annotated. Tagging every healthy drive
|
||||
# with "(OK)" would bury the one that is not.
|
||||
if status.lower() not in ('ok', 'connected'):
|
||||
label = '{} ({})'.format(label, status)
|
||||
rows.append(label)
|
||||
return rows
|
||||
if row['statecolor'] != 'green':
|
||||
label = '{} ({})'.format(label, row['statuslabel'])
|
||||
labels.append(label)
|
||||
return labels
|
||||
|
||||
|
||||
def _mapped_drives_unhealthy_first(rows):
|
||||
"""Order drive rows so anything not healthy sorts first.
|
||||
|
||||
A user with a dozen mappings should not have to hunt for the broken one.
|
||||
Sorting only on health keeps it stable: drive-letter order survives within
|
||||
each group, so the list does not reshuffle between refreshes.
|
||||
"""
|
||||
return sorted(rows, key=lambda row: row['statecolor'] == 'green')
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/mappeddrives', methods=['GET'])
|
||||
@require_permission('endpoints.view')
|
||||
def panel_mappeddrives(asset_id: int):
|
||||
"""List panel: mapped network drives with a health badge each.
|
||||
|
||||
Session-scoped: mapped drives do not exist in the agent's session 0, so
|
||||
these come from the interactive user probe. Nothing reported when nobody
|
||||
is signed in, which is a valid state and not a failure.
|
||||
"""
|
||||
state = EndpointState.query.filter_by(assetid=asset_id).first()
|
||||
context = _state_json(state, 'usercontextjson') if state else None
|
||||
if not isinstance(context, dict) or not context:
|
||||
return success_response([])
|
||||
return success_response(_mapped_drives_unhealthy_first(_mapped_drive_rows(context)))
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/usercontext', methods=['GET'])
|
||||
|
||||
Reference in New Issue
Block a user