created views for trackers in UI, added leaflet
This commit is contained in:
25
management-ui/package-lock.json
generated
25
management-ui/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"reka-ui": "^2.5.0",
|
"reka-ui": "^2.5.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/radix-icons": "^1.2.2",
|
"@iconify-json/radix-icons": "^1.2.2",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.1.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
@@ -1273,6 +1275,23 @@
|
|||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.1.0",
|
"version": "24.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||||
@@ -1996,6 +2015,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"reka-ui": "^2.5.0",
|
"reka-ui": "^2.5.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/radix-icons": "^1.2.2",
|
"@iconify-json/radix-icons": "^1.2.2",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.1.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ onBeforeUnmount(() => {
|
|||||||
<TabsTrigger value="devices">
|
<TabsTrigger value="devices">
|
||||||
Devices
|
Devices
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="trackers">
|
||||||
|
Trackers
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<DataTableNoCheckbox :columns="user_columns" :data="user_data" :dropdownComponent="AdminUserDropdonw" />
|
<DataTableNoCheckbox :columns="user_columns" :data="user_data" :dropdownComponent="AdminUserDropdonw" />
|
||||||
|
|||||||
58
management-ui/src/customcompometns/TrackerComponent.vue
Normal file
58
management-ui/src/customcompometns/TrackerComponent.vue
Normal 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: '© 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>
|
||||||
77
management-ui/src/customcompometns/TrackerDashboard.vue
Normal file
77
management-ui/src/customcompometns/TrackerDashboard.vue
Normal 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>
|
||||||
@@ -39,6 +39,7 @@ const tracker_columns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
let treckerCtrl: AbortController | null = null
|
let treckerCtrl: AbortController | null = null
|
||||||
|
|
||||||
async function loadTrackers() {
|
async function loadTrackers() {
|
||||||
error.value = null
|
error.value = null
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -68,11 +69,11 @@ onBeforeUnmount(() => treckerCtrl?.abort())
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<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-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">
|
<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"
|
<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">
|
<Card class="h-full transition-shadow group-hover:shadow-lg">
|
||||||
<CardHeader class="bg-primary/5">
|
<CardHeader class="bg-primary/5">
|
||||||
<CardTitle class="text-lg">
|
<CardTitle class="text-lg">
|
||||||
|
|||||||
16
management-ui/src/pages/TrackerViev.vue
Normal file
16
management-ui/src/pages/TrackerViev.vue
Normal 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>
|
||||||
@@ -8,6 +8,7 @@ import DeviceView from './pages/DeviceView.vue';
|
|||||||
import Forbidden from './pages/Forbidden.vue';
|
import Forbidden from './pages/Forbidden.vue';
|
||||||
import Create from './pages/Create.vue';
|
import Create from './pages/Create.vue';
|
||||||
import Trackers from '@/pages/Trackers.vue';
|
import Trackers from '@/pages/Trackers.vue';
|
||||||
|
import TrackerViev from '@/pages/TrackerViev.vue';
|
||||||
import { auth } from './lib/auth';
|
import { auth } from './lib/auth';
|
||||||
|
|
||||||
|
|
||||||
@@ -72,6 +73,13 @@ const routes = [
|
|||||||
component: Trackers,
|
component: Trackers,
|
||||||
meta: { requiresAuth: true }
|
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({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -20,5 +20,7 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.Tracker{},
|
&models.Tracker{},
|
||||||
&models.UserTracker{},
|
&models.UserTracker{},
|
||||||
&models.DEviceTask{},
|
&models.DEviceTask{},
|
||||||
|
&models.DeviceCertificate{},
|
||||||
|
&models.RevokedSerial{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user