feat(tags): add bulk tagging and tag-based filtering
Some checks failed
CI / lint-and-test (push) Failing after 1m24s
Some checks failed
CI / lint-and-test (push) Failing after 1m24s
- Add include_tags/exclude_tags query params to /api/events - Add POST /api/events/bulk-tags endpoint with append/replace modes - Frontend: add Include tags / Exclude tags filter inputs - Frontend: add Bulk tag matching button with prompt for tag and mode - Update filter layout to accommodate new tag fields - Add tests for tag filtering and bulk tag append/replace
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AOC Events</title>
|
||||
<link rel="stylesheet" href="/style.css?v=7" />
|
||||
<link rel="stylesheet" href="/style.css?v=8" />
|
||||
<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>
|
||||
</head>
|
||||
@@ -76,12 +76,20 @@
|
||||
To
|
||||
<input name="end" type="datetime-local" x-model="filters.end" />
|
||||
</label>
|
||||
<label>
|
||||
Include tags
|
||||
<input name="includeTags" type="text" placeholder="backup, critical" x-model="filters.includeTags" />
|
||||
</label>
|
||||
<label>
|
||||
Exclude tags
|
||||
<input name="excludeTags" type="text" placeholder="noise, auto" x-model="filters.excludeTags" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label class="span-2">
|
||||
Search (raw/full-text)
|
||||
<input name="search" type="text" placeholder="Any text to search in raw/summary" x-model="filters.search" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-row filter-row--tall">
|
||||
<div class="filter-group span-2">
|
||||
<span>App / Service</span>
|
||||
<div class="multi-select">
|
||||
@@ -104,6 +112,7 @@
|
||||
<div class="actions">
|
||||
<button type="submit">Apply filters</button>
|
||||
<button type="button" id="clearBtn" class="ghost" @click="clearFilters()">Clear</button>
|
||||
<button type="button" class="ghost" @click="bulkTagMatching()">Bulk tag matching</button>
|
||||
<button type="button" class="ghost" @click="exportJSON()">Export JSON</button>
|
||||
<button type="button" class="ghost" @click="exportCSV()">Export CSV</button>
|
||||
</div>
|
||||
@@ -188,7 +197,7 @@
|
||||
accessToken: null,
|
||||
authScopes: [],
|
||||
filters: {
|
||||
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 100,
|
||||
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 100, includeTags: '', excludeTags: '',
|
||||
},
|
||||
options: { actors: [], services: [], operations: [], results: [] },
|
||||
|
||||
@@ -320,6 +329,12 @@
|
||||
if (this.filters.selectedServices && this.filters.selectedServices.length) {
|
||||
this.filters.selectedServices.forEach((s) => params.append('services', s));
|
||||
}
|
||||
if (this.filters.includeTags) {
|
||||
this.filters.includeTags.split(/[,;]+/).map((t) => t.trim()).filter(Boolean).forEach((t) => params.append('include_tags', t));
|
||||
}
|
||||
if (this.filters.excludeTags) {
|
||||
this.filters.excludeTags.split(/[,;]+/).map((t) => t.trim()).filter(Boolean).forEach((t) => params.append('exclude_tags', t));
|
||||
}
|
||||
if (this.filters.start) {
|
||||
const d = new Date(this.filters.start);
|
||||
if (!isNaN(d.getTime())) params.append('start', d.toISOString());
|
||||
@@ -417,11 +432,53 @@
|
||||
},
|
||||
|
||||
clearFilters() {
|
||||
this.filters = { actor: '', selectedServices: [...this.options.services], search: '', operation: '', result: '', start: '', end: '', limit: 100 };
|
||||
this.filters = { actor: '', selectedServices: [...this.options.services], search: '', operation: '', result: '', start: '', end: '', limit: 100, includeTags: '', excludeTags: '' };
|
||||
this.resetPagination();
|
||||
this.loadEvents();
|
||||
},
|
||||
|
||||
async bulkTagMatching() {
|
||||
const tag = prompt('Enter tag to apply to all matching events:');
|
||||
if (!tag || !tag.trim()) return;
|
||||
const mode = confirm('Click OK to REPLACE existing tags.\nClick Cancel to APPEND the new tag.') ? 'replace' : 'append';
|
||||
const params = new URLSearchParams();
|
||||
['actor', 'operation', 'result', 'search'].forEach((key) => {
|
||||
const val = this.filters[key];
|
||||
if (val) params.append(key, val);
|
||||
});
|
||||
if (this.filters.selectedServices && this.filters.selectedServices.length) {
|
||||
this.filters.selectedServices.forEach((s) => params.append('services', s));
|
||||
}
|
||||
if (this.filters.includeTags) {
|
||||
this.filters.includeTags.split(/[,;]+/).map((t) => t.trim()).filter(Boolean).forEach((t) => params.append('include_tags', t));
|
||||
}
|
||||
if (this.filters.excludeTags) {
|
||||
this.filters.excludeTags.split(/[,;]+/).map((t) => t.trim()).filter(Boolean).forEach((t) => params.append('exclude_tags', t));
|
||||
}
|
||||
if (this.filters.start) {
|
||||
const d = new Date(this.filters.start);
|
||||
if (!isNaN(d.getTime())) params.append('start', d.toISOString());
|
||||
}
|
||||
if (this.filters.end) {
|
||||
const d = new Date(this.filters.end);
|
||||
if (!isNaN(d.getTime())) params.append('end', d.toISOString());
|
||||
}
|
||||
this.statusText = 'Applying bulk tag…';
|
||||
try {
|
||||
const res = await fetch(`/api/events/bulk-tags?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...this.authHeader() },
|
||||
body: JSON.stringify({ tags: [tag.trim()], mode }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const body = await res.json();
|
||||
this.statusText = `Tagged ${body.matched} events (${body.modified} modified).`;
|
||||
await this.loadEvents();
|
||||
} catch (err) {
|
||||
this.statusText = err.message || 'Failed to apply bulk tag.';
|
||||
}
|
||||
},
|
||||
|
||||
displayActor(e) {
|
||||
const app = e.actor?.application || e.actor?.app;
|
||||
if (app?.displayName) return app.displayName;
|
||||
|
||||
Reference in New Issue
Block a user