internal/dashboard/frontend/src/components/DetailView/DetailChart.vue

<script lang="ts" setup>
import LoaderOverlay from "components/LoaderOverlay.vue";
import SectionCard from "components/SectionCard.vue";
import { setRangeToFilter, usePlotlyChart } from "composables/usePlotlyChart";
import type { FilterActions } from "src/store";
import { type ChartDataPoint } from "utils/filters";
import { icons } from "utils/icons";
import { useTemplateRef, watch, type Ref } from "vue";

const props = defineProps<{
  chartData: ChartDataPoint[];
  localFilterActions: FilterActions;
  title: string;
  loading: boolean;
  isPort?: boolean;
  isHoneypot?: boolean;
  chartLink?: string;
}>();

const chartContainer = useTemplateRef<HTMLDivElement>(
  "chartContainer",
) as Ref<HTMLDivElement>;

const selectedPoints = defineModel<number>("selectedPoints", { default: 0 });
const selectedIPs = defineModel<number>("selectedIPs", { default: 0 });

const options: Record<string, any> = {};
if (props.isPort) {
  options.yField = "remote_addr";
  options.yTitle = "Remote Address";
  options.categoricalY = true;
}

const { debouncedDrawPlot } = usePlotlyChart(
  chartContainer,
  () => props.chartData,
  {
    ...options,
    onRelayout(event) {
      if (props.isPort) {
        return;
      }
      setRangeToFilter(event, props.localFilterActions);
    },
    onSelected(event) {
      selectedPoints.value = event?.points?.length ?? 0;
      if (event?.points) {
        const ips = new Set(
          event.points.map((p) => props.chartData[p.pointIndex]?.remote_addr),
        );
        selectedIPs.value = ips.size;
      } else {
        selectedIPs.value = 0;
      }
    },
  },
);

watch(
  () => props.chartData,
  () => {
    debouncedDrawPlot();
  },
  { deep: true },
);
</script>

<template>
  <SectionCard
    :title="
      selectedPoints > 0
        ? `${title} (${selectedPoints} points, ${selectedIPs} IPs selected)`
        : title
    "
    :icon="icons.chart"
    body-class="relative h-[calc(100%-3rem)] min-h-[300px]"
  >
    <template #header-right>
      <router-link v-if="chartLink" :to="chartLink" class="btn-secondary">
        Open in Chart View
        <component :is="icons.externalLink" class="text-[10px]" />
      </router-link>
    </template>
    <LoaderOverlay v-if="loading" />
    <div ref="chartContainer" class="h-full w-full"></div>
  </SectionCard>
</template>