created views for trackers in UI, added leaflet

This commit is contained in:
tdv
2025-10-06 12:00:39 +03:00
parent 6a5ddd66ba
commit a404a37a60
9 changed files with 194 additions and 2 deletions

View File

@@ -14,6 +14,7 @@
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"leaflet": "^1.9.4",
"lucide-vue-next": "^0.525.0",
"reka-ui": "^2.5.0",
"tailwind-merge": "^3.3.1",
@@ -28,6 +29,7 @@
"devDependencies": {
"@iconify-json/radix-icons": "^1.2.2",
"@iconify/vue": "^5.0.0",
"@types/leaflet": "^1.9.20",
"@types/node": "^24.1.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
@@ -1273,6 +1275,23 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.20",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz",
"integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/node": {
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
@@ -1996,6 +2015,12 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",

View File

@@ -15,6 +15,7 @@
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"leaflet": "^1.9.4",
"lucide-vue-next": "^0.525.0",
"reka-ui": "^2.5.0",
"tailwind-merge": "^3.3.1",
@@ -29,6 +30,7 @@
"devDependencies": {
"@iconify-json/radix-icons": "^1.2.2",
"@iconify/vue": "^5.0.0",
"@types/leaflet": "^1.9.20",
"@types/node": "^24.1.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",

View File

@@ -200,6 +200,9 @@ onBeforeUnmount(() => {
<TabsTrigger value="devices">
Devices
</TabsTrigger>
<TabsTrigger value="trackers">
Trackers
</TabsTrigger>
</TabsList>
<TabsContent value="users">
<DataTableNoCheckbox :columns="user_columns" :data="user_data" :dropdownComponent="AdminUserDropdonw" />

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import TrackerDashboard from './TrackerDashboard.vue';
import { nextTick, onMounted, ref, type PropType } from 'vue';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import L, {
Map as LeafletMap,
} from 'leaflet';
import 'leaflet/dist/leaflet.css'
const props = defineProps({
guid: { type: String as PropType<string>, required: true },
})
const mapContainer = ref<HTMLDivElement | null>(null)
onMounted(async () => {
// wait until the flex layout has settled
await nextTick()
if (!mapContainer.value) return
const map = L.map(mapContainer.value).setView([48.38, 31.17], 6)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap contributors',
}).addTo(map)
// force Leaflet to recalc its size
map.invalidateSize()
})
</script>
<template>
<Tabs default-value="records">
<TabsList class="space-x-8">
<TabsTrigger value="dashboard">
Dashboard
</TabsTrigger>
<TabsTrigger value="locations">
Locations
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard">
<TrackerDashboard />
</TabsContent>
<TabsContent value="locations">
<div class="flex h-full">
<Card class="w-1/2 p-4 flex flex-col">
</Card>
<div class="w-1/2 p-4">
<div ref="mapContainer" class="w-full h-full rounded-lg shadow-inner">
</div>
</div>
</div>
</TabsContent>
</Tabs>
</template>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import {
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
} from '@/components/ui/number-field'
import AssignDevice from "./AssignDevice.vue";
import { ref } from "vue";
import Separator from "@/components/ui/separator/Separator.vue";
import DataRangePicker from "./DataRangePicker.vue";
const selectedUserIds = ref<string[]>([])
const usrIDs = selectedUserIds.value
</script>
<template>
<Card class="flex w-full max-w-4xl mx-auto">
<CardHeader class="pb-4">
<CardTitle>
Dashboard
</CardTitle>
</CardHeader>
<CardContent class="flex-1 space-y-6">
<div class="grid gap-5">
<div class="grid space-y-2 grid-cols-4 items-center">
<Label for="users">Allowed users</Label>
<AssignDevice v-model="usrIDs" class="col-span-3 w-full"></AssignDevice>
</div>
</div>
<div class="flex space-x-4 pt-2 gap-4">
<Button>Start</Button>
<Button>Stop</Button>
</div>
<Separator class="my-6">Communication settings</Separator>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<NumberField id="polling" :default-value="60" :min="30">
<Label for="polling"> Timeout interwal in seconds </Label>
<NumberFieldContent>
<NumberFieldDecrement></NumberFieldDecrement>
<NumberFieldInput></NumberFieldInput>
<NumberFieldIncrement></NumberFieldIncrement>
</NumberFieldContent>
</NumberField>
</div>
<div class="space-y-2">
<NumberField id="jitter" :default-value="10" :min="5">
<Label for="jitter"> Jitter in seconds </Label>
<NumberFieldContent>
<NumberFieldDecrement></NumberFieldDecrement>
<NumberFieldInput></NumberFieldInput>
<NumberFieldIncrement></NumberFieldIncrement>
</NumberFieldContent>
</NumberField>
</div>
</div>
<Separator></Separator>
<div class="space-y-2 gap-4">
<Label for="sleepdate">Date/time to sleep</Label>
<DataRangePicker id="sleepdate"></DataRangePicker>
</div>
</CardContent>
</Card>
</template>

View File

@@ -39,6 +39,7 @@ const tracker_columns = [
]
let treckerCtrl: AbortController | null = null
async function loadTrackers() {
error.value = null
loading.value = true
@@ -68,11 +69,11 @@ onBeforeUnmount(() => treckerCtrl?.abort())
<template>
<div class="space-y-4">
<div v-if="loading">Loading devices</div>
<div v-if="loading">Loading trackers</div>
<div v-else-if="error" class="text-red-600">{{ error }}</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<router-link v-for="device in trackers_data" :key="device.guid"
:to="{ name: 'DeviceView', params: { guid: device.guid } }" class="block group hover:no-underline">
:to="{ name: 'TrackerView', params: { guid: device.guid } }" class="block group hover:no-underline">
<Card class="h-full transition-shadow group-hover:shadow-lg">
<CardHeader class="bg-primary/5">
<CardTitle class="text-lg">

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { useColorMode } from '@vueuse/core'
import Navbar from '@/customcompometns/Navbar.vue';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import TrackerComponent from '@/customcompometns/TrackerComponent.vue';
const mode = useColorMode()
const route = useRoute()
const guid = computed(() => String(route.params.guid ?? route.query.guid ?? ''))
</script>
<template>
<Navbar>
<TrackerComponent :guid="guid"></TrackerComponent>
</Navbar>
</template>

View File

@@ -8,6 +8,7 @@ import DeviceView from './pages/DeviceView.vue';
import Forbidden from './pages/Forbidden.vue';
import Create from './pages/Create.vue';
import Trackers from '@/pages/Trackers.vue';
import TrackerViev from '@/pages/TrackerViev.vue';
import { auth } from './lib/auth';
@@ -72,6 +73,13 @@ const routes = [
component: Trackers,
meta: { requiresAuth: true }
},
{
path: '/tracker/:guid', // ← new dynamic segment
name: 'TrackerView',
component: TrackerViev,
props: true, // so `guid` shows up as a prop
meta: { requiresAuth: true }
},
]
const router = createRouter({

View File

@@ -20,5 +20,7 @@ func AutoMigrate(db *gorm.DB) error {
&models.Tracker{},
&models.UserTracker{},
&models.DEviceTask{},
&models.DeviceCertificate{},
&models.RevokedSerial{},
)
}