Initial
This commit is contained in:
240
client/src/components/hosts/VCenterImportModal.vue
Normal file
240
client/src/components/hosts/VCenterImportModal.vue
Normal 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>
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user