<script setup lang="ts">
import { IconMap } from "@tabler/icons-vue";
import FilterBar from "components/Filter/FilterBar.vue";
import LiveUpdateButton from "components/LiveUpdateButton.vue";
import { useChartData } from "composables/useChartData";
import { useQuerySync } from "composables/useQuerySync";
import { useChartStore, useUIStore } from "src/store";
import { icons } from "utils/icons";
import { computed, watch } from "vue";
import { useRoute } from "vue-router";
const chartStore = useChartStore();
const route = useRoute();
// Adjusted initialization based on current route
function updateColumns() {
if (route.path.includes("/map")) {
chartStore.state.columns = [
"time",
"remote_addr",
"latitude",
"longitude",
"country",
"city",
"type",
"event",
];
} else {
chartStore.state.columns = [
"time",
"remote_addr",
"dst_port",
"type",
"event",
];
}
}
updateColumns();
watch(() => route.path, updateColumns);
const uiStore = useUIStore();
const pageStat = computed(() => {
const total = chartData.value.length;
const selected = uiStore.selectedCount;
const selectedIPs = uiStore.selectedIPCount;
if (selected > 0) {
return `(${selected} selected, ${selectedIPs} IPs / ${total} events)`;
}
return `(${total} events)`;
});
useQuerySync(chartStore.state, {
limit: { number: true },
remote_addr: { array: true, comma: true },
dst_port: { array: true, comma: true },
time_start: {},
time_end: {},
x_range: { array: true, comma: true },
y_range: { array: true, comma: true },
domain: { array: true, comma: true },
fqdn: { array: true, comma: true },
sync_time_with_chart: { boolean: true },
sync_ports_with_chart: { boolean: true },
asn: { array: true, comma: true },
city: { array: true, comma: true },
country: { array: true, comma: true },
});
const { chartData, connectionStatus, connect, disconnect, loading } =
useChartData({
enabled: () => route.name !== "charts-activity",
});
function toggleConnection() {
if (connectionStatus.value === "open") {
disconnect();
} else {
connect();
}
}
</script>
<template>
<div class="page-container">
<div
class="flex flex-wrap items-center justify-between gap-x-4 gap-y-4 md:gap-x-8"
>
<div
class="text-muted flex flex-wrap items-center gap-x-4 gap-y-4 md:gap-x-8"
>
<router-link
:to="{ path: '/charts/port', query: route.query }"
class="chart-link"
>
<component :is="icons.port" size="24" />
<span>Port Chart</span>
</router-link>
<router-link
:to="{ path: '/charts/activity', query: route.query }"
class="chart-link"
>
<component :is="icons.activity" size="24" />
<span>Recent Activity</span>
</router-link>
<router-link :to="{ path: '/charts/map' }" class="chart-link">
<component :is="IconMap" size="24" />
<span>Threat Map</span>
</router-link>
<p class="stat-label" v-if="route.name !== 'charts-activity'">
{{ pageStat }}
</p>
</div>
<LiveUpdateButton
:st="connectionStatus"
@toggleConnection="toggleConnection"
v-if="route.name !== 'charts-activity'"
/>
</div>
<div class="card relative flex flex-col gap-4 md:h-[calc(100vh-9rem)]">
<div class="flex min-h-8 flex-col gap-4 md:flex-row md:items-start">
<FilterBar :filters="chartStore.state">
<template #right>
<div
class="flex min-h-9 flex-wrap items-center gap-4 rounded border border-stone-800 bg-stone-900 px-1.5 py-1"
>
<div
id="chart-actions"
class="flex items-center gap-2 empty:hidden"
></div>
<div class="flex items-center gap-2">
<span class="filter-label">Max Events:</span>
<input
type="number"
v-model.lazy="chartStore.state.limit"
class="w-20"
/>
</div>
</div>
</template>
</FilterBar>
</div>
<div class="min-h-0 flex-1">
<router-view
:chart-data="chartData"
:loading="loading"
:connection-status="connectionStatus"
/>
</div>
</div>
</div>
</template>
<style scoped>
@reference "src/style.css";
.chart-link {
@apply flex items-center gap-1 whitespace-nowrap no-underline transition-colors;
span {
@apply border-b-2 border-transparent transition-colors;
}
&:hover {
@apply text-stone-200;
}
}
.router-link-active {
@apply text-white;
.tabler-icon {
@apply text-primary-400;
}
span {
@apply border-primary-400;
}
}
</style>