internal/dashboard/frontend/src/components/EventTable/EventTableMobileRow.vue

<script setup lang="ts">
import { IconArrowsMaximize, IconArrowsMinimize } from "@tabler/icons-vue";
import EventDetails from "components/EventTable/EventDetails.vue";
import ScoreTag from "components/ScoreTag.vue";
import {
  type FilterActions,
  type GeoData,
  getHoneypot,
  useBlocklistStore,
  useFilterActions,
  useIPInfoStore,
} from "src/store";
import { type HoneypotEvent } from "src/types";
import { formatTimestamp } from "utils/formatting";
import { icons } from "utils/icons";
import { computed } from "vue";
import EventFilterMenu from "./EventFilterMenu.vue";
import EventSourceInfo from "./EventSourceInfo.vue";

const ipInfoStore = useIPInfoStore();
const { getIPInfo } = ipInfoStore;

const blocklistStore = useBlocklistStore();

const props = withDefaults(
  defineProps<{
    evt: HoneypotEvent;
    columns?: string[];
    filterActions?: FilterActions;
    width?: number;
  }>(),
  {
    columns: () => ["time", "event", "remote_addr", "details"],
  },
);

const filterActions = props.filterActions || useFilterActions();

const geo = computed<GeoData | undefined>(() => {
  if (!filterActions.state.resolve_ips) return undefined;
  return getIPInfo(props.evt.remote_addr!).value;
});

const hasDetails = computed(() => {
  return props.evt.fields && Object.keys(props.evt.fields).length > 0;
});
</script>

<template>
  <tr class="group border-b border-stone-800/50">
    <td colspan="100%" class="p-0">
      <div class="flex flex-col gap-3 py-2">
        <!-- Row 1: Header / Meta -->
        <div class="flex items-center justify-between">
          <div class="flex items-center gap-2">
            <span
              class="text-muted text-sm font-medium tracking-wide uppercase"
            >
              {{ formatTimestamp(evt.time, true) }}
            </span>
          </div>

          <div class="flex items-center gap-2">
            <EventFilterMenu
              :evt="evt"
              :geo="geo"
              :filter-actions="filterActions"
              show-links
            />
          </div>
        </div>

        <!-- Row 2: Event Name & Badges -->
        <div class="flex flex-wrap items-center justify-between gap-4">
          <div class="flex gap-1">
            <span class="text-muted font-semibold">
              <router-link
                :to="`/honeypot/${evt.type}`"
                class="hover:text-secondary-400"
              >
                {{ getHoneypot(evt.type)?.label || evt.type }} </router-link
              >:
            </span>
            <span class="font-bold uppercase">{{ evt.event }}</span>
          </div>
          <div
            class="flex shrink-0 gap-1"
            v-if="blocklistStore.getTagsByIp(evt.remote_addr!).length"
          >
            <ScoreTag
              v-for="tag in blocklistStore.getTagsByIp(evt.remote_addr!)"
              :key="tag"
              :tag="tag"
            />
          </div>
        </div>

        <!-- Row 3: Source & Destination -->
        <div class="flex flex-col gap-2.5 py-0.5">
          <div class="flex items-center gap-2">
            <component :is="icons.address" size="20" class="text-muted mt-px" />
            <EventSourceInfo
              :evt="evt"
              :filter-actions="filterActions"
              :geo="geo"
              :is-mobile="true"
            />
          </div>

          <div v-if="evt.dst_port" class="flex items-center gap-2">
            <component :is="icons.port" class="text-muted mt-px" size="20" />
            <router-link
              :to="`/port/${evt.dst_port}`"
              class="hover:text-stone-50"
            >
              {{ evt.dst_port }}
            </router-link>
          </div>
        </div>

        <!-- Row 4: Expandable Details -->
        <div v-if="hasDetails" class="border-stone-800 pt-4 duration-300">
          <div class="mb-3 flex items-center gap-2">
            <span
              class="text-muted text-[10px] font-bold tracking-widest uppercase"
            >
              Details
            </span>
          </div>
          <div class="relative">
            <button
              @click="
                filterActions.state.expand_details =
                  !filterActions.state.expand_details
              "
              class="icon-button absolute -top-8 right-0 text-xs"
            >
              <component
                :is="
                  filterActions.state.expand_details
                    ? IconArrowsMinimize
                    : IconArrowsMaximize
                "
                size="16"
              />
            </button>
            <EventDetails
              :evt="evt"
              :filter-actions="filterActions"
              :width="width || 1000"
            />
          </div>
        </div>
      </div>
    </td>
  </tr>
</template>