alerts overview work

This commit is contained in:
sadnub
2021-01-25 18:01:25 -05:00
parent 3fe83f81be
commit db5ff372a4
12 changed files with 451 additions and 147 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-24 18:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='alert',
name='snoozed',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-24 18:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0002_alert_snoozed'),
]
operations = [
migrations.AddField(
model_name='alert',
name='alert_type',
field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20),
),
]

View File

@@ -8,6 +8,13 @@ SEVERITY_CHOICES = [
("error", "Error"),
]
ALERT_TYPE_CHOICES = [
("availability", "Availability"),
("check", "Check"),
("task", "Task"),
("custom", "Custom"),
]
class Alert(models.Model):
agent = models.ForeignKey(
@@ -31,8 +38,12 @@ class Alert(models.Model):
null=True,
blank=True,
)
alert_type = models.CharField(
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
)
message = models.TextField(null=True, blank=True)
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
snoozed = models.BooleanField(default=False)
snooze_until = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False)
resolved_time = models.DateTimeField(null=True, blank=True)

View File

@@ -10,8 +10,8 @@ from .models import Alert, AlertTemplate
class AlertSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname")
client = ReadOnlyField(source="agent.client")
site = ReadOnlyField(source="agent.site")
client = ReadOnlyField(source="agent.client.name")
site = ReadOnlyField(source="agent.site.name")
alert_time = DateTimeField(format="iso-8601")
class Meta:

View File

@@ -1,8 +1,10 @@
from django.shortcuts import get_object_or_404
from django.db.models import Q
from datetime import datetime as dt
from django.utils import timezone as djangotime
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Alert, AlertTemplate
@@ -10,11 +12,84 @@ from .serializers import AlertSerializer, AlertTemplateSerializer
class GetAddAlerts(APIView):
def get(self, request):
def patch(self, request):
# top 10 alerts for dashboard icon
if "top" in request.data.keys():
alerts = Alert.objects.filter(resolved=False, snooze_until=None).order_by(
"alert_time"
)[: int(request.data["top"])]
count = Alert.objects.filter(resolved=False).count()
return Response(
{
"alerts_count": count,
"alerts": AlertSerializer(alerts, many=True).data,
}
)
elif any(
key
in [
"timeFilter",
"clientFilter",
"severityFilter",
"resolvedFilter",
"snoozedFilter",
]
for key in request.data.keys()
):
clientFilter = Q()
severityFilter = Q()
timeFilter = Q()
resolvedFilter = Q()
snoozedFilter = Q()
if (
"snoozedFilter" in request.data.keys()
and not request.data["snoozedFilter"]
):
snoozedFilter = Q(snoozed=request.data["snoozedFilter"])
if (
"resolvedFilter" in request.data.keys()
and not request.data["resolvedFilter"]
):
resolvedFilter = Q(resolved=request.data["resolvedFilter"])
if "clientFilter" in request.data.keys():
from agents.models import Agent
from clients.models import Client
clients = Client.objects.filter(
pk__in=request.data["clientFilter"]
).values_list("id")
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
"id"
)
clientFilter = Q(agent__in=agents)
if "severityFilter" in request.data.keys():
severityFilter = Q(severity__in=request.data["severityFilter"])
if "timeFilter" in request.data.keys():
timeFilter = Q(
alert_time__lte=djangotime.make_aware(dt.today()),
alert_time__gt=djangotime.make_aware(dt.today())
- djangotime.timedelta(days=int(request.data["timeFilter"])),
)
alerts = (
Alert.objects.filter(clientFilter)
.filter(severityFilter)
.filter(resolvedFilter)
.filter(snoozedFilter)
.filter(timeFilter)
)
return Response(AlertSerializer(alerts, many=True).data)
else:
alerts = Alert.objects.all()
# Add time and severity filters
return Response(AlertSerializer(alerts, many=True).data)
def post(self, request):

View File

@@ -1,18 +1,14 @@
<template>
<q-btn dense flat icon="notifications">
<q-badge v-if="alerts.length !== 0" color="red" floating transparent>{{ alertsLengthText() }}</q-badge>
<q-badge v-if="alertsCount > 0" color="red" floating transparent>{{ alertsCountText() }}</q-badge>
<q-menu>
<q-list separator>
<q-item v-if="alerts.length === 0">No New Alerts</q-item>
<q-item v-for="alert in alerts" :key="alert.id">
<q-item v-if="alertsCount === 0">No New Alerts</q-item>
<q-item v-for="alert in topAlerts" :key="alert.id">
<q-item-section>
<q-item-label overline>{{ alert.client }} - {{ alert.site }} - {{ alert.hostname }}</q-item-label>
<q-item-label>
<q-icon
size="xs"
:class="`text-${alertColor(alert.severity)}`"
:name="alert.severity"
></q-icon>
<q-icon size="xs" :class="`text-${alertIconColor(alert.severity)}`" :name="alert.severity"></q-icon>
{{ alert.message }}
</q-item-label>
</q-item-section>
@@ -20,51 +16,56 @@
<q-item-section side top>
<q-item-label caption>{{ alertTime(alert.alert_time) }}</q-item-label>
<q-item-label>
<q-icon name="snooze" size="xs">
<q-icon name="snooze" size="xs" class="cursor-pointer">
<q-tooltip>Snooze the alert for 24 hours</q-tooltip>
</q-icon>
<q-icon name="alarm_off" size="xs">
<q-icon name="alarm_off" size="xs" class="cursor-pointer">
<q-tooltip>Dismiss alert</q-tooltip>
</q-icon>
</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="showAlertsModal = true">View All Alerts ({{ alerts.length }})</q-item>
<q-item clickable @click="showOverview">View All Alerts ({{ alertsCount }})</q-item>
</q-list>
</q-menu>
<q-dialog
v-model="showAlertsModal"
maximized
transition-show="slide-up"
transition-hide="slide-down"
>
<AlertsOverview @close="showAlertsModal = false" />
</q-dialog>
</q-btn>
</template>
<script>
import { mapGetters } from "vuex";
import mixins from "@/mixins/mixins";
import AlertsOverview from "@/components/modals/alerts/AlertsOverview";
export default {
name: "AlertsIcon",
components: { AlertsOverview },
mixins: [mixins],
data() {
return {
showAlertsModal: false,
alertsCount: 0,
topAlerts: [],
};
},
methods: {
getAlerts() {
this.$store.dispatch("alerts/getAlerts").catch(error => {
console.error(error);
this.$q.loading.show();
this.$axios
.patch("alerts/alerts/", { top: 10 })
.then(r => {
this.alertsCount = r.data.alerts_count;
this.topAlerts = r.data.alerts;
this.$q.loading.hide();
})
.catch(error => {
this.$q.loading.hide();
this.notifyError("Unable to get alerts");
});
},
alertColor(type) {
showOverview() {
this.$q.dialog({
component: AlertsOverview,
parent: this,
});
},
alertIconColor(type) {
if (type === "error") {
return "red";
}
@@ -72,20 +73,14 @@ export default {
return "orange";
}
},
alertsLengthText() {
if (this.alerts.length > 9) {
return "9+";
alertsCountText() {
if (this.alertsCount > 99) {
return "99+";
} else {
return this.alerts.length;
return this.alertsCount;
}
},
},
computed: {
...mapGetters({
newAlerts: "alerts/getNewAlerts",
alerts: "alerts/getAlerts",
}),
},
mounted() {
this.getAlerts();
},

View File

@@ -1,5 +1,5 @@
<template>
<div style="width: 60vw; max-width: 90vw">
<div style="width: 90vw; max-width: 90vw">
<q-card>
<q-bar>
<q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager

View File

@@ -311,7 +311,7 @@ export default {
this.getPolicies();
this.clearRow();
},
deletePolicy(id) {
deletePolicy(policy) {
this.$q
.dialog({
title: "Delete policy?",
@@ -319,12 +319,16 @@ export default {
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/automation/policies/${pk}/`)
.delete(`/automation/policies/${policy.id}/`)
.then(r => {
this.refresh();
this.$q.loading.hide();
this.notifySuccess("Policy was deleted!");
})
.catch(error => {
this.$q.loading.hide();
this.notifyError("An Error occured while deleting policy");
});
});
@@ -422,6 +426,7 @@ export default {
this.$axios
.put(`/automation/policies/${data.id}/`, data)
.then(r => {
this.refresh();
this.$q.loading.hide();
this.notifySuccess(text);
})

View File

@@ -16,8 +16,7 @@
:nodes="clientSiteTree"
node-key="id"
selected-color="primary"
@update:selected="setSelectedPolicyId(key)"
default-expand-all
:selected.sync="selectedPolicyId"
></q-tree>
</div>
</template>
@@ -39,10 +38,10 @@
</q-tabs>
<q-tab-panels v-model="selectedTab" animated transition-prev="jump-up" transition-next="jump-up">
<q-tab-panel name="checks">
<PolicyChecksTab :selectedPolicy="selectedPolicyId" />
<PolicyChecksTab v-if="!!selectedPolicyId" :selectedPolicy="selectedPolicyId" />
</q-tab-panel>
<q-tab-panel name="tasks">
<PolicyAutomatedTasksTab :selectedPolicy="selectedPolicyId" />
<PolicyAutomatedTasksTab v-if="!!selectedPolicyId" :selectedPolicy="selectedPolicyId" />
</q-tab-panel>
</q-tab-panels>
</template>
@@ -85,12 +84,6 @@ export default {
this.notifyError("Error getting policy tree data");
});
},
setSelectedPolicyId(key) {
if (!key) {
return;
}
this.selectedPolicyId = this.$refs.tree.getNodeByKey(key);
},
processTreeDataFromApi(data) {
/* Structure
* [{
@@ -114,10 +107,10 @@ export default {
// Used by tree for unique identification
let unique_id = 0;
for (let client in data) {
for (let client of data) {
var client_temp = {};
client_temp["label"] = data[client].name;
client_temp["label"] = client.name;
client_temp["id"] = unique_id;
client_temp["icon"] = "business";
client_temp["selectable"] = false;
@@ -126,41 +119,41 @@ export default {
unique_id--;
// Add any server policies assigned to client
if (data[client].server_policy !== null) {
if (!!client.server_policy) {
let disabled = "";
// Indicate if the policy is active or not
if (!data[client].server_policy.active) {
if (!client.server_policy.active) {
disabled = " (disabled)";
}
client_temp["children"].push({
label: data[client].server_policy.name + " (Servers)" + disabled,
label: client.server_policy.name + " (Servers)" + disabled,
icon: "policy",
id: data[client].server_policy.id,
id: client.server_policy.id,
});
}
// Add any workstation policies assigned to client
if (data[client].workstation_policy !== null) {
if (!!client.workstation_policy) {
let disabled = "";
// Indicate if the policy is active or not
if (!data[client].workstation_policy.active) {
if (!client.workstation_policy.active) {
disabled = " (disabled)";
}
client_temp["children"].push({
label: data[client].workstation_policy.name + " (Workstations)" + disabled,
label: client.workstation_policy.name + " (Workstations)" + disabled,
icon: "policy",
id: data[client].workstation_policy.id,
id: client.workstation_policy.id,
});
}
// Iterate through Sites
for (let site in data[client].sites) {
for (let site of client.sites) {
var site_temp = {};
site_temp["label"] = data[client].sites[site].name;
site_temp["label"] = site.name;
site_temp["id"] = unique_id;
site_temp["icon"] = "apartment";
site_temp["selectable"] = false;
@@ -168,36 +161,36 @@ export default {
unique_id--;
// Add any server policies assigned to site
if (data[client].sites[site].server_policy !== null) {
if (!!site.server_policy) {
site_temp["children"] = [];
// Indicate if the policy is active or not
let disabled = "";
if (!data[client].sites[site].server_policy.active) {
if (!site.server_policy.active) {
disabled = " (disabled)";
}
site_temp["children"].push({
label: data[client].sites[site].server_policy.name + " (Servers)" + disabled,
label: site.server_policy.name + " (Servers)" + disabled,
icon: "policy",
id: data[client].sites[site].server_policy.id,
id: site.server_policy.id,
});
}
// Add any server policies assigned to site
if (data[client].sites[site].workstation_policy !== null) {
if (!!site.workstation_policy) {
site_temp["children"] = [];
// Indicate if the policy is active or not
let disabled = "";
if (!data[client].sites[site].workstation_policy.active) {
if (!site.workstation_policy.active) {
disabled = " (disabled)";
}
site_temp["children"].push({
label: data[client].sites[site].workstation_policy.name + " (Workstations)" + disabled,
label: site.workstation_policy.name + " (Workstations)" + disabled,
icon: "policy",
id: data[client].sites[site].workstation_policy.id,
id: site.workstation_policy.id,
});
}

View File

@@ -11,7 +11,7 @@
<q-form @submit="submit">
<q-card-section v-if="options.length > 0">
<q-select
v-if="type !== 'agent'"
v-if="type === 'client' || type === 'site'"
class="q-mb-md"
v-model="selectedServerPolicy"
:options="options"
@@ -25,7 +25,7 @@
>
</q-select>
<q-select
v-if="type !== 'agent'"
v-if="type === 'client' || type === 'site'"
v-model="selectedWorkstationPolicy"
:options="options"
outlined
@@ -91,7 +91,7 @@ export default {
methods: {
submit() {
// check if data was changed
if (this.type !== "agent") {
if (this.type === "client" || this.type === "site") {
if (
this.object.workstation_policy === this.selectedWorkstationPolicy &&
this.object.server_policy === this.selectedServerPolicy
@@ -99,11 +99,13 @@ export default {
this.hide();
return;
}
} else {
} else if (this.type === "agent") {
if (this.object.policy === this.selectedAgentPolicy) {
this.hide();
return;
}
} else {
return;
}
this.$q.loading.show();
@@ -112,14 +114,13 @@ export default {
type: this.type,
};
if (this.type !== "agent") {
if (this.type === "client" || this.type === "site") {
data.server_policy = this.selectedServerPolicy;
data.workstation_policy = this.selectedWorkstationPolicy;
} else {
} else if (this.type === "agent") {
data.policy = this.selectedAgentPolicy;
}
console.log(data);
this.$axios
.post(`/automation/related/`, data)
.then(r => {

View File

@@ -1,6 +1,9 @@
<template>
<q-dialog ref="dialog" @hide="onHide" maximized transition-show="slide-up" transition-hide="slide-down">
<q-card>
<q-bar>
<q-btn @click="search" class="q-mr-sm" dense flat push icon="refresh" />
<q-space />
Alerts Overview
<q-space />
<q-btn dense flat icon="close" v-close-popup>
@@ -8,79 +11,260 @@
</q-btn>
</q-bar>
<q-separator />
<q-card-section class="row">
<div class="col-3">
<q-input outlined dense v-model="search">
<template v-slot:append>
<q-icon v-if="search !== ''" name="close" @click="search = ''" class="cursor-pointer" />
<q-icon name="search" />
</template>
<template v-slot:hint>Type in client, site, or agent name</template>
</q-input>
<div class="text-h6 q-pl-sm q-pt-sm">Filter</div>
<div class="row">
<div class="q-pa-sm col-3">
<q-select
v-model="clientFilter"
:options="clientsOptions"
label="Clients"
multiple
outlined
dense
use-chips
map-options
emit-value
/>
</div>
<div class="q-pa-sm col-3">
<q-select
v-model="severityFilter"
:options="severityOptions"
label="Severity"
multiple
outlined
dense
use-chips
map-options
emit-value
/>
</div>
<div class="q-pa-sm col-2">
<q-select outlined dense v-model="timeFilter" label="Time" emit-value map-options :options="timeOptions" />
</div>
<div class="q-pa-sm col-2">
<q-checkbox outlined dense v-model="includeSnoozed" label="Include snoozed" />
<q-checkbox outlined dense v-model="includeResolved" label="Include resolved" />
</div>
<div class="q-pa-sm col-1">
<q-btn color="primary" label="Search" @click="search" />
</div>
<div class="col-3">
<q-checkbox outlined dense v-model="includeDismissed" label="Include dismissed alerts?" />
</div>
</q-card-section>
<q-separator />
<q-list separator>
<q-item v-if="alerts.length === 0">No Alerts!</q-item>
<q-item v-for="alert in alerts" :key="alert.id">
<q-card-section>
<q-table
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="tabs-tbl-sticky"
:data="alerts"
:columns="columns"
:rows-per-page-options="[0]"
:pagination.sync="pagination"
:no-data-label="noDataText"
:visible-columns="visibleColumns"
:selected.sync="selectedAlerts"
selection="multiple"
binary-state-sort
row-key="id"
dense
virtual-scroll
>
<template v-slot:top="props">
<div class="col-2 q-table__title">Treats</div>
<q-btn-dropdown label="Bulk Actions" v-if="props.selected > 0">
<q-list dense>
<q-item clickable v-close-popup @click="snoozeAlert(props.selected)">
<q-item-section>
<q-item-label overline>{{ alert.client }} - {{ alert.site }} - {{ alert.hostname }}</q-item-label>
<q-item-label>
<q-icon size="sm" :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
{{ alert.message }}
</q-item-label>
<q-item-label>Snooze alerts</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label caption>{{ alertTime(alert.alert_time) }}</q-item-label>
<q-item-label>
<q-icon name="snooze" size="sm">
<q-tooltip>Snooze the alert for 24 hours</q-tooltip>
</q-icon>
<q-icon name="alarm_off" size="sm">
<q-tooltip>Dismiss alert</q-tooltip>
</q-icon>
</q-item-label>
</q-item>
<q-item clickable v-close-popup @click="resolveAlert(props.selected)">
<q-item-section>
<q-item-label>Resolve alert</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props">
<q-icon name="snooze" size="sm" class="cursor-pointer" @click="snoozeAlert(props.row)">
<q-tooltip>Snooze alert</q-tooltip>
</q-icon>
<q-icon name="alarm_off" size="sm" class="cursor-pointer" @click="resolveAlert(props.row)">
<q-tooltip>Dismiss alert</q-tooltip>
</q-icon>
</q-td>
</template>
<template v-slot:body-cell-severity="props">
<q-td :props="props">
<q-badge :color="alertColor(props.row.severity)">{{ capitalize(props.row.severity) }}</q-badge>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex";
export default {
name: "AlertsOverview",
mixins: [mixins],
data() {
return {
search: "",
includeDismissed: false,
alerts: [],
selectedAlerts: [],
severityFilter: [],
clientFilter: [],
timeFilter: 30,
includeResolved: false,
includeSnoozed: false,
searched: false,
clientsOptions: [],
severityOptions: [
{ label: "Informational", value: "info" },
{ label: "Warning", value: "warning" },
{ label: "Error", value: "error" },
],
timeOptions: [
{ value: 1, label: "1 Day Ago" },
{ value: 7, label: "1 Week Ago" },
{ value: 30, label: "30 Days Ago" },
{ value: 90, label: "3 Months Ago" },
{ value: 180, label: "6 Months Ago" },
{ value: 365, label: "1 Year Ago" },
{ value: 0, label: "Everything" },
],
columns: [
{ name: "alert_time", label: "Time", field: "alert_time", align: "left", sortable: true },
{ name: "hostname", label: "Agent", field: "hostname", align: "left", sortable: true },
{
name: "alert_type",
label: "Type",
field: "alert_type",
align: "left",
sortable: true,
format: a => this.capitalize(a),
},
{ name: "severity", label: "Severity", field: "severity", align: "left", sortable: true },
{ name: "message", label: "Message", field: "message", align: "left", sortable: true },
{ name: "resolved_on", label: "Resolved On", field: "resolved_on", align: "left", sortable: true },
{ name: "snooze_until", label: "Snoozed Until", field: "snoozed_until", align: "left", sortable: true },
{ name: "actions", label: "Actions", align: "left" },
],
pagination: {
rowsPerPage: 0,
sortBy: "alert_time",
descending: true,
},
};
},
computed: {
noDataText() {
return this.searched ? "No data found. Try to refine you search" : "Click search to find alerts";
},
visibleColumns() {
return this.columns.map(column => {
if (column.name === "snooze_until") {
if (this.includeSnoozed) return column.name;
} else if (column.name === "resolved_on") {
if (this.includeResolved) return column.name;
} else {
return column.name;
}
});
},
},
methods: {
getAlerts() {
this.$q.loading.show();
this.$store
.dispatch("alerts/getAlerts")
.then(response => {
this.$axios
.get("alerts/alerts/")
.then(r => {
this.alerts = r.data;
this.$q.loading.hide();
})
.catch(error => {
this.$q.loading.hide();
this.notifyError("Something went wrong");
this.notifyError("There was an issue getting alerts");
});
},
getClients() {
this.$axios.get("/clients/clients/").then(r => {
this.clientsOptions = Object.freeze(r.data.map(client => ({ label: client.name, value: client.id })));
});
},
search() {
this.$q.loading.show();
this.searched = true;
let data = {
snoozedFilter: this.includeSnoozed,
resolvedFilter: this.includeResolved,
};
if (this.clientFilter.length > 0) data["clientFilter"] = this.clientFilter;
if (this.timeFilter) data["timeFilter"] = this.timeFilter;
if (this.severityFilter.length > 0) data["severityFilter"] = this.severityFilter;
this.$axios
.patch("/alerts/alerts/", data)
.then(r => {
this.$q.loading.hide();
this.alerts = Object.freeze(r.data);
})
.catch(e => {
this.notifyError("There was an issue getting alerts");
this.$q.loading.hide();
});
},
snoozeAlert(alert) {
this.$q.loading.show();
const data = {
id: alert.id,
snoozed: true,
};
this.$axios
.put(`alerts/alerts/${alert.id}/`, data)
.then(r => {
this.search();
this.$q.loading.hide();
this.notifySuccess("The alert has been snoozed for 48 hours");
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("There was an issue snoozing alert");
});
},
resolveAlert(alert) {
this.$q.loading.show();
const data = {
id: alert.id,
resolved: true,
};
this.$axios
.put(`alerts/alerts/${alert.id}/`, data)
.then(r => {
this.search();
this.$q.loading.hide();
this.notifySuccess("The alert has been snoozed for 48 hours");
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("There was an issue snoozing alert");
});
},
alertColor(severity) {
@@ -94,14 +278,18 @@ export default {
return "blue";
}
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
computed: {
...mapGetters({
alerts: "alerts/getAlerts",
}),
},
mounted() {
this.getAlerts();
this.getClients();
},
};
</script>

View File

@@ -69,7 +69,7 @@
</q-menu>
</q-chip>
<!--<AlertsIcon />-->
<AlertsIcon />
<q-btn-dropdown flat no-caps stretch :label="user">
<q-list>