Drop the Actions tab and give wide panels the full width

This commit is contained in:
2026-08-31 14:41:20 -04:00
parent 0d49341a2a
commit 2fce339735
5 changed files with 45 additions and 21 deletions

View File

@@ -3,7 +3,7 @@
a panel as JSON (title, endpoint, render mode, field map); this renders it
on any asset detail page, so a plugin adds detail-page UI with no Vue. -->
<template v-for="panel in groupedPanels" :key="panel.id">
<div v-if="panelVisible(panel)" class="section-card">
<div v-if="panelVisible(panel)" class="section-card" :class="{ 'pap-span': panel.render === 'table' }">
<!-- collapsible panels get a clickable header with a caret and, when the
panel declares a status rollup, a green/yellow/red dot for the worst
row (a stopped service or a warning/critical event). --><h3
@@ -430,7 +430,18 @@ defineExpose({ refresh: load, populatedGroups })
font-weight: 700;
}
.pap-table-wrap { overflow-x: auto; }
/* A table panel SPANS BOTH COLUMNS rather than scrolling inside one.
Network interfaces is the case that forced this: adapter, address, MAC,
speed and state at half page width either squeezed every cell to three
lines or handed the reader a horizontal scrollbar, and a scrollbar for
data that simply needs room is a layout failure, not a feature. column-span
is the multi-column layout's own answer, so the short fact cards stay two-up
around it.
The wrapper keeps overflow-x as a floor for the genuinely too-wide case on
a narrow viewport, where nothing else can help. */
.pap-span { column-span: all; }
.pap-table-wrap { overflow-x: auto; max-width: 100%; }
.pap-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.pap-table th, .pap-table td { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); }
.pap-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; }

View File

@@ -272,11 +272,14 @@
</div>
</div>
<!-- Software. The core installed-applications list plus anything a plugin
declared for this group. -->
<div v-show="activegroup === 'software'" class="content-grid">
<!-- Software and Activity run FULL WIDTH, not in the two-column flow the
Details tab uses. Both are long lists rather than short fact cards:
an application inventory or an event feed split into balanced columns
reads as two unrelated lists, and the eye has to find where the first
one ended. -->
<div v-show="activegroup === 'software'" class="detail-full">
<PluginAssetPanels :assetid="computer.assetid" group="software" />
<div class="content-column">
<div>
<div class="section-card" v-if="installedApps.length > 0">
<h3 class="section-title">Installed Applications</h3>
<div class="app-list">
@@ -299,16 +302,15 @@
</div>
</div>
<!-- Metrics, Actions and Activity are entirely plugin-contributed: the
telemetry, the service controls and the event feed all belong to the
endpoints plugin, and core knows nothing about them. -->
<!-- Metrics and Activity are entirely plugin-contributed: the telemetry,
the service state and its controls, and the event feed all belong to
the endpoints plugin, and core knows nothing about them.
Metrics keeps the two-column flow because it is short cards. -->
<div v-show="activegroup === 'metrics'" class="content-grid">
<PluginAssetPanels :assetid="computer.assetid" group="metrics" />
</div>
<div v-show="activegroup === 'actions'" class="content-grid">
<PluginAssetPanels :assetid="computer.assetid" group="actions" />
</div>
<div v-show="activegroup === 'activity'" class="content-grid">
<div v-show="activegroup === 'activity'" class="detail-full">
<PluginAssetPanels :assetid="computer.assetid" group="activity" />
</div>
@@ -386,7 +388,6 @@ const detailtabs = computed(() => {
tabs.push({ key: 'software', label: 'Software', count: installedApps.value.length || null })
}
if (has('metrics')) tabs.push({ key: 'metrics', label: 'Metrics' })
if (has('actions')) tabs.push({ key: 'actions', label: 'Actions' })
if (has('activity')) tabs.push({ key: 'activity', label: 'Activity' })
return tabs
})
@@ -602,6 +603,12 @@ async function confirmRetire() {
</script>
<style scoped>
/* Software and Activity are single long lists, so they use the full page width
rather than .content-grid's two balanced columns. Cards inside still need
their own vertical rhythm, which the column flow was providing. */
.detail-full { display: flex; flex-direction: column; gap: 1rem; }
.detail-full > * { min-width: 0; }
/* PC-specific styles - shared styles are in global style.css */
/* Remote-access protocol links */

View File

@@ -351,8 +351,9 @@ class EndpointsPlugin(BasePlugin):
'permission': 'endpoints.command',
'empty': 'No monitored services reported.',
'position': 12,
# Start/Stop act on the machine, so this is a control surface
'group': 'actions',
# Service state is telemetry first; the Start/Stop controls
# belong beside it rather than in a tab of their own.
'group': 'metrics',
},
{
'id': 'endpoints-tracked-processes',

View File

@@ -25,7 +25,7 @@ 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', 'software', 'metrics', 'actions', 'activity')
ASSET_PANEL_GROUPS = ('details', 'software', 'metrics', 'activity')
def _collect(hookname):

View File

@@ -372,8 +372,7 @@ class BasePlugin(ABC):
'position': int, # order among panels
'group': str, # OPTIONAL. Which tab on the detail page:
# 'details' | 'software' | 'metrics'
# | 'actions' | 'activity'. Omitted means
# 'details'.
# | 'activity'. Omitted means 'details'.
}
ON 'group'. The detail pages are tabbed, and a panel says which tab it
@@ -384,8 +383,14 @@ class BasePlugin(ABC):
existing plugin working untouched: an unknown or absent group resolves
to 'details', which is the tab that holds what an asset IS. Reach for
another group only when the panel is the thing that tab exists for -
an inventory listing, live telemetry, controls that act on the machine,
or a history feed.
an inventory listing, live telemetry, or a history feed.
There is deliberately no group for controls. A panel that acts on the
machine belongs beside the state it acts on: tracked services read as
telemetry first and a Start/Stop second, so they live in 'metrics' with
the rest of what the endpoint reports. Splitting the buttons away from
the state they change made a tab that held one card and answered no
question on its own.
Consumed by GET /api/pluginui/asset-panels?assetid=<id>, which returns
the panels matching that asset's type. Disabled plugins