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>
|