The policy builder only offered "Matches everything" (the `true` scope node), which also targets servers. Authoring an estate-wide policy almost always means "every PC an operator sits at", and a reboot or script push against a server is a materially different event. Adds an `allpcs` scope node that matches every endpoint except those whose computer type is server-designated. Server designation reads the computer type's ICON slug, which is nominally a UI hint and is now load-bearing; it was chosen over matching the type NAME (a site can rename "Server") and over a new isserver column (needs a migration). It is isolated behind SERVER_ICON so promoting it to a real flag later is a one-place change. The `true` node is retained and still matches servers, so existing policies keep their exact meaning rather than silently re-scoping. It stays visible in the dropdown only when editing a policy that already uses it; new policies default to `allpcs`. Covered by tests/test_plugins/test_computers_policy_allpcs.py, verified against a planted canary so the server-exclusion assertions are known to bite.
108 lines
3.2 KiB
Vue
108 lines
3.2 KiB
Vue
<template>
|
|
<div class="scope-node">
|
|
<div class="scope-node-row">
|
|
<select v-model="node.type" class="form-control scope-type" @change="onTypeChange">
|
|
<option value="allpcs">All PCs (excludes servers)</option>
|
|
<option v-if="node.type === 'true'" value="true">Matches everything (incl. servers)</option>
|
|
<option value="pctype">PC type is</option>
|
|
<option value="app">App is installed</option>
|
|
<option value="and">ALL of (AND)</option>
|
|
<option value="or">ANY of (OR)</option>
|
|
<option value="not">NOT</option>
|
|
</select>
|
|
|
|
<select v-if="node.type === 'pctype'" v-model.number="node.id" class="form-control">
|
|
<option :value="undefined" disabled>Select PC type...</option>
|
|
<option v-for="pt in pctypes" :key="pt.computertypeid" :value="pt.computertypeid">
|
|
{{ pt.computertype }}
|
|
</option>
|
|
</select>
|
|
|
|
<select v-if="node.type === 'app'" v-model.number="node.id" class="form-control">
|
|
<option :value="undefined" disabled>Select app...</option>
|
|
<option v-for="app in apps" :key="app.appid" :value="app.appid">
|
|
{{ app.appname }}
|
|
</option>
|
|
</select>
|
|
|
|
<button v-if="removable" type="button" class="btn btn-danger btn-sm" @click="$emit('remove')">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="node.type === 'and' || node.type === 'or'" class="scope-children">
|
|
<ScopeNodeEditor
|
|
v-for="(child, index) in node.nodes"
|
|
:key="index"
|
|
:node="child"
|
|
:pctypes="pctypes"
|
|
:apps="apps"
|
|
:removable="true"
|
|
@remove="removeChild(index)"
|
|
/>
|
|
<button type="button" class="btn btn-secondary btn-sm" @click="addChild">+ Add condition</button>
|
|
</div>
|
|
|
|
<div v-if="node.type === 'not'" class="scope-children">
|
|
<ScopeNodeEditor
|
|
:node="node.node"
|
|
:pctypes="pctypes"
|
|
:apps="apps"
|
|
:removable="false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
node: { type: Object, required: true },
|
|
pctypes: { type: Array, default: () => [] },
|
|
apps: { type: Array, default: () => [] },
|
|
removable: { type: Boolean, default: false }
|
|
})
|
|
|
|
defineEmits(['remove'])
|
|
|
|
function onTypeChange() {
|
|
const node = props.node
|
|
// Reset payload keys so a switched node never carries stale fields from its
|
|
// previous type. Keep only what the new type needs.
|
|
delete node.id
|
|
delete node.nodes
|
|
delete node.node
|
|
if (node.type === 'and' || node.type === 'or') {
|
|
node.nodes = [{ type: 'allpcs' }]
|
|
} else if (node.type === 'not') {
|
|
node.node = { type: 'allpcs' }
|
|
}
|
|
}
|
|
|
|
function addChild() {
|
|
props.node.nodes.push({ type: 'allpcs' })
|
|
}
|
|
|
|
function removeChild(index) {
|
|
props.node.nodes.splice(index, 1)
|
|
if (props.node.nodes.length === 0) {
|
|
props.node.nodes.push({ type: 'allpcs' })
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
export default { name: 'ScopeNodeEditor' }
|
|
</script>
|
|
|
|
<style scoped>
|
|
.scope-node { margin: 4px 0; }
|
|
.scope-node-row { display: flex; gap: 8px; align-items: center; }
|
|
.scope-type { max-width: 220px; }
|
|
.scope-children {
|
|
margin-left: 24px;
|
|
padding-left: 12px;
|
|
border-left: 2px solid var(--border-color, #444);
|
|
margin-top: 6px;
|
|
}
|
|
</style>
|