Files
shopdb-flask/plugins/computers/frontend/views/BusinessPoliciesList.vue
benjamin.baker d973ce6c26 Add configuration policy monitors and action picker for computers
Configuration policies need a way to attach process/service monitors so drift can be evaluated against a machine's desired state. This wires the policy resolver and routes to the shared monitor catalog and adds an action picker UI that can create process monitors with an optional launch path.
2026-09-01 15:45:52 -04:00

224 lines
7.5 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Site Policies</h2>
<button class="btn btn-primary" @click="openModal()">+ Add Site Policy</button>
</div>
<div class="card">
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>Priority</th>
<th>Name</th>
<th>Description</th>
<th>Configuration policies</th>
<th>Source</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="p in policies" :key="p.businesspolicyid">
<td>{{ p.priority }}</td>
<td>{{ p.name }}</td>
<td class="cell-truncate" :title="p.description">{{ p.description || '-' }}</td>
<td>
<span v-for="it in p.items" :key="it.businesspolicyitemid" class="badge" style="margin-right:4px;">
{{ it.name }}
</span>
<span v-if="!p.items || p.items.length === 0" style="color: var(--text-light);">none</span>
</td>
<td>
<span class="badge" :class="p.source === 'local' ? 'badge-secondary' : 'badge-warning'">
{{ p.source }}
</span>
</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" :disabled="p.source !== 'local'" @click="openModal(p)">Edit</button>
<button class="btn btn-danger btn-sm" :disabled="p.source !== 'local'" @click="remove(p)">Delete</button>
</td>
</tr>
<tr v-if="policies.length === 0">
<td colspan="6" style="text-align:center; color: var(--text-light);">No site policies</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<div v-if="showModal" class="modal-overlay">
<div class="modal modal-lg">
<div class="modal-header">
<h3>{{ editing ? 'Edit Site Policy' : 'Add Site Policy' }}</h3>
</div>
<form @submit.prevent="save">
<div class="modal-body">
<div class="form-group">
<label>Name *</label>
<input v-model="form.name" type="text" class="form-control" required />
</div>
<div class="form-group">
<label>Priority <span class="hint">(lower runs first; first match wins per action)</span></label>
<input v-model.number="form.priority" type="number" class="form-control" />
</div>
<div class="form-group">
<label>Description</label>
<textarea v-model="form.description" class="form-control" rows="2"></textarea>
</div>
<div class="form-group">
<label>Configuration policies <span class="hint">(evaluated top to bottom)</span></label>
<div v-for="(it, index) in form.items" :key="index" class="item-row">
<span class="order-badge">{{ index + 1 }}</span>
<select v-model.number="it.configurationpolicyid" class="form-control">
<option :value="undefined" disabled>Select configuration policy...</option>
<option v-for="cp in configPolicies" :key="cp.configurationpolicyid" :value="cp.configurationpolicyid">
{{ cp.name }}
</option>
</select>
<button type="button" class="btn btn-secondary btn-sm" :disabled="index === 0" @click="moveUp(index)">Up</button>
<button type="button" class="btn btn-secondary btn-sm" :disabled="index === form.items.length - 1" @click="moveDown(index)">Down</button>
<button type="button" class="btn btn-danger btn-sm" @click="form.items.splice(index, 1)">Remove</button>
</div>
<button type="button" class="btn btn-secondary btn-sm" @click="addItem">+ Add configuration policy</button>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { computersApi } from '@/api'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
const toast = useToast()
const policies = ref([])
const configPolicies = ref([])
const loading = ref(true)
const showModal = ref(false)
const editing = ref(null)
const saving = ref(false)
const error = ref('')
const form = ref(emptyForm())
function emptyForm() {
return { name: '', description: '', priority: 100, items: [] }
}
onMounted(() => loadData())
async function loadData() {
loading.value = true
try {
const [biz, config] = await Promise.all([
computersApi.policies.business.list(),
computersApi.policies.configuration.list()
])
policies.value = biz.data.data || []
configPolicies.value = config.data.data || []
} catch (err) {
console.error('Error loading business policies:', err)
} finally {
loading.value = false
}
}
function openModal(item = null) {
editing.value = item
error.value = ''
if (item) {
form.value = {
name: item.name || '',
description: item.description || '',
priority: item.priority ?? 100,
items: (item.items || [])
.slice()
.sort((a, b) => a.sortorder - b.sortorder)
.map(it => ({ configurationpolicyid: it.configurationpolicyid }))
}
} else {
form.value = emptyForm()
}
showModal.value = true
}
function closeModal() { showModal.value = false; editing.value = null }
function addItem() {
form.value.items.push({ configurationpolicyid: undefined })
}
function moveUp(index) {
const items = form.value.items
;[items[index - 1], items[index]] = [items[index], items[index - 1]]
}
function moveDown(index) {
const items = form.value.items
;[items[index + 1], items[index]] = [items[index], items[index + 1]]
}
async function save() {
saving.value = true
error.value = ''
try {
const payload = {
name: form.value.name,
description: form.value.description,
priority: form.value.priority,
items: form.value.items.map((it, order) => ({
configurationpolicyid: it.configurationpolicyid,
sortorder: order
}))
}
if (editing.value) {
await computersApi.policies.business.update(editing.value.businesspolicyid, payload)
} else {
await computersApi.policies.business.create(payload)
}
closeModal()
loadData()
} catch (err) {
error.value = apiError(err, 'Failed to save site policy')
} finally {
saving.value = false
}
}
async function remove(p) {
if (!confirm(`Delete site policy "${p.name}"?`)) return
try {
await computersApi.policies.business.remove(p.businesspolicyid)
loadData()
} catch (err) {
toast.error(apiError(err, 'Failed to delete'))
}
}
</script>
<style scoped>
.item-row { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.order-badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 24px; height: 24px; border-radius: 12px;
background: var(--border-color, #444);
}
</style>