feat(ui): Implement run context for actioned events

* Refactor events view to show checks within runs
* Build cohesive runs server-side before rendering so user can see all checks in a run together
* Add collapse/expand behavior for activity/run/check with ability to toggle based on triggered state
* Default to collapsing all non-triggered states
* Build check summary on-the-fly instead of storing in event result data
This commit is contained in:
FoxxMD
2022-02-09 13:08:48 -05:00
parent f7a7e817f9
commit 8d9fb29848
6 changed files with 174 additions and 73 deletions

View File

@@ -51,7 +51,6 @@ export const up = async (context: any, next: any) => {
run: 'Run1',
postBehavior: 'nextRun',
triggered: true,
ruleSummary,
condition: ruleSummary.includes('OR') ? 'OR' : 'AND',
ruleResults,
actionResults,
@@ -106,7 +105,6 @@ export const down = async (context: any, next: any) => {
ruleResults: y.ruleResults,
actionResults: y.actionResults,
check: y.name,
ruleSummary: y.ruleSummary
}
});
return acc.concat(singleEvents);

View File

@@ -1971,11 +1971,7 @@ export interface ActionedEvent {
}
author: string
timestamp: number
//check: string
//ruleSummary: string,
subreddit: string,
//ruleResults: RuleResult[]
//actionResults: ActionResult[]
runResults: CheckSummary[]
}
@@ -1991,8 +1987,7 @@ export interface CheckSummary extends CheckResult {
postBehavior: string
error?: string
actionResults: ActionResult[]
condition: string
ruleSummary: string
condition: 'AND' | 'OR'
}
export interface UserResultCache {

View File

@@ -647,7 +647,7 @@ export class Manager extends EventEmitter {
ePeek = peek;
this.logger.info(`<EVENT> ${peek}`);
} catch (err: any) {
this.logger.error(`Error occurred while generate item peek for ${checkType} Activity ${itemId}`, err);
this.logger.error(`Error occurred while generating item peek for ${checkType} Activity ${itemId}`, err);
}
let checksRun = 0;
@@ -911,7 +911,6 @@ export class Manager extends EventEmitter {
actionResults: runActions,
postBehavior: behavior,
run: currRun.name,
ruleSummary: isFromCache ? `Check result was found in cache: ${triggeredIndicator(triggered)}` : resultsSummary(currentResults, check.condition)
});
}

View File

@@ -6,7 +6,7 @@ import cookieParser from 'cookie-parser';
import CacheManagerStore from 'express-session-cache-manager'
import passport from 'passport';
import {Strategy as CustomStrategy} from 'passport-custom';
import {OperatorConfig, BotConnection, LogInfo} from "../../Common/interfaces";
import {OperatorConfig, BotConnection, LogInfo, CheckSummary} from "../../Common/interfaces";
import {
buildCachePrefix,
createCacheManager, defaultFormat, filterLogBySubreddit,
@@ -14,7 +14,7 @@ import {
intersect, isLogLineMinLevel,
LogEntry, parseInstanceLogInfoName, parseInstanceLogName, parseRedditEntity,
parseSubredditLogName, permissions,
randomId, sleep, triggeredIndicator
randomId, resultsSummary, sleep, triggeredIndicator
} from "../../util";
import {Cache} from "cache-manager";
import session, {Session, SessionData} from "express-session";
@@ -954,57 +954,80 @@ const webClient = async (options: OperatorConfig) => {
}
}).json() as [any];
return res.render('events', {
data: resp.map((x) => {
const {timestamp, activity: {peek, link}, ruleResults = [], actionResults = [], ...rest} = x;
const time = dayjs(timestamp).local().format('YY-MM-DD HH:mm:ss z');
const formattedPeek = Autolinker.link(peek, {
email: false,
phone: false,
mention: false,
hashtag: false,
stripPrefix: false,
sanitizeHtml: true,
});
const actionedEvents = resp.map((x) => {
const {timestamp, activity: {peek, link}, runResults = [], ...rest} = x;
const time = dayjs(timestamp).local().format('YY-MM-DD HH:mm:ss z');
const formattedPeek = Autolinker.link(peek, {
email: false,
phone: false,
mention: false,
hashtag: false,
stripPrefix: false,
sanitizeHtml: true,
});
const mergedRunResults = runResults.reduce((acc: any, summ: CheckSummary) => {
let currentRun = acc.curr;
if(currentRun.name !== undefined && summ.run !== currentRun.name) {
currentRun.triggeredVal = currentRun.checkResults.some((x: any) => x.triggered);
currentRun.triggered = triggeredIndicator(currentRun.triggeredVal);
acc.all.push(currentRun);
currentRun = {name: undefined, triggered: false, checkResults: []};
}
currentRun.name = summ.run;
const {actionResults = [], ruleResults = [], triggered: checkTriggered, ...rest} = summ;
const formattedRuleResults = ruleResults.map((y: any) => {
const {triggered, result, ...restY} = y;
let t = triggeredIndicator(false);
if(triggered === null) {
t = 'Skipped';
} else if(triggered === true) {
t = triggeredIndicator(true);
}
return {
...restY,
triggered: t,
triggered: triggeredIndicator(triggered, 'Skipped'),
result: result || '-'
};
});
const formattedActionResults = actionResults.map((y: any) => {
const {run, runReason, success, result, dryRun, ...restA} = y;
let res = '';
if(!run) {
res = `Not Run - ${runReason === undefined ? '(No Reason)' : runReason}`;
} else {
res = `${triggeredIndicator(success)}${result !== undefined ? ` - ${result}` : ''}`;
}
return {
...restA,
dryRun: dryRun ? ' (DRYRUN)' : '',
result: res
};
const {run, runReason, success, result, dryRun, ...restA} = y;
let res = '';
if(!run) {
res = `Not Run - ${runReason === undefined ? '(No Reason)' : runReason}`;
} else {
res = `${triggeredIndicator(success)}${result !== undefined ? ` - ${result}` : ''}`;
}
return {
...restA,
dryRun: dryRun ? ' (DRYRUN)' : '',
result: res
};
});
return {
currentRun.checkResults.push({
...rest,
timestamp: time,
activity: {
link,
peek: formattedPeek,
},
triggered: triggeredIndicator(checkTriggered, 'Skipped'),
triggeredVal: checkTriggered,
ruleResults: formattedRuleResults,
actionResults: formattedActionResults
}
}),
actionResults: formattedActionResults,
ruleSummary: summ.fromCache ? `Check result was found in cache: ${triggeredIndicator(checkTriggered, 'Skipped')}` : resultsSummary(ruleResults, summ.condition)
});
acc.curr = currentRun;
return acc;
}, {curr: {name: undefined, triggered: false, checkResults: []}, all: []});
// handle last current run
const currentRun = mergedRunResults.curr;
currentRun.triggeredVal = currentRun.checkResults.some((x: any) => x.triggered);
currentRun.triggered = triggeredIndicator(currentRun.triggeredVal);
const rr = mergedRunResults.all.concat(currentRun);
return {
...rest,
timestamp: time,
activity: {
link,
peek: formattedPeek,
},
triggered: rr.some((x: any) => x.triggeredVal),
runResults: rr,
}
});
return res.render('events', {
data: actionedEvents,
title: `${subreddit !== undefined ? `${subreddit} ` : ''}Actioned Events`
});
});

View File

@@ -152,3 +152,19 @@ a {
#saveTip .tooltip:hover {
transition-delay: 1s;
}
.triggeredState.notTriggered {
display: none;
}
.triggeredState.hide {
display: none;
}
.triggeredState.notTriggered.show {
display: inherit;
}
.triggeredStateToggle {
cursor: pointer;
}

View File

@@ -16,51 +16,121 @@
</style>
</head>
<body class="bg-gray-900 text-white font-sans">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen">
<%- include('partials/title') %>
<div class="container mx-auto">
<div class="grid">
<div class="pr-3 pt-3" style="text-align: right">
<input type="checkbox" id="showAll">
<label for="showAll">Show all non-triggered details</label>
</div>
<div class="px-3 py-6 space-y-3">
<% if(data.length === 0) { %>
No events have been actioned yet!
<% } %>
<% data.forEach(function (eRes){ %>
<div class="shadow-lg">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-700">
<% data.forEach(function (eRes) { %>
<div class="shadow-lg bg-gray-700 py-1 triggeredStateWrapper">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold font-semibold triggeredStateToggle">
<div class="flex items-center justify-between">
<div>
<span class="peek"><%- eRes.activity.peek %></span><a target="_blank" href="https://reddit.com<%= eRes.activity.link%>">(Link)</a>
<span class="peek"><%- eRes.activity.peek %></span><a class="activityLink" target="_blank" href="https://reddit.com<%= eRes.activity.link%>">(Link)</a>
</div>
<div class="flex items-center flex-end">
<%= eRes.subreddit %> @ <%= eRes.timestamp %>
</div>
</div>
</div>
<div class="p-4 pl-6 pt-3 space-y-2 bg-gray-500">
<div><span class="font-semibold">Check:</span> <%= eRes.check %><span class="px-3">&#10132;</span><%= eRes.ruleSummary %></div>
<div>
<span class="font-semibold">Rules:</span>
<ul class="list-inside list-disc">
<% eRes.ruleResults.forEach(function (ruleResult) { %>
<li><%= ruleResult.name %> - <%= ruleResult.triggered%> - <%= ruleResult.result %></li>
<% }) %>
</ul>
<div class="border-t-2 border-gray-500 triggeredState <%= eRes.triggered ? 'triggered' : 'notTriggered'%>">
<% eRes.runResults.forEach(function (runSum, index) { %>
<div class="m-4 p-2 px-4 space-y-2 bg-gray-600 triggeredStateWrapper">
<div class="triggeredStateToggle">
<span class="font-semibold">Run:</span>
<span class="px-1"><%= runSum.triggered %></span> <%= runSum.name %>
</div>
<div><span class="font-semibold">Actions</span>
<ul class="list-inside list-disc">
<% eRes.actionResults.forEach(function (aRes) { %>
<li><%= aRes.name %><%= aRes.dryRun %> - <%= aRes.result %></li>
<% }) %>
</ul>
<div class="space-y-2 triggeredState <%= runSum.triggeredVal ? 'triggered' : 'notTriggered'%>">
<% runSum.checkResults.forEach(function (chkSum, index) { %>
<div class="py-3 px-4 space-y-2 bg-gray-500 triggeredStateWrapper <%= chkSum.triggeredVal ? 'border-2 border-gray-100' : ''%>">
<div class="triggeredStateToggle">
<span class="font-semibold">Check:</span><span class="px-1"><%= chkSum.triggered %></span><%= chkSum.name %><span class="px-3">&#10132;</span><%= chkSum.ruleSummary %>
</div>
<div class="space-y-2 triggeredState <%= chkSum.triggeredVal ? 'triggered' : 'notTriggered'%>">
<div>
<span class="font-semibold">Rules:</span>
<ul class="list-inside list-disc">
<% chkSum.ruleResults.forEach(function (ruleResult) { %>
<li><%= ruleResult.name %> - <%= ruleResult.triggered%> - <%= ruleResult.result %></li>
<% }) %>
</ul>
</div>
<% if(chkSum.actionResults.length !== 0) { %>
<div><span class="font-semibold">Actions</span>
<ul class="list-inside list-disc">
<% chkSum.actionResults.forEach(function (aRes) { %>
<li><%= aRes.name %><%= aRes.dryRun %> - <%= aRes.result %></li>
<% }) %>
</ul>
</div>
<% } %>
<div>
<span class="font-semibold">Post <%= chkSum.triggered ? 'Trigger' : 'Fail' %> Behavior <span class="px-1">&#10132;</span> <%= chkSum.postBehavior %></span>
</div>
</div>
</div>
<% }) %>
</div>
</div>
</div>
<% }) %>
</div>
</div>
<% }) %>
</div>
</div>
</div>
<%- include('partials/footer') %>
</div>
<script>
function toggleShowNonTriggered(val) {
document.querySelectorAll('.triggeredState').forEach(el => {
el.classList.remove('show','hide');
if(val && el.classList.contains('notTriggered')) {
el.classList.add('show');
}
});
}
document.querySelector('#showAll').addEventListener('change', (e) => {
toggleShowNonTriggered(e.target.checked);
localStorage.setItem('showNonTriggered', e.target.checked);
});
const initShowNonTiggeredState = localStorage.getItem('showNonTriggered');
if(initShowNonTiggeredState === 'true') {
document.querySelector('#showAll').checked = true;
toggleShowNonTriggered(true);
}
document.querySelectorAll('.triggeredStateToggle').forEach(el => {
el.addEventListener('click', (e) => {
if(e.target.nodeName.toLowerCase() !== 'a') {
const tElm = e.currentTarget.closest('.triggeredStateWrapper').querySelector('.triggeredState');
if(tElm !== null) {
if(tElm.classList.contains('triggered')) {
if(tElm.classList.contains('hide')) {
tElm.classList.remove('hide');
} else {
tElm.classList.add('hide');
}
} else {
if(tElm.classList.contains('show')) {
tElm.classList.remove('show');
} else {
tElm.classList.add('show');
}
}
}
}
});
})
</script>
</body>
</html>