diff --git a/src/components/standalone/openvpn_rw/ConnectionsHistory.vue b/src/components/standalone/openvpn_rw/ConnectionsHistory.vue index 8b252ea0e..e04aaf3d5 100644 --- a/src/components/standalone/openvpn_rw/ConnectionsHistory.vue +++ b/src/components/standalone/openvpn_rw/ConnectionsHistory.vue @@ -1,5 +1,5 @@ @@ -19,7 +19,11 @@ import { type FilterOption } from '@nethesis/vue-components' import { getAxiosErrorMessage } from '@nethesis/vue-components' -import { onMounted, ref, computed, watch } from 'vue' +import { ref, computed, watch } from 'vue' +import { useQuery, keepPreviousData } from '@tanstack/vue-query' +import { useRouteQuery } from '@vueuse/router' +import { refDebounced } from '@vueuse/core' +import { faClockRotateLeft } from '@fortawesome/free-solid-svg-icons' const { t } = useI18n() @@ -34,134 +38,146 @@ export type ConnectionsRecord = { virtualIpAddress: string } -const isLoading = ref(false) -const connectionsRecords = ref([]) -const error = ref({ - notificationDescription: '', - notificationDetails: '', - notificationTitle: '' -}) -const textFilter = ref('') +type ConnectionsHistoryResponse = { + data: { + connections: ConnectionsRecord[] + current_page: number + last_page: number + per_page: number + results: number + total: number + filters: { + accounts: string[] + } + } +} const props = defineProps<{ instance: string }>() -type RangeFilterOptions = 'all' | 'last_3_months' | 'last_month' | 'last_week' | 'today' +type RangeFilterOption = 'all' | 'last_3_months' | 'last_month' | 'last_week' | 'today' -// filter time range to populate NeDropdownFilter timeRangeFilter options -const timeRangeFilter = ref<[RangeFilterOptions, ...RangeFilterOptions[]]>(['today']) -const timeRangeFilterOptions = ref([ - { - id: 'today', - label: t('standalone.openvpn_rw.history.today') - }, - { - id: 'last_week', - label: t('standalone.openvpn_rw.history.last_week') - }, - { - id: 'last_month', - label: t('standalone.openvpn_rw.history.last_month') - }, - { - id: 'last_3_months', - label: t('standalone.openvpn_rw.history.last_3_months') - }, - { - id: 'all', - label: t('standalone.openvpn_rw.history.all') - } -]) +const timeRangeFilterOptions: FilterOption[] = [ + { id: 'today', label: t('standalone.openvpn_rw.history.today') }, + { id: 'last_week', label: t('standalone.openvpn_rw.history.last_week') }, + { id: 'last_month', label: t('standalone.openvpn_rw.history.last_month') }, + { id: 'last_3_months', label: t('standalone.openvpn_rw.history.last_3_months') }, + { id: 'all', label: t('standalone.openvpn_rw.history.all') } +] -// filter accounts to populate NeDropdownFilter accountsFilter options -const accountsFilter = ref>([]) +// route-persisted filters +const textFilter = useRouteQuery('q', '') +const textFilterDebounced = refDebounced(textFilter, 500) -// Computed property to get unique accounts within the selected time range -const uniqueAccounts = computed(() => { - const filteredRecords = applyFilterToConntrackRecords( - connectionsRecords.value, - '', - timeRangeFilter.value[0], - [] - ) - return Array.from(new Set(filteredRecords.map((record) => record.account))).sort((a, b) => - a.localeCompare(b) - ) +const timeRangeFilter = useRouteQuery('time_range', 'all', { + transform: { + get: (val) => (val ? ([val] as RangeFilterOption[]) : ['all']), + set: (val) => val[0] ?? 'all' + } }) -const accountFilterOptions = computed(() => { - return uniqueAccounts.value.map((account) => ({ - id: account, - label: account - })) +const accountsFilter = useRouteQuery('accounts', '', { + transform: { + get: (val) => (val ? val.split(',') : []), + set: (val) => val.join(',') + } }) -// filter items based on timeRange and accountsFilter -function applyFilterToConntrackRecords( - records: ConnectionsRecord[], - textFilter: string, - timeRange: string, - accountsFilter: string[] -) { - const lowerCaseFilter = textFilter.toLowerCase() - const now = new Date() - let startDate: Date | null = null +const currentPage = ref(1) +const perPage = ref(10) - if (timeRange === 'today') { - startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - } else if (timeRange === 'last_week') { - startDate = new Date(now) - startDate.setDate(now.getDate() - 7) - } else if (timeRange === 'last_month') { - startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1) - } else if (timeRange === 'last_3_months') { - startDate = new Date(now) - startDate.setMonth(now.getMonth() - 3) - startDate.setDate(1) // Set to the start of the month - } else if (timeRange === 'all') { - startDate = null +type SortableKeys = + | 'account' + | 'startTime' + | 'endTime' + | 'duration' + | 'virtualIpAddress' + | 'remoteIpAddress' + | 'bytesReceived' + | 'bytesSent' +const sortKey = useRouteQuery('sort', 'startTime') +const sortDescending = useRouteQuery('descending', 'true', { + transform: { + get: (val) => val == 'true', + set: (val) => val.toString() } +}) - return records.filter((connectionsRecord: ConnectionsRecord) => { - // Assuming startTime is in seconds, convert to ms - const recordDate = connectionsRecord.startTime - ? new Date(connectionsRecord.startTime * 1000) - : new Date(0) +// reset page when filters change +watch(textFilterDebounced, () => { + if (currentPage.value !== 1) { + currentPage.value = 1 + } +}) - const matchesTextFilter = - connectionsRecord.account.toLowerCase().includes(lowerCaseFilter) || - connectionsRecord.remoteIpAddress.toLowerCase().includes(lowerCaseFilter) || - connectionsRecord.virtualIpAddress.toLowerCase().includes(lowerCaseFilter) +watch(timeRangeFilter, () => { + if (currentPage.value !== 1) { + currentPage.value = 1 + } +}) - const matchesTimeRangeFilter = startDate ? recordDate >= startDate : true +watch(accountsFilter, () => { + if (currentPage.value !== 1) { + currentPage.value = 1 + } +}) - const matchesAccountFilter = - accountsFilter.length === 0 || accountsFilter.includes(connectionsRecord.account) +watch(perPage, () => { + if (currentPage.value !== 1) { + currentPage.value = 1 + } +}) - return matchesTextFilter && matchesTimeRangeFilter && matchesAccountFilter - }) -} +const { data, isError, error, isPending, isSuccess } = useQuery({ + queryKey: [ + 'ovpnrw', + 'connection-history', + textFilterDebounced, + timeRangeFilter, + accountsFilter, + perPage, + currentPage, + sortKey, + sortDescending + ], + queryFn: async () => + ubusCall('ns.ovpnrw', 'connection-history', { + instance: props.instance, + q: textFilterDebounced.value.toLowerCase(), + time_range: timeRangeFilter.value[0], + accounts: accountsFilter.value, + page: currentPage.value, + per_page: perPage.value, + sort_by: sortKey.value, + desc: sortDescending.value + }), + placeholderData: keepPreviousData, + select: (res) => res.data, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + refetchInterval: false, + retry: false +}) -// filter items -const filteredItems = computed(() => { - if ( - ['all', 'today', 'last_week', 'last_month', 'last_3_months'].includes(timeRangeFilter.value[0]) - ) { - return applyFilterToConntrackRecords( - connectionsRecords.value, - textFilter.value, - timeRangeFilter.value[0], - accountsFilter.value - ) - } else { - return connectionsRecords.value +watch(data, (newData) => { + if (newData != undefined && currentPage.value > newData.last_page) { + currentPage.value = newData.last_page } }) +const accountFilterOptions = computed(() => { + return ( + data.value?.filters.accounts.map((account) => ({ + id: account, + label: account + })) ?? [] + ) +}) + // clean error object function cleanError() { - error.value = { + downloadError.value = { notificationTitle: '', notificationDescription: '', notificationDetails: '' @@ -171,11 +187,17 @@ function cleanError() { // clear filters function clearFilters() { textFilter.value = '' - timeRangeFilter.value = ['today'] + timeRangeFilter.value = ['all'] accountsFilter.value = [] } // download all history from csv file +const downloadError = ref({ + notificationDescription: '', + notificationDetails: '', + notificationTitle: '' +}) + async function downloadAllHistory() { cleanError() try { @@ -192,46 +214,16 @@ async function downloadAllHistory() { link.href = fileURL link.download = res.data.csv_path.replace('.csv', '') + '-' + Date.now().toString() + '.csv' link.click() - await deleteFile(res.data.csv_path) } } catch (exception: any) { - error.value.notificationTitle = t('standalone.openvpn_rw.history.cannot_download_history') - error.value.notificationDescription = t(getAxiosErrorMessage(exception)) - error.value.notificationDetails = exception.toString() - } -} - -// fetch connection history -async function fetchConnectionHistory() { - error.value.notificationDescription = '' - error.value.notificationDetails = '' - try { - isLoading.value = true - connectionsRecords.value = ( - await ubusCall('ns.ovpnrw', 'connection-history', { - instance: props.instance, - time_interval: 'all' - }) - ).data - } catch (err: any) { - error.value.notificationTitle = t('standalone.openvpn_rw.history.cannot_fetch_history') - error.value.notificationDescription = t(getAxiosErrorMessage(err)) - error.value.notificationDetails = err.toString() - } finally { - isLoading.value = false + downloadError.value.notificationTitle = t( + 'standalone.openvpn_rw.history.cannot_download_history' + ) + downloadError.value.notificationDescription = t(getAxiosErrorMessage(exception)) + downloadError.value.notificationDetails = exception.toString() } } - -// fetch connection history on component mount -onMounted(() => { - fetchConnectionHistory() -}) - -// when timeRangeFilter changes, reset to [] accountsFilter -watch(timeRangeFilter, () => { - accountsFilter.value = [] -})