<script setup lang="ts">
import {
IconBraces,
IconChevronDown,
IconFilterPlus,
IconX,
} from "@tabler/icons-vue";
import { onClickOutside } from "@vueuse/core";
import { eventTypes } from "src/constants";
import { honeypotTypes, useFilterStore, type FilterState } from "src/store";
import { icons } from "utils/icons";
import { computed, ref, useTemplateRef } from "vue";
import { useRoute } from "vue-router";
import CommaSeparatedFilter from "./CommaSeparatedFilter.vue";
import DropdownFilter from "./DropdownFilter.vue";
import HostFilter from "./HostFilter.vue";
import JsonFieldFilter from "./JsonFieldFilter.vue";
import PortFilter from "./PortFilter.vue";
import TimeRangeFilter from "./TimeRangeFilter.vue";
import ToggleSwitches from "./ToggleSwitches.vue";
const props = defineProps<{
filters?: FilterState;
}>();
const route = useRoute();
const filterStore = props.filters || useFilterStore().state;
const showAddMenu = ref(false);
const typeOptions = computed(() =>
honeypotTypes.value.map((t) => ({ value: t.name, label: t.label })),
);
// List of available filter types
const availableFilters = [
{ id: "type", label: "Honeypot Type", icon: icons.honeypotType },
{ id: "event", label: "Event", icon: icons.eventType },
{ id: "remote_addr", label: "Source IP", icon: icons.address },
{ id: "dst_port", label: "Destination Port", icon: icons.port },
{ id: "timerange", label: "Time Range", icon: icons.time },
{ id: "country", label: "Country", icon: icons.country },
{ id: "city", label: "City", icon: icons.city },
{ id: "asn", label: "ASN", icon: icons.asn },
{ id: "domain", label: "Domain", icon: icons.domain },
{ id: "fqdn", label: "FQDN", icon: icons.domain },
{ id: "json", label: "JSON Field", icon: IconBraces },
];
const visibleFilters = ref<Set<string>>(new Set());
const pendingJsonFilters = ref<{ key: string; values: string[] }[]>([]);
const addableFilters = computed(() => {
return availableFilters.filter((f) => !visibleFilters.value.has(f.id));
});
// Initialize visible filters based on current state
if (filterStore.remote_addr?.length) visibleFilters.value.add("remote_addr");
if (filterStore.dst_port?.length) visibleFilters.value.add("dst_port");
if (filterStore.country?.length) visibleFilters.value.add("country");
if (filterStore.city?.length) visibleFilters.value.add("city");
if (filterStore.asn?.length) visibleFilters.value.add("asn");
if (filterStore.domain?.length) visibleFilters.value.add("domain");
if (filterStore.fqdn?.length) visibleFilters.value.add("fqdn");
if (filterStore.time_start || filterStore.time_end)
visibleFilters.value.add("timerange");
if (filterStore.type?.length) visibleFilters.value.add("type");
if (filterStore.event?.length) visibleFilters.value.add("event");
// Helper to check if a filter is active
const isActive = (key: keyof FilterState) => {
const val = filterStore[key];
if (Array.isArray(val)) return val.length > 0;
if (typeof val === "string") return val !== "";
return !!val;
};
function addFilter(id: string) {
if (id === "json") {
pendingJsonFilters.value.push({ key: "", values: [] });
} else {
visibleFilters.value.add(id);
}
showAddMenu.value = false;
}
function removeFilter(id: string) {
visibleFilters.value.delete(id);
if (id === "remote_addr") filterStore.remote_addr = [];
if (id === "dst_port") filterStore.dst_port = [];
if (id === "type") filterStore.type = [];
if (id === "country") filterStore.country = [];
if (id === "city") filterStore.city = [];
if (id === "asn") filterStore.asn = [];
if (id === "domain") filterStore.domain = [];
if (id === "fqdn") filterStore.fqdn = [];
if (id === "event") filterStore.event = [];
if (id === "timerange") {
filterStore.time_start = "";
filterStore.time_end = "";
}
filterStore.offset = 0;
}
const addMenu = useTemplateRef("addMenu");
onClickOutside(addMenu, () => {
showAddMenu.value = false;
});
</script>
<template>
<div class="flex w-full flex-col gap-3">
<div class="flex flex-wrap justify-between gap-4">
<div class="flex flex-wrap items-center gap-2">
<ToggleSwitches
:filters="filterStore"
v-if="!['ip', 'charts'].includes(route.name as string)"
/>
<div class="relative" ref="addMenu">
<button
@click="showAddMenu = !showAddMenu"
class="flex h-8 items-center gap-2 rounded border border-stone-600 bg-stone-800 px-3 text-xs font-semibold text-stone-300 hover:bg-stone-700 hover:text-white"
>
<IconFilterPlus size="18" />
Add Filter
<IconChevronDown
size="14"
class="transition-transform"
:class="{ 'rotate-180': showAddMenu }"
/>
</button>
<div
v-if="showAddMenu"
class="absolute top-full left-0 z-50 mt-1 w-max overflow-hidden rounded border border-stone-700 bg-stone-800 shadow-xl"
>
<button
v-for="filter in addableFilters"
:key="filter.id"
@click="addFilter(filter.id)"
class="flex w-full items-center gap-2 rounded-none border-b border-stone-700/50 px-3 py-1.5 text-left text-xs text-stone-300 last:border-0 hover:bg-stone-700 hover:text-white"
>
<component :is="filter.icon" size="16" />
{{ filter.label }}
</button>
</div>
</div>
</div>
<slot name="right" />
</div>
<div class="flex flex-wrap items-start gap-2">
<div
v-if="
visibleFilters.size === 0 &&
!Object.keys(filterStore.json_fields || {}).length &&
pendingJsonFilters.length === 0 &&
!isActive('type') &&
!isActive('remote_addr') &&
!isActive('dst_port') &&
!isActive('country') &&
!isActive('city') &&
!isActive('asn') &&
!isActive('time_start') &&
!isActive('time_end') &&
!isActive('event')
"
class="py-1 text-xs text-stone-500 italic"
>
No active filters
</div>
<div
v-if="visibleFilters.has('type') || isActive('type')"
class="group relative"
>
<DropdownFilter
label="Type"
:icon="icons.honeypotType"
v-model="filterStore.type"
:options="typeOptions"
@change="filterStore.offset = 0"
placeholder="Select type"
class="filter-card"
/>
<button
@click="removeFilter('type')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('event') || isActive('event')"
class="group relative"
>
<DropdownFilter
label="Event"
:icon="icons.eventType"
v-model="filterStore.event"
:options="eventTypes"
@change="filterStore.offset = 0"
placeholder="Select event"
class="filter-card"
/>
<button
@click="removeFilter('event')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('remote_addr') || isActive('remote_addr')"
class="group relative"
>
<HostFilter
v-model="filterStore.remote_addr"
@change="filterStore.offset = 0"
class="filter-card"
/>
<button
@click="removeFilter('remote_addr')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('dst_port') || isActive('dst_port')"
class="group relative"
>
<PortFilter
v-model="filterStore.dst_port"
v-model:sync-with-chart="filterStore.sync_ports_with_chart"
:show-sync="route.name === 'charts-port'"
@change="filterStore.offset = 0"
class="filter-card"
/>
<button
@click="removeFilter('dst_port')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('country') || isActive('country')"
class="group relative"
>
<CommaSeparatedFilter
label="Country"
:icon="icons.country"
v-model="filterStore.country"
@change="filterStore.offset = 0"
placeholder="US, DE"
inputClass="w-24"
class="filter-card"
/>
<button
@click="removeFilter('country')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('city') || isActive('city')"
class="group relative"
>
<CommaSeparatedFilter
label="City"
:icon="icons.city"
v-model="filterStore.city"
@change="filterStore.offset = 0"
placeholder="New York, Zurich"
inputClass="w-24"
class="filter-card"
/>
<button
@click="removeFilter('city')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('asn') || isActive('asn')"
class="group relative"
>
<CommaSeparatedFilter
label="ASN"
:icon="icons.asn"
v-model="filterStore.asn"
@change="filterStore.offset = 0"
placeholder="12345"
inputClass="w-24"
class="filter-card"
/>
<button
@click="removeFilter('asn')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('domain') || isActive('domain')"
class="group relative"
>
<CommaSeparatedFilter
label="Domain"
:icon="icons.domain"
v-model="filterStore.domain"
@change="filterStore.offset = 0"
placeholder="example.com"
inputClass="w-32"
class="filter-card"
/>
<button
@click="removeFilter('domain')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="visibleFilters.has('fqdn') || isActive('fqdn')"
class="group relative"
>
<CommaSeparatedFilter
label="FQDN"
:icon="icons.domain"
v-model="filterStore.fqdn"
@change="filterStore.offset = 0"
placeholder="*.example.com"
inputClass="w-32"
class="filter-card"
/>
<button
@click="removeFilter('fqdn')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-if="
visibleFilters.has('timerange') ||
isActive('time_start') ||
isActive('time_end')
"
class="group relative"
>
<TimeRangeFilter
v-model:time-start="filterStore.time_start"
v-model:time-end="filterStore.time_end"
v-model:sync-with-chart="filterStore.sync_time_with_chart"
:show-sync="
route.name === 'charts-activity' || route.name === 'charts-port'
"
@change="filterStore.offset = 0"
class="filter-card"
/>
<button
@click="removeFilter('timerange')"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-for="(values, key) in filterStore.json_fields"
:key="key"
class="group relative"
>
<JsonFieldFilter
:field-key="key as string"
:model-value="values"
fixed-key
@update:model-value="
(vals) => (filterStore.json_fields[key as string] = vals)
"
class="filter-card"
/>
<button
@click="delete filterStore.json_fields[key as string]"
title="Remove filter"
class="filter-remove-button"
>
<IconX size="12" />
</button>
</div>
<div
v-for="(item, index) in pendingJsonFilters"
:key="index"
class="group relative"
>
<JsonFieldFilter
v-model:field-key="item.key"
:model-value="[]"
@update:model-value="
(vals) => {
if (item.key && vals.length > 0) {
useFilterStore().addJsonField(item.key, vals[0] as string);
pendingJsonFilters.splice(index, 1);
}
}
"
class="filter-card"
/>
<button
@click="pendingJsonFilters.splice(index, 1)"
class="filter-remove-button"
title="Remove filter"
>
<IconX size="12" />
</button>
</div>
</div>
</div>
</template>
<style scoped>
@reference "src/style.css";
.filter-remove-button {
@apply absolute -top-1.5 -right-1.5 rounded-full border border-stone-600 bg-stone-800 p-0.5 text-stone-400 opacity-0 shadow-md transition-opacity hover:text-white;
}
.group:hover .filter-remove-button {
@apply opacity-100;
}
.filter-card {
@apply rounded border border-stone-800 bg-stone-800/50 px-1.5 py-1;
}
</style>