<script setup lang="ts">
import { IconDeselect } from "@tabler/icons-vue";
import ChartSidebar from "components/ChartSidebar.vue";
import SidebarList from "components/SidebarList.vue";
import { useChartSelection } from "composables/useChartSelection";
import { useChartStats } from "composables/useChartStats";
import { usePlotlyChart } from "composables/usePlotlyChart";
import type { PlotDatum } from "plotly.js-dist-min";
import { useChartStore, useUIStore } from "src/store";
import { excludeIP, excludePort } from "utils/chart";
import type { ChartDataPoint } from "utils/filters";
import { computed, onMounted, ref, useTemplateRef, watch, type Ref } from "vue";
const props = defineProps<{
chartData: ChartDataPoint[];
loading: boolean;
connectionStatus?: string;
}>();
const chartStore = useChartStore();
const uiStore = useUIStore();
const chartContainer = useTemplateRef<HTMLDivElement>(
"chartContainer",
) as Ref<HTMLDivElement>;
const selectedPoints = ref<PlotDatum[]>([]);
const { debouncedDrawPlot, allColors, allRemoteAddrs, allPorts } =
usePlotlyChart(
chartContainer,
computed(() => props.chartData),
{
onIPClick: (ip) => highlightIP(ip),
onSelected(event) {
if (event && event.points) {
selectedPoints.value = event.points;
uiStore.selectedIP = undefined;
uiStore.selectedPort = undefined;
uiStore.selectedCount = event.points.length;
const uniqueIPs = new Set<string>();
event.points.forEach((p) => {
const addr = allRemoteAddrs.value[p.pointIndex];
if (addr) {
if (Array.isArray(addr))
addr.forEach((a) => a && uniqueIPs.add(a));
else uniqueIPs.add(addr);
}
});
uiStore.selectedIPCount = uniqueIPs.size;
} else {
selectedPoints.value = [];
uiStore.selectedCount = 0;
uiStore.selectedIPCount = 0;
}
},
xRange: chartStore.state.x_range,
yRange: chartStore.state.y_range,
onRelayout: (event) => {
if (event["xaxis.range[0]"] && event["xaxis.range[1]"]) {
const start = event["xaxis.range[0]"].toString();
const end = event["xaxis.range[1]"].toString();
chartStore.state.x_range = [start, end];
if (chartStore.state.sync_time_with_chart) {
chartStore.state.time_start = start;
chartStore.state.time_end = end;
}
}
if (event["yaxis.range[0]"] && event["yaxis.range[1]"]) {
const start = event["yaxis.range[0]"];
const end = event["yaxis.range[1]"];
chartStore.state.y_range = [start.toString(), end.toString()];
if (chartStore.state.sync_ports_with_chart) {
const min = Math.max(Math.round(Number(start)), 0);
const max = Math.min(Math.round(Number(end)), 65535);
chartStore.state.dst_port = [`${min}-${max}`];
}
}
if (event["xaxis.autorange"] || event["yaxis.autorange"]) {
if (event["xaxis.autorange"]) {
chartStore.state.x_range = [];
if (chartStore.state.sync_time_with_chart) {
chartStore.state.time_start = "";
chartStore.state.time_end = "";
}
}
if (event["yaxis.autorange"]) {
chartStore.state.y_range = [];
if (chartStore.state.sync_ports_with_chart) {
chartStore.state.dst_port = [];
}
}
}
},
},
);
const {
selectedIP,
selectedPort,
selectedColor,
highlightIP,
highlightPort,
resetSelection,
} = useChartSelection(chartContainer, allColors, allRemoteAddrs, allPorts);
const xRange = computed(() => chartStore.state.x_range);
const yRange = computed(() => chartStore.state.y_range);
const { ipCounts, portCounts } = useChartStats(
computed(() => props.chartData),
xRange,
yRange,
);
watch(
() => props.chartData,
() => {
debouncedDrawPlot();
},
{ deep: true },
);
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});
</script>
<template>
<teleport v-if="isMounted" to="#chart-actions">
<button
v-if="selectedIP || selectedPort || uiStore.selectedCount > 0"
type="button"
@click="resetSelection()"
class="border border-stone-700 bg-stone-800 px-2 py-1 text-xs text-nowrap text-stone-100 hover:bg-stone-700"
>
<IconDeselect size="16" :color="selectedColor" />
{{ uiStore.selectedCount }} events ({{ uiStore.selectedIPCount }} IPs)
<kbd class="ml-1 text-[10px] opacity-50">ESC</kbd>
</button>
</teleport>
<div class="flex h-full flex-col-reverse gap-4 md:flex-row">
<ChartSidebar>
<SidebarList
v-if="ipCounts.length > 0"
open
title="Top IPs"
:items="ipCounts"
type="ip"
label-key="ip"
:selected-item="selectedIP"
@click="highlightIP"
@exclude="excludeIP"
/>
<SidebarList
v-if="portCounts.length > 0"
title="Top Ports"
:items="portCounts"
type="port"
label-key="port"
:selected-item="selectedPort"
@click="highlightPort"
@exclude="excludePort"
/>
</ChartSidebar>
<div class="chart-container">
<div class="loader" v-if="loading"></div>
<div ref="chartContainer" class="h-full min-h-0 w-full"></div>
</div>
</div>
</template>