541 lines
11 KiB
Vue
541 lines
11 KiB
Vue
<template>
|
||
<section class="profile-section">
|
||
<div class="section-header">
|
||
<h3 class="heading">Alerts</h3>
|
||
|
||
<button
|
||
class="button"
|
||
@click="showAlertDialog = true"
|
||
>
|
||
<Icon icon="alarm-add" />
|
||
<span class="button-label">New alert</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="filters">
|
||
<input
|
||
v-model="query"
|
||
type="search"
|
||
class="input filters-search"
|
||
placeholder="Search alerts"
|
||
>
|
||
|
||
<div class="filters-section">
|
||
<Icon
|
||
icon="star"
|
||
title="Only show actor alerts"
|
||
class="noselect"
|
||
:class="{ active: filterActors }"
|
||
@click="filterActors = !filterActors"
|
||
/>
|
||
|
||
<Icon
|
||
icon="price-tags"
|
||
title="Only show tag alerts"
|
||
class="noselect"
|
||
:class="{ active: filterTags }"
|
||
@click="filterTags = !filterTags"
|
||
/>
|
||
|
||
<Icon
|
||
icon="device_hub"
|
||
title="Only show channel alerts"
|
||
class="noselect"
|
||
:class="{ active: filterEntities }"
|
||
@click="filterEntities = !filterEntities"
|
||
/>
|
||
|
||
<Icon
|
||
icon="regexp"
|
||
title="Only show expression alerts"
|
||
class="noselect"
|
||
:class="{ active: filterExpressions }"
|
||
@click="filterExpressions = !filterExpressions"
|
||
/>
|
||
|
||
<Icon
|
||
v-if="filterCombined === false"
|
||
icon="target3"
|
||
title="Only show uncombined alerts"
|
||
class="noselect active filters-uncombined"
|
||
@click="toggleFilterCombined"
|
||
/>
|
||
|
||
<Icon
|
||
v-else
|
||
icon="make-group"
|
||
title="Only show combined alerts"
|
||
class="noselect filters-uncombined"
|
||
:class="{ active: filterCombined }"
|
||
@click="toggleFilterCombined"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<ul class="alerts nolist">
|
||
<li
|
||
v-for="alert in filteredAlerts"
|
||
:key="`alert-${alert.id}`"
|
||
class="alert"
|
||
>
|
||
<div
|
||
class="alert-details"
|
||
:class="{ and: alert.and.fields, or: !alert.and.fields }"
|
||
>
|
||
<span
|
||
v-if="alert.tags.length > 0"
|
||
class="alert-detail alert-tags"
|
||
:class="{ and: alert.and.tags, or: !alert.and.tags }"
|
||
>
|
||
<span class="alert-values">
|
||
<span
|
||
v-for="tag in alert.tags"
|
||
:key="`tag-${alert.id}-${tag.id}`"
|
||
class="alert-key"
|
||
>
|
||
<a
|
||
:href="`/tag/${tag.slug}`"
|
||
class="alert-value link"
|
||
>{{ tag.name }}</a>
|
||
</span>
|
||
</span>
|
||
</span>
|
||
|
||
<span
|
||
v-if="alert.actors.length > 0"
|
||
class="alert-detail alert-actors"
|
||
:class="{ and: alert.and.actors, or: !alert.and.actors }"
|
||
>
|
||
<span class="alert-values">with
|
||
<span
|
||
v-for="actor in alert.actors"
|
||
:key="`actor-${alert.id}-${actor.id}`"
|
||
class="alert-key"
|
||
>
|
||
<a
|
||
:href="`/actor/${actor.id}/${actor.slug}`"
|
||
class="alert-value link"
|
||
>{{ actor.name }}</a>
|
||
</span>
|
||
</span>
|
||
</span>
|
||
|
||
<span
|
||
v-if="alert.entities.length > 0"
|
||
class="alert-detail alert-entities or"
|
||
>
|
||
<span class="alert-values">for
|
||
<span
|
||
v-for="entity in alert.entities"
|
||
:key="`entity-${alert.id}-${entity.id}`"
|
||
class="alert-key"
|
||
>
|
||
<a
|
||
:href="`/${entity.type}/${entity.slug}`"
|
||
class="alert-value link"
|
||
>
|
||
<Icon
|
||
v-if="entity.type === 'network'"
|
||
icon="device_hub"
|
||
/>{{ entity.name }}
|
||
</a>
|
||
</span>
|
||
</span>
|
||
</span>
|
||
|
||
<span
|
||
v-if="alert.matches.length > 0"
|
||
class="alert-detail alert-matches"
|
||
:class="{ and: alert.and.matches, or: !alert.and.matches }"
|
||
>
|
||
<span class="alert-values">matching
|
||
<span
|
||
v-for="match in alert.matches"
|
||
:key="`match-${alert.id}-${match.id}`"
|
||
class="alert-key"
|
||
>
|
||
<span class="alert-value">{{ match.property }}:
|
||
<span
|
||
class="alert-regex"
|
||
title="If your original expression was not a /regular expression/, it was converted, and new characters may have been added for syntactical purposes. These characters do not alter the function of the expression; they ensure it."
|
||
>{{ match.expression }}</span>
|
||
</span>
|
||
</span>
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="alert-meta">
|
||
<div class="alert-triggers">
|
||
<Icon
|
||
v-if="alert.notify && alert.isFromPreset"
|
||
v-tooltip="'Notify in traxxx, added as quick alert'"
|
||
icon="bell-plus"
|
||
class="trigger"
|
||
/>
|
||
|
||
<Icon
|
||
v-else-if="alert.notify"
|
||
v-tooltip="'Notify in traxxx'"
|
||
icon="bell2"
|
||
class="trigger"
|
||
/>
|
||
|
||
<Icon
|
||
v-if="alert.stashes.some((stash) => !stash.isPrimary)"
|
||
v-tooltip="`Add to ${alert.stashes.map((stash) => stash.name).join(', ')}`"
|
||
icon="folder-heart"
|
||
class="trigger"
|
||
/>
|
||
|
||
<Icon
|
||
v-else-if="alert.stashes.length > 0"
|
||
v-tooltip="alert.stashes.length > 0 ? 'Add to Favorites' : undefined"
|
||
icon="heart7"
|
||
class="trigger"
|
||
/>
|
||
|
||
<!--
|
||
<Icon
|
||
icon="envelop5"
|
||
title="E-mail me"
|
||
:class="{ trigger: alert.email }"
|
||
/>
|
||
-->
|
||
</div>
|
||
|
||
<div class="alert-actions">
|
||
<span
|
||
v-tooltip="format(alert.createdAt, 'yyyy-MM-dd hh:mm')"
|
||
class="alert-id"
|
||
title="Alert ID"
|
||
>#{{ alert.id }}</span>
|
||
|
||
<Icon
|
||
icon="bin"
|
||
@click="removeAlert(alert)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
|
||
<AlertDialog
|
||
v-if="showAlertDialog"
|
||
@close="showAlertDialog = false; reloadAlerts();"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, inject } from 'vue';
|
||
import { format } from 'date-fns';
|
||
|
||
import AlertDialog from '#/components/alerts/create.vue';
|
||
|
||
import { get, del } from '#/src/api.js';
|
||
|
||
const pageContext = inject('pageContext');
|
||
|
||
const alerts = ref(pageContext.pageProps.alerts);
|
||
const showAlertDialog = ref(false);
|
||
const done = ref(true);
|
||
|
||
const query = ref('');
|
||
const filterActors = ref(false);
|
||
const filterEntities = ref(false);
|
||
const filterTags = ref(false);
|
||
const filterExpressions = ref(false);
|
||
const filterCombined = ref(null);
|
||
|
||
const filteredAlerts = computed(() => {
|
||
const queryRegex = new RegExp(query.value, 'i');
|
||
|
||
return alerts.value.filter((alert) => {
|
||
if (filterActors.value && alert.actors.length === 0) {
|
||
return false;
|
||
}
|
||
|
||
if (filterEntities.value && alert.entities.length === 0) {
|
||
return false;
|
||
}
|
||
|
||
if (filterTags.value && alert.tags.length === 0) {
|
||
return false;
|
||
}
|
||
|
||
if (filterExpressions.value && alert.matches.length === 0) {
|
||
return false;
|
||
}
|
||
|
||
if (filterCombined.value === false && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length > 1) {
|
||
return false;
|
||
}
|
||
|
||
if (filterCombined.value === true && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length === 1) {
|
||
return false;
|
||
}
|
||
|
||
if (queryRegex.test(alert.id)) {
|
||
return true;
|
||
}
|
||
|
||
if (alert.actors.some((actor) => queryRegex.test(actor.name))) {
|
||
return true;
|
||
}
|
||
|
||
if (alert.tags.some((tag) => queryRegex.test(tag.name))) {
|
||
return true;
|
||
}
|
||
|
||
if (alert.entities.some((entity) => queryRegex.test(entity.name))) {
|
||
return true;
|
||
}
|
||
|
||
if (alert.matches.some((match) => queryRegex.test(match.expression))) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
});
|
||
});
|
||
|
||
async function reloadAlerts() {
|
||
alerts.value = await get('/alerts');
|
||
}
|
||
|
||
async function removeAlert(alert) {
|
||
if (done.value === false) {
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Are you sure you want to remove alert #${alert.id}?`)) { // eslint-disable-line no-restricted-globals, no-alert
|
||
return;
|
||
}
|
||
|
||
done.value = false;
|
||
|
||
const alertLabel = [
|
||
...alert.actors.map((actor) => actor.name),
|
||
...alert.tags.map((tag) => tag.name),
|
||
...alert.entities.map((entity) => entity.name),
|
||
...alert.matches.map((match) => match.expression),
|
||
].filter(Boolean).join(', ');
|
||
|
||
try {
|
||
await del(`/alerts/${alert.id}`, {
|
||
undoFeedback: `Removed alert for '${alertLabel}'`,
|
||
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
||
appendErrorMessage: true,
|
||
});
|
||
|
||
await reloadAlerts();
|
||
} finally {
|
||
done.value = true;
|
||
}
|
||
}
|
||
|
||
const filterCombinedStates = [null, true, false];
|
||
|
||
function toggleFilterCombined() {
|
||
const index = filterCombinedStates.indexOf(filterCombined.value);
|
||
|
||
filterCombined.value = filterCombinedStates[(index + 1) % filterCombinedStates.length];
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.alerts {
|
||
width: 100%;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
box-sizing: border-box;
|
||
padding: 0 .5rem;
|
||
margin: .5rem 0 .75rem 0;
|
||
overflow: hidden;
|
||
|
||
.input {
|
||
margin-right: 1rem;
|
||
}
|
||
|
||
.icon {
|
||
width: 1.25rem;
|
||
height: 1.25rem;
|
||
padding: .25rem .5rem;
|
||
fill: var(--glass);
|
||
|
||
&:hover {
|
||
fill: var(--glass-strong-10);
|
||
cursor: pointer;
|
||
}
|
||
|
||
&.active {
|
||
fill: var(--primary);
|
||
}
|
||
|
||
&.success {
|
||
fill: var(--success);
|
||
}
|
||
|
||
&.error {
|
||
fill: var(--error);
|
||
}
|
||
}
|
||
}
|
||
|
||
.filters-uncombined.icon {
|
||
padding-left: .75rem;
|
||
border-left: solid 1px var(--glass-weak-30);
|
||
margin-left: .5rem;
|
||
}
|
||
|
||
.alert {
|
||
padding: 0 0 0 .5rem;
|
||
display: flex;
|
||
align-items: stretch;
|
||
border-bottom: solid 1px var(--glass-weak-40);
|
||
|
||
&:hover {
|
||
border-color: var(--glass-weak-30);
|
||
}
|
||
}
|
||
|
||
.alert-triggers {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
margin-right: .75rem;
|
||
|
||
.icon {
|
||
fill: var(--glass-weak-40);
|
||
}
|
||
|
||
.icon.trigger {
|
||
fill: var(--glass-weak-10);
|
||
}
|
||
}
|
||
|
||
.alert-details {
|
||
padding: .25rem 0;
|
||
flex-grow: 1;
|
||
line-height: 2.5;
|
||
color: var(--glass);
|
||
|
||
&.and .alert-detail:not(:last-child):after {
|
||
content: ' and ';
|
||
}
|
||
|
||
&.or .alert-detail:not(:last-child):after {
|
||
content: ' or ';
|
||
}
|
||
}
|
||
|
||
.alert-value {
|
||
color: var(--text);
|
||
|
||
.icon {
|
||
margin-right: .25rem;
|
||
fill: var(--glass);
|
||
transform: translateY(2px);
|
||
}
|
||
}
|
||
|
||
.alert-values {
|
||
padding: .5rem .5rem;
|
||
border-bottom: solid 1px var(--primary-light-20);
|
||
border-radius: .3rem;
|
||
}
|
||
|
||
.alert-detail {
|
||
&.and .alert-key:not(:last-child):after {
|
||
content: ' and ';
|
||
}
|
||
|
||
&.or .alert-key:not(:last-child):after {
|
||
content: ' or ';
|
||
}
|
||
}
|
||
|
||
.alert-regex {
|
||
&:before,
|
||
&:after {
|
||
content: '╱';
|
||
padding: 0 .1rem;
|
||
color: var(--primary-light-20);
|
||
}
|
||
}
|
||
|
||
.alert-meta {
|
||
display: flex;
|
||
}
|
||
|
||
.alert-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: .9rem;
|
||
color: var(--glass-weak-10);
|
||
|
||
.icon {
|
||
height: 100%;
|
||
padding: 0 .75rem;
|
||
fill: var(--glass);
|
||
|
||
&:hover {
|
||
cursor: pointer;
|
||
fill: var(--primary);
|
||
}
|
||
}
|
||
}
|
||
|
||
@media(--compact) {
|
||
.profile-header {
|
||
border-radius: 0;
|
||
}
|
||
|
||
.section-header {
|
||
padding: .5rem 1rem .5rem 1rem;
|
||
}
|
||
|
||
.stashes {
|
||
padding: 0 1rem 1rem 1rem;
|
||
}
|
||
|
||
.alert {
|
||
padding: 0 .5rem;
|
||
}
|
||
|
||
.filters {
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
|
||
@media(--small-20) {
|
||
.alert {
|
||
flex-direction: column;
|
||
padding-right: 0;
|
||
}
|
||
|
||
.alert-meta {
|
||
padding: .5rem 0 .5rem 0;
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
|
||
@media(--small-30) {
|
||
.filters {
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
|
||
.input {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
</style>
|