feat: all panels are now collapsible
- Source Health, Alerts, Alert Rules, Filters, Ask, Events panels all collapsible - Click panel header to expand/collapse - Chevron indicator rotates to show state - Collapsed state persisted to localStorage (aoc_panels key)
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Admin Operations Center</title>
|
<title>Admin Operations Center</title>
|
||||||
<link rel="stylesheet" href="/style.css?v=14" />
|
<link rel="stylesheet" href="/style.css?v=15" />
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -56,8 +56,11 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('sourceHealth')">
|
||||||
<h3>Source Health</h3>
|
<h3>Source Health</h3>
|
||||||
<div class="source-health">
|
<span class="panel-toggle" :class="panelState.sourceHealth ? 'panel-toggle--open' : ''">▸</span>
|
||||||
|
</div>
|
||||||
|
<div x-show="panelState.sourceHealth">
|
||||||
<template x-for="src in sourceHealth" :key="src.source">
|
<template x-for="src in sourceHealth" :key="src.source">
|
||||||
<div class="health-card">
|
<div class="health-card">
|
||||||
<strong x-text="src.source"></strong>
|
<strong x-text="src.source"></strong>
|
||||||
@@ -71,10 +74,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('alerts')">
|
||||||
<h3>Alerts</h3>
|
<h3>Alerts</h3>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
<span x-text="`${alertSummary.total_open} open`" class="alert-open-count"></span>
|
<span x-text="`${alertSummary.total_open} open`" class="alert-open-count"></span>
|
||||||
|
<span class="panel-toggle" :class="panelState.alerts ? 'panel-toggle--open' : ''">▸</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="panelState.alerts">
|
||||||
<div class="alert-filters">
|
<div class="alert-filters">
|
||||||
<select x-model="alertsFilter.status" @change="alertsPage = 1; loadAlerts()">
|
<select x-model="alertsFilter.status" @change="alertsPage = 1; loadAlerts()">
|
||||||
<option value="">All statuses</option>
|
<option value="">All statuses</option>
|
||||||
@@ -117,13 +124,18 @@
|
|||||||
<span x-text="`Page ${alertsPage}`"></span>
|
<span x-text="`Page ${alertsPage}`"></span>
|
||||||
<button type="button" :disabled="alertsPage * 20 >= alertsTotal" @click="alertsPage++; loadAlerts()">Next</button>
|
<button type="button" :disabled="alertsPage * 20 >= alertsTotal" @click="alertsPage++; loadAlerts()">Next</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('rules')">
|
||||||
<h3>Alert Rules</h3>
|
<h3>Alert Rules</h3>
|
||||||
<button type="button" class="btn--compact" @click="openRuleEditor()">+ Add rule</button>
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<button type="button" class="btn--compact" @click.stop="openRuleEditor()">+ Add rule</button>
|
||||||
|
<span class="panel-toggle" :class="panelState.rules ? 'panel-toggle--open' : ''">▸</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="panelState.rules">
|
||||||
<div class="rules-list">
|
<div class="rules-list">
|
||||||
<template x-for="rule in rules" :key="rule.id">
|
<template x-for="rule in rules" :key="rule.id">
|
||||||
<div class="rule-card" :class="rule.enabled ? '' : 'rule-card--disabled'">
|
<div class="rule-card" :class="rule.enabled ? '' : 'rule-card--disabled'">
|
||||||
@@ -151,6 +163,7 @@
|
|||||||
<div class="rules-empty" x-show="rules.length === 0">
|
<div class="rules-empty" x-show="rules.length === 0">
|
||||||
<p>No custom rules yet. Pre-built admin-ops rules are active by default. Add your own rules to detect specific patterns.</p>
|
<p>No custom rules yet. Pre-built admin-ops rules are active by default. Add your own rules to detect specific patterns.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="ruleModal" class="modal hidden" role="dialog" aria-modal="true" :class="{ 'hidden': !ruleModalOpen }">
|
<div id="ruleModal" class="modal hidden" role="dialog" aria-modal="true" :class="{ 'hidden': !ruleModalOpen }">
|
||||||
<div class="modal__content" style="max-width: 600px;">
|
<div class="modal__content" style="max-width: 600px;">
|
||||||
@@ -210,7 +223,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<form id="filters" class="filters" @submit.prevent="resetPagination(); loadEvents()">
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('filters')">
|
||||||
|
<h3>Filters</h3>
|
||||||
|
<span class="panel-toggle" :class="panelState.filters ? 'panel-toggle--open' : ''">▸</span>
|
||||||
|
</div>
|
||||||
|
<form id="filters" class="filters" @submit.prevent="resetPagination(); loadEvents()" x-show="panelState.filters">
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<label>
|
<label>
|
||||||
User (name/UPN)
|
User (name/UPN)
|
||||||
@@ -304,8 +321,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" x-show="aiFeaturesEnabled">
|
<section class="panel" x-show="aiFeaturesEnabled">
|
||||||
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('ask')">
|
||||||
<h3>Ask a question</h3>
|
<h3>Ask a question</h3>
|
||||||
<form class="ask-form" @submit.prevent="askQuestion()">
|
<span class="panel-toggle" :class="panelState.ask ? 'panel-toggle--open' : ''">▸</span>
|
||||||
|
</div>
|
||||||
|
<form class="ask-form" @submit.prevent="askQuestion()" x-show="panelState.ask">
|
||||||
<div class="ask-row">
|
<div class="ask-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -347,10 +367,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('events')">
|
||||||
<h2>Events</h2>
|
<h2>Events</h2>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
<span id="count" x-text="countText"></span>
|
<span id="count" x-text="countText"></span>
|
||||||
|
<span class="panel-toggle" :class="panelState.events ? 'panel-toggle--open' : ''">▸</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="panelState.events">
|
||||||
<div id="status" class="status" aria-live="polite" x-text="statusText"></div>
|
<div id="status" class="status" aria-live="polite" x-text="statusText"></div>
|
||||||
<div id="events" class="events">
|
<div id="events" class="events">
|
||||||
<template x-for="(evt, idx) in events" :key="evt._id || evt.id || idx">
|
<template x-for="(evt, idx) in events" :key="evt._id || evt.id || idx">
|
||||||
@@ -391,6 +415,7 @@
|
|||||||
<span x-text="`Page ${cursorStack.length + 1}`"></span>
|
<span x-text="`Page ${cursorStack.length + 1}`"></span>
|
||||||
<button type="button" id="nextPage" :disabled="!nextCursor" @click="goNext()">Next</button>
|
<button type="button" id="nextPage" :disabled="!nextCursor" @click="goNext()">Next</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div id="modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle" :class="{ 'hidden': !modalOpen }">
|
<div id="modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle" :class="{ 'hidden': !modalOpen }">
|
||||||
@@ -452,6 +477,7 @@
|
|||||||
filters: {
|
filters: {
|
||||||
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 24, includeTags: '', excludeTags: '',
|
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 24, includeTags: '', excludeTags: '',
|
||||||
},
|
},
|
||||||
|
panelState: { sourceHealth: true, alerts: true, rules: true, filters: true, ask: true, events: true },
|
||||||
options: { actors: [], services: [], operations: [], results: [] },
|
options: { actors: [], services: [], operations: [], results: [] },
|
||||||
savedSearches: [],
|
savedSearches: [],
|
||||||
appVersion: '',
|
appVersion: '',
|
||||||
@@ -479,6 +505,7 @@
|
|||||||
await this.loadVersion();
|
await this.loadVersion();
|
||||||
await this.initAuth();
|
await this.initAuth();
|
||||||
this.loadSavedFilters();
|
this.loadSavedFilters();
|
||||||
|
this.loadPanelState();
|
||||||
if (!this.authConfig?.auth_enabled || this.accessToken) {
|
if (!this.authConfig?.auth_enabled || this.accessToken) {
|
||||||
await this.loadFilterOptions();
|
await this.loadFilterOptions();
|
||||||
await this.loadSavedSearches();
|
await this.loadSavedSearches();
|
||||||
@@ -508,6 +535,27 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadPanelState() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('aoc_panels');
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
Object.keys(parsed).forEach((k) => { if (this.panelState[k] !== undefined) this.panelState[k] = parsed[k]; });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
|
savePanelState() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('aoc_panels', JSON.stringify(this.panelState));
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePanel(key) {
|
||||||
|
this.panelState[key] = !this.panelState[key];
|
||||||
|
this.savePanelState();
|
||||||
|
},
|
||||||
|
|
||||||
async loadVersion() {
|
async loadVersion() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/version');
|
const res = await fetch('/api/version');
|
||||||
|
|||||||
@@ -274,6 +274,31 @@ input {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header--collapsible {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding: 4px 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header--collapsible:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle--open {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
#count {
|
#count {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user