This commit is contained in:
2026-06-25 13:20:58 -05:00
parent 7b622fb2fd
commit ff94c0cce5
16 changed files with 968 additions and 8 deletions

View File

@@ -0,0 +1,240 @@
<template>
<BaseModal
:open="open"
title="Import hosts from vCenter"
eyebrow="VMWARE VCENTER"
description="Search vCenter virtual machines, preview discovered identity data, and create managed hosts with one shared vault credential."
wide
@close="$emit('close')"
>
<form class="vcenter-import-form" @submit.prevent="submitImport">
<section class="wizard-section">
<div class="section-heading">
<span>1</span>
<div>
<h4>Search filter</h4>
<p>Use vCenter VM names, guest hostnames, FQDNs, or IPs to narrow the import set before writing hosts.</p>
</div>
</div>
<div class="vcenter-filter-grid">
<label>
<span>Field</span>
<select v-model="draft.filter.field">
<option value="hostname">Hostname / VM name</option>
<option value="name">VM name only</option>
<option value="fqdn">Guest FQDN</option>
<option value="ip">IP address</option>
</select>
</label>
<label>
<span>Operator</span>
<select v-model="draft.filter.operator">
<option value="contains">Contains</option>
<option value="equals">Equals</option>
<option value="startsWith">Starts with</option>
<option value="endsWith">Ends with</option>
</select>
</label>
<label class="filter-value">
<span>Value</span>
<input v-model="draft.filter.value" placeholder="ENG-ENT" />
</label>
<label>
<span>Preview limit</span>
<input v-model.number="draft.limit" type="number" min="1" max="500" />
</label>
</div>
<div class="wizard-actions">
<button class="ghost-button" type="button" :disabled="loading" @click="emitPreview">
<Search :size="16" />Preview matches
</button>
<span v-if="status" class="vcenter-status">
<span :class="['status-dot', status.configured ? 'success' : 'warning']"></span>
{{ status.configured ? `Connected settings for ${status.baseUrl}` : 'vCenter settings incomplete' }}
</span>
</div>
</section>
<section class="wizard-section">
<div class="section-heading">
<span>2</span>
<div>
<h4>Import options</h4>
<p>Attach authentication and host defaults once so every created host is ready for RunPlans.</p>
</div>
</div>
<div class="vcenter-options-grid">
<label>
<span>Vault credential</span>
<select v-model="draft.credentialId">
<option :value="null">No credential</option>
<option v-for="credential in credentials" :key="credential.id" :value="credential.id">{{ credential.name }}</option>
</select>
</label>
<label>
<span>Transport</span>
<select v-model="draft.transport">
<option value="winrm">WinRM</option>
<option value="ssh">SSH</option>
<option value="local">Local</option>
<option value="api">API</option>
</select>
</label>
<label>
<span>Port</span>
<input v-model.number="draft.port" type="number" min="1" max="65535" placeholder="Default" />
</label>
<label>
<span>Visibility</span>
<select v-model="draft.visibility">
<option value="shared">Shared</option>
<option value="personal">Personal</option>
<option value="group">Group</option>
</select>
</label>
<label v-if="draft.visibility === 'group'">
<span>Group</span>
<select v-model="draft.groupId">
<option :value="null">Choose group</option>
<option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option>
</select>
</label>
<label class="option-tags">
<span>Additional tags</span>
<input v-model="draft.tagsText" placeholder="engineering, intune-ready" />
</label>
</div>
</section>
<section class="wizard-section preview-section">
<div class="section-heading">
<span>3</span>
<div>
<h4>Preview and select</h4>
<p>Guest FQDN/IP data depends on VMware Tools. Missing values are imported with the VM name as the address fallback.</p>
</div>
</div>
<div v-if="error" class="inline-error">{{ error }}</div>
<div class="preview-toolbar">
<button class="ghost-button compact" type="button" :disabled="!previewRows.length" @click="toggleAll">
<CheckSquare :size="15" />{{ allSelected ? 'Clear selection' : 'Select all' }}
</button>
<span>{{ selectedIds.length }} of {{ previewRows.length }} selected</span>
</div>
<div class="vcenter-preview-table">
<table>
<thead>
<tr>
<th>Select</th>
<th>VM / Hostname</th>
<th>FQDN</th>
<th>Address</th>
<th>OS</th>
<th>Power</th>
</tr>
</thead>
<tbody>
<tr v-for="row in previewRows" :key="row.id">
<td>
<input type="checkbox" :checked="selectedIds.includes(row.id)" @change="toggleRow(row.id)" />
</td>
<td>
<strong>{{ row.name }}</strong>
<small>{{ row.id }}</small>
</td>
<td><code>{{ row.fqdn || 'Unavailable' }}</code></td>
<td><code>{{ row.address || row.ipAddress || 'Unavailable' }}</code></td>
<td><span class="status-pill">{{ row.osFamily }}</span></td>
<td>{{ row.powerState || '-' }}</td>
</tr>
</tbody>
</table>
<div v-if="!previewRows.length" class="wizard-empty">
Run a preview to see matching virtual machines before importing hosts.
</div>
</div>
</section>
<div class="form-actions modal-actions">
<button class="ghost-button" type="button" @click="$emit('close')">Cancel</button>
<button class="primary-action" type="submit" :disabled="loading || !selectedIds.length">
<DownloadCloud :size="17" />Import {{ selectedIds.length || '' }} host{{ selectedIds.length === 1 ? '' : 's' }}
</button>
</div>
</form>
</BaseModal>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue';
import { CheckSquare, DownloadCloud, Search } from '@lucide/vue';
import BaseModal from '../ui/BaseModal.vue';
const props = defineProps({
open: Boolean,
previewRows: { type: Array, default: () => [] },
credentials: { type: Array, default: () => [] },
groups: { type: Array, default: () => [] },
loading: Boolean,
error: { type: String, default: '' },
status: { type: Object, default: null }
});
const emit = defineEmits(['close', 'preview', 'import']);
const draft = reactive({
filter: { field: 'hostname', operator: 'contains', value: 'ENG-ENT' },
limit: 100,
credentialId: null,
transport: 'winrm',
port: null,
visibility: 'shared',
groupId: null,
tagsText: ''
});
const selectedIds = ref([]);
const allSelected = computed(() => props.previewRows.length > 0 && selectedIds.value.length === props.previewRows.length);
watch(() => props.previewRows, (rows) => {
selectedIds.value = rows.map((row) => row.id);
});
watch(() => props.open, (open) => {
if (open && props.previewRows.length) selectedIds.value = props.previewRows.map((row) => row.id);
});
function payloadBase() {
return {
filter: { ...draft.filter, value: draft.filter.value.trim() },
limit: Number(draft.limit) || 100
};
}
function emitPreview() {
emit('preview', payloadBase());
}
function toggleRow(id) {
selectedIds.value = selectedIds.value.includes(id)
? selectedIds.value.filter((selectedId) => selectedId !== id)
: [...selectedIds.value, id];
}
function toggleAll() {
selectedIds.value = allSelected.value ? [] : props.previewRows.map((row) => row.id);
}
function submitImport() {
emit('import', {
...payloadBase(),
vmIds: selectedIds.value,
credentialId: draft.credentialId || null,
transport: draft.transport,
port: draft.port ? Number(draft.port) : null,
visibility: draft.visibility,
groupId: draft.visibility === 'group' ? draft.groupId : null,
tags: draft.tagsText.split(',').map((tag) => tag.trim()).filter(Boolean)
});
}
</script>

View File

@@ -29,7 +29,7 @@
<strong>{{ normalizedBooleanSetting(row.value) ? 'Enabled' : 'Disabled' }}</strong>
</button>
<textarea v-else-if="key === 'trusted_origins'" v-model="row.value" class="settings-input settings-textarea" :placeholder="placeholderFor(key)"></textarea>
<input v-else v-model="row.value" class="settings-input" :placeholder="placeholderFor(key)" />
<input v-else v-model="row.value" :type="isSecretSetting(key) ? 'password' : 'text'" class="settings-input" :placeholder="placeholderFor(key)" />
</div>
<span :class="['setting-source', row.source]">{{ sourceLabel(row.source) }}</span>
</div>
@@ -48,6 +48,7 @@ defineProps({
labelFor: { type: Function, required: true },
descriptionFor: { type: Function, required: true },
placeholderFor: { type: Function, required: true },
isSecretSetting: { type: Function, default: () => false },
sourceLabel: { type: Function, required: true },
isBooleanSetting: { type: Function, required: true },
normalizedBooleanSetting: { type: Function, required: true }