Initial
This commit is contained in:
@@ -213,6 +213,7 @@
|
||||
:open="scriptTestModalOpen"
|
||||
:script="scriptTestTarget"
|
||||
:hosts="hosts"
|
||||
:host-groups="hostGroups"
|
||||
:credentials="credentials"
|
||||
:job="scriptTestJob"
|
||||
:running="scriptTestRunning"
|
||||
@@ -331,10 +332,21 @@
|
||||
<template #cell-name="{ row }"><strong>{{ row.name }}</strong></template>
|
||||
<template #cell-address="{ row }"><code>{{ row.address }}</code></template>
|
||||
<template #cell-transport="{ row }"><span :class="['status-pill', row.transport]">{{ row.transport }}</span></template>
|
||||
<template #cell-sourceType="{ row }"><span :class="['status-pill', row.sourceType]">{{ row.sourceType === 'vmware' ? 'VMware' : 'Manual' }}</span></template>
|
||||
<template #cell-credentialName="{ row }">{{ row.credentialName || 'No credential' }}</template>
|
||||
<template #cell-tags="{ row }"><span class="tag-row">{{ row.tags?.join(', ') || '-' }}</span></template>
|
||||
<template #cell-actions="{ row }"><button class="ghost-button compact" type="button" @click="openHostModal(row)">Edit</button></template>
|
||||
</ResourceTable>
|
||||
|
||||
<HostGroupsPanel
|
||||
:host-groups="hostGroups"
|
||||
:hosts="hosts"
|
||||
:groups="groups"
|
||||
@save="saveHostGroup"
|
||||
@delete="deleteHostGroup"
|
||||
@sync="syncHostGroup"
|
||||
@sync-all="syncHostGroups"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="view === 'credentials'" class="resource-page">
|
||||
@@ -406,7 +418,7 @@
|
||||
<template #cell-name="{ row }"><strong>{{ row.name }}</strong></template>
|
||||
<template #cell-scriptName="{ row }">{{ row.scriptName || 'No script selected' }}</template>
|
||||
<template #cell-visibility="{ row }"><span :class="['status-pill', row.visibility]">{{ row.visibility }}</span></template>
|
||||
<template #cell-hostCount="{ row }">{{ row.hostCount || row.hostIds?.length || 0 }} host(s)</template>
|
||||
<template #cell-hostCount="{ row }">{{ row.hostCount || row.hostIds?.length || 0 }} host(s)<span v-if="row.hostGroupCount"> · {{ row.hostGroupCount }} group(s)</span></template>
|
||||
<template #cell-mode="{ row }">{{ row.parallel ? 'Parallel' : 'Serial' }}</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="table-actions">
|
||||
@@ -422,6 +434,7 @@
|
||||
:preview-rows="vcenterPreviewRows"
|
||||
:credentials="credentials"
|
||||
:groups="groups"
|
||||
:connections="vcenterConnections"
|
||||
:loading="vcenterImportLoading"
|
||||
:error="vcenterImportError"
|
||||
:status="vcenterImportStatus"
|
||||
@@ -498,6 +511,15 @@
|
||||
<label v-if="runPlanForm.visibility === 'group'"><span>Group</span><select v-model="runPlanForm.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="toggle modal-toggle"><input v-model="runPlanForm.parallel" type="checkbox" /><span><strong>Parallel execution</strong><small>Run against selected hosts concurrently.</small></span></label>
|
||||
<label class="full"><span>Description</span><textarea v-model="runPlanForm.description" rows="3" placeholder="Purpose, operator notes, approval context"></textarea></label>
|
||||
<div class="full modal-host-picker">
|
||||
<div class="list-header"><span>Target host groups</span><strong>{{ runPlanForm.hostGroupIds.length }} selected</strong></div>
|
||||
<div class="host-picker">
|
||||
<label v-for="hostGroup in hostGroups" :key="hostGroup.id">
|
||||
<input v-model="runPlanForm.hostGroupIds" type="checkbox" :value="hostGroup.id" />
|
||||
<span><strong>{{ hostGroup.name }}</strong><small>{{ hostGroup.mode }} · {{ hostGroup.memberCount || 0 }} host(s)</small></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="full modal-host-picker">
|
||||
<div class="list-header"><span>Target hosts</span><strong>{{ runPlanForm.hostIds.length }} selected</strong></div>
|
||||
<div class="host-picker">
|
||||
@@ -585,6 +607,14 @@
|
||||
@test="testGraphConnection"
|
||||
/>
|
||||
|
||||
<VCenterConnectionsConfig
|
||||
:connections="vcenterConnections"
|
||||
:groups="groups"
|
||||
@save="saveVCenterConnection"
|
||||
@delete="deleteVCenterConnection"
|
||||
@test="testVCenterConnection"
|
||||
/>
|
||||
|
||||
<article :class="['glass-panel form-panel settings-layout', { 'panel-collapsed': panelCollapsed.applicationConfig }]">
|
||||
<div class="section-title"><h3>Application Config</h3><div class="section-actions"><SlidersHorizontal :size="18" /><button class="panel-toggle" type="button" @click="togglePanel('applicationConfig')"><component :is="panelCollapsed.applicationConfig ? ChevronDown : ChevronUp" :size="14" /></button></div></div>
|
||||
<p class="panel-intro">Database settings are editable here; environment-sourced values are shown so Docker configuration stays visible.</p>
|
||||
@@ -936,6 +966,7 @@ import AppSidebar from './components/AppSidebar.vue';
|
||||
import AppTopbar from './components/AppTopbar.vue';
|
||||
import ConfigSection from './components/settings/ConfigSection.vue';
|
||||
import IntuneGraphConfig from './components/settings/IntuneGraphConfig.vue';
|
||||
import VCenterConnectionsConfig from './components/settings/VCenterConnectionsConfig.vue';
|
||||
import HelpCenter from './components/help/HelpCenter.vue';
|
||||
import ParticleBackdrop from './components/ParticleBackdrop.vue';
|
||||
import BaseModal from './components/ui/BaseModal.vue';
|
||||
@@ -943,6 +974,7 @@ import ResourceTable from './components/ui/ResourceTable.vue';
|
||||
import ScriptExplorer from './components/scripts/ScriptExplorer.vue';
|
||||
import ScriptTestRunModal from './components/scripts/ScriptTestRunModal.vue';
|
||||
import VariableEditorModal from './components/scripts/VariableEditorModal.vue';
|
||||
import HostGroupsPanel from './components/hosts/HostGroupsPanel.vue';
|
||||
import VCenterImportModal from './components/hosts/VCenterImportModal.vue';
|
||||
import VCenterTraceModal from './components/hosts/VCenterTraceModal.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
@@ -1112,6 +1144,12 @@ const settingMetadata = {
|
||||
description: 'Master safety switch that controls whether RunPlans can execute scripts on target hosts.',
|
||||
section: 'Execution'
|
||||
},
|
||||
host_group_sync_interval_minutes: {
|
||||
label: 'Host Group Sync Interval',
|
||||
description: 'Minutes between background sync runs for dynamic host groups. Set 0 to disable the scheduler.',
|
||||
placeholder: '60',
|
||||
section: 'Execution'
|
||||
},
|
||||
vcenter_enabled: {
|
||||
label: 'VMware vCenter',
|
||||
description: 'Enable VM discovery and import through the VMware vCenter REST API.',
|
||||
@@ -1158,6 +1196,7 @@ const users = ref([]);
|
||||
const groups = ref([]);
|
||||
const credentials = ref([]);
|
||||
const hosts = ref([]);
|
||||
const hostGroups = ref([]);
|
||||
const folders = ref([]);
|
||||
const scripts = ref([]);
|
||||
const assetFolders = ref([]);
|
||||
@@ -1172,6 +1211,7 @@ const catalogApplications = ref([]);
|
||||
const changeRequests = ref([]);
|
||||
const reportingOverview = ref(null);
|
||||
const graphConnections = ref([]);
|
||||
const vcenterConnections = ref([]);
|
||||
const graphStatusResult = ref(null);
|
||||
const graphAuditResult = ref([]);
|
||||
const driftResult = ref(null);
|
||||
@@ -1198,9 +1238,9 @@ const loginForm = reactive({ email: 'admin@posh.local', password: 'change-me-now
|
||||
const scriptDraft = reactive({ id: null, folderId: null, name: '', description: '', content: '', visibility: 'personal', groupId: null });
|
||||
const scriptRenameForm = reactive({ name: '' });
|
||||
const scriptCloneForm = reactive({ name: '' });
|
||||
const hostForm = reactive({ id: null, name: '', address: '', fqdn: '', osFamily: 'windows', transport: 'winrm', credentialId: null, tagsText: '', visibility: 'shared', groupId: null });
|
||||
const hostForm = reactive({ id: null, name: '', address: '', fqdn: '', osFamily: 'windows', transport: 'winrm', credentialId: null, tagsText: '', visibility: 'shared', groupId: null, sourceType: 'manual', sourceConnectionId: null, sourceRef: '' });
|
||||
const credentialForm = reactive({ id: null, name: '', kind: 'username_password', username: '', secret: '', visibility: 'personal', groupId: null });
|
||||
const runPlanForm = reactive({ id: null, name: '', description: '', scriptId: '', visibility: 'personal', groupId: null, parallel: true, hostIds: [] });
|
||||
const runPlanForm = reactive({ id: null, name: '', description: '', scriptId: '', visibility: 'personal', groupId: null, parallel: true, hostIds: [], hostGroupIds: [] });
|
||||
const groupForm = reactive({ name: '', description: '' });
|
||||
const userForm = reactive({ email: '', displayName: '', password: '', role: 'user', groupIds: [] });
|
||||
const profileForm = reactive({ displayName: '', email: '', jobTitle: '', phone: '', timezone: '', avatarUrl: '' });
|
||||
@@ -1286,6 +1326,7 @@ const hostColumns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'address', label: 'Address' },
|
||||
{ key: 'transport', label: 'Transport' },
|
||||
{ key: 'sourceType', label: 'Source' },
|
||||
{ key: 'credentialName', label: 'Credential' },
|
||||
{ key: 'tags', label: 'Tags', sortable: false },
|
||||
{ key: 'actions', label: 'Actions', sortable: false }
|
||||
@@ -1428,6 +1469,8 @@ const filteredHosts = computed(() => filterRows(hosts.value, hostFilters, (host)
|
||||
host.address,
|
||||
host.fqdn,
|
||||
host.transport,
|
||||
host.sourceType,
|
||||
host.sourceConnectionName,
|
||||
host.credentialName,
|
||||
host.tags?.join(' ')
|
||||
]));
|
||||
@@ -1618,21 +1661,51 @@ async function safeRefresh(label, promise, fallback, failures, quiet = false) {
|
||||
return await promise;
|
||||
} catch (err) {
|
||||
console.warn(`POSHManager refresh failed for ${label}`, err);
|
||||
if (!quiet) failures.push(`${label}: ${err.message || 'request failed'}`);
|
||||
if (!quiet) failures.push(normalizeRefreshFailure(label, err));
|
||||
return typeof fallback === 'function' ? fallback() : fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRefreshFailure(label, err) {
|
||||
const message = err?.message || 'request failed';
|
||||
return {
|
||||
label,
|
||||
message,
|
||||
path: err?.path || '',
|
||||
status: err?.status || null,
|
||||
network: !err?.status
|
||||
};
|
||||
}
|
||||
|
||||
function formatRefreshFailures(failures) {
|
||||
const visibleFailures = failures.filter(Boolean);
|
||||
if (!visibleFailures.length) return '';
|
||||
const networkFailures = visibleFailures.filter((failure) => failure.network);
|
||||
if (networkFailures.length === visibleFailures.length) {
|
||||
const first = networkFailures[0];
|
||||
return `API is unreachable (${first.message}). Using last known data where available.`;
|
||||
}
|
||||
return `Some data did not refresh: ${visibleFailures.slice(0, 2).map((failure) => `${failure.label}: ${failure.message}`).join('; ')}`;
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
// Bootstrap all API-backed view state together so navigation does not trigger partial reload races.
|
||||
try {
|
||||
await api.get('/api/health');
|
||||
} catch (err) {
|
||||
console.warn('POSHManager refresh skipped because the API health probe failed', err);
|
||||
notify(`API is unreachable (${err.message || 'health probe failed'}). Using last known data where available.`);
|
||||
return;
|
||||
}
|
||||
const refreshFailures = [];
|
||||
const [meResult, boot, userRows, groupRows, credentialRows, hostRows, folderRows, scriptRows, assetFolderRows, assetRows, variableRows, psadtCatalogRows, psadtProfileRows, psadtIntuneRows, graphRows, runplanRows, jobRows] = await Promise.all([
|
||||
api.get('/api/auth/me'),
|
||||
const [meResult, boot, userRows, groupRows, credentialRows, hostRows, hostGroupRows, folderRows, scriptRows, assetFolderRows, assetRows, variableRows, psadtCatalogRows, psadtProfileRows, psadtIntuneRows, graphRows, vcenterRows, runplanRows, jobRows] = await Promise.all([
|
||||
safeRefresh('session', api.get('/api/auth/me'), () => ({ user: user.value }), refreshFailures),
|
||||
safeRefresh('bootstrap', api.get('/api/bootstrap'), () => ({ summary: summary.value, settings: settings.value }), refreshFailures),
|
||||
safeRefresh('users', api.get('/api/users'), () => users.value, refreshFailures, true),
|
||||
safeRefresh('groups', api.get('/api/groups'), () => groups.value, refreshFailures),
|
||||
safeRefresh('credentials', api.get('/api/credentials'), () => credentials.value, refreshFailures),
|
||||
safeRefresh('hosts', api.get('/api/hosts'), () => hosts.value, refreshFailures),
|
||||
safeRefresh('host groups', api.get('/api/hosts/groups'), () => hostGroups.value, refreshFailures),
|
||||
safeRefresh('folders', api.get('/api/folders'), () => folders.value, refreshFailures),
|
||||
safeRefresh('scripts', api.get('/api/scripts'), () => scripts.value, refreshFailures),
|
||||
safeRefresh('asset folders', api.get('/api/assets/folders'), () => assetFolders.value, refreshFailures),
|
||||
@@ -1642,6 +1715,7 @@ async function refreshAll() {
|
||||
safeRefresh('PSADT profiles', api.get('/api/psadt/profiles'), () => psadtProfiles.value, refreshFailures),
|
||||
safeRefresh('Intune deployments', api.get('/api/psadt/intune/deployments'), () => psadtIntuneDeployments.value, refreshFailures),
|
||||
safeRefresh('Graph connections', api.get('/api/graph/connections'), () => graphConnections.value, refreshFailures),
|
||||
safeRefresh('vCenter connections', api.get('/api/hosts/import/vcenter/connections'), () => vcenterConnections.value, refreshFailures),
|
||||
safeRefresh('RunPlans', api.get('/api/runplans'), () => runplans.value, refreshFailures),
|
||||
safeRefresh('jobs', api.get('/api/jobs'), () => jobs.value, refreshFailures)
|
||||
]);
|
||||
@@ -1654,6 +1728,7 @@ async function refreshAll() {
|
||||
groups.value = groupRows;
|
||||
credentials.value = credentialRows;
|
||||
hosts.value = hostRows;
|
||||
hostGroups.value = hostGroupRows;
|
||||
folders.value = folderRows;
|
||||
scripts.value = scriptRows;
|
||||
assetFolders.value = assetFolderRows;
|
||||
@@ -1663,6 +1738,7 @@ async function refreshAll() {
|
||||
psadtProfiles.value = psadtProfileRows;
|
||||
psadtIntuneDeployments.value = psadtIntuneRows;
|
||||
graphConnections.value = graphRows;
|
||||
vcenterConnections.value = vcenterRows;
|
||||
runplans.value = runplanRows;
|
||||
jobs.value = jobRows;
|
||||
if (selectedScript.value) {
|
||||
@@ -1671,7 +1747,8 @@ async function refreshAll() {
|
||||
} else if (scriptRows.length) {
|
||||
selectScript(scriptRows[0]);
|
||||
}
|
||||
if (refreshFailures.length) notify(`Some data did not refresh: ${refreshFailures.slice(0, 2).join('; ')}`);
|
||||
const refreshMessage = formatRefreshFailures(refreshFailures);
|
||||
if (refreshMessage) notify(refreshMessage);
|
||||
}
|
||||
|
||||
async function openHelpCenter() {
|
||||
@@ -2237,6 +2314,28 @@ async function testGraphConnection(id) {
|
||||
notify(result.message || 'Graph connection test completed');
|
||||
}
|
||||
|
||||
async function saveVCenterConnection(payload) {
|
||||
const body = { ...payload };
|
||||
const connectionId = body.id;
|
||||
delete body.id;
|
||||
if (connectionId) await api.put(`/api/hosts/import/vcenter/connections/${connectionId}`, body);
|
||||
else await api.post('/api/hosts/import/vcenter/connections', body);
|
||||
vcenterConnections.value = await api.get('/api/hosts/import/vcenter/connections');
|
||||
notify('vCenter system saved');
|
||||
}
|
||||
|
||||
async function deleteVCenterConnection(id) {
|
||||
await api.delete(`/api/hosts/import/vcenter/connections/${id}`);
|
||||
vcenterConnections.value = await api.get('/api/hosts/import/vcenter/connections');
|
||||
notify('vCenter system deleted');
|
||||
}
|
||||
|
||||
async function testVCenterConnection(id) {
|
||||
const result = await api.post(`/api/hosts/import/vcenter/connections/${id}/test`, {});
|
||||
vcenterConnections.value = await api.get('/api/hosts/import/vcenter/connections');
|
||||
notify(result.message || 'vCenter connection test completed');
|
||||
}
|
||||
|
||||
async function linkGraphApp(payload) {
|
||||
const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/link`, {
|
||||
connectionId: payload.connectionId,
|
||||
@@ -2371,6 +2470,35 @@ async function saveHost() {
|
||||
notify('Host saved');
|
||||
}
|
||||
|
||||
async function saveHostGroup(payload) {
|
||||
const body = { ...payload };
|
||||
const groupId = body.id;
|
||||
delete body.id;
|
||||
if (groupId) await api.put(`/api/hosts/groups/${groupId}`, body);
|
||||
else await api.post('/api/hosts/groups', body);
|
||||
hostGroups.value = await api.get('/api/hosts/groups');
|
||||
notify('Host group saved');
|
||||
}
|
||||
|
||||
async function deleteHostGroup(groupId) {
|
||||
await api.delete(`/api/hosts/groups/${groupId}`);
|
||||
hostGroups.value = await api.get('/api/hosts/groups');
|
||||
await refreshAll();
|
||||
notify('Host group deleted');
|
||||
}
|
||||
|
||||
async function syncHostGroup(groupId) {
|
||||
await api.post(`/api/hosts/groups/${groupId}/sync`, {});
|
||||
hostGroups.value = await api.get('/api/hosts/groups');
|
||||
notify('Host group synced');
|
||||
}
|
||||
|
||||
async function syncHostGroups() {
|
||||
const result = await api.post('/api/hosts/groups/sync', {});
|
||||
hostGroups.value = await api.get('/api/hosts/groups');
|
||||
notify(`Dynamic host groups synced: ${result.synced || 0}`);
|
||||
}
|
||||
|
||||
async function openVCenterImport() {
|
||||
vcenterImportOpen.value = true;
|
||||
vcenterImportError.value = '';
|
||||
@@ -2379,6 +2507,7 @@ async function openVCenterImport() {
|
||||
vcenterTraceOpen.value = false;
|
||||
try {
|
||||
vcenterImportStatus.value = await api.get('/api/hosts/import/vcenter/status');
|
||||
vcenterConnections.value = vcenterImportStatus.value.connections || vcenterConnections.value;
|
||||
} catch (err) {
|
||||
vcenterImportError.value = err.message;
|
||||
}
|
||||
@@ -2488,7 +2617,7 @@ async function importVCenterHosts(payload) {
|
||||
}
|
||||
|
||||
function resetHostForm() {
|
||||
Object.assign(hostForm, { id: null, name: '', address: '', fqdn: '', osFamily: 'windows', transport: 'winrm', credentialId: null, tagsText: '', visibility: 'shared', groupId: null });
|
||||
Object.assign(hostForm, { id: null, name: '', address: '', fqdn: '', osFamily: 'windows', transport: 'winrm', credentialId: null, tagsText: '', visibility: 'shared', groupId: null, sourceType: 'manual', sourceConnectionId: null, sourceRef: '' });
|
||||
}
|
||||
|
||||
function openHostModal(host = null) {
|
||||
@@ -2529,11 +2658,11 @@ async function saveRunPlan() {
|
||||
}
|
||||
|
||||
function resetRunPlanForm() {
|
||||
Object.assign(runPlanForm, { id: null, name: '', description: '', scriptId: '', visibility: 'personal', groupId: null, parallel: true, hostIds: [] });
|
||||
Object.assign(runPlanForm, { id: null, name: '', description: '', scriptId: '', visibility: 'personal', groupId: null, parallel: true, hostIds: [], hostGroupIds: [] });
|
||||
}
|
||||
|
||||
function openRunPlanModal(runplan = null) {
|
||||
if (runplan) Object.assign(runPlanForm, { ...runplan, hostIds: runplan.hostIds || [] });
|
||||
if (runplan) Object.assign(runPlanForm, { ...runplan, hostIds: runplan.hostIds || [], hostGroupIds: runplan.hostGroupIds || [] });
|
||||
else resetRunPlanForm();
|
||||
runPlanModalOpen.value = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user