Separate shipped policies from site policies in the list

The list mixed both kinds of configuration policy into one table, so the only
way to tell a shipped default from something the site had written was to read
the source column. Split them into two tables under their own headings.

The split is presentational only. Both tables render the same Edit and Delete
buttons gated on the same iseditable(p) rule, so a shipped policy is not
stranded behind a dead button the way it was when the buttons gated on
source === 'local'.

The spec counted exactly two disabled bindings, which the second table would
have broken while the behaviour it was guarding stayed correct. It now counts
Edit and Delete in pairs and asserts the two always agree, so adding a group
does not fail a test about button gating.
This commit is contained in:
benjamin.baker
2026-09-20 13:23:23 -04:00
parent 3c91e7c314
commit 790ed4ec66
3 changed files with 143 additions and 13 deletions

View File

@@ -12,7 +12,21 @@
has been configured.</p>
</div>
<!-- Split into two cards rather than one list with a Source column.
There is one shipped policy PER MONITOR, so shipped rows outnumber
site-authored ones and a single sorted list buries the handful of
policies the site actually wrote. The split also gives the shipped
group somewhere to carry its warning, which a per-row badge cannot
do. Both groups render the same row markup and the same editable
rule - the separation is presentational, not a permission boundary. -->
<div class="card">
<h3>Shipped policies</h3>
<p class="shipped-warning">
These ship with the product as starting points - one per monitor. You
can edit or delete them and your changes are kept, but an upgrade may
add new rows here. Deleting one does not stop the monitor it refers to;
it only removes this policy's opinion about it.
</p>
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
@@ -27,7 +41,49 @@
</tr>
</thead>
<tbody>
<tr v-for="p in policies" :key="p.configurationpolicyid">
<tr v-for="p in shippedPolicies" :key="p.configurationpolicyid">
<td>{{ p.name }}</td>
<td class="cell-truncate" :title="p.description">{{ p.description || '-' }}</td>
<td>
<span v-for="a in p.actions" :key="a.action" class="badge" style="margin-right:4px;">
{{ actionLabel(a.action) }} ({{ a.mode }})
</span>
<span v-if="!p.actions || p.actions.length === 0" style="color: var(--text-light);">none</span>
</td>
<td>
<span class="badge badge-warning">{{ p.source }}</span>
</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" :disabled="!iseditable(p)" @click="openModal(p)">Edit</button>
<button class="btn btn-danger btn-sm" :disabled="!iseditable(p)" @click="remove(p)">Delete</button>
</td>
</tr>
<tr v-if="shippedPolicies.length === 0">
<td colspan="5" style="text-align:center; color: var(--text-light);">No shipped policies</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<div class="card">
<h3>Site policies</h3>
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Actions in policy</th>
<th>Source</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="p in localPolicies" :key="p.configurationpolicyid">
<td>{{ p.name }}</td>
<td class="cell-truncate" :title="p.description">{{ p.description || '-' }}</td>
<td>
@@ -46,7 +102,7 @@
<button class="btn btn-danger btn-sm" :disabled="!iseditable(p)" @click="remove(p)">Delete</button>
</td>
</tr>
<tr v-if="policies.length === 0">
<tr v-if="localPolicies.length === 0">
<td colspan="5" style="text-align:center; color: var(--text-light);">No configuration policies</td>
</tr>
</tbody>
@@ -115,7 +171,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { computersApi, applicationsApi, endpointsApi } from '@/api'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
@@ -299,6 +355,17 @@ function iseditable(p) {
return EDITABLE_SOURCES.includes(p.source)
}
// Presentational split only. Anything that is not 'shipped' lands in the site
// list - including 'autoapp', which is generated rather than hand-written but
// is still this site's own row and carries its own badge there. Written as
// "not shipped" rather than "=== local" so a future source value shows up in a
// list instead of vanishing from both, which is the failure mode that made the
// old Edit buttons dead.
const shippedPolicies = computed(
() => policies.value.filter(p => p.source === 'shipped'))
const localPolicies = computed(
() => policies.value.filter(p => p.source !== 'shipped'))
async function remove(p) {
if (!confirm(`Delete configuration policy "${p.name}"?`)) return
try {
@@ -318,6 +385,14 @@ async function remove(p) {
.feature-off-notice p {
margin: 0.4rem 0 0 0;
}
/* Matches the feature-off notice rather than inventing a second warning
treatment: same left rule, same warning colour. */
.shipped-warning {
border-left: 3px solid var(--warning);
padding-left: 0.75rem;
margin: 0 0 1rem 0;
color: var(--text-light);
}
.modal-lg {
max-width: 760px;
}