Initial
This commit is contained in:
@@ -8,16 +8,46 @@
|
||||
</button>
|
||||
</div>
|
||||
<nav class="metismenu">
|
||||
<div class="menu-label">Operations</div>
|
||||
<button v-for="item in primaryNav" :key="item.id" :class="{ active: view === item.id }" :title="collapsed ? item.label : undefined" @click="$emit('update:view', item.id)">
|
||||
<span class="parent-icon"><component :is="item.icon" :size="18" /></span>
|
||||
<span class="menu-title">{{ item.label }}</span>
|
||||
</button>
|
||||
<div class="menu-label">Administration</div>
|
||||
<button v-for="item in adminNav" :key="item.id" :class="{ active: view === item.id }" :title="collapsed ? item.label : undefined" @click="$emit('update:view', item.id)">
|
||||
<span class="parent-icon"><component :is="item.icon" :size="18" /></span>
|
||||
<span class="menu-title">{{ item.label }}</span>
|
||||
</button>
|
||||
<template v-for="item in nav" :key="item.id">
|
||||
<button
|
||||
v-if="!item.children?.length"
|
||||
:class="['nav-root-button', { active: view === item.id }]"
|
||||
:title="collapsed ? item.label : undefined"
|
||||
type="button"
|
||||
@click="$emit('update:view', item.id)"
|
||||
>
|
||||
<span class="parent-icon"><component :is="item.icon" :size="18" /></span>
|
||||
<span class="menu-title">{{ item.label }}</span>
|
||||
</button>
|
||||
|
||||
<div v-else :class="['nav-group', { open: isGroupOpen(item.id), active: groupContainsView(item, view) }]">
|
||||
<button
|
||||
class="nav-group-toggle"
|
||||
:aria-expanded="isGroupOpen(item.id)"
|
||||
:title="collapsed ? item.label : undefined"
|
||||
type="button"
|
||||
@click="toggleGroup(item.id)"
|
||||
>
|
||||
<span class="parent-icon"><component :is="item.icon" :size="18" /></span>
|
||||
<span class="menu-title">{{ item.label }}</span>
|
||||
<ChevronDown class="group-chevron" :size="15" />
|
||||
</button>
|
||||
<div v-if="isGroupOpen(item.id)" class="nav-submenu">
|
||||
<button
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:class="['nav-child-button', { active: view === child.id }]"
|
||||
:title="collapsed ? child.label : undefined"
|
||||
type="button"
|
||||
@click="$emit('update:view', child.id)"
|
||||
>
|
||||
<span class="child-rail" aria-hidden="true"></span>
|
||||
<span class="parent-icon child-icon"><component :is="child.icon" :size="16" /></span>
|
||||
<span class="menu-title">{{ child.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-user">
|
||||
<span class="user-orb">{{ initials }}</span>
|
||||
@@ -30,8 +60,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { PanelLeftClose, PanelLeftOpen } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ChevronDown, PanelLeftClose, PanelLeftOpen } from '@lucide/vue';
|
||||
|
||||
const props = defineProps({
|
||||
nav: { type: Array, required: true },
|
||||
@@ -42,8 +72,34 @@ const props = defineProps({
|
||||
|
||||
defineEmits(['update:view', 'toggle-collapse']);
|
||||
|
||||
const accountNav = ['profile', 'users', 'admin'];
|
||||
const primaryNav = computed(() => props.nav.filter((item) => !accountNav.includes(item.id)));
|
||||
const adminNav = computed(() => props.nav.filter((item) => accountNav.includes(item.id)));
|
||||
const collapsedGroups = ref(loadCollapsedGroups());
|
||||
const initials = computed(() => (props.user?.displayName || 'PS').split(/\s+/).map((part) => part[0]).join('').slice(0, 2).toUpperCase());
|
||||
|
||||
watch(collapsedGroups, (value) => {
|
||||
localStorage.setItem('poshmanager.navGroupsCollapsed', JSON.stringify([...value]));
|
||||
}, { deep: true });
|
||||
|
||||
function loadCollapsedGroups() {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem('poshmanager.navGroupsCollapsed') || '[]');
|
||||
return new Set(Array.isArray(parsed) ? parsed : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function isGroupOpen(groupId) {
|
||||
return !collapsedGroups.value.has(groupId);
|
||||
}
|
||||
|
||||
function toggleGroup(groupId) {
|
||||
const next = new Set(collapsedGroups.value);
|
||||
if (next.has(groupId)) next.delete(groupId);
|
||||
else next.add(groupId);
|
||||
collapsedGroups.value = next;
|
||||
}
|
||||
|
||||
function groupContainsView(group, view) {
|
||||
return group.children?.some((child) => child.id === view);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
type="button"
|
||||
:class="['switch-control', { active: normalizedBooleanSetting(row.value) }]"
|
||||
:aria-pressed="normalizedBooleanSetting(row.value)"
|
||||
:disabled="isLockedSetting(key, row)"
|
||||
@click="$emit('toggle-boolean', row)"
|
||||
>
|
||||
<i></i>
|
||||
<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" :type="isSecretSetting(key) ? 'password' : 'text'" class="settings-input" :placeholder="placeholderFor(key)" />
|
||||
<textarea v-else-if="key === 'trusted_origins'" v-model="row.value" class="settings-input settings-textarea" :placeholder="placeholderFor(key)" :disabled="isLockedSetting(key, row)"></textarea>
|
||||
<input v-else v-model="row.value" :type="isSecretSetting(key) ? 'password' : 'text'" class="settings-input" :placeholder="placeholderFor(key)" :disabled="isLockedSetting(key, row)" />
|
||||
</div>
|
||||
<span :class="['setting-source', row.source]">{{ sourceLabel(row.source) }}</span>
|
||||
</div>
|
||||
@@ -51,7 +52,8 @@ defineProps({
|
||||
isSecretSetting: { type: Function, default: () => false },
|
||||
sourceLabel: { type: Function, required: true },
|
||||
isBooleanSetting: { type: Function, required: true },
|
||||
normalizedBooleanSetting: { type: Function, required: true }
|
||||
normalizedBooleanSetting: { type: Function, required: true },
|
||||
isLockedSetting: { type: Function, default: () => false }
|
||||
});
|
||||
|
||||
defineEmits(['toggle', 'toggle-boolean']);
|
||||
|
||||
@@ -11,35 +11,61 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="intune-config-grid">
|
||||
<section class="publisher-card">
|
||||
<strong>Inventory sources</strong>
|
||||
<div class="vmware-config-grid">
|
||||
<section class="publisher-card vmware-summary-card">
|
||||
<div class="vmware-card-heading">
|
||||
<span class="vmware-card-icon"><i class="mdi mdi-view-grid-outline" aria-hidden="true"></i></span>
|
||||
<div>
|
||||
<strong>Inventory sources</strong>
|
||||
<small>Credential-backed VMware inventory and power-state checks.</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>vCenter and standalone ESXi entries use Credential Vault entries for VMware API authentication, inventory import, and power-state checks.</p>
|
||||
<div class="config-stat-row">
|
||||
<span><b>{{ connections.length }}</b><small>systems</small></span>
|
||||
<span><b>{{ enabledCount }}</b><small>enabled</small></span>
|
||||
<div class="vmware-stat-grid" aria-label="VMware connection summary">
|
||||
<span><b>{{ connections.length }}</b><small>Systems</small></span>
|
||||
<span><b>{{ enabledCount }}</b><small>Enabled</small></span>
|
||||
<span><b>{{ vcenterCount }}</b><small>vCenter</small></span>
|
||||
<span><b>{{ standaloneCount }}</b><small>hosts</small></span>
|
||||
<span><b>{{ testedCount }}</b><small>tested</small></span>
|
||||
<span><b>{{ standaloneCount }}</b><small>ESXi Hosts</small></span>
|
||||
<span><b>{{ testedCount }}</b><small>Tested</small></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="publisher-card graph-config-list">
|
||||
<strong>VMware systems</strong>
|
||||
<div v-for="connection in connections" :key="connection.id" class="check-row">
|
||||
<b>{{ connection.name }}</b>
|
||||
<small>{{ connection.baseUrl }} - {{ connectionTypeLabel(connection) }} - {{ connection.credentialName || connection.username || 'No vault credential' }} - {{ connection.lastTestStatus || 'untested' }}</small>
|
||||
<span>
|
||||
<button class="ghost-button compact" type="button" @click="openModal(connection)">
|
||||
<section class="publisher-card vmware-system-list">
|
||||
<div class="vmware-list-heading">
|
||||
<strong>VMware systems</strong>
|
||||
<small>{{ connections.length ? `${connections.length} configured` : 'No systems configured' }}</small>
|
||||
</div>
|
||||
<div v-for="connection in connections" :key="connection.id" class="vmware-system-row">
|
||||
<div class="vmware-system-lead" aria-hidden="true">
|
||||
<i :class="connection.targetType === 'host' ? 'mdi mdi-server' : 'mdi mdi-server-network'"></i>
|
||||
</div>
|
||||
|
||||
<div class="vmware-system-main">
|
||||
<div class="vmware-system-title">
|
||||
<b>{{ connection.name }}</b>
|
||||
<span :class="['status-pill', connection.enabled ? 'success' : 'skipped']">{{ connection.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
<span :class="['status-pill', connectionStatusTone(connection)]">{{ connection.lastTestStatus || 'Untested' }}</span>
|
||||
</div>
|
||||
<div class="vmware-system-meta">
|
||||
<span><i class="mdi mdi-source-branch" aria-hidden="true"></i>{{ connectionTypeLabel(connection) }}</span>
|
||||
<span><i class="mdi mdi-link-variant" aria-hidden="true"></i>{{ connection.baseUrl }}</span>
|
||||
<span><i class="mdi mdi-key-variant" aria-hidden="true"></i>{{ credentialLabel(connection) }}</span>
|
||||
<span v-if="connection.lastTestAt"><i class="mdi mdi-clock-outline" aria-hidden="true"></i>{{ formatDate(connection.lastTestAt) }}</span>
|
||||
</div>
|
||||
<small v-if="connection.lastTestMessage" class="vmware-test-message">{{ connection.lastTestMessage }}</small>
|
||||
</div>
|
||||
|
||||
<div class="vmware-system-actions">
|
||||
<button class="ghost-button icon-text compact" type="button" :aria-label="`Edit ${connection.name}`" @click="openModal(connection)">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i>Edit
|
||||
</button>
|
||||
<button class="ghost-button compact" type="button" @click="$emit('test', connection.id)">
|
||||
<button class="ghost-button icon-text compact" type="button" :aria-label="`Test ${connection.name}`" @click="$emit('test', connection.id)">
|
||||
<i class="mdi mdi-lan-check" aria-hidden="true"></i>Test
|
||||
</button>
|
||||
<button class="ghost-button compact danger-text" type="button" @click="$emit('delete', connection.id)">
|
||||
<button class="ghost-button icon-text compact danger-text" type="button" :aria-label="`Delete ${connection.name}`" @click="$emit('delete', connection.id)">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>Delete
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!connections.length" class="widget-empty">No saved VMware systems configured. The legacy env/settings default can still be used by vCenter import.</p>
|
||||
</section>
|
||||
@@ -165,4 +191,272 @@ function save() {
|
||||
function connectionTypeLabel(connection) {
|
||||
return connection.targetType === 'host' ? 'Standalone ESXi host' : 'vCenter inventory';
|
||||
}
|
||||
|
||||
function credentialLabel(connection) {
|
||||
return connection.credentialName || connection.username || 'No vault credential';
|
||||
}
|
||||
|
||||
function connectionStatusTone(connection) {
|
||||
const status = String(connection.lastTestStatus || '').toLowerCase();
|
||||
if (!status) return 'neutral';
|
||||
if (status === 'success' || status === 'ok' || status === 'passed') return 'success';
|
||||
if (status === 'failed' || status === 'error') return 'failed';
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short'
|
||||
}).format(new Date(value));
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vmware-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, .72fr) minmax(0, 1.28fr);
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.vmware-summary-card,
|
||||
.vmware-system-list {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vmware-card-heading,
|
||||
.vmware-list-heading,
|
||||
.vmware-system-title,
|
||||
.vmware-system-meta,
|
||||
.vmware-system-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vmware-card-heading {
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.vmware-card-heading strong,
|
||||
.vmware-list-heading strong {
|
||||
display: block;
|
||||
color: rgba(247, 250, 255, .96);
|
||||
}
|
||||
|
||||
.vmware-card-heading small,
|
||||
.vmware-list-heading small {
|
||||
color: rgba(213, 225, 246, .62);
|
||||
font-size: .76rem;
|
||||
}
|
||||
|
||||
.vmware-card-icon,
|
||||
.vmware-system-lead {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgba(88, 221, 255, .2);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(88, 221, 255, .16), rgba(154, 107, 255, .1)),
|
||||
rgba(10, 16, 34, .72);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08), 0 14px 30px rgba(0, 0, 0, .18);
|
||||
color: #8be0ff;
|
||||
}
|
||||
|
||||
.vmware-card-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.vmware-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.vmware-stat-grid span {
|
||||
min-width: 0;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, .095);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(88, 221, 255, .08), rgba(154, 107, 255, .055)),
|
||||
rgba(255, 255, 255, .045);
|
||||
}
|
||||
|
||||
.vmware-stat-grid b,
|
||||
.vmware-stat-grid small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vmware-stat-grid b {
|
||||
color: #f7faff;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.vmware-stat-grid small {
|
||||
margin-top: 4px;
|
||||
color: rgba(213, 225, 246, .58);
|
||||
font-size: .68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.vmware-list-heading {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vmware-system-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.vmware-system-row {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, .095);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(11, 21, 43, .88), rgba(26, 24, 58, .74)),
|
||||
rgba(4, 9, 22, .42);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .055);
|
||||
}
|
||||
|
||||
.vmware-system-lead {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.vmware-system-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vmware-system-title {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vmware-system-title b {
|
||||
min-width: 0;
|
||||
max-width: min(34rem, 100%);
|
||||
overflow: hidden;
|
||||
color: #f7faff;
|
||||
font-size: .96rem;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vmware-system-meta {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
margin-top: 8px;
|
||||
color: rgba(213, 225, 246, .68);
|
||||
font-size: .76rem;
|
||||
}
|
||||
|
||||
.vmware-system-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.vmware-system-meta i {
|
||||
flex: 0 0 auto;
|
||||
color: #8be0ff;
|
||||
opacity: .82;
|
||||
}
|
||||
|
||||
.vmware-system-meta span:nth-child(2) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vmware-test-message {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
color: rgba(255, 196, 204, .72);
|
||||
font-size: .73rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vmware-system-actions {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vmware-system-actions .ghost-button {
|
||||
min-height: 40px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.vmware-config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.vmware-stat-grid {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.intune-config-panel :deep(.section-title) {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.intune-config-panel :deep(.section-title .primary-action) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vmware-stat-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.vmware-system-row {
|
||||
grid-template-columns: 40px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.vmware-system-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: stretch;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.vmware-system-actions .ghost-button {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.vmware-stat-grid,
|
||||
.vmware-system-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user