Initial
This commit is contained in:
@@ -426,9 +426,17 @@
|
||||
:error="vcenterImportError"
|
||||
:status="vcenterImportStatus"
|
||||
:diagnostics="vcenterImportDiagnostics"
|
||||
:trace-count="vcenterTraceEntries.length"
|
||||
@close="vcenterImportOpen = false"
|
||||
@preview="previewVCenterImport"
|
||||
@import="importVCenterHosts"
|
||||
@open-trace="vcenterTraceOpen = true"
|
||||
/>
|
||||
|
||||
<VCenterTraceModal
|
||||
:open="vcenterTraceOpen"
|
||||
:trace="vcenterTraceEntries"
|
||||
@close="vcenterTraceOpen = false"
|
||||
/>
|
||||
|
||||
<BaseModal
|
||||
@@ -936,6 +944,7 @@ import ScriptExplorer from './components/scripts/ScriptExplorer.vue';
|
||||
import ScriptTestRunModal from './components/scripts/ScriptTestRunModal.vue';
|
||||
import VariableEditorModal from './components/scripts/VariableEditorModal.vue';
|
||||
import VCenterImportModal from './components/hosts/VCenterImportModal.vue';
|
||||
import VCenterTraceModal from './components/hosts/VCenterTraceModal.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
import ErrorView from './views/ErrorView.vue';
|
||||
import LoginView from './views/LoginView.vue';
|
||||
@@ -982,6 +991,8 @@ const vcenterImportError = ref('');
|
||||
const vcenterPreviewRows = ref([]);
|
||||
const vcenterImportStatus = ref(null);
|
||||
const vcenterImportDiagnostics = ref(null);
|
||||
const vcenterTraceEntries = ref([]);
|
||||
const vcenterTraceOpen = ref(false);
|
||||
const credentialModalOpen = ref(false);
|
||||
const runPlanModalOpen = ref(false);
|
||||
const variableModalOpen = ref(false);
|
||||
@@ -2352,6 +2363,8 @@ async function openVCenterImport() {
|
||||
vcenterImportOpen.value = true;
|
||||
vcenterImportError.value = '';
|
||||
vcenterImportDiagnostics.value = null;
|
||||
vcenterTraceEntries.value = [];
|
||||
vcenterTraceOpen.value = false;
|
||||
try {
|
||||
vcenterImportStatus.value = await api.get('/api/hosts/import/vcenter/status');
|
||||
} catch (err) {
|
||||
@@ -2366,9 +2379,13 @@ async function previewVCenterImport(payload) {
|
||||
const result = await api.post('/api/hosts/import/vcenter/preview', payload);
|
||||
vcenterImportStatus.value = result.status;
|
||||
vcenterImportDiagnostics.value = result.diagnostics || null;
|
||||
vcenterTraceEntries.value = result.trace || [];
|
||||
vcenterPreviewRows.value = result.candidates || [];
|
||||
if (vcenterTraceEntries.value.length) vcenterTraceOpen.value = true;
|
||||
notify(`Found ${result.count || 0} vCenter host candidate(s) from ${result.diagnostics?.totalFromVCenter ?? 'unknown'} VM(s)`);
|
||||
} catch (err) {
|
||||
vcenterTraceEntries.value = err.payload?.trace || [];
|
||||
if (vcenterTraceEntries.value.length) vcenterTraceOpen.value = true;
|
||||
vcenterImportError.value = err.message;
|
||||
notify(err.message, 'error');
|
||||
} finally {
|
||||
@@ -2381,13 +2398,17 @@ async function importVCenterHosts(payload) {
|
||||
vcenterImportError.value = '';
|
||||
try {
|
||||
const result = await api.post('/api/hosts/import/vcenter', payload);
|
||||
vcenterTraceEntries.value = result.trace || vcenterTraceEntries.value;
|
||||
await refreshAll();
|
||||
vcenterPreviewRows.value = [];
|
||||
vcenterImportDiagnostics.value = null;
|
||||
vcenterTraceOpen.value = false;
|
||||
vcenterImportOpen.value = false;
|
||||
const skipped = result.skipped?.length || 0;
|
||||
notify(`Imported ${result.imported?.length || 0} host(s)${skipped ? `, skipped ${skipped} duplicate(s)` : ''}`);
|
||||
} catch (err) {
|
||||
vcenterTraceEntries.value = err.payload?.trace || [];
|
||||
if (vcenterTraceEntries.value.length) vcenterTraceOpen.value = true;
|
||||
vcenterImportError.value = err.message;
|
||||
notify(err.message, 'error');
|
||||
} finally {
|
||||
|
||||
@@ -15,7 +15,12 @@ export function createApi(getToken, onUnauthorized) {
|
||||
if (response.status === 401) onUnauthorized?.();
|
||||
if (response.status === 204) return null;
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || `Request failed: ${response.status}`);
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.error || `Request failed: ${response.status}`);
|
||||
error.status = response.status;
|
||||
error.payload = data;
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@
|
||||
<span :class="['status-dot', status.configured ? 'success' : 'warning']"></span>
|
||||
{{ status.configured ? `Connected settings for ${status.baseUrl}` : 'vCenter settings incomplete' }}
|
||||
</span>
|
||||
<button v-if="traceCount" class="ghost-button compact" type="button" @click="$emit('open-trace')">
|
||||
<FileSearch :size="15" />View API trace
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -120,6 +123,9 @@
|
||||
<span>{{ diagnostics.totalFromVCenter }} VM(s) returned by vCenter</span>
|
||||
<span>{{ diagnostics.summaryMatched }} VM(s) matched before guest enrichment</span>
|
||||
<span>{{ diagnostics.scanned }} VM(s) scanned for guest FQDN/IP details</span>
|
||||
<button v-if="traceCount" class="ghost-button compact" type="button" @click="$emit('open-trace')">
|
||||
<FileSearch :size="15" />Trace
|
||||
</button>
|
||||
<small v-if="diagnostics.sampleNames?.length">Sample inventory names: {{ diagnostics.sampleNames.join(', ') }}</small>
|
||||
</div>
|
||||
<div class="preview-toolbar">
|
||||
@@ -174,7 +180,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { CheckSquare, DownloadCloud, Search } from '@lucide/vue';
|
||||
import { CheckSquare, DownloadCloud, FileSearch, Search } from '@lucide/vue';
|
||||
import BaseModal from '../ui/BaseModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -185,10 +191,11 @@ const props = defineProps({
|
||||
loading: Boolean,
|
||||
error: { type: String, default: '' },
|
||||
status: { type: Object, default: null },
|
||||
diagnostics: { type: Object, default: null }
|
||||
diagnostics: { type: Object, default: null },
|
||||
traceCount: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'preview', 'import']);
|
||||
const emit = defineEmits(['close', 'preview', 'import', 'open-trace']);
|
||||
|
||||
const draft = reactive({
|
||||
filter: { field: 'hostname', operator: 'contains', value: 'ENG-ENT' },
|
||||
|
||||
85
client/src/components/hosts/VCenterTraceModal.vue
Normal file
85
client/src/components/hosts/VCenterTraceModal.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:open="open"
|
||||
title="vCenter API trace"
|
||||
eyebrow="REQUEST / RESPONSE"
|
||||
description="Redacted HTTP requests and responses captured during the last vCenter preview/import call."
|
||||
wide
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<section class="vcenter-trace-modal">
|
||||
<div class="trace-toolbar">
|
||||
<div>
|
||||
<strong>{{ trace.length }} captured call{{ trace.length === 1 ? '' : 's' }}</strong>
|
||||
<span>Authorization and session identifiers are redacted.</span>
|
||||
</div>
|
||||
<button class="ghost-button compact" type="button" :disabled="!trace.length" @click="copyTrace">
|
||||
<Clipboard :size="15" />Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!trace.length" class="wizard-empty">
|
||||
Run a vCenter preview to capture API request and response details.
|
||||
</div>
|
||||
|
||||
<article v-for="(entry, index) in trace" :key="index" class="trace-entry">
|
||||
<template v-if="entry.truncated">
|
||||
<div class="trace-entry-header warning">
|
||||
<AlertTriangle :size="16" />
|
||||
<strong>Trace truncated</strong>
|
||||
</div>
|
||||
<p>{{ entry.message }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="trace-entry-header">
|
||||
<span class="trace-method">{{ entry.method || 'INFO' }}</span>
|
||||
<strong :class="statusClass(entry.status)">{{ entry.status || 'network' }}</strong>
|
||||
<small>{{ entry.durationMs ?? '-' }} ms</small>
|
||||
</div>
|
||||
<code class="trace-url">{{ entry.url }}</code>
|
||||
<div class="trace-grid">
|
||||
<div>
|
||||
<h4>Request headers</h4>
|
||||
<pre>{{ format(entry.requestHeaders) }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Response summary</h4>
|
||||
<pre>{{ format(entry.responseSummary || { networkError: entry.networkError, statusText: entry.statusText }) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Response body</h4>
|
||||
<pre class="trace-body">{{ entry.responseBody || entry.networkError || 'No body returned.' }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</article>
|
||||
</section>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { AlertTriangle, Clipboard } from '@lucide/vue';
|
||||
import BaseModal from '../ui/BaseModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
open: Boolean,
|
||||
trace: { type: Array, default: () => [] }
|
||||
});
|
||||
|
||||
defineEmits(['close']);
|
||||
|
||||
function format(value) {
|
||||
return JSON.stringify(value || {}, null, 2);
|
||||
}
|
||||
|
||||
function statusClass(status) {
|
||||
if (!status) return 'status-warn';
|
||||
if (status >= 200 && status < 300) return 'status-ok';
|
||||
if (status === 404) return 'status-warn';
|
||||
return 'status-error';
|
||||
}
|
||||
|
||||
async function copyTrace() {
|
||||
await navigator.clipboard?.writeText(JSON.stringify(props.trace, null, 2));
|
||||
}
|
||||
</script>
|
||||
@@ -7074,6 +7074,141 @@ input:read-only {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vcenter-trace-modal {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
max-height: min(74vh, 820px);
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.trace-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(139, 224, 255, .14);
|
||||
border-radius: 16px;
|
||||
background: rgba(6, 12, 28, .92);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.trace-toolbar div {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.trace-toolbar strong {
|
||||
color: rgba(255, 255, 255, .9);
|
||||
}
|
||||
|
||||
.trace-toolbar span {
|
||||
color: rgba(219, 227, 255, .58);
|
||||
font-size: .78rem;
|
||||
}
|
||||
|
||||
.trace-entry {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(139, 224, 255, .12);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(88, 221, 255, .06), rgba(166, 112, 255, .035)),
|
||||
rgba(3, 8, 18, .52);
|
||||
}
|
||||
|
||||
.trace-entry-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trace-entry-header.warning {
|
||||
color: #f8c66a;
|
||||
}
|
||||
|
||||
.trace-method,
|
||||
.trace-entry-header strong,
|
||||
.trace-entry-header small {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 25px;
|
||||
padding: 4px 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(139, 224, 255, .1);
|
||||
border: 1px solid rgba(139, 224, 255, .14);
|
||||
font-size: .74rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.trace-entry-header .status-ok {
|
||||
color: #8ef5c0;
|
||||
border-color: rgba(102, 242, 181, .24);
|
||||
background: rgba(102, 242, 181, .08);
|
||||
}
|
||||
|
||||
.trace-entry-header .status-warn {
|
||||
color: #f8c66a;
|
||||
border-color: rgba(248, 198, 106, .24);
|
||||
background: rgba(248, 198, 106, .08);
|
||||
}
|
||||
|
||||
.trace-entry-header .status-error {
|
||||
color: #ff9aaa;
|
||||
border-color: rgba(255, 124, 152, .28);
|
||||
background: rgba(255, 65, 112, .08);
|
||||
}
|
||||
|
||||
.trace-url {
|
||||
display: block;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid rgba(139, 224, 255, .1);
|
||||
border-radius: 12px;
|
||||
color: #99e8ff;
|
||||
background: rgba(0, 0, 0, .22);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.trace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.trace-entry h4 {
|
||||
margin: 0 0 6px;
|
||||
color: rgba(151, 221, 255, .78);
|
||||
font-size: .68rem;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.trace-entry pre {
|
||||
margin: 0;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
padding: 11px;
|
||||
border: 1px solid rgba(139, 224, 255, .1);
|
||||
border-radius: 12px;
|
||||
color: rgba(231, 244, 255, .84);
|
||||
background: rgba(0, 0, 0, .3);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: .74rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.trace-body {
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.deployment-section-body.three,
|
||||
.deployment-section-body.two,
|
||||
@@ -7082,7 +7217,8 @@ input:read-only {
|
||||
.relationship-row,
|
||||
.intune-deployment-form .installer-analyze-row,
|
||||
.vcenter-filter-grid,
|
||||
.vcenter-options-grid {
|
||||
.vcenter-options-grid,
|
||||
.trace-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user