Fleet console gap closures: critical tile, filters, policy card, action placement
This commit is contained in:
@@ -397,3 +397,51 @@ def get_effective_policy(asset_id: int):
|
||||
from ..services.policyresolver import effective_policy_for
|
||||
effective = effective_policy_for(asset_id)
|
||||
return success_response(effective.to_dict())
|
||||
|
||||
|
||||
@computers_bp.route('/asset/<int:asset_id>/policypanel', methods=['GET'])
|
||||
@require_permission('computers.view')
|
||||
def policy_panel(asset_id: int):
|
||||
"""Keyvalue panel: which policy governs this endpoint, and what it allows.
|
||||
|
||||
Provenance, not a control. It matters because the effective policy already
|
||||
narrows the command allowlist, so it is the answer to "why is Start greyed
|
||||
out on this PC" - which is otherwise invisible.
|
||||
|
||||
Returns an EMPTY field list rather than the feature-gate 404 when the
|
||||
feature is off. The panel contract hides a card with no fields; an error
|
||||
would render a broken card on every site that has not enabled policies,
|
||||
which is most of them.
|
||||
"""
|
||||
if not policy_flag_enabled():
|
||||
return success_response({'fields': []})
|
||||
|
||||
from ..services.policyresolver import effective_policy_for
|
||||
effective = effective_policy_for(asset_id)
|
||||
actions = effective.to_dict()
|
||||
if not actions:
|
||||
return success_response({'fields': []})
|
||||
|
||||
# One endpoint resolves through one winning business policy, but read the
|
||||
# ids off the actions rather than assuming: a fold can draw from more than
|
||||
# one configuration policy.
|
||||
businesspolicyids = {a['businesspolicyid'] for a in actions.values()
|
||||
if a.get('businesspolicyid')}
|
||||
configpolicyids = {a['configurationpolicyid'] for a in actions.values()
|
||||
if a.get('configurationpolicyid')}
|
||||
|
||||
names = []
|
||||
if businesspolicyids:
|
||||
names = [p.name for p in BusinessPolicy.query
|
||||
.filter(BusinessPolicy.businesspolicyid.in_(businesspolicyids)).all()]
|
||||
|
||||
gated = sum(1 for a in actions.values() if a.get('mode') == ACTION_MODE_GATE)
|
||||
desired = sum(1 for a in actions.values() if a.get('mode') == ACTION_MODE_DESIRED)
|
||||
|
||||
fields = []
|
||||
if names:
|
||||
fields.append({'label': 'Policy', 'value': ', '.join(sorted(names))})
|
||||
fields.append({'label': 'Configuration policies', 'value': str(len(configpolicyids))})
|
||||
fields.append({'label': 'Allowed actions', 'value': str(gated)})
|
||||
fields.append({'label': 'Desired state', 'value': str(desired)})
|
||||
return success_response({'fields': fields})
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
<div class="detail-page">
|
||||
<div class="page-header">
|
||||
<h2>Computer Details</h2>
|
||||
<!-- Record actions only. The two that act on the MACHINE - refresh and
|
||||
restart - live in the triage band beside the state they change and
|
||||
the machine they would take down with it. Restart sitting between
|
||||
Print Label and Edit made the most consequential button on the page
|
||||
look like a peer of printing a sticker. -->
|
||||
<div class="header-actions">
|
||||
<button v-if="canCommand" @click="refreshStatus" class="btn btn-secondary" :disabled="restarting || refreshing">{{ refreshing ? 'Refreshing...' : 'Refresh Status' }}</button>
|
||||
<button v-if="canRestart" @click="confirmRestart" class="btn btn-warning" :disabled="restarting">Restart Computer</button>
|
||||
<router-link :to="`/print/asset-label/computer/${$route.params.id}`" class="btn btn-secondary" target="_blank">Print Label</router-link>
|
||||
<router-link :to="`/pcs/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
||||
<button @click="confirmRetire" class="btn btn-danger">Retire</button>
|
||||
@@ -58,7 +61,16 @@
|
||||
:assetid="computer.assetid"
|
||||
:liveness="liveness"
|
||||
:accessmethods="computer.accessmethods || []"
|
||||
/>
|
||||
>
|
||||
<template #actions>
|
||||
<button v-if="canCommand" @click="refreshStatus" class="btn btn-secondary btn-sm"
|
||||
:disabled="restarting || refreshing">
|
||||
{{ refreshing ? 'Refreshing...' : 'Refresh Status' }}
|
||||
</button>
|
||||
<button v-if="canRestart" @click="confirmRestart" class="btn btn-warning btn-sm"
|
||||
:disabled="restarting">Restart Computer</button>
|
||||
</template>
|
||||
</AssetTriageBand>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1967,6 +1967,29 @@ class ComputersPlugin(BasePlugin):
|
||||
},
|
||||
]
|
||||
|
||||
def get_asset_panels(self) -> List[Dict]:
|
||||
"""Policy provenance on the PC detail page.
|
||||
|
||||
The effective policy already narrows which commands an endpoint will
|
||||
accept, so without this the reason a control is unavailable is not
|
||||
visible anywhere. Right column, near the services it governs: this is
|
||||
provenance, not a 6am triage signal.
|
||||
|
||||
The route returns an empty field list when the policy feature is off,
|
||||
so the card disappears on sites that have not enabled it rather than
|
||||
rendering an error.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'id': 'computers-policy',
|
||||
'title': 'Policy',
|
||||
'assettypes': ['computer'],
|
||||
'endpoint': '/api/computers/asset/{assetid}/policypanel',
|
||||
'render': 'keyvalue',
|
||||
'position': 40,
|
||||
},
|
||||
]
|
||||
|
||||
def get_navigation_items(self) -> List[Dict]:
|
||||
"""Return navigation menu items."""
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user