mirror of
https://github.com/ntop/ntopng.git
synced 2026-05-03 09:20:10 +00:00
411 lines
16 KiB
Vue
411 lines
16 KiB
Vue
<template>
|
|
<div style="width:100%">
|
|
<div class="mb-1">
|
|
<modal-filters :filters_options="modal_data" @apply="apply_modal" ref="modal_filters"
|
|
:id="id_modal_filters">
|
|
</modal-filters>
|
|
<date-time-range-picker :id="id_data_time_range_picker" :min_time_interval_id="min_time_interval_id"
|
|
:round_time="round_time">
|
|
<template v-slot:begin>
|
|
<div v-if="is_alert_stats_url" style="margin-right:0.1rem;" class="d-flex align-items-center me-2">
|
|
<div class="btn-group" id="statusSwitch" role="group">
|
|
<a v-if="page != 'flow'" href="#" @click="update_status_view('engaged')" class="btn btn-sm"
|
|
:class="{ 'active': status_view == 'engaged', 'btn-seconday': status_view != 'engaged', 'btn-primary': status_view == 'engaged' }"><i
|
|
class="fa-solid fa-fire" title="Engaged"></i></a>
|
|
<a href="#" @click="update_status_view('historical')" class="btn btn-sm"
|
|
:class="{ 'active': status_view == 'historical' || (page == 'flow' && status_view == 'engaged'), 'btn-seconday': status_view != 'historical', 'btn-primary': status_view == 'historical' || (page == 'flow' && status_view == 'engaged') }"><i
|
|
class="fa-regular fa-eye" title="Require Attention"></i></a>
|
|
<!-- <a href="#" @click="update_status_view('acknowledged')" class="btn btn-sm"
|
|
:class="{ 'active': status_view == 'acknowledged', 'btn-seconday': status_view != 'acknowledged', 'btn-primary': status_view == 'acknowledged' }"><i class="fa-solid fa-check-double" title="Acknowledged"></i></a>-->
|
|
<a href="#" @click="update_status_view('any')" class="btn btn-sm"
|
|
:class="{ 'active': status_view == 'any', 'btn-seconday': status_view != 'any', 'btn-primary': status_view == 'any' }"><i
|
|
class="fa-solid fa-inbox" title="All"></i></a>
|
|
</div>
|
|
</div>
|
|
<slot name="begin"></slot>
|
|
</template>
|
|
<template v-slot:extra_buttons>
|
|
<slot name="extra_range_buttons"></slot>
|
|
</template>
|
|
</date-time-range-picker>
|
|
</div>
|
|
|
|
<!-- tagify -->
|
|
<div v-if="page != 'all'" class="d-flex mt-1" style="width:100%">
|
|
<input class="w-100 form-control h-auto" name="tags" ref="tagify"
|
|
:placeholder="i18n('show_alerts.filters')">
|
|
|
|
<button v-show="modal_data && modal_data.length > 0" class="btn btn-link" aria-controls="flow-alerts-table"
|
|
type="button" id="btn-add-alert-filter" @click="show_modal_filters"><span><i class="fas fa-plus"
|
|
data-original-title="" title="Add Filter"></i></span>
|
|
</button>
|
|
|
|
<button v-show="modal_data && modal_data.length > 0" data-bs-toggle="tooltip" data-placement="bottom"
|
|
:title="i18n('show_alerts.remove_filters')" @click="remove_filters"
|
|
class="btn ms-1 my-auto btn-sm btn-remove-tags">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<!-- end tagify -->
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script type="text/javascript">
|
|
import { default as DateTimeRangePicker } from "./date-time-range-picker.vue";
|
|
import { default as ModalFilters } from "./modal-filters.vue";
|
|
import { default as dataUtils } from "../utilities/data-utils.js";
|
|
import filtersManager from "../utilities/filters-manager.js";
|
|
import Tagify from '@yaireo/tagify'
|
|
|
|
function get_page(alert_stats_page) {
|
|
let page = ntopng_url_manager.get_url_entry("page");
|
|
if (page == null) {
|
|
if (alert_stats_page) {
|
|
page = "all";
|
|
} else {
|
|
page = "overview";
|
|
}
|
|
}
|
|
return page;
|
|
}
|
|
|
|
async function get_filter_const(is_alert_stats_url, page) {
|
|
let url_request;
|
|
let query_preset = ntopng_url_manager.get_url_entry("query_preset");
|
|
if (query_preset == null) { query_preset = ""; }
|
|
if (is_alert_stats_url) {
|
|
url_request = `${http_prefix}/lua/rest/v2/get/alert/filter/consts.lua?page=${page}&query_preset=${query_preset}`;
|
|
} else {
|
|
let aggregated = ntopng_url_manager.get_url_entry("aggregated") || "";
|
|
url_request = `${http_prefix}/lua/pro/rest/v2/get/db/filter/consts.lua?page=${page}&query_preset=${query_preset}&aggregated=${aggregated}`;
|
|
}
|
|
let filter_consts = await ntopng_utility.http_request(url_request);
|
|
return filter_consts;
|
|
}
|
|
|
|
let FILTERS_CONST = [];
|
|
let TAG_OPERATORS;
|
|
let DEFINED_TAGS;
|
|
const VIEW_ONLY_TAGS = true;
|
|
/* Initial Tags */
|
|
let initialTags;
|
|
//let pageHandle = {};
|
|
let TAGIFY;
|
|
let IS_ALERT_STATS_URL = window.location.toString().match(/alert_stats.lua/) != null;
|
|
let STATUS_VIEW = ntopng_url_manager.get_url_entry("status");
|
|
if (STATUS_VIEW == null || STATUS_VIEW == "") {
|
|
STATUS_VIEW = "historical";
|
|
}
|
|
|
|
let PAGE = get_page(IS_ALERT_STATS_URL);
|
|
|
|
const create_tag_from_filter = function (filter) {
|
|
let f_const = FILTERS_CONST.find((f) => f.id == filter.id);
|
|
if (f_const == null) { console.error("create_tag_from_filter: filter const not found;"); }
|
|
|
|
let value_label = filter.value;
|
|
if (f_const.options != null) {
|
|
let opt = f_const.options.find((o) => o.value == filter.value);
|
|
if (opt != null) {
|
|
value_label = opt.label;
|
|
}
|
|
}
|
|
const tag = {
|
|
label: f_const.label,
|
|
key: f_const.id,
|
|
value: value_label,
|
|
realValue: filter.value,
|
|
title: `${f_const.label}${filter.operator}${value_label}`,
|
|
selectedOperator: filter.operator,
|
|
};
|
|
if (tag.value == "") { tag.value = "''" }
|
|
if (tag.realValue == null || tag.selectedOperator == null || tag.selectedOperator == "") {
|
|
return null;
|
|
}
|
|
return tag;
|
|
}
|
|
|
|
const load_filters_data = async function () {
|
|
FILTERS_CONST = await get_filter_const(IS_ALERT_STATS_URL, PAGE);
|
|
FILTERS_CONST.filter((x) => x.label == null).forEach((x) => { console.error(`label not defined for filter ${JSON.stringify(x)}`); x.label = ""; });
|
|
FILTERS_CONST.sort((a, b) => a.label.localeCompare(b.label));
|
|
i18n_ext.tags = {};
|
|
TAG_OPERATORS = {};
|
|
DEFINED_TAGS = {};
|
|
FILTERS_CONST.forEach((f_def) => {
|
|
i18n_ext.tags[f_def.id] = f_def.label;
|
|
f_def.operators.forEach((op) => TAG_OPERATORS[op.id] = op.label);
|
|
DEFINED_TAGS[f_def.id] = f_def.operators.map((op) => op.id);
|
|
});
|
|
let entries = ntopng_url_manager.get_url_entries();
|
|
let filters = [];
|
|
for (const [key, value] of entries) {
|
|
let filter_def = FILTERS_CONST.find((fc) => fc.id == key);
|
|
if (filter_def != null) {
|
|
let options_string = value.split(",");
|
|
options_string.forEach((opt_string) => {
|
|
let [value, operator] = opt_string.split(";");
|
|
let value_label = value;
|
|
if (filter_def.value_type == "array") {
|
|
value_label = filter_def?.options?.find((opt) => opt.value == value)?.label;
|
|
}
|
|
filters.push({ id: filter_def.id, operator: operator, value: value, label: filter_def.label, value_label });
|
|
});
|
|
}
|
|
}
|
|
return filters;
|
|
// "l7proto=XXX;eq"
|
|
}
|
|
|
|
export default {
|
|
props: {
|
|
id: String,
|
|
min_time_interval_id: String,
|
|
round_time: Boolean,
|
|
},
|
|
components: {
|
|
'date-time-range-picker': DateTimeRangePicker,
|
|
'modal-filters': ModalFilters,
|
|
},
|
|
/**
|
|
* First method called when the component is created.
|
|
*/
|
|
created() {
|
|
},
|
|
async mounted() {
|
|
let dt_range_picker_mounted = ntopng_sync.on_ready(this.id_data_time_range_picker);
|
|
let modal_filters_mounted = ntopng_sync.on_ready(this.id_modal_filters);
|
|
await dt_range_picker_mounted;
|
|
|
|
if (this.page != 'all') {
|
|
let filters = await load_filters_data();
|
|
|
|
TAGIFY = create_tagify(this);
|
|
ntopng_events_manager.emit_event(ntopng_events.FILTERS_CHANGE, { filters });
|
|
ntopng_events_manager.on_event_change(this.$props["id"], ntopng_events.FILTERS_CHANGE, (status) => this.reload_status(status), true);
|
|
}
|
|
this.modal_data = FILTERS_CONST;
|
|
|
|
//await modal_filters_mounted;
|
|
ntopng_sync.ready(this.$props["id"]);
|
|
},
|
|
data() {
|
|
return {
|
|
i18n: i18n,
|
|
id_modal_filters: `${this.$props.id}_modal_filters`,
|
|
id_data_time_range_picker: `${this.$props.id}_date-time-range-picker`,
|
|
show_filters: false,
|
|
edit_tag: null,
|
|
is_alert_stats_url: IS_ALERT_STATS_URL,
|
|
status_view: STATUS_VIEW,
|
|
page: PAGE,
|
|
modal_data: [],
|
|
last_filters: [],
|
|
};
|
|
},
|
|
methods: {
|
|
is_filter_defined: function (filter) {
|
|
return DEFINED_TAGS[filter.id] != null;
|
|
},
|
|
update_status_view: function (status) {
|
|
ntopng_url_manager.set_key_to_url("status", status);
|
|
ntopng_url_manager.reload_url();
|
|
},
|
|
show_modal_filters: function () {
|
|
this.$refs["modal_filters"].show();
|
|
},
|
|
remove_filters: function () {
|
|
let filters = [];
|
|
ntopng_events_manager.emit_event(ntopng_events.FILTERS_CHANGE, { filters });
|
|
},
|
|
reload_status: function (status) {
|
|
let filters = status.filters;
|
|
if (filters == null) { return; }
|
|
// delete all previous filter
|
|
ntopng_url_manager.delete_params(FILTERS_CONST.map((f) => f.id));
|
|
TAGIFY.tagify.removeAllTags();
|
|
const filters_object = filtersManager.get_filters_object(filters);
|
|
let filters_to_add = {}
|
|
for (const key in filters_object) {
|
|
const value = filters_object[key]
|
|
filters_to_add[key] = filters_object[key]
|
|
}
|
|
ntopng_url_manager.add_obj_to_url(filters_to_add);
|
|
filters.forEach((f) => {
|
|
//if (!dataUtils.isEmptyOrNull(f.value)) {
|
|
let tag = create_tag_from_filter(f);
|
|
if (tag == null) { return; }
|
|
TAGIFY.addFilterTag(tag);
|
|
//}
|
|
});
|
|
this.last_filters = filters;
|
|
},
|
|
apply_modal: function (params) {
|
|
let status = ntopng_status_manager.get_status();
|
|
let filters = status.filters;
|
|
if (filters == null) { filters = []; }
|
|
if (this.edit_tag != null) {
|
|
filters = filters.filter((f) => f.id != this.edit_tag.key || f.value != this.edit_tag.realValue);
|
|
this.edit_tag = null;
|
|
}
|
|
filters.push(params);
|
|
// trigger event and then call reload_status
|
|
ntopng_events_manager.emit_event(ntopng_events.FILTERS_CHANGE, { filters });
|
|
},
|
|
},
|
|
};
|
|
|
|
function create_tagify(range_picker_vue) {
|
|
// create tagify
|
|
const tagify = new Tagify(range_picker_vue.$refs["tagify"], {
|
|
duplicates: true,
|
|
delimiters: null,
|
|
dropdown: {
|
|
enabled: 1, // suggest tags after a single character input
|
|
classname: 'extra-properties' // custom class for the suggestions dropdown
|
|
},
|
|
autoComplete: { enabled: false },
|
|
templates: {
|
|
tag: function (tagData) {
|
|
try {
|
|
return `<tag title='${tagData.value}' contenteditable='false' spellcheck="false" class='tagify__tag'>
|
|
<x title='remove tag' class='tagify__tag__removeBtn'></x>
|
|
<div>
|
|
<b>${tagData.label ? tagData.label : tagData.key}</b>
|
|
<b class='operator'>${tagData.selectedOperator ? TAG_OPERATORS[tagData.selectedOperator] : '='}</b>
|
|
<span class='tagify__tag-text'>${tagData.value == "''" ? '' : tagData.value}</span>
|
|
</div>
|
|
</tag>`
|
|
}
|
|
catch (err) {
|
|
console.error(`An error occured when creating a new tag: ${err}`);
|
|
}
|
|
},
|
|
},
|
|
validate: function (tagData) {
|
|
return (typeof tagData.key !== 'undefined' &&
|
|
typeof tagData.selectedOperator !== 'undefined' &&
|
|
typeof tagData.value !== 'undefined');
|
|
}
|
|
});
|
|
|
|
$(document).ready(function () {
|
|
// add existing tags
|
|
tagify.addTags(initialTags);
|
|
}); /* $(document).ready() */
|
|
|
|
const createValueFromTag = function (tag) {
|
|
if (!tag.selectedOperator) tag.selectedOperator = 'eq';
|
|
let val = tag.realValue != null ? tag.realValue : tag.value;
|
|
let value = `${val};${tag.selectedOperator}`;
|
|
return value;
|
|
}
|
|
|
|
const addFilterTag = async function (tag) {
|
|
/* Convert values to string (this avoids issues e.g. with 0) */
|
|
if (typeof tag.realValue === 'number') { tag.realValue = '' + tag.realValue; }
|
|
if (typeof tag.value === 'number') { tag.value = '' + tag.value; }
|
|
|
|
const existingTagElms = tagify.getTagElms();
|
|
|
|
/* Lookup by key, value and operator (do not add the same key and value multiple times) */
|
|
let existingTagElement = existingTagElms.find(htmlTag =>
|
|
htmlTag.getAttribute('key') === tag.key
|
|
&& htmlTag.getAttribute('realValue') === tag.realValue
|
|
);
|
|
if (existingTagElement && tagify.getSetTagData(existingTagElement) !== undefined) {
|
|
return;
|
|
}
|
|
|
|
// has the tag an operator object?
|
|
if (DEFINED_TAGS[tag.key] && !Array.isArray(DEFINED_TAGS[tag.key])) {
|
|
tag.operators = DEFINED_TAGS[tag.key].operators;
|
|
}
|
|
|
|
if (!tag.selectedOperator) {
|
|
tag.selectedOperator = 'eq';
|
|
}
|
|
// add filter!
|
|
tagify.addTags([tag]);
|
|
}
|
|
|
|
// when an user remove the tag
|
|
tagify.on('remove', async function (e) {
|
|
const detail = e.detail;
|
|
if (detail.data === undefined) { return; }
|
|
const tag = detail.data;
|
|
const key = tag?.key;
|
|
const value = tag?.realValue;
|
|
const status = ntopng_status_manager.get_status();
|
|
|
|
if (key === undefined) { return; }
|
|
if (status.filters == null) { return; }
|
|
|
|
const filters = status.filters.filter((f) => (f.id != key || (f.id == key && f.value != value)));
|
|
ntopng_events_manager.emit_event(ntopng_events.FILTERS_CHANGE, { filters });
|
|
});
|
|
|
|
tagify.on('add', async function (e) {
|
|
const detail = e.detail;
|
|
if (detail.data === undefined) { return; }
|
|
const tag = detail.data;
|
|
// let's check if the tag has a key field
|
|
if (!tag.key) {
|
|
tagify.removeTags([e.detail.tag]);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
});
|
|
|
|
// Tag 'click' event handler to open the 'Edit' modal. Note: this prevents
|
|
// inline editing of the tag ('edit:updated' is never called as a consequence)
|
|
tagify.on('click', async function (e) {
|
|
const detail = e.detail;
|
|
if (detail.data === undefined) { return; }
|
|
if (detail.data.key === undefined) { return; }
|
|
const tag = detail.data;
|
|
// remember that this tag already exixts
|
|
range_picker_vue.edit_tag = tag;
|
|
// show modal-filters
|
|
ntopng_events_manager.emit_custom_event(ntopng_custom_events.SHOW_MODAL_FILTERS, { id: tag.key, operator: tag.selectedOperator, value: tag.realValue });
|
|
});
|
|
|
|
return {
|
|
tagify,
|
|
addFilterTag,
|
|
};
|
|
}
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
.tagify__input {
|
|
min-width: 175px;
|
|
}
|
|
|
|
.tagify__tag {
|
|
white-space: nowrap;
|
|
margin: 3px 0px 5px 5px;
|
|
}
|
|
|
|
.tagify__tag select.operator {
|
|
margin: 0px 4px;
|
|
border: 1px solid #c4c4c4;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.tagify__tag b.operator {
|
|
margin: 0px 4px;
|
|
background-color: white;
|
|
border: 1px solid #c4c4c4;
|
|
border-radius: 4px;
|
|
padding: 0.05em 0.2em;
|
|
}
|
|
|
|
.tagify__tag>div {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
</style>
|