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.
266 lines
8.3 KiB
Vue
266 lines
8.3 KiB
Vue
<template>
|
|
<div class="action-picker" @keydown.esc="closeDropdown">
|
|
<input
|
|
ref="inputEl"
|
|
type="text"
|
|
class="form-control"
|
|
:placeholder="placeholder"
|
|
v-model="query"
|
|
@focus="openDropdown"
|
|
@input="openDropdown"
|
|
/>
|
|
|
|
<div v-if="open" class="action-dropdown">
|
|
<!-- Command level: commands + a Monitor branch entry -->
|
|
<template v-if="level === 'command'">
|
|
<div
|
|
v-for="opt in filteredCommands"
|
|
:key="opt.action"
|
|
class="action-option"
|
|
@mousedown.prevent="selectCommand(opt)"
|
|
>
|
|
{{ opt.label || opt.action }}
|
|
</div>
|
|
<div
|
|
class="action-option action-branch"
|
|
@mousedown.prevent="enterMonitorLevel"
|
|
>
|
|
Monitor...
|
|
</div>
|
|
<div v-if="filteredCommands.length === 0" class="action-empty">No matching actions</div>
|
|
</template>
|
|
|
|
<!-- Monitor level: two create entries pinned on top, then existing monitors -->
|
|
<template v-else-if="level === 'monitor'">
|
|
<div class="action-option action-create" @mousedown.prevent="startCreate('process')">
|
|
<Create Process Monitor>
|
|
</div>
|
|
<div class="action-option action-create" @mousedown.prevent="startCreate('service')">
|
|
<Create Service Monitor>
|
|
</div>
|
|
<div class="action-divider"></div>
|
|
<div
|
|
v-for="m in filteredMonitors"
|
|
:key="m.endpointmonitorid"
|
|
class="action-option"
|
|
@mousedown.prevent="selectMonitor(m)"
|
|
>
|
|
{{ m.displayname }} <span class="monitor-kind">({{ m.kind }})</span>
|
|
</div>
|
|
<div v-if="filteredMonitors.length === 0" class="action-empty">No matching monitors</div>
|
|
</template>
|
|
|
|
<!-- Create level: inline text field to name the new process/service monitor -->
|
|
<template v-else>
|
|
<div class="action-create-form" @mousedown.prevent>
|
|
<label class="action-create-label">
|
|
{{ createkind === 'service' ? 'Service name to monitor' : 'Process name to monitor' }}
|
|
</label>
|
|
<input
|
|
ref="createInputEl"
|
|
type="text"
|
|
class="form-control"
|
|
:placeholder="createkind === 'service' ? 'e.g. Spooler' : 'e.g. agent.exe'"
|
|
v-model="createname"
|
|
@keydown.enter.prevent="submitCreate"
|
|
@keydown.esc.prevent="cancelCreate"
|
|
/>
|
|
<template v-if="createkind === 'process'">
|
|
<label class="action-create-label">
|
|
Launch path (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="e.g. C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
|
v-model="createlaunchpath"
|
|
@keydown.enter.prevent="submitCreate"
|
|
@keydown.esc.prevent="cancelCreate"
|
|
/>
|
|
</template>
|
|
<div class="action-create-actions">
|
|
<button type="button" class="btn btn-primary btn-sm" :disabled="!createname.trim() || creating" @mousedown.prevent="submitCreate">
|
|
Create
|
|
</button>
|
|
<button type="button" class="btn btn-secondary btn-sm" @mousedown.prevent="cancelCreate">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, nextTick } from 'vue'
|
|
import { endpointsApi } from '@/api'
|
|
import { apiError } from '@/utils/apiError'
|
|
import { useToast } from '@/composables/toast'
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: String, default: '' },
|
|
commandActions: { type: Array, default: () => [] },
|
|
monitors: { type: Array, default: () => [] }
|
|
})
|
|
const emit = defineEmits(['update:modelValue', 'monitor-created'])
|
|
|
|
const toast = useToast()
|
|
|
|
const inputEl = ref(null)
|
|
const createInputEl = ref(null)
|
|
const open = ref(false)
|
|
// 'command' -> top level, 'monitor' -> monitor list, 'create' -> inline name form
|
|
const level = ref('command')
|
|
const query = ref('')
|
|
const createkind = ref('process')
|
|
const createname = ref('')
|
|
const createlaunchpath = ref('')
|
|
const creating = ref(false)
|
|
|
|
// Show the chosen action's friendly label in the input when not actively typing.
|
|
watch(() => props.modelValue, syncLabel, { immediate: true })
|
|
watch(() => props.monitors, () => { if (!open.value) syncLabel() })
|
|
|
|
function syncLabel() {
|
|
query.value = labelFor(props.modelValue)
|
|
}
|
|
|
|
function labelFor(action) {
|
|
if (!action) return ''
|
|
if (action.startsWith('monitor:')) {
|
|
const id = Number(action.slice('monitor:'.length))
|
|
const monitor = props.monitors.find(m => m.endpointmonitorid === id)
|
|
return monitor ? `Monitor: ${monitor.displayname} (${monitor.kind})` : action
|
|
}
|
|
const command = props.commandActions.find(c => c.action === action)
|
|
return command ? (command.label || command.action) : action
|
|
}
|
|
|
|
const placeholder = computed(() =>
|
|
level.value === 'monitor' ? 'Search monitors...' : 'Search actions...')
|
|
|
|
const filteredCommands = computed(() => {
|
|
const term = query.value.trim().toLowerCase()
|
|
if (!term) return props.commandActions
|
|
return props.commandActions.filter(c =>
|
|
(c.label || c.action).toLowerCase().includes(term))
|
|
})
|
|
|
|
const filteredMonitors = computed(() => {
|
|
const term = query.value.trim().toLowerCase()
|
|
if (!term) return props.monitors
|
|
return props.monitors.filter(m => m.displayname.toLowerCase().includes(term))
|
|
})
|
|
|
|
function openDropdown() {
|
|
open.value = true
|
|
}
|
|
|
|
function closeDropdown() {
|
|
open.value = false
|
|
level.value = 'command'
|
|
resetCreate()
|
|
syncLabel()
|
|
}
|
|
|
|
function enterMonitorLevel() {
|
|
level.value = 'monitor'
|
|
query.value = ''
|
|
inputEl.value?.focus()
|
|
}
|
|
|
|
function selectCommand(opt) {
|
|
emit('update:modelValue', opt.action)
|
|
open.value = false
|
|
level.value = 'command'
|
|
query.value = opt.label || opt.action
|
|
}
|
|
|
|
function selectMonitor(monitor) {
|
|
emit('update:modelValue', `monitor:${monitor.endpointmonitorid}`)
|
|
open.value = false
|
|
level.value = 'command'
|
|
query.value = `Monitor: ${monitor.displayname} (${monitor.kind})`
|
|
}
|
|
|
|
async function submitCreate() {
|
|
const displayname = createname.value.trim()
|
|
if (!displayname || creating.value) return
|
|
const kind = createkind.value
|
|
const payload = {
|
|
kind,
|
|
displayname,
|
|
processname: kind === 'process' ? displayname : '',
|
|
servicename: kind === 'service' ? displayname : '',
|
|
isactive: true
|
|
}
|
|
if (kind === 'process' && createlaunchpath.value.trim()) {
|
|
payload.launchpath = createlaunchpath.value.trim()
|
|
}
|
|
creating.value = true
|
|
try {
|
|
const response = await endpointsApi.monitors.create(payload)
|
|
const created = response.data.data
|
|
emit('monitor-created', created)
|
|
emit('update:modelValue', `monitor:${created.endpointmonitorid}`)
|
|
open.value = false
|
|
level.value = 'command'
|
|
resetCreate()
|
|
query.value = `Monitor: ${created.displayname} (${created.kind})`
|
|
} catch (err) {
|
|
toast.error(apiError(err, 'Failed to create monitor'))
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
function startCreate(kind) {
|
|
createkind.value = kind
|
|
createname.value = ''
|
|
createlaunchpath.value = ''
|
|
level.value = 'create'
|
|
nextTick(() => createInputEl.value?.focus())
|
|
}
|
|
|
|
function resetCreate() {
|
|
createname.value = ''
|
|
createlaunchpath.value = ''
|
|
creating.value = false
|
|
}
|
|
|
|
function cancelCreate() {
|
|
resetCreate()
|
|
level.value = 'monitor'
|
|
nextTick(() => inputEl.value?.focus())
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.action-picker { position: relative; flex: 1; min-width: 220px; }
|
|
.action-dropdown {
|
|
position: absolute;
|
|
z-index: 20;
|
|
left: 0;
|
|
right: 0;
|
|
margin-top: 2px;
|
|
max-height: 320px;
|
|
overflow-y: auto;
|
|
background: var(--bg-card-solid);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
|
|
}
|
|
.action-option { padding: 8px 12px; cursor: pointer; color: var(--text); }
|
|
.action-option:hover { background: var(--bg); }
|
|
.action-branch { font-style: italic; }
|
|
.action-create { font-weight: 600; color: var(--primary); }
|
|
.action-divider { height: 1px; background: var(--border); margin: 2px 0; }
|
|
.action-empty { padding: 6px 10px; color: var(--text-light); }
|
|
.monitor-kind { color: var(--text-light); }
|
|
.action-create-form { padding: 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
.action-create-label { font-size: 12px; color: var(--text-light); }
|
|
.action-create-actions { display: flex; gap: 8px; }
|
|
</style>
|