Admit serviceprocess server-side and give Operator Reports its own tab
asset_panels() rewrites any group missing from ASSET_PANEL_GROUPS to 'details' before the client sees it, and Phase 3 never added 'serviceprocess' there. Both panels were relabelled on the wire, so the tab could not appear no matter how consistent the frontend was. That is why the tab was still missing after the schema fix - a second, unrelated cause wearing the same symptom. Operator Reports moves out of System Monitors into a tab of its own: that tab is machine-collected health, this is a person reporting something, and the thumbnail grid dwarfed the fact cards it sat between. Full width, since it is alone in the tab. panelGroups.spec.js claimed to cover four files and checked two; it now reads the server tuple as well. The plugin test's hand-copied set was a fifth copy and now derives from the server tuple instead.
This commit is contained in:
@@ -461,7 +461,7 @@ const panels = ref([])
|
||||
// 'actions', this build had no such tab, and the card was simply gone with
|
||||
// nothing logged. The same applies to the 'metrics' to 'monitors' rename.
|
||||
// Fall back the same way the server does rather than drop it.
|
||||
const RENDERED_GROUPS = ['details', 'monitors', 'serviceprocess', 'software', 'activity']
|
||||
const RENDERED_GROUPS = ['details', 'monitors', 'serviceprocess', 'reports', 'software', 'activity']
|
||||
function groupOf(panel) {
|
||||
return RENDERED_GROUPS.includes(panel.group) ? panel.group : 'details'
|
||||
}
|
||||
|
||||
@@ -66,4 +66,30 @@ describe('panel group vocabulary', () => {
|
||||
expect(renderedGroups()).toContain('monitors')
|
||||
expect(pcDetailSource()).toContain("label: 'System Monitors'")
|
||||
})
|
||||
|
||||
it('agrees with the server vocabulary that resolves groups first', () => {
|
||||
// The fourth file this comment block claims to cover, and the one that was
|
||||
// NOT actually being checked. asset_panels() rewrites any group missing
|
||||
// from ASSET_PANEL_GROUPS to 'details' before the client sees it, so the
|
||||
// frontend can be perfectly consistent with itself and still show nothing:
|
||||
// that is exactly how the Service/Process tab went missing after Phase 3.
|
||||
const source = read('../../../shopdb/core/api/pluginui.py')
|
||||
const match = source.match(/ASSET_PANEL_GROUPS = \(([\s\S]*?)\)/)
|
||||
if (!match) throw new Error('ASSET_PANEL_GROUPS not found in pluginui.py')
|
||||
const serverGroups = match[1].split(',')
|
||||
.map((entry) => entry.trim().replace(/^'|'$/g, ''))
|
||||
.filter(Boolean)
|
||||
|
||||
for (const group of renderedGroups()) {
|
||||
expect(serverGroups).toContain(group)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps operator reports in a tab of its own', () => {
|
||||
const source = pcDetailSource()
|
||||
|
||||
expect(renderedGroups()).toContain('reports')
|
||||
expect(source).toContain("key: 'reports'")
|
||||
expect(source).toContain('group="reports"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -349,6 +349,12 @@
|
||||
<div v-show="activegroup === 'serviceprocess'" class="content-grid">
|
||||
<PluginAssetPanels :assetid="computer.assetid" group="serviceprocess" />
|
||||
</div>
|
||||
<!-- Full width, unlike the two above: the thumbnail grid wants the room,
|
||||
and it is the only card in this tab so there is nothing to flow
|
||||
beside it. -->
|
||||
<div v-show="activegroup === 'reports'" class="detail-full">
|
||||
<PluginAssetPanels :assetid="computer.assetid" group="reports" />
|
||||
</div>
|
||||
<div v-show="activegroup === 'activity'" class="detail-full">
|
||||
<PluginAssetPanels :assetid="computer.assetid" group="activity" />
|
||||
</div>
|
||||
@@ -456,6 +462,10 @@ const detailtabs = computed(() => {
|
||||
if (has('serviceprocess')) {
|
||||
tabs.push({ key: 'serviceprocess', label: 'Service/Process Monitors' })
|
||||
}
|
||||
// Operator Reports earns a tab rather than a card in System Monitors: it is
|
||||
// what a person reported, not what the agent measured, and the thumbnail
|
||||
// grid is much bulkier than the fact cards around it.
|
||||
if (has('reports')) tabs.push({ key: 'reports', label: 'Operator Reports' })
|
||||
if (has('software') || installedApps.value.length > 0) {
|
||||
tabs.push({ key: 'software', label: 'Software', count: installedApps.value.length || null })
|
||||
}
|
||||
|
||||
@@ -647,8 +647,11 @@ class EndpointsPlugin(BasePlugin):
|
||||
},
|
||||
{
|
||||
# Operator-submitted issue reports: a free-text note and an
|
||||
# optional screenshot the operator captured from the tray. Sits
|
||||
# in the Monitors tab beside the other watch-and-observe panels.
|
||||
# optional screenshot the operator captured from the tray. Has
|
||||
# a tab of its own rather than sitting in System Monitors: that
|
||||
# tab is machine-collected health, this is a human telling you
|
||||
# something, and the thumbnail grid is far bulkier than the
|
||||
# fact cards it used to sit between.
|
||||
# A custom render carries the thumbnail grid and the expanding
|
||||
# lightbox with left/right cycling the generic modes cannot.
|
||||
'id': 'endpoints-operator-reports',
|
||||
@@ -660,7 +663,7 @@ class EndpointsPlugin(BasePlugin):
|
||||
'collapsible': True,
|
||||
'empty': 'No operator reports for this endpoint.',
|
||||
'position': 18,
|
||||
'group': 'monitors',
|
||||
'group': 'reports',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -25,7 +25,14 @@ pluginui_bp = Blueprint('pluginui', __name__)
|
||||
#
|
||||
# 'details' is first and is the fallback: it holds what an asset IS, which is
|
||||
# what a panel declaring nothing almost always contributes.
|
||||
ASSET_PANEL_GROUPS = ('details', 'monitors', 'software', 'activity')
|
||||
#
|
||||
# This tuple is a THIRD copy of the tab vocabulary - RENDERED_GROUPS in
|
||||
# PluginAssetPanels.vue and the tab list in PCDetail.vue are the other two.
|
||||
# Adding a group means touching all three: a value missing here is rewritten to
|
||||
# 'details' before the client ever sees it, so the panels quietly render in the
|
||||
# wrong tab and the intended tab never appears. No error, no log line.
|
||||
ASSET_PANEL_GROUPS = ('details', 'monitors', 'serviceprocess', 'reports',
|
||||
'software', 'activity')
|
||||
|
||||
|
||||
def _collect(hookname):
|
||||
|
||||
44
tests/test_core/test_pluginui_panel_groups.py
Normal file
44
tests/test_core/test_pluginui_panel_groups.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""The server-side panel group vocabulary must admit every group a plugin uses.
|
||||
|
||||
`asset_panels()` resolves a panel's group BEFORE the client sees it, coercing
|
||||
any unrecognised value to 'details'. That makes ASSET_PANEL_GROUPS a third copy
|
||||
of the tab vocabulary, alongside RENDERED_GROUPS in PluginAssetPanels.vue and
|
||||
the tab list in PCDetail.vue. Phase 3 added 'serviceprocess' to the two
|
||||
frontend copies and missed this one, so both Service/Process panels were
|
||||
silently relabelled 'details' on the wire and the tab never appeared.
|
||||
|
||||
The failure is invisible: no error, no log line, and the panels still render -
|
||||
just in the wrong tab, underneath the Details cards.
|
||||
"""
|
||||
from shopdb.core.api.pluginui import ASSET_PANEL_GROUPS
|
||||
from plugins.endpoints.plugin import EndpointsPlugin
|
||||
|
||||
|
||||
def test_every_group_the_endpoints_plugin_declares_survives_resolution():
|
||||
declared = {
|
||||
str(panel.get('group') or '').strip().lower()
|
||||
for panel in EndpointsPlugin().get_asset_panels()
|
||||
}
|
||||
declared.discard('')
|
||||
unknown = declared - set(ASSET_PANEL_GROUPS)
|
||||
assert not unknown, (
|
||||
f'panels declare {sorted(unknown)}, which the server rewrites to '
|
||||
"'details' - the tab will not appear"
|
||||
)
|
||||
|
||||
|
||||
def test_serviceprocess_is_admitted():
|
||||
# Named explicitly: the generic test above would also pass if the
|
||||
# serviceprocess panels were deleted outright.
|
||||
assert 'serviceprocess' in ASSET_PANEL_GROUPS
|
||||
|
||||
|
||||
def test_reports_is_admitted():
|
||||
# Same reasoning as serviceprocess above. Operator Reports was moved out of
|
||||
# System Monitors into a tab of its own, which is the same three-copy
|
||||
# change that Phase 3 got wrong.
|
||||
assert 'reports' in ASSET_PANEL_GROUPS
|
||||
|
||||
|
||||
def test_details_remains_the_fallback():
|
||||
assert ASSET_PANEL_GROUPS[0] == 'details'
|
||||
@@ -16,12 +16,16 @@ a group nothing renders. The frontend half is pinned by its own spec.
|
||||
"""
|
||||
|
||||
from plugins.endpoints.plugin import EndpointsPlugin
|
||||
from shopdb.core.api.pluginui import ASSET_PANEL_GROUPS
|
||||
|
||||
|
||||
# Must match RENDERED_GROUPS in frontend/src/components/PluginAssetPanels.vue.
|
||||
# A panel in any group not listed here is fetched, held, and shown nowhere.
|
||||
RENDERED_GROUPS = {'details', 'monitors', 'serviceprocess', 'software',
|
||||
'activity'}
|
||||
# Derived from the server tuple rather than hand-copied. This used to be a
|
||||
# literal set duplicating RENDERED_GROUPS, which made it a FIFTH copy of the
|
||||
# vocabulary and one more thing to forget: it passed for a group the server
|
||||
# would rewrite to 'details' anyway. ASSET_PANEL_GROUPS is the value that
|
||||
# actually decides what reaches the client, and panelGroups.spec.js pins it
|
||||
# against the frontend copy.
|
||||
RENDERED_GROUPS = set(ASSET_PANEL_GROUPS)
|
||||
|
||||
|
||||
def _panels():
|
||||
|
||||
Reference in New Issue
Block a user