Peer history

This commit is contained in:
Donald Zou
2025-09-01 17:16:03 +08:00
parent caacbfae03
commit 632c4f3dc7
13 changed files with 830 additions and 38 deletions

View File

@@ -908,6 +908,37 @@ def API_GetPeerSessions():
return ResponseObject(data=p.getSessions(startDate, endDate))
return ResponseObject(False, "Peer does not exist")
@app.get(f'{APP_PREFIX}/api/getPeerTraffics')
def API_GetPeerTraffics():
configurationName = request.args.get("configurationName")
id = request.args.get('id')
try:
interval = request.args.get('interval', 30)
startDate = request.args.get('startDate', None)
endDate = request.args.get('endDate', None)
if type(interval) is str:
if not interval.isdigit():
return ResponseObject(False, "Interval must be integers in minutes")
interval = int(interval)
if startDate is None:
endDate = None
else:
startDate = datetime.strptime(startDate, "%Y-%m-%d")
if endDate:
endDate = datetime.strptime(endDate, "%Y-%m-%d")
if startDate > endDate:
return ResponseObject(False, "startDate must be smaller than endDate")
except Exception as e:
return ResponseObject(False, "Dates are invalid" + e)
if not configurationName or not id:
return ResponseObject(False, "Please provide configurationName and id")
fp, p = WireguardConfigurations.get(configurationName).searchPeer(id)
if fp:
return ResponseObject(data=p.getTraffics(interval, startDate, endDate))
return ResponseObject(False, "Peer does not exist")
@app.get(f'{APP_PREFIX}/api/getDashboardTheme')
def API_getDashboardTheme():
return ResponseObject(data=DashboardConfig.GetConfig("Server", "dashboard_theme")[1])

View File

@@ -236,17 +236,48 @@ class Peer:
return False
return True
def getTraffics(self, interval: int = 30, startDate: datetime.datetime = None, endDate: datetime.datetime = None):
if startDate is None and endDate is None:
endDate = datetime.datetime.now()
startDate = endDate - timedelta(minutes=interval)
else:
endDate = endDate.replace(hour=23, minute=59, second=59, microsecond=999999)
startDate = startDate.replace(hour=0, minute=0, second=0, microsecond=0)
with self.configuration.engine.connect() as conn:
result = conn.execute(
db.select(
self.configuration.peersTransferTable.c.cumu_data,
self.configuration.peersTransferTable.c.total_data,
self.configuration.peersTransferTable.c.cumu_receive,
self.configuration.peersTransferTable.c.total_receive,
self.configuration.peersTransferTable.c.cumu_sent,
self.configuration.peersTransferTable.c.total_sent,
self.configuration.peersTransferTable.c.time
).where(
db.and_(
self.configuration.peersTransferTable.c.id == self.id,
self.configuration.peersTransferTable.c.time <= endDate,
self.configuration.peersTransferTable.c.time >= startDate,
)
).order_by(
self.configuration.peersTransferTable.c.time
)
).mappings().fetchall()
return list(result)
def getSessions(self, startDate: datetime.datetime = None, endDate: datetime.datetime = None):
if endDate is None:
endDate = datetime.datetime.now()
if startDate is None:
startDate = (endDate - datetime.timedelta(days=1))
startDate = endDate
endDate = endDate.replace(hour=23, minute=59, second=59, microsecond=999999)
startDate = startDate.replace(hour=0, minute=0, second=0, microsecond=0)
with self.configuration.engine.connect() as conn:
result = conn.execute(
db.select(
@@ -261,23 +292,27 @@ class Peer:
self.configuration.peersTransferTable.c.time
)
).fetchall()
time = list(map(lambda x : x[0], result))
return time
# sessions = []
# current_session = [time[0]]
# if len(time) > 1:
# current_session = [time[0]]
#
# for ts in time[1:]:
# if ts - current_session[-1] <= datetime.timedelta(minutes=3):
# current_session.append(ts)
# else:
# sessions.append({
# "duration": self.__duration(current_session[-1], current_session[0]),
# "timestamps": current_session
# })
# current_session = [ts]
# sessions.append({
# "duration": self.__duration(current_session[-1], current_session[0]),
# "timestamps": current_session
# })
return list(map(lambda x : x[0], result))
# for ts in time[1:]:
# if ts - current_session[-1] <= datetime.timedelta(minutes=3):
# current_session.append(ts)
# else:
# sessions.append({
# "duration": self.__duration(current_session[-1], current_session[0]),
# "timestamps": current_session
# })
# current_session = [ts]
# sessions.append({
# "duration": self.__duration(current_session[-1], current_session[0]),
# "timestamps": current_session
# })
# print(sessions)
# return sessions
def __duration(self, t1: datetime.datetime, t2: datetime.datetime):
delta = t1 - t2

View File

@@ -16,9 +16,6 @@ export default {
},
props: {
Peer: Object, ConfigurationInfo: Object, order: Number, searchPeersLength: Number
},
mounted() {
},
setup(){
const target = ref(null);
@@ -136,7 +133,12 @@ export default {
</div>
</div>
</div>
</div>
<div class="card-footer" role="button" @click="$emit('details')">
<small class="d-flex align-items-center">
<LocaleText t="Details"></LocaleText>
<i class="bi bi-chevron-right ms-auto"></i>
</small>
</div>
</div>
</template>

View File

@@ -0,0 +1,178 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import {computed, ref, watch} from "vue";
import dayjs from "dayjs";
import {GetLocale} from "@/utilities/locale.js"
import {
Chart,
LineElement,
BarElement,
BarController,
LineController,
LinearScale,
Legend,
Title,
Tooltip,
CategoryScale,
PointElement,
Filler
} from 'chart.js';
Chart.register(
LineElement,
BarElement,
BarController,
LineController,
LinearScale,
Legend,
Title,
Tooltip,
CategoryScale,
PointElement,
Filler
);
import {Line} from "vue-chartjs";
import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue";
import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue";
const props = defineProps(['selectedPeer'])
const selectedDate = ref(undefined)
defineEmits(['close'])
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
<div class=" d-flex h-100 w-100">
<div class="w-100 p-2">
<div class="card rounded-3 shadow" style="width: 100%">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
<h4 class="mb-0 fw-normal">
<LocaleText t="Peer Details"></LocaleText>
</h4>
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
</div>
<div class="card-body px-4">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Peer"></LocaleText>
</small></p>
<h2>
{{ selectedPeer.name }}
</h2>
</div>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Status"></LocaleText>
</small></p>
<div class="d-flex align-items-center">
<span class="dot ms-0 me-2" :class="{active: selectedPeer.status === 'running'}"></span>
<LocaleText t="Connected" v-if="selectedPeer.status === 'running'"></LocaleText>
<LocaleText t="Disconnected" v-else></LocaleText>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Allowed IPs"></LocaleText>
</small></p>
{{selectedPeer.allowed_ip}}
</div>
</div>
</div>
<div style="word-break: break-all" class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Public Key"></LocaleText>
</small></p>
<samp>{{selectedPeer.id}}</samp>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Latest Handshake Time"></LocaleText>
</small></p>
<strong class="h4">
<LocaleText :t="selectedPeer.latest_handshake + ' ago'"></LocaleText>
</strong>
</div>
<i class="bi bi-person-raised-hand ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Usage"></LocaleText>
</small></p>
<strong class="h4 text-warning">
{{ (selectedPeer.total_data + selectedPeer.cumu_data).toFixed(4) }} GB
</strong>
</div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Received"></LocaleText>
</small></p>
<strong class="h4 text-primary">{{(selectedPeer.total_receive + selectedPeer.cumu_receive).toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Sent"></LocaleText>
</small></p>
<strong class="h4 text-success">{{(selectedPeer.total_sent + selectedPeer.cumu_sent).toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12">
<PeerSessions
:selectedDate="selectedDate"
@selectDate="args => selectedDate = args"
:selectedPeer="selectedPeer"></PeerSessions>
</div>
<div class="col-12">
<PeerTraffics
:selectedDate="selectedDate"
:selectedPeer="selectedPeer"></PeerTraffics>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,5 +1,5 @@
<script setup async>
import {computed, defineAsyncComponent, onBeforeUnmount, ref, watch} from "vue";
import {computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, watch} from "vue";
import {useRoute} from "vue-router";
import {fetchGet} from "@/utilities/fetch.js";
import ProtocolBadge from "@/components/protocolBadge.vue";
@@ -12,6 +12,7 @@ import Peer from "@/components/configurationComponents/peer.vue";
import PeerListModals from "@/components/configurationComponents/peerListComponents/peerListModals.vue";
import PeerIntersectionObserver from "@/components/configurationComponents/peerIntersectionObserver.vue";
import ConfigurationDescription from "@/components/configurationComponents/configurationDescription.vue";
import PeerDetailsModal from "@/components/configurationComponents/peerDetailsModal.vue";
// Async Components
const PeerSearchBar = defineAsyncComponent(() => import("@/components/configurationComponents/peerSearchBar.vue"))
@@ -73,6 +74,9 @@ const configurationModals = ref({
},
assignPeer: {
modalOpen: false
},
peerDetails: {
modalOpen: false
}
})
const peerSearchBar = ref(false)
@@ -205,10 +209,6 @@ const searchPeers = computed(() => {
}).slice(0, showPeersCount.value)
})
const dropup = (index) => {
return searchPeers.value.length - (index + 1) <= 3
}
watch(() => route.query.id, (newValue) => {
if (newValue){
wireguardConfigurationStore.searchString = newValue
@@ -220,7 +220,9 @@ watch(() => route.query.id, (newValue) => {
})
// onMounted(() => {
// configurationModalSelectedPeer.value = searchPeers.value[0]
// })
</script>
<template>
@@ -393,8 +395,10 @@ watch(() => route.query.id, (newValue) => {
:searchPeersLength="searchPeers.length"
:order="order"
:ConfigurationInfo="configurationInfo"
@details="configurationModals.peerDetails.modalOpen = true; configurationModalSelectedPeer = peer"
@share="configurationModals.peerShare.modalOpen = true; configurationModalSelectedPeer = peer"
@refresh="fetchPeerList()"
@jobs="configurationModals.peerScheduleJobs.modalOpen = true; configurationModalSelectedPeer = peer"
@setting="configurationModals.peerSetting.modalOpen = true; configurationModalSelectedPeer = peer"
@qrcode="configurationModalSelectedPeer = peer; configurationModals.peerQRCode.modalOpen = true;"
@@ -456,6 +460,13 @@ watch(() => route.query.id, (newValue) => {
:configurationPeers="configurationPeers"
@close="configurationModals.selectPeers.modalOpen = false"
></SelectPeersModal>
<PeerDetailsModal
key="PeerDetailsModal"
v-if="configurationModals.peerDetails.modalOpen"
:selectedPeer="searchPeers.find(x => x.id === configurationModalSelectedPeer.id)"
@close="configurationModals.peerDetails.modalOpen = false"
>
</PeerDetailsModal>
</TransitionGroup>
<PeerIntersectionObserver
:showPeersCount="showPeersCount"

View File

@@ -13,7 +13,8 @@ import {
Title,
Tooltip,
CategoryScale,
PointElement
PointElement,
Filler
} from 'chart.js';
Chart.register(
LineElement,
@@ -25,13 +26,15 @@ Chart.register(
Title,
Tooltip,
CategoryScale,
PointElement
PointElement,
Filler
);
import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import dayjs from "dayjs";
import {useRoute, useRouter} from "vue-router";
import {GetLocale} from "@/utilities/locale.js";
const props = defineProps({
configurationPeers: Array,
configurationInfo: Object
@@ -124,12 +127,14 @@ const peersRealtimeSentData = computed(() => {
labels: [...historySentData.value.timestamp],
datasets: [
{
label: 'Data Sent',
label: GetLocale('Data Sent'),
data: [...historySentData.value.data],
fill: false,
fill: 'start',
borderColor: '#198754',
backgroundColor: '#198754',
tension: 0
backgroundColor: '#19875490',
tension: 0,
pointRadius: 2,
borderWidth: 1,
},
],
}
@@ -139,12 +144,14 @@ const peersRealtimeReceivedData = computed(() => {
labels: [...historyReceivedData.value.timestamp],
datasets: [
{
label: 'Data Received',
label: GetLocale('Data Received'),
data: [...historyReceivedData.value.data],
fill: false,
fill: 'start',
borderColor: '#0d6efd',
backgroundColor: '#0d6efd',
tension: 0
backgroundColor: '#0d6efd90',
tension: 0,
pointRadius: 2,
borderWidth: 1,
},
],
}

View File

@@ -12,6 +12,8 @@ const PeerJobsModal = defineAsyncComponent(() => import("@/components/configurat
const PeerQRCodeModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerQRCode.vue"))
const PeerConfigurationFileModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerConfigurationFile.vue"))
const PeerSettingsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerSettings.vue"))
const PeerDetailsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerDetailsModal.vue"))
</script>
<template>
@@ -43,14 +45,17 @@ const PeerSettingsModal = defineAsyncComponent(() => import("@/components/config
:selectedPeer="configurationModalSelectedPeer">
</PeerShareLinkModal>
<PeerConfigurationFileModal
key="PeerConfigurationFileModal"
@close="configurationModals.peerConfigurationFile.modalOpen = false"
v-if="configurationModals.peerConfigurationFile.modalOpen"
:selectedPeer="configurationModalSelectedPeer"
></PeerConfigurationFileModal>
<PeerAssignModal
key="PeerAssignModal"
:selectedPeer="configurationModalSelectedPeer"
@close="configurationModals.assignPeer.modalOpen = false"
v-if="configurationModals.assignPeer.modalOpen"></PeerAssignModal>
</TransitionGroup>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(customParseFormat);
const props = defineProps(['session'])
const heightPerSecond = 1224 / 24 / 60 / 60;
const start = dayjs(props.session.timestamps[0])
const offsetTop = (start.hour() * 60 * 60 + start.minute() * 60 + start.second()) * heightPerSecond
const duration = dayjs(props.session.duration, "HH:mm:ss")
const height = (duration.hour() * 60 * 60 + duration.minute() * 60 + duration.second()) * heightPerSecond
console.log()
</script>
<template>
<div class="position-absolute"
:style="{top: offsetTop + 'px'}"
style="width: 100%; padding-left: calc(18px + 2.5rem)">
<div class="rounded-3"
style="background: rgba(25,135,84,0.7); min-height: 5px !important;"
:style="{height: height + 'px'}">
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import {computed} from "vue";
const props = defineProps(['sessions', 'day'])
import dayjs from "dayjs";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import duration from "dayjs/plugin/duration"
import LocaleText from "@/components/text/localeText.vue";
dayjs.extend(isSameOrBefore)
dayjs.extend(duration)
const sessionsOfToday = computed(() => {
let sessions = props.sessions.map(x => dayjs(x)).filter(x => x.isSame(props.day, 'D')).reverse()
let result = []
if (sessions.length > 1){
let cur = [sessions[0]]
for (let s of sessions.slice(1)){
if (s.isSameOrBefore(cur[cur.length - 1].add(3, 'minute'))){
cur.push(s)
}else{
result.push({
timestamps: cur,
duration: dayjs.duration(
cur[cur.length - 1].diff(cur[0])
)
})
cur = [s]
}
}
result.push({
timestamps: cur,
duration: dayjs.duration(
cur[cur.length - 1].diff(cur[0])
)
})
}
return result
})
defineEmits(['openDetails'])
</script>
<template>
<div class="d-flex gap-1 flex-column" @click="$emit('openDetails', sessionsOfToday)">
<small v-if="sessionsOfToday.length > 0" class="sessions-label">
<LocaleText :t="sessionsOfToday.length + ' Session' + (sessionsOfToday.length > 1 ? 's':'')"></LocaleText>
</small>
<div class="d-flex flex-wrap gap-1 session-dot">
<div class="bg-warning"
style="height: 5px; width: 5px; border-radius: 100%; vertical-align: top" v-for="_ in sessionsOfToday.length"></div>
</div>
<div class="p-1 badge text-bg-warning text-start session-list" v-for="s in sessionsOfToday">
<div>
<i class="bi bi-stopwatch me-1"></i>{{ s.timestamps[0].format("HH:mm:ss") }}<i class="bi bi-arrow-right mx-1"></i>{{ s.timestamps[s.timestamps.length - 1].format("HH:mm:ss") }}
</div>
<div class="mt-1">
<LocaleText t="Duration:"></LocaleText> {{ s.duration.format('HH:mm:ss')}}
</div>
</div>
</div>
</template>
<style scoped>
@media screen and (max-width: 992px) {
.calendar-day .session-list{
display: none;
}
.sessions-label{
display: none;
}
}
@media screen and (min-width: 992px) {
.session-dot{
display: none !important;
}
}
</style>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import dayjs from "dayjs";
import {computed, ref} from "vue";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
dayjs.extend(isSameOrBefore)
const props = defineProps(['sessions'])
const offsetMonth = ref(0)
const today = ref(dayjs().subtract(offsetMonth.value, 'month'))
const firstDay = ref(today.value.startOf('month'))
const lastDay = ref(today.value.endOf('month'))
const startOfWeek = ref(firstDay.value.startOf('week'))
const endOfWeek = ref(lastDay.value.endOf('week'))
const calendarDays = computed(() => {
let dates = []
let cur = startOfWeek.value;
while (cur.isSameOrBefore(endOfWeek.value, 'day')){
dates.push(cur)
cur = cur.add(1, 'day')
}
return dates
})
</script>
<template>
<div class="calendar-grid">
<div class="calendar-day p-2"
:class="{
'bg-body-secondary': day.isSame(today, 'D'),
'border-end': day.day() < 6,
'border-bottom': index < 28}"
v-for="(day, index) in calendarDays">
<h5>
{{ day.isSame(day.startOf('month')) ? day.format("MMM") : '' }} {{ day.format('D')}}
</h5>
</div>
</div>
</template>
<style scoped>
.calendar-grid{
display: grid;
grid-template-areas: "sun mon tue wed thu fri sat";
grid-template-columns: repeat(7, 1fr);
}
.calendar-day.day-6{
border-right: none !important;
}
.calendar-day{
min-height: 150px;
}
.calendar-day.active{
}
</style>

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import { fetchGet } from "@/utilities/fetch.js"
import {computed, onBeforeUnmount, ref, watch} from "vue";
const props = defineProps(['selectedPeer', 'selectedDate'])
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"
const store = DashboardConfigurationStore()
const sessions = ref([])
import dayjs from "dayjs";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import PeerSessionCalendarDay from "@/components/peerDetailsModalComponents/peerSessionCalendarDay.vue";
dayjs.extend(isSameOrBefore)
const interval = ref(undefined)
const offsetMonth = ref(0)
const todayNoOffset = ref(dayjs())
const today = computed(() => dayjs().add(offsetMonth.value, 'month'))
const firstDay = computed(() => today.value.startOf('month'))
const lastDay = computed(() => today.value.endOf('month'))
const startOfWeek = computed(() => firstDay.value.startOf('week'))
const endOfWeek = computed(() => lastDay.value.endOf('week'))
const calendarDays = computed(() => {
let dates = []
let cur = startOfWeek.value;
while (cur.isSameOrBefore(endOfWeek.value, 'day')){
dates.push(cur)
cur = cur.add(1, 'day')
}
return dates
})
const getSessions = async () => {
await fetchGet("/api/getPeerSessions", {
configurationName: props.selectedPeer.configuration.Name,
id: props.selectedPeer.id,
startDate: startOfWeek.value.format("YYYY-MM-DD"),
endDate: endOfWeek.value.format("YYYY-MM-DD")
}, (res) => {
sessions.value = res.data.reverse()
})
}
await getSessions()
interval.value = setInterval(async () => {
await getSessions()
}, 60000)
onBeforeUnmount(() => {
clearInterval(interval.value)
})
watch(() => today.value, () => getSessions())
const dayDetails = ref(false)
const dayDetailsData = ref(undefined)
const emits = defineEmits(['selectDate'])
</script>
<template>
<div>
<div class="card rounded-3 bg-transparent">
<div class="card-header d-flex align-items-center">
<button class="btn btn-sm rounded-3" @click="offsetMonth -= 1">
<i class="bi bi-chevron-left"></i>
</button>
<button class="btn btn-sm rounded-3" v-if="offsetMonth !== 0" @click="offsetMonth = 0; $emit('selectDate', day)">
<LocaleText t="Today"></LocaleText>
</button>
<h5 class="mx-auto mb-0">
{{ today.format('YYYY-MM')}}
</h5>
<button class="btn btn-sm rounded-3" v-if="offsetMonth !== 0" @click="offsetMonth = 0; $emit('selectDate', day)">
<LocaleText t="Today"></LocaleText>
</button>
<button class="btn btn-sm rounded-3" @click="offsetMonth += 1">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="card-body p-0 position-relative">
<div class="calendar-grid">
<div class="calendar-day p-2 d-flex flex-column"
:key="day"
@click="$emit('selectDate', day)"
style="cursor: pointer"
:class="{
'bg-body-secondary': day.isSame(todayNoOffset, 'D'),
'border-end': day.day() < 6,
'border-bottom': index < calendarDays.length - 7,
'extra-day': !day.isSame(today, 'month')}"
v-for="(day, index) in calendarDays">
<h5 class="d-flex">
{{ day.format('D')}}
<i class="bi bi-check-circle-fill ms-auto" v-if="selectedDate && selectedDate.isSame(day, 'D')"></i>
</h5>
<PeerSessionCalendarDay
class="flex-grow-1"
@openDetails="args => {dayDetailsData = {day: day, details: args}; dayDetails = true}"
:sessions="sessions"
:day="day" :key="day"></PeerSessionCalendarDay>
</div>
</div>
<Transition name="zoom">
<div class="position-absolute rounded-bottom-3 dayDetail p-3"
v-if="dayDetails"
style="bottom: 0; height: 100%; width: 100%; z-index: 9999; background: #00000050; backdrop-filter: blur(8px); overflow: scroll">
<div class="d-flex mb-3">
<h5 class="mb-0">
{{ dayDetailsData.day.format("YYYY-MM-DD") }}
</h5>
<a role="button" class="ms-auto text-white" @click="dayDetails = false">
<h5 class="mb-0">
<i class="bi bi-x-lg"></i>
</h5>
</a>
</div>
<div class="d-flex flex-column gap-2">
<div class="p-1 badge text-bg-warning text-start session-list d-flex align-items-center" v-for="s in dayDetailsData.details">
<div>
<i class="bi bi-stopwatch me-1"></i>{{ s.timestamps[0].format("HH:mm:ss") }}<i class="bi bi-arrow-right mx-1"></i>{{ s.timestamps[s.timestamps.length - 1].format("HH:mm:ss") }}
</div>
<div class="ms-auto">
<LocaleText t="Duration:"></LocaleText> {{ s.duration.format('HH:mm:ss')}}
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</div>
</template>
<style scoped>
.calendar-grid{
display: grid;
grid-template-areas: "sun mon tue wed thu fri sat";
grid-template-columns: repeat(7, 1fr);
}
.calendar-day.day-6{
border-right: none !important;
}
.calendar-day{
min-height: 150px;
}
@media screen and (max-width: 992px) {
.calendar-day{
min-height: 100px !important;
}
}
@media screen and (min-width: 992px) {
.dayDetail{
display: none;
}
}
.extra-day h5{
opacity: 0.5;
}
</style>

View File

@@ -0,0 +1,178 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import {Line} from "vue-chartjs";
import {GetLocale} from "@/utilities/locale.js"
import { fetchGet } from "@/utilities/fetch.js"
import {computed, onBeforeUnmount, ref, watch} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"
import dayjs from "dayjs";
const props = defineProps(['selectedDate', 'selectedPeer'])
const store = DashboardConfigurationStore()
const date = computed(() => props.selectedDate ? props.selectedDate : dayjs())
const traffics = ref([])
const getTraffic = async () => {
await fetchGet("/api/getPeerTraffics", {
configurationName: props.selectedPeer.configuration.Name,
id: props.selectedPeer.id,
startDate: date.value.format("YYYY-MM-DD"),
endDate: date.value.format("YYYY-MM-DD")
}, (res) => {
traffics.value = res.data
console.log(traffics.value)
})
}
const interval = ref(undefined)
await getTraffic()
interval.value = setInterval(async () => {
await getTraffic()
}, 60000)
onBeforeUnmount(() => {
clearInterval(interval.value)
})
watch(() => date.value, () => {getTraffic()})
const dataUsageChartOption = computed(() => {
return {
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} MB`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: true
},
},
y:{
ticks: {
callback: (val) => {
return `${val.toFixed(4)} MB`
}
},
grid: {
display: true
},
}
}
}
})
const historicalSent = computed(() => {
let h = traffics.value.map(x => x.cumu_sent + x.total_sent)
let r = [0]
if (h.length > 1){
for (let i = 1; i < h.length; i++){
if (h[i] >= h[i - 1]){
r.push((h[i] - h[i - 1]) * 1024)
}else{
r.push(h[i] * 1024)
}
}
}
return r
})
const historicalReceive = computed(() => {
let h = traffics.value.map(x => x.cumu_receive + x.total_receive)
let r = [0]
if (h.length > 1){
for (let i = 1; i < h.length; i++){
if (h[i] >= h[i - 1]){
r.push((h[i] - h[i - 1]) * 1024)
}else{
r.push(h[i] * 1024)
}
}
}
return r
})
const historicalSentData = computed(() => {
return {
labels: traffics.value.map(x => x.time),
datasets: [
{
label: GetLocale('Data Sent'),
data: historicalSent.value,
fill: 'start',
borderColor: '#198754',
backgroundColor: '#19875490',
tension: 0,
pointRadius: 2,
borderWidth: 1,
},
],
}
})
const historicalReceivedData = computed(() => {
return {
labels: traffics.value.map(x => x.time),
datasets: [
{
label: GetLocale('Data Received'),
data: historicalReceive.value,
fill: 'start',
borderColor: '#0d6efd',
backgroundColor: '#0d6efd90',
tension: 0.3,
pointRadius: 2,
borderWidth: 1,
},
],
}
})
</script>
<template>
<div class="card rounded-3 bg-transparent">
<div class="card-body">
<h6 class="text-muted">
<LocaleText :t="'Peer Historical Data Usage of ' + date.format('YYYY-MM-DD')"></LocaleText>
</h6>
<div class="d-flex flex-column gap-3">
<div>
<p>
<LocaleText t="Data Received"></LocaleText>
</p>
<Line
:options="dataUsageChartOption"
:data="historicalReceivedData"
style="width: 100%; height: 300px; max-height: 300px"
></Line>
</div>
<div>
<p>
<LocaleText t="Data Sent"></LocaleText>
</p>
<Line
:options="dataUsageChartOption"
:data="historicalSentData"
style="width: 100%; height: 300px; max-height: 300px"
></Line>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -56,7 +56,7 @@ export default {
<template>
<div class="position-absolute w-100 h-100 top-0 start-0 rounded-bottom-3 p-3 d-flex"
style="background-color: #00000060; backdrop-filter: blur(3px)">
style="background-color: #00000060; backdrop-filter: blur(3px); z-index: 9999">
<div class="card m-auto rounded-3 mt-5">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
<h6 class="mb-0">
@@ -70,6 +70,7 @@ export default {
</small>
<div class="d-flex align-items-center gap-2">
<VueDatePicker
style="z-index: 9999"
:is24="true"
:min-date="new Date()"
:model-value="this.newKeyData.ExpiredAt"