internal/dashboard/frontend/src/views/PortChart.vue

<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>