Files
tacticalrmm/web/src/components/modals/alerts/AlertsOverview.vue

548 lines
15 KiB
Vue

<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>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<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-2">
<q-btn color="primary" label="Search" @click="search" />
</div>
</div>
<q-separator />
<q-card-section>
<q-table
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
class="audit-mgr-tbl-sticky"
:rows="alerts"
:columns="columns"
:rows-per-page-options="[25, 50, 100, 500, 1000]"
v-model:pagination="pagination"
:no-data-label="noDataText"
:visible-columns="visibleColumns"
v-model:selected="selectedAlerts"
selection="multiple"
binary-state-sort
row-key="id"
dense
virtual-scroll
>
<template v-slot:top>
<div class="col-1 q-table__title">Alerts</div>
<q-btn-dropdown
flat
label="Bulk Actions"
:disable="selectedAlerts.length === 0 || includeResolved"
>
<q-list dense>
<q-item
clickable
v-close-popup
@click="snoozeAlertBulk(selectedAlerts)"
>
<q-item-section avatar>
<q-icon name="alarm_off" />
</q-item-section>
<q-item-section>
<q-item-label>Snooze alerts</q-item-label>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="resolveAlertBulk(selectedAlerts)"
>
<q-item-section avatar>
<q-icon name="flag" />
</q-item-section>
<q-item-section>
<q-item-label>Resolve alerts</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
v-if="props.row.action_run"
name="mdi-archive-alert"
size="sm"
class="cursor-pointer"
@click="showScriptOutput(props.row, true)"
>
<q-tooltip>Show failure action run results</q-tooltip>
</q-icon>
<q-icon
v-if="props.row.resolved_action_run"
name="mdi-archive-check"
size="sm"
class="cursor-pointer"
@click="showScriptOutput(props.row, false)"
>
<q-tooltip>Show resolved action run results</q-tooltip>
</q-icon>
<q-icon
v-if="!props.row.resolved && !props.row.snoozed"
name="snooze"
size="sm"
class="cursor-pointer"
@click="snoozeAlert(props.row)"
>
<q-tooltip>Snooze alert</q-tooltip>
</q-icon>
<q-icon
v-else-if="!props.row.resolved && props.row.snoozed"
name="alarm_off"
size="sm"
class="cursor-pointer"
@click="unsnoozeAlert(props.row)"
>
<q-tooltip>Unsnooze alert</q-tooltip>
</q-icon>
<q-icon
v-if="!props.row.resolved"
name="flag"
size="sm"
class="cursor-pointer"
@click="resolveAlert(props.row)"
>
<q-tooltip>Resolve 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>
<template v-slot:body-cell-alert_time="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-resolve_on="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-snoozed_until="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
import ScriptOutput from "@/components/checks/ScriptOutput";
import { computed } from "vue";
import { useStore } from "vuex";
export default {
name: "AlertsOverview",
emits: ["hide"],
mixins: [mixins],
setup() {
// setup vuex store
const store = useStore();
const formatDate = computed(() => store.getters.formatDate);
return {
formatDate,
};
},
data() {
return {
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, true),
},
{
name: "severity",
label: "Severity",
field: "severity",
align: "left",
sortable: true,
},
{
name: "message",
label: "Message",
field: "message",
align: "left",
sortable: true,
},
{
name: "resolve_on",
label: "Resolved On",
field: "resolve_on",
align: "left",
sortable: true,
},
{
name: "snoozed_until",
label: "Snoozed Until",
field: "snoozed_until",
align: "left",
sortable: true,
},
{ name: "actions", label: "Actions", align: "left" },
],
pagination: {
rowsPerPage: 50,
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 === "snoozed_until") {
if (this.includeSnoozed) return column.name;
} else if (column.name === "resolve_on") {
if (this.includeResolved) return column.name;
} else {
return column.name;
}
});
},
},
methods: {
getClients() {
this.$axios.get("/clients/").then((r) => {
this.clientsOptions = Object.freeze(
r.data.map((client) => ({ label: client.name, value: client.id }))
);
});
},
search() {
this.$q.loading.show();
this.selectedAlerts = [];
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/", data)
.then((r) => {
this.$q.loading.hide();
this.alerts = Object.freeze(r.data);
})
.catch(() => {
this.$q.loading.hide();
});
},
snoozeAlert(alert) {
this.$q
.dialog({
title: "Snooze Alert",
message: "How many days to snooze alert?",
prompt: {
model: "",
type: "number",
isValid: (val) => !!val && val > 0 && val < 9999,
},
cancel: true,
})
.onOk((days) => {
this.$q.loading.show();
const data = {
id: alert.id,
type: "snooze",
snooze_days: days,
};
this.$axios
.put(`alerts/${alert.id}/`, data)
.then(() => {
this.search();
this.$q.loading.hide();
this.notifySuccess(`The alert has been snoozed for ${days} days`);
})
.catch(() => {
this.$q.loading.hide();
});
});
},
unsnoozeAlert(alert) {
this.$q.loading.show();
const data = {
id: alert.id,
type: "unsnooze",
};
this.$axios
.put(`alerts/${alert.id}/`, data)
.then(() => {
this.search();
this.$q.loading.hide();
this.notifySuccess("The alert has been unsnoozed");
})
.catch(() => {
this.$q.loading.hide();
});
},
resolveAlert(alert) {
this.$q.loading.show();
const data = {
id: alert.id,
type: "resolve",
};
this.$axios
.put(`alerts/${alert.id}/`, data)
.then(() => {
this.search();
this.$q.loading.hide();
this.notifySuccess("The alert has been resolved");
})
.catch(() => {
this.$q.loading.hide();
});
},
resolveAlertBulk(alerts) {
this.$q.loading.show();
const data = {
alerts: alerts.map((alert) => alert.id),
bulk_action: "resolve",
};
this.$axios
.post("alerts/bulk/", data)
.then(() => {
this.search();
this.$q.loading.hide();
this.notifySuccess("Alerts were resolved");
})
.catch(() => {
this.$q.loading.hide();
});
},
snoozeAlertBulk(alerts) {
this.$q
.dialog({
title: "Snooze Alert",
message: "How many days to snooze alert?",
prompt: {
model: "",
type: "number",
isValid: (val) => !!val && val > 0 && val < 9999,
},
cancel: true,
})
.onOk((days) => {
this.$q.loading.show();
const data = {
alerts: alerts.map((alert) => alert.id),
bulk_action: "snooze",
snooze_days: days,
};
this.$axios
.post("alerts/bulk/", data)
.then(() => {
this.search();
this.$q.loading.hide();
this.notifySuccess(`Alerts were snoozed for ${days} days`);
})
.catch(() => {
this.$q.loading.hide();
});
});
},
showScriptOutput(alert, failure = false) {
let results = {};
if (failure) {
results.readable_desc = `${alert.alert_type} failure action results`;
results.execution_time = alert.action_execution_time;
results.retcode = alert.action_retcode;
results.stdout = alert.action_stdout;
results.errout = alert.action_errout;
results.last_run = alert.action_run;
} else {
results.readable_desc = `${alert.alert_type} resolved action results`;
results.execution_time = alert.resolved_action_execution_time;
results.retcode = alert.resolved_action_retcode;
results.stdout = alert.resolved_action_stdout;
results.errout = alert.resolved_action_errout;
results.last_run = alert.resolved_action_run;
}
this.$q.dialog({
component: ScriptOutput,
componentProps: {
scriptInfo: results,
},
});
},
alertColor(severity) {
if (severity === "error") {
return "red";
}
if (severity === "warning") {
return "orange";
}
if (severity === "info") {
return "info";
}
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
},
mounted() {
this.getClients();
},
};
</script>