mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-29 23:49:33 +00:00
818 lines
30 KiB
Vue
818 lines
30 KiB
Vue
<!-- (C) 2022 - ntop.org -->
|
|
<template>
|
|
<slot name="custom_header2"></slot>
|
|
<div ref="tableContainerRef" :id="id">
|
|
<Loading :isLoading="isLoading"></Loading>
|
|
<div class="button-group mb-2 d-flex align-items-center"> <!-- TableHeader -->
|
|
<div class="form-group d-flex align-items-end" style="flex-wrap: wrap;">
|
|
<!-- Slot for custom header-->
|
|
<slot name="custom_header"></slot>
|
|
</div>
|
|
|
|
<div style="text-align:right;" class="form-group d-flex align-items-center ms-auto">
|
|
<div class="d-flex align-items-center">
|
|
<!-- Rows per page selector-->
|
|
<div class="me-2">
|
|
<label>
|
|
<select v-model="rowsPerPage" @change="change_per_page">
|
|
<option v-for="rowsPerPage in rowsPerPageOptions" :value="rowsPerPage">{{ rowsPerPage }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Custom buttons slot -->
|
|
<slot name="custom_buttons"></slot>
|
|
|
|
<!-- Reset columns size-->
|
|
<button class="btn btn-link" type="button" @click="reset_column_size">
|
|
<i class="fas fa-columns" data-bs-toggle="tooltip" data-bs-placement="top"
|
|
:title="_i18n('reset_column')"></i>
|
|
</button>
|
|
|
|
<!-- Refresh table -->
|
|
<button class="btn btn-link" type="button" @click="refresh_table()">
|
|
<i class="fas fa-refresh" data-bs-toggle="tooltip" data-bs-placement="top"
|
|
:title="_i18n('refresh')"></i>
|
|
</button>
|
|
|
|
<!-- Autorefresh toggle -->
|
|
<div v-if="show_autorefresh > 0" class="d-inline-block">
|
|
<Switch v-model:value="isAutoRefreshEnabled" class="me-2 mt-1" :title="autorefresh_title"
|
|
style="" @change_value="update_autorefresh">
|
|
</Switch>
|
|
</div>
|
|
|
|
<Dropdown :id="id + '_dropdown'" ref="dropdownRef"> <!-- Dropdown columns -->
|
|
<template v-slot:title>
|
|
<i class="fas fa-eye" data-bs-toggle="tooltip" data-bs-placement="top"
|
|
:title="_i18n('visible_columns')"></i>
|
|
</template>
|
|
<template v-slot:menu>
|
|
<div v-for="col in processedColumns" class="form-check form-switch ms-1">
|
|
<input class="form-check-input" style="cursor:pointer;" :checked="col.visible == true"
|
|
@click="change_columns_visibility(col)" type="checkbox" :id="get_col_id(col)">
|
|
<label class="form-check-label" :for="get_col_id(col)"
|
|
v-html="print_column_name(col.data)">
|
|
</label>
|
|
</div>
|
|
</template>
|
|
</Dropdown> <!-- Dropdown columns -->
|
|
|
|
<!-- Columns search if enabled in table json definition -->
|
|
<div v-if="enable_search" class="d-inline me-2 ms-auto">
|
|
<label>{{ _i18n('search') }}:
|
|
<input type="search" v-model="searchString" @input="on_change_map_search" class="">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div> <!-- TableHeader -->
|
|
|
|
<Transition name="list" mode="out-in">
|
|
<div :key="table_key" style="overflow:auto;width:100%;"> <!-- Table -->
|
|
|
|
<!-- Message display -->
|
|
<div v-if="display_message == true" class="centered-message">
|
|
<span v-html="message_to_display"></span>
|
|
</div>
|
|
|
|
<table ref="tableRef" class="table table-striped table-bordered ml-0 mr-0 mb-0 ntopng-table"
|
|
data-resizable="true" :data-resizable-columns-id="id"> <!-- Table -->
|
|
<thead>
|
|
<tr>
|
|
<!-- Column Headers -->
|
|
<template v-for="(col, col_index) in processedColumns">
|
|
<th v-if="col.visible" scope="col" :class="[
|
|
{ 'pointer': col.sortable, 'unset': !col.sortable, },
|
|
{ 'sticky-column-th': col.sticky }
|
|
]" style="white-space: nowrap;" :style="[
|
|
(col.min_width ? 'min-width: ' + col.min_width + ';' : ''),
|
|
]" @click="change_column_sort(col, col_index)"
|
|
:data-resizable-column-id="get_column_id(col.data)">
|
|
<div style="display:flex;">
|
|
<!-- Print column name -->
|
|
<span v-html="print_column_name(col.data)" class="wrap-column"></span>
|
|
|
|
<!-- Sort indicators, 0 double arrow, else up or down-->
|
|
<!-- <i v-show="col.sort == 0" class="fa fa-fw fa-sort"></i> -->
|
|
<i v-show="col.sort == 1 && col.sortable" class="fa fa-fw fa-sort-up"></i>
|
|
<i v-show="col.sort == 2 && col.sortable" class="fa fa-fw fa-sort-down"></i>
|
|
</div>
|
|
</th>
|
|
</template>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Data rows -->
|
|
<tr v-if="!isChangingColumnVisibility && !isChangingRows" v-for="row in displayedRows">
|
|
<template v-for="(col, col_index) in processedColumns">
|
|
<td v-if="col.visible" scope="col" :class="[
|
|
{ 'sticky-column-td': col.sticky }
|
|
]">
|
|
<!-- HTML content if provided -->
|
|
<div v-if="print_html_row != null && print_html_row(col.data, row, true) != null"
|
|
:class="col.classes" class="wrap-column" :style="col.style"
|
|
v-html="print_html_row(col.data, row)">
|
|
</div>
|
|
<div :style="col.style" style="" class="wrap-column margin-sm" :class="col.classes">
|
|
<!-- Vue node if provided -->
|
|
<VueNode :key="row"
|
|
v-if="print_vue_node_row != null && print_vue_node_row(col.data, row, vue_obj, true) != null"
|
|
:content="print_vue_node_row(col.data, row, vue_obj)"></VueNode>
|
|
</div>
|
|
</td>
|
|
</template>
|
|
</tr>
|
|
<!-- Show empty rows if present -->
|
|
<tr v-if="display_empty_rows && displayedRows.length < rowsPerPage"
|
|
v-for="index in (rowsPerPage - displayedRows.length)">
|
|
<template v-for="(col, col_index) in processedColumns">
|
|
<td style="" class="" v-if="col.visible" scope="col">
|
|
<div class="wrap-column"></div>
|
|
</td>
|
|
</template>
|
|
</tr>
|
|
</tbody>
|
|
</table> <!-- Table -->
|
|
</div> <!-- Table div-->
|
|
</Transition>
|
|
|
|
<div>
|
|
<!-- Pagination component, bottom right -->
|
|
<SelectTablePage ref="paginationRef" :key="searchDelay" :total_rows="totalRowCount" :per_page="rowsPerPage"
|
|
@change_active_page="change_active_page">
|
|
</SelectTablePage>
|
|
</div>
|
|
|
|
<!-- SQL Query info footer, if present -->
|
|
<div v-if="query_info != null" class="mt-2">
|
|
<div class="text-end">
|
|
<small style="" class="query text-end"><span class="records">{{ query_info.num_records_processed
|
|
}}</span>.</small>
|
|
</div>
|
|
<div class="text-start">
|
|
<small id="historical_flows_table-query-time" style="" class="query">Query performed in <span
|
|
class="seconds">{{
|
|
(query_info.query_duration_msec / 1000).toFixed(3) }}</span> seconds. <span
|
|
id="historical_flows_table-query" style="cursor: pointer;" class="badge bg-secondary"
|
|
:title=query_info.query @click="copy_query_into_clipboard" ref="sqlButtonRef">SQL</span></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed, watch, nextTick, onUpdated, Transition } from "vue";
|
|
import { h } from 'vue';
|
|
import { ntopng_utility, ntopng_url_manager } from "../services/context/ntopng_globals_services.js";
|
|
import { default as Dropdown } from "./dropdown.vue";
|
|
import { default as SelectTablePage } from "./select_table_page.vue";
|
|
import { default as VueNode } from "./vue_node.vue";
|
|
import { default as Switch } from "./switch.vue";
|
|
import { default as Loading } from "./loading.vue";
|
|
import NtopUtils from "../utilities/ntop-utils.js";
|
|
|
|
/* rows_loaded, is emitted every time the rows are loaded,
|
|
* loaded, is emitted when the table is loaded (mounted)
|
|
*/
|
|
const emit = defineEmits(['custom_event', 'loaded', 'rows_loaded']);
|
|
const vue_obj = {
|
|
emit,
|
|
h,
|
|
nextTick,
|
|
};
|
|
|
|
const props = defineProps({
|
|
id: String, // table id
|
|
columns: Array, // table columns
|
|
get_rows: Function, // async (currentPage: number, rowsPerPage: number, processedColumns: any[], search_map: string, isFirstDataLoad: boolean) => { totalRowCount: number, rows: any[], query_info: { query_duration_msec: number, num_records_processed: string, query: string } }
|
|
get_column_id: Function, // function to
|
|
print_column_name: Function, // Function to render column header text/HTML
|
|
print_html_row: Function, // Function to render cell HTML content
|
|
print_vue_node_row: Function, // Function to render Vue component in cell
|
|
f_is_column_sortable: Function, // Function to determine if column is sortable
|
|
f_column_min_width: Function, // Function to determine column minimum width
|
|
f_sort_rows: Function, // Function for custom row sorting
|
|
f_get_column_classes: Function, // Function to get CSS classes for column
|
|
f_get_column_style: Function, // Function to get inline styles for column
|
|
enable_search: Boolean, // Enable search functionality
|
|
display_empty_rows: Boolean, // Show empty rows to maintain table height
|
|
show_autorefresh: Number, // autorefresh seconds, if null or 0 autorefresh switch will not showed
|
|
default_sort: Object, // { column_id: string, sort: number (0, 1, 2) }
|
|
csrf: String, // CSRF token for API calls
|
|
paging: Boolean, // Enable server-side pagination
|
|
display_message: Boolean, // Display a message instead of the table
|
|
message_to_display: String, // Message to display
|
|
showLoading: Boolean,
|
|
});
|
|
|
|
// Get number of rows shown per page from local storage
|
|
const get_num_pages = function () {
|
|
return Number(localStorage.getItem("ntopng.tables.rowPerPage")) || 10;
|
|
}
|
|
|
|
const _i18n = (t) => i18n(t);
|
|
|
|
const tableContainerRef = ref(null); // Reference to the table container
|
|
const tableRef = ref(null); // Reference to the table element
|
|
const dropdownRef = ref(null); // Reference to the column visibility dropdown
|
|
const rowElementRefs = ref([]); // References to row HTML elements
|
|
const showLoading = ref(props.showLoading)
|
|
let currentPage = 0; // Current active page
|
|
let allRows = []; // All fetched rows data
|
|
const processedColumns = ref([]); // Wrapped column definitions with extra properties
|
|
const displayedRows = ref([]); // Rows displayed in the current page
|
|
const totalRowCount = ref(0); // Total number of rows (for pagination)
|
|
const rowsPerPageOptions = [10, 20, 40, 50, 80, 100]; // Available rows per page options
|
|
const rowsPerPage = ref(get_num_pages()); // Current rows per page setting
|
|
const columnWidthStore = window.store; // Store for column width persistence
|
|
const searchString = ref(""); // Search term
|
|
const isLoading = ref(showLoading.value ? showLoading.value : false);
|
|
|
|
const paginationRef = ref(null); // Reference to pagination component
|
|
const query_info = ref(null); // Query execution info (time, records, SQL)
|
|
const sqlButtonRef = ref(null); // Reference to SQL button for copy to clipboard
|
|
const isChangingColumnVisibility = ref(false); // Flag for column visibility changes
|
|
const isChangingRows = ref(false); // Flag for row data changes
|
|
const isAutoRefreshEnabled = ref(false); // Auto-refresh state
|
|
|
|
onMounted(async () => {
|
|
if (props.columns != null) {
|
|
load_table();
|
|
}
|
|
});
|
|
|
|
// refresh bootstrap tooltip
|
|
onUpdated(() => {
|
|
NtopUtils.reloadBSTooltips();
|
|
});
|
|
|
|
// autorefresh tooltip text
|
|
const autorefresh_title = computed(() => {
|
|
if (props.show_autorefresh == null || props.show_autorefresh <= 0) {
|
|
return "";
|
|
}
|
|
let text = _i18n("table.autorefresh");
|
|
return text.replace("%time", props.show_autorefresh);
|
|
});
|
|
|
|
watch(() => [props.id, props.columns], (cur_value, old_value) => {
|
|
load_table();
|
|
}, { flush: 'pre' });
|
|
|
|
// get column id as defined in the json of the table definition
|
|
function get_col_id(col) {
|
|
if (col != null && col.id != null) {
|
|
return col.id;
|
|
} else {
|
|
return "toggle-Begin";
|
|
}
|
|
}
|
|
|
|
function set_dropdown_listener() {
|
|
document.querySelectorAll('.dropdown').forEach(dropdown => {
|
|
dropdown.addEventListener('show.bs.dropdown', function () {
|
|
this.closest('td')?.classList.add('dropdown-active');
|
|
});
|
|
|
|
dropdown.addEventListener('hide.bs.dropdown', function () {
|
|
this.closest('td')?.classList.remove('dropdown-active');
|
|
});
|
|
});
|
|
}
|
|
|
|
// init table with columns and rows
|
|
async function load_table() {
|
|
await set_columns_wrap(); // Prepare column definitions
|
|
await set_rows(); // Fetch initial rows
|
|
set_columns_resizable(); // Initialize resizable columns
|
|
dropdownRef.value.load_menu(); // Initialize dropdown menu
|
|
set_dropdown_listener();
|
|
emit("loaded"); // Emit loaded event
|
|
}
|
|
|
|
// autorefresh interval and enabling
|
|
let refreshInterval;
|
|
function update_autorefresh() {
|
|
if (isAutoRefreshEnabled.value == false) {
|
|
clearInterval(refreshInterval);
|
|
return;
|
|
}
|
|
refreshInterval = setInterval(() => {
|
|
change_active_page();
|
|
}, props.show_autorefresh * 1000);
|
|
}
|
|
|
|
async function change_columns_visibility(col) {
|
|
isChangingColumnVisibility.value = true;
|
|
col.visible = !col.visible;
|
|
|
|
// if server side pagination, get rows
|
|
if (props.paging) {
|
|
await set_rows();
|
|
}
|
|
|
|
// redraw with new visibility
|
|
// redraw_table();
|
|
await redraw_table_resizable();
|
|
await set_columns_visibility();
|
|
// set_columns_resizable();
|
|
isChangingColumnVisibility.value = false;
|
|
}
|
|
|
|
async function redraw_table_resizable() {
|
|
await redraw_table();
|
|
set_columns_resizable();
|
|
}
|
|
|
|
// increase table key to force vuejs to rerender table
|
|
const table_key = ref(0);
|
|
async function redraw_table() {
|
|
table_key.value += 1;
|
|
await nextTick();
|
|
}
|
|
|
|
function set_columns_resizable() {
|
|
let options = {
|
|
store: columnWidthStore, // persist column width
|
|
minWidth: 70,
|
|
};
|
|
$(tableRef.value).resizableColumns(options);
|
|
}
|
|
|
|
// get table configuration
|
|
async function get_columns_visibility_dict() {
|
|
if (props.csrf == null) { return {}; }
|
|
|
|
// build request parameters
|
|
// fetches tableId (in httpdocs/tables_config/<tableId>.json where the table scheme is defined)
|
|
const params = { table_id: props.id };
|
|
const url_params = ntopng_url_manager.obj_to_url_params(params);
|
|
const url = `${http_prefix}/lua/rest/v2/get/tables/user_columns_config.lua?${url_params}`;
|
|
let columns_visible = await ntopng_utility.http_request(url);
|
|
let columns_visible_dict = {};
|
|
|
|
// convert to dictionary
|
|
columns_visible.forEach((c) => {
|
|
columns_visible_dict[c.id] = c;
|
|
});
|
|
|
|
return columns_visible_dict;
|
|
}
|
|
|
|
// save columns visibility in backend
|
|
async function set_columns_visibility() {
|
|
if (props.csrf == null) { return; }
|
|
|
|
// prepare request parameters with column configuration
|
|
let params = { table_id: props.id, visible_columns_ids: [], csrf: props.csrf };
|
|
params.visible_columns_ids = processedColumns.value.map((c, i) => {
|
|
return {
|
|
id: c.id,
|
|
visible: c.visible,
|
|
order: c.order,
|
|
sort: c.sort,
|
|
};
|
|
});
|
|
|
|
// post configuration to server
|
|
const url = `${http_prefix}/lua/rest/v2/add/tables/user_columns_config.lua`;
|
|
await ntopng_utility.http_post_request(url, params);
|
|
}
|
|
|
|
// preapre column definitions with visibility and sort
|
|
async function set_columns_wrap() {
|
|
|
|
// get configuration
|
|
let cols_visibility_dict = await get_columns_visibility_dict();
|
|
|
|
// check if table has sorting enabled
|
|
let is_table_not_sorted = true;
|
|
for (let id in cols_visibility_dict) {
|
|
is_table_not_sorted &= (cols_visibility_dict[id]?.sort);
|
|
}
|
|
|
|
// process column definitions
|
|
processedColumns.value = props.columns.map((c, i) => {
|
|
let classes = [];
|
|
let style = "";
|
|
|
|
// get column CSS class from json config
|
|
if (props.f_get_column_classes != null) {
|
|
classes = props.f_get_column_classes(c);
|
|
}
|
|
// get column CSS style from json config
|
|
if (props.f_get_column_style != null) {
|
|
style = props.f_get_column_style(c);
|
|
}
|
|
|
|
// get column id and condiguration
|
|
let id = props.get_column_id(c);
|
|
let col_opt = cols_visibility_dict[id];
|
|
|
|
// determine sort state, 0 = no sort, 1 = asc, 2 = desc
|
|
let sort = col_opt?.sort;
|
|
if (is_table_not_sorted == true && sort == null && props.default_sort != null && id == props.default_sort.column_id) {
|
|
sort = props.default_sort.sort;
|
|
} else if (col_opt?.sort) {
|
|
sort = col_opt?.sort;
|
|
} else {
|
|
sort = 0;
|
|
}
|
|
return {
|
|
id,
|
|
visible: col_opt?.visible == null || col_opt?.visible == true, // defaults column to visible
|
|
sort: sort,
|
|
sortable: is_column_sortable(c),
|
|
min_width: column_min_width(c),
|
|
order: col_opt?.order || i,
|
|
sticky: c?.sticky,
|
|
classes,
|
|
style,
|
|
data: c,
|
|
};
|
|
});
|
|
|
|
// save column config to server
|
|
await set_columns_visibility();
|
|
}
|
|
|
|
// reset all column widths to default
|
|
async function reset_column_size() {
|
|
props.columns.forEach((c) => {
|
|
let id = `${props.id}-${props.get_column_id(c)}`;
|
|
columnWidthStore.remove(id); // remove saved width from local storage
|
|
});
|
|
await redraw_table_resizable();
|
|
}
|
|
|
|
// handle row number count per page
|
|
function change_per_page() {
|
|
save_num_pages()
|
|
redraw_select_pages();
|
|
change_active_page(0);
|
|
}
|
|
|
|
// save rows count per page to local storage
|
|
const save_num_pages = function () {
|
|
localStorage.setItem("ntopng.tables.rowPerPage", rowsPerPage.value);
|
|
}
|
|
|
|
// force pagination to redraw
|
|
const searchDelay = ref(0);
|
|
function redraw_select_pages() {
|
|
searchDelay.value += 1;
|
|
}
|
|
|
|
|
|
// force table content to redraw
|
|
const table_content_id = ref(0);
|
|
function refresh_table_content() {
|
|
table_content_id.value += 1;
|
|
}
|
|
|
|
|
|
// change pagination active page and update displayed rows
|
|
async function change_active_page(new_active_page) {
|
|
if (new_active_page != null) {
|
|
currentPage = new_active_page;
|
|
}
|
|
if (currentPage == null) {
|
|
currentPage = 0;
|
|
}
|
|
|
|
// fetch new rows if paging is enabled
|
|
if (props.paging == true || shouldForceRefresh) {
|
|
await set_rows();
|
|
} else {
|
|
set_active_rows();
|
|
}
|
|
refresh_table_content();
|
|
set_dropdown_listener();
|
|
}
|
|
|
|
// handle column sort
|
|
async function change_column_sort(col, col_index) {
|
|
if (!col.sortable) {
|
|
return;
|
|
}
|
|
|
|
// cycle states: no sort -> asc desc -> none
|
|
col.sort = (col.sort + 1) % 3;
|
|
|
|
// reset sort on all other columns
|
|
processedColumns.value.filter((c, i) => i != col_index).forEach((c) => c.sort = 0);
|
|
|
|
// no sort, stop
|
|
if (col.sort == 0) { return; }
|
|
|
|
if (props.paging) {
|
|
await set_rows(); // server side sorting
|
|
} else {
|
|
set_active_rows(); // client side sorting
|
|
}
|
|
await set_columns_visibility();
|
|
set_dropdown_listener();
|
|
}
|
|
|
|
|
|
// get sort function for available functions
|
|
function get_sort_function() {
|
|
if (props.f_sort_rows != null) {
|
|
return props.f_sort_rows;
|
|
}
|
|
|
|
// default sort strings
|
|
return (col, r0, r1) => {
|
|
let r0_col = props.print_html_row(col.data, r0);
|
|
let r1_col = props.print_html_row(col.data, r1);
|
|
if (col.sort == 1) {
|
|
return r0_col.localeCompare(r1_col); // ascending
|
|
}
|
|
return r1_col.localeCompare(r0_col); // descending
|
|
};
|
|
}
|
|
|
|
let shouldForceRefresh = false;
|
|
|
|
/* ********************************************* */
|
|
|
|
/* Refresh table contents
|
|
- If disable_loading is true, no loading indicator will be shown
|
|
- If disable_loading is true, current page will be maintained
|
|
*/
|
|
|
|
async function refresh_table(disable_loading) {
|
|
/* NOTE: first refresh_table is called then set_rows */
|
|
shouldForceRefresh = true;
|
|
const shouldDisableLoading = disable_loading || false;
|
|
|
|
if (shouldDisableLoading) {
|
|
disableLoading()
|
|
/* In case of disabled loading, reload the same page */
|
|
paginationRef.value.change_active_page();
|
|
} else {
|
|
/* Otherwise reload from page 1 */
|
|
paginationRef.value.change_active_page(0, 0);
|
|
}
|
|
await nextTick();
|
|
|
|
/* Reset the refresh/loading params */
|
|
shouldForceRefresh = false;
|
|
|
|
if (shouldDisableLoading) {
|
|
enableLoading();
|
|
}
|
|
}
|
|
|
|
/* ********************************************* */
|
|
|
|
let isFirstDataLoad = true;
|
|
|
|
// get and update rows data
|
|
async function set_rows() {
|
|
if (showLoading.value) {
|
|
isLoading.value = true;
|
|
}
|
|
// get rows from backend
|
|
let res = await props.get_rows(
|
|
currentPage, // current page
|
|
rowsPerPage.value, // rows per page
|
|
processedColumns.value, // columns definition
|
|
searchString.value, // search term
|
|
isFirstDataLoad // first load
|
|
);
|
|
|
|
// update query info if available
|
|
query_info.value = null;
|
|
if (res.query_info != null) {
|
|
query_info.value = res.query_info;
|
|
}
|
|
|
|
isFirstDataLoad = false;
|
|
|
|
// If we're not using server-side paging, we need to filter the rows client-side
|
|
if (props.paging !== true && searchString.value.trim() !== '') {
|
|
res.rows = filterRows(res.rows, searchString.value);
|
|
}
|
|
|
|
// update total rows count
|
|
totalRowCount.value = res.rows.length;
|
|
if (props.paging == true) {
|
|
totalRowCount.value = res.total_rows; // use number of rows provided by server
|
|
}
|
|
|
|
// store fetched rows and update displayed rows
|
|
allRows = res.rows;
|
|
set_active_rows();
|
|
|
|
// wait for dom to update and emit event
|
|
await nextTick();
|
|
emit('rows_loaded', res);
|
|
if (showLoading.value) {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// New function to filter rows based on search string
|
|
function filterRows(rows, searchTerm) {
|
|
if (!searchTerm || searchTerm.trim() === '') {
|
|
return rows;
|
|
}
|
|
|
|
searchTerm = searchTerm.toLowerCase();
|
|
|
|
return rows.filter(row => {
|
|
// Check each visible column for the search term
|
|
return processedColumns.value.some(col => {
|
|
if (!col.visible) return false;
|
|
|
|
let cellContent = '';
|
|
|
|
// Get cell content based on how it's rendered
|
|
if (props.print_html_row && props.print_html_row(col.data, row, true) != null) {
|
|
// For HTML content, get the rendered text and strip HTML tags
|
|
const htmlContent = props.print_html_row(col.data, row);
|
|
cellContent = htmlContent ? stripHtmlTags(htmlContent).toLowerCase() : '';
|
|
} else if (props.print_vue_node_row && props.print_vue_node_row(col.data, row, vue_obj, true) != null) {
|
|
// For Vue nodes, we might need to extract text differently
|
|
// This is a simplification and might need adjustment based on your Vue node structure
|
|
const vueContent = props.print_vue_node_row(col.data, row, vue_obj);
|
|
cellContent = typeof vueContent === 'string' ? vueContent.toLowerCase() : '';
|
|
} else {
|
|
// Fallback: try to access the raw value from the row using column data
|
|
const rawValue = row[col.data];
|
|
cellContent = rawValue ? String(rawValue).toLowerCase() : '';
|
|
}
|
|
|
|
return cellContent.includes(searchTerm);
|
|
});
|
|
});
|
|
}
|
|
|
|
function stripHtmlTags(html) {
|
|
if (!html) return '';
|
|
html = html.toString()
|
|
return html.replace(/<\/?[^>]+(>|$)/g, " ").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
// determine if column is sortable
|
|
function is_column_sortable(col) {
|
|
if (props.f_is_column_sortable != null) {
|
|
return props.f_is_column_sortable(col); // use custom function to sort
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// determine column minimum width
|
|
function column_min_width(col) {
|
|
if (props.f_column_min_width != null) {
|
|
return props.f_column_min_width(col); // use custom function for min width
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// set displayed columns for current page
|
|
function set_active_rows() {
|
|
let start_row_index = 0;
|
|
|
|
// start index for client side pagination
|
|
if (props.paging == false) {
|
|
start_row_index = currentPage * rowsPerPage.value;
|
|
}
|
|
|
|
// sort rows client side if enabled
|
|
if (props.paging == false) {
|
|
let f_sort = get_sort_function();
|
|
let col_to_sort = get_column_to_sort();
|
|
|
|
// sort rows if enabled
|
|
|
|
if (col_to_sort) {
|
|
allRows = allRows.sort((r0, r1) => {
|
|
return f_sort(col_to_sort, r0, r1);
|
|
});
|
|
}
|
|
}
|
|
|
|
// update displayed rows for current page
|
|
displayedRows.value = allRows.slice(start_row_index, start_row_index + rowsPerPage.value);
|
|
}
|
|
|
|
// get columns with active sort
|
|
function get_column_to_sort() {
|
|
let col_to_sort = processedColumns.value.find((c) => c.sort != 0);
|
|
if (col_to_sort) {
|
|
localStorage.setItem(`tablesort_${props.id}`, col_to_sort.id);
|
|
localStorage.setItem(`tablesort_${props.id}_sort`, col_to_sort.sort);
|
|
} else {
|
|
col_to_sort = localStorage.getItem(`tablesort_${props.id}`);
|
|
const sort = localStorage.getItem(`tablesort_${props.id}_sort`);
|
|
if (col_to_sort) {
|
|
col_to_sort = processedColumns.value.find((c) => c.id === col_to_sort);
|
|
if (!col_to_sort) {
|
|
col_to_sort = processedColumns.value[0]
|
|
}
|
|
col_to_sort.sort = sort
|
|
}
|
|
}
|
|
return col_to_sort;
|
|
}
|
|
|
|
// add timeout to search to prevent request flooding
|
|
let map_search_change_timeout;
|
|
async function on_change_map_search() {
|
|
let timeout = 300; // Reduced timeout for better responsiveness
|
|
|
|
// clear existing timeouts
|
|
if (map_search_change_timeout != null) {
|
|
clearTimeout(map_search_change_timeout);
|
|
}
|
|
|
|
map_search_change_timeout = setTimeout(async () => {
|
|
if (props.paging) {
|
|
// For server-side pagination, reset to first page when searching
|
|
currentPage = 0;
|
|
paginationRef.value.change_active_page(0, 0);
|
|
}
|
|
|
|
await set_rows(); // get filtered rows
|
|
|
|
// Update pagination after search
|
|
if (!props.paging) {
|
|
redraw_select_pages();
|
|
}
|
|
|
|
map_search_change_timeout = null;
|
|
}, timeout);
|
|
}
|
|
|
|
// set search value
|
|
function search_value(value) {
|
|
searchString.value = value; /* Add the new value */
|
|
on_change_map_search(); // trigger search
|
|
}
|
|
|
|
// copy executed SQL query to clipboard
|
|
function copy_query_into_clipboard($event) {
|
|
NtopUtils.copyToClipboard(query_info.value.query, sqlButtonRef.value);
|
|
}
|
|
|
|
// get current column definition
|
|
function get_columns_defs() {
|
|
return processedColumns.value;
|
|
}
|
|
|
|
// get total number of rows
|
|
function get_rows_num() {
|
|
return totalRowCount.value;
|
|
}
|
|
|
|
const disableLoading = () => {
|
|
showLoading.value = false;
|
|
}
|
|
|
|
const enableLoading = () => {
|
|
showLoading.value = true;
|
|
}
|
|
|
|
// expose methods for parent components
|
|
defineExpose({ load_table, refresh_table, get_columns_defs, get_rows_num, search_value, disableLoading, enableLoading });
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Used just for the action column in case of dropdowns, otherwise the
|
|
* visualization stack is incorrect, need to increment the z-index of the
|
|
* selected td
|
|
*/
|
|
td.sticky-column-td.dropdown-active {
|
|
z-index: 3 !important;
|
|
}
|
|
|
|
.list-move,
|
|
/* apply transition to moving elements */
|
|
.list-enter-active,
|
|
.list-leave-active {
|
|
transition: all 0.35s ease;
|
|
}
|
|
|
|
.list-enter-from {
|
|
opacity: 0;
|
|
transform: translateX(-60px);
|
|
/* entra da sinistra */
|
|
}
|
|
|
|
.list-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(0);
|
|
/* esce verso destra */
|
|
}
|
|
|
|
/* ensure leaving items are taken out of layout flow so that moving
|
|
animations can be calculated correctly. */
|
|
.list-leave-active {
|
|
position: absolute;
|
|
}
|
|
</style>
|