ntopng/http_src/vue/range-picker.vue
2025-09-18 16:38:33 +02:00

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>&nbsp;
<b class='operator'>${tagData.selectedOperator ? TAG_OPERATORS[tagData.selectedOperator] : '='}</b>&nbsp;
<span class='tagify__tag-text'>${tagData.value == "&#039;&#039;" ? '' : 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>