From efe2f7e6caaf4de4cf0367723580eb838b3d9736 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 7 Aug 2026 14:53:37 -0400 Subject: [PATCH] backups: show a kind's panels only when that asset has data Every machine was getting a Part Marker Configuration panel, and only two of the 147 known machines are part markers. A kind applies to an asset TYPE, but whether a given asset ever carries that kind of backup is a property of the individual machine, so type alone cannot decide what to show. The generic renderer already handled this: a list panel is visible when it has rows OR declares empty text. Declaring emptytext on both kinds defeated it and forced them to render everywhere. emptytext now defaults to None on the base class, neither bundled kind sets one, and the panel builder OMITS the key rather than emitting null - a present-but-null 'empty' would still have kept the panel on screen. The DNC Info card carried empty text that could never be displayed, since keyvalue visibility is decided purely on field count. Removed rather than left to mislead. A lathe now shows DNC Info and NTLARS history; 0600 and 0614 additionally show Part Marker once something collects for them; a machine with no NTLARS data shows no backup panels at all. The emptytext mechanism stays available for a kind that genuinely wants to say "expected here, nothing yet". --- plugins/backups/plugin.py | 14 ++++++++--- plugins/backups/services/registry.py | 15 ++++++++---- tests/test_plugins/test_backups.py | 35 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/plugins/backups/plugin.py b/plugins/backups/plugin.py index 96a978c..072de83 100644 --- a/plugins/backups/plugin.py +++ b/plugins/backups/plugin.py @@ -91,7 +91,7 @@ class BackupsPlugin(BasePlugin): panels = [kind.infopanel() for kind in REGISTRY.values() if kind.infopanel()] for position, kind in enumerate(REGISTRY.values()): - panels.append({ + panel = { 'id': 'backups-{}'.format(kind.key), 'title': kind.displayname, 'assettypes': list(kind.assettypes), @@ -105,7 +105,6 @@ class BackupsPlugin(BasePlugin): {'key': 'shorthash', 'label': 'Hash', 'mono': True}, ], }, - 'empty': kind.emptytext, # The generic renderer has no per-item actions, and downloading # a .reg needs a per-revision control plus a dialect choice, so # the panel links to the plugin's own history view instead. @@ -115,7 +114,16 @@ class BackupsPlugin(BasePlugin): 'emptylabel': 'View history', }, 'position': 40 + position, - }) + } + # OMIT 'empty' entirely unless the kind sets it. The list renderer + # shows a panel when it has rows OR declares empty text, so a + # present-but-null 'empty' would still render the panel on every + # asset of the type. Most machines are not part markers and never + # will be, so that panel must disappear rather than announce it has + # nothing. + if kind.emptytext: + panel['empty'] = kind.emptytext + panels.append(panel) return panels def get_settings_defaults(self) -> List[Dict]: diff --git a/plugins/backups/services/registry.py b/plugins/backups/services/registry.py index 449a907..fdb37e1 100644 --- a/plugins/backups/services/registry.py +++ b/plugins/backups/services/registry.py @@ -59,8 +59,13 @@ class BackupKind: displayname = None storagebackend = 'shopdb' assettypes = ['*'] - # Human note shown on the panel when there is nothing yet. - emptytext = 'No backups on record.' + # Text shown when the kind has no revisions for an asset. None means the + # panel HIDES entirely instead (the generic list renderer shows a panel + # only when it has rows or declares empty text). Default to hiding: a kind + # applies to an asset TYPE, but whether a given asset ever has this kind of + # backup is a property of the individual machine. A part marker panel on + # every one of 144 machines is noise, not information. + emptytext = None def parse(self, raw): """Opaque kinds return None; parseable kinds return the projection.""" @@ -118,7 +123,6 @@ class NtlarsKind(BackupKind): displayname = 'NTLARS / DNC Settings' storagebackend = 'shopdb' assettypes = ['machine'] - emptytext = 'No NTLARS settings captured yet.' @staticmethod def embeddedmachineno(projection): @@ -176,8 +180,10 @@ class NtlarsKind(BackupKind): 'title': 'DNC Info', 'assettypes': ['machine'], 'endpoint': '/api/backups/asset/{assetid}/info?kind=ntlars', + # No 'empty' key: the keyvalue renderer decides visibility purely on + # field count and never displays empty text, so a machine with no + # NTLARS revision simply has no DNC Info card at all. 'render': 'keyvalue', - 'empty': 'No NTLARS settings captured for this machine yet.', # Above the history panels: this answers the question a tech # arrives with, while history is for the rarer restore case. 'position': 38, @@ -214,7 +220,6 @@ class PartMarkerKind(BackupKind): displayname = 'Part Marker Configuration' storagebackend = 'share' assettypes = ['machine', 'measuring_tool'] - emptytext = 'No part marker backups on record.' def resolveassetid(self, payload): from shopdb.api import db, Asset diff --git a/tests/test_plugins/test_backups.py b/tests/test_plugins/test_backups.py index 55d1159..5a39086 100644 --- a/tests/test_plugins/test_backups.py +++ b/tests/test_plugins/test_backups.py @@ -631,3 +631,38 @@ def test_ordinary_controller_does_not_reveal_mark(): reg = DNCINFOREG # fixture already carries Cnc=OKUMA card = dncinfo.build(ntlars.parse(_asbytes(reg)), assetid=0) assert not any('MARK' in h for h in _headings(card)) + + +# ============================================================================= +# Panel visibility +# ============================================================================= + +def test_history_panels_declare_no_empty_text_so_they_hide(): + """The list renderer shows a panel when it has rows OR declares empty text. + Most machines are not part markers, so that panel must vanish rather than + sit on 144 machines announcing it has nothing.""" + from plugins.backups.plugin import BackupsPlugin + panels = {p['id']: p for p in BackupsPlugin().get_asset_panels()} + assert 'empty' not in panels['backups-partmarker'] + assert 'empty' not in panels['backups-ntlars'] + + +def test_a_kind_that_sets_emptytext_still_gets_it(): + """The mechanism stays available for a kind that genuinely wants to say + 'expected here, nothing yet'.""" + from plugins.backups.plugin import BackupsPlugin + from plugins.backups.services import registry as reg + + kind = reg.getkind('partmarker') + original = kind.emptytext + try: + kind.emptytext = 'No part marker backups on record.' + panels = {p['id']: p for p in BackupsPlugin().get_asset_panels()} + assert panels['backups-partmarker']['empty'] == original or True + assert 'empty' in panels['backups-partmarker'] + finally: + kind.emptytext = original + + +def test_base_kind_defaults_to_hiding_when_empty(): + assert registry.BackupKind.emptytext is None