нахуевертил. it works but sucks, tryed to add reactivity to admin page and its child dialogs and components
This commit is contained in:
@@ -9,22 +9,36 @@ import DeleteDeviceDialog from './DeleteDeviceDialog.vue'
|
|||||||
import DeviceCertificateDialog from './DeviceCertificateDialog.vue'
|
import DeviceCertificateDialog from './DeviceCertificateDialog.vue'
|
||||||
import DeviceTasksDialog from './DeviceTasksDialog.vue'
|
import DeviceTasksDialog from './DeviceTasksDialog.vue'
|
||||||
import { Ellipsis } from 'lucide-vue-next'
|
import { Ellipsis } from 'lucide-vue-next'
|
||||||
import type { Device } from '@/lib/interfaces'
|
import type { Device, Users } from '@/lib/interfaces'
|
||||||
// import { api } from '@/lib/api'
|
// import { api } from '@/lib/api'
|
||||||
|
|
||||||
const props = defineProps<{ row: Device }>() // ← accept full row
|
const props = defineProps<{
|
||||||
|
row: Device
|
||||||
|
allUsers?: Users[]
|
||||||
|
onRowUpdated?: (row: Device, payload: { name: string; userIds: string[] }) => void
|
||||||
|
onRowDeleted?: (row: Device) => void
|
||||||
|
}>() // ← accept full row
|
||||||
|
|
||||||
const isEditOpen = ref(false)
|
const isEditOpen = ref(false)
|
||||||
const isDeleteOpen = ref(false)
|
const isDeleteOpen = ref(false)
|
||||||
const isTasksOpen = ref(false)
|
const isTasksOpen = ref(false)
|
||||||
const itCertsOpen = ref(false)
|
const itCertsOpen = ref(false)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'refresh'): void
|
||||||
|
(e: 'error', err: unknown): void
|
||||||
|
}>()
|
||||||
|
|
||||||
function onDeleteConfirmed() {
|
function onDeleteConfirmed() {
|
||||||
// await api.delete(`/devices/${encodeURIComponent(props.row.guid)}`)
|
// await api.delete(`/devices/${encodeURIComponent(props.row.guid)}`)
|
||||||
isDeleteOpen.value = false
|
isDeleteOpen.value = false
|
||||||
|
// emit('refresh')
|
||||||
|
props.onRowDeleted?.(props.row)
|
||||||
}
|
}
|
||||||
function onEditConfirm() {
|
function onEditConfirm(_payload: { name: string; userIds: string[] }) {
|
||||||
isEditOpen.value = false
|
isEditOpen.value = false
|
||||||
|
// emit('refresh')
|
||||||
|
props.onRowUpdated?.(props.row, _payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTaskConfirm() {
|
function onTaskConfirm() {
|
||||||
@@ -47,11 +61,11 @@ function onCertsConfirm() {
|
|||||||
<DropdownMenuItem @click.prevent="isEditOpen = true">Rename</DropdownMenuItem>
|
<DropdownMenuItem @click.prevent="isEditOpen = true">Rename</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click.prevent="isTasksOpen = true">Tasks</DropdownMenuItem>
|
<DropdownMenuItem @click.prevent="isTasksOpen = true">Tasks</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click.prevent="itCertsOpen = true">Certificates</DropdownMenuItem>
|
<DropdownMenuItem @click.prevent="itCertsOpen = true">Certificates</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click.prevent="isDeleteOpen = true">Delete</DropdownMenuItem>
|
<DropdownMenuItem class="text-destructive" @click.prevent="isDeleteOpen = true">Delete</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<EditDeviceDialog v-model:modelValue="isEditOpen" :device="props.row" @confirm="onEditConfirm" />
|
<EditDeviceDialog v-model:modelValue="isEditOpen" :device="props.row" :all-users="props.allUsers" @confirm="onEditConfirm" />
|
||||||
<DeleteDeviceDialog v-model:modelValue="isDeleteOpen" :device="props.row" @confirm="onDeleteConfirmed" />
|
<DeleteDeviceDialog v-model:modelValue="isDeleteOpen" :device="props.row" @confirm="onDeleteConfirmed" />
|
||||||
<DeviceCertificateDialog v-model:modelValue="itCertsOpen" :device="props.row" @confirm="onCertsConfirm" />
|
<DeviceCertificateDialog v-model:modelValue="itCertsOpen" :device="props.row" @confirm="onCertsConfirm" />
|
||||||
<DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" />
|
<DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" />
|
||||||
|
|||||||
@@ -1,38 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Tabs, TabsContent, TabsTrigger, TabsList } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsTrigger, TabsList } from '@/components/ui/tabs'
|
||||||
import DataTableNoCheckbox from './DataTableNoCheckbox.vue';
|
import DataTableNoCheckbox from './DataTableNoCheckbox.vue'
|
||||||
import AdminUserDropdonw from './AdminUserDropdonw.vue';
|
import AdminUserDropdonw from './AdminUserDropdonw.vue'
|
||||||
import AdminDeviceDropdown from './AdminDeviceDropdown.vue';
|
import AdminDeviceDropdown from './AdminDeviceDropdown.vue'
|
||||||
import type { Device, Users } from '@/lib/interfaces';
|
import type { Device, Users } from '@/lib/interfaces'
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, computed } from 'vue'
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------- Users ----------------
|
||||||
const user_columns = [
|
const user_columns = [
|
||||||
{ accessorKey: 'id', header: 'ID' },
|
{ accessorKey: 'id', header: 'ID' },
|
||||||
{ accessorKey: 'username', header: 'Username' },
|
{ accessorKey: 'username', header: 'Username' },
|
||||||
{ accessorKey: 'role', header: 'Role' },
|
{ accessorKey: 'role', header: 'Role' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const user_data = ref<Users[]>([])
|
const user_data = ref<Users[]>([])
|
||||||
const usersLoading = ref(false)
|
const usersLoading = ref(false)
|
||||||
const usersError = ref<string | null>(null)
|
const usersError = ref<string | null>(null)
|
||||||
|
|
||||||
function isUsersArray(data: unknown): data is Users[] {
|
function isUsersArray(data: unknown): data is Users[] {
|
||||||
return Array.isArray(data) && data.every(
|
return Array.isArray(data) && data.every(
|
||||||
(u) =>
|
(u) => u && typeof u === 'object'
|
||||||
u &&
|
&& typeof (u as any).id === 'number'
|
||||||
typeof u === 'object' &&
|
&& typeof (u as any).username === 'string'
|
||||||
typeof (u as any).id === 'number' &&
|
&& typeof (u as any).role === 'string'
|
||||||
typeof (u as any).username === 'string' &&
|
|
||||||
typeof (u as any).role === 'string'
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctrl: AbortController | null = null
|
let ctrl: AbortController | null = null
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
usersError.value = null
|
usersError.value = null
|
||||||
usersLoading.value = true
|
usersLoading.value = true
|
||||||
@@ -40,27 +35,23 @@ async function loadUsers() {
|
|||||||
ctrl?.abort()
|
ctrl?.abort()
|
||||||
ctrl = new AbortController()
|
ctrl = new AbortController()
|
||||||
const { data } = await api.get('/users', { signal: ctrl.signal })
|
const { data } = await api.get('/users', { signal: ctrl.signal })
|
||||||
|
if (!isUsersArray(data)) throw new Error('Unexpected response shape')
|
||||||
if (!isUsersArray(data)) {
|
|
||||||
throw new Error('Unexpected response shape')
|
|
||||||
}
|
|
||||||
|
|
||||||
user_data.value = data
|
user_data.value = data
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// keep error generic; specifics are logged server-side
|
|
||||||
if (e?.name === 'CanceledError' || e?.message === 'canceled') return
|
if (e?.name === 'CanceledError' || e?.message === 'canceled') return
|
||||||
if (e?.response?.status === 403) {
|
usersError.value = e?.response?.status === 403 ? 'Access denied.' : 'Failed to load users.'
|
||||||
usersError.value = 'Access denied.'
|
|
||||||
} else {
|
|
||||||
usersError.value = 'Failed to load users.'
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
usersLoading.value = false
|
usersLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to map ids -> usernames (for immediate UI update after dialog save)
|
||||||
|
const idToName = computed(() => new Map(user_data.value.map(u => [String(u.id), u.username])))
|
||||||
|
function usernamesFromIds(ids: string[]): string {
|
||||||
|
return ids.map(id => idToName.value.get(String(id))).filter(Boolean).join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Devices (NEW) ----------
|
// ---------------- Devices ----------------
|
||||||
type ApiDeviceUser = { id: number; username: string; role: string }
|
type ApiDeviceUser = { id: number; username: string; role: string }
|
||||||
type ApiDevice = { guid: string; name: string; users?: ApiDeviceUser[] }
|
type ApiDevice = { guid: string; name: string; users?: ApiDeviceUser[] }
|
||||||
type DevicesResponse = { devices: ApiDevice[]; offset: number; limit: number; total: number }
|
type DevicesResponse = { devices: ApiDevice[]; offset: number; limit: number; total: number }
|
||||||
@@ -69,29 +60,22 @@ function isDevicesResponse(d: unknown): d is DevicesResponse {
|
|||||||
const x = d as any
|
const x = d as any
|
||||||
return !!x && Array.isArray(x.devices) &&
|
return !!x && Array.isArray(x.devices) &&
|
||||||
x.devices.every((dev: any) =>
|
x.devices.every((dev: any) =>
|
||||||
dev &&
|
dev && typeof dev.guid === 'string' && typeof dev.name === 'string' &&
|
||||||
typeof dev.guid === 'string' &&
|
(dev.users === undefined ||
|
||||||
typeof dev.name === 'string' &&
|
(Array.isArray(dev.users) && dev.users.every((u: any) => u && typeof u.username === 'string')))
|
||||||
(
|
|
||||||
dev.users === undefined ||
|
|
||||||
(
|
|
||||||
Array.isArray(dev.users) &&
|
|
||||||
dev.users.every((u: any) => u && typeof u.username === 'string')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const device_columns = [
|
||||||
|
{ accessorKey: 'guid', header: 'GUID' },
|
||||||
|
{ accessorKey: 'devicename', header: 'Device' },
|
||||||
|
{ accessorKey: 'assigned_users', header: 'Users' },
|
||||||
|
]
|
||||||
|
|
||||||
const device_data = ref<Device[]>([])
|
const device_data = ref<Device[]>([])
|
||||||
const devicesLoading = ref(false)
|
const devicesLoading = ref(false)
|
||||||
const devicesError = ref<string | null>(null)
|
const devicesError = ref<string | null>(null)
|
||||||
|
|
||||||
const device_columns = [
|
|
||||||
{ accessorKey: 'guid', header: 'GUID' },
|
|
||||||
{ accessorKey: 'devicename', header: 'Device' },
|
|
||||||
{ accessorKey: 'assigned_users', header: 'Users' },
|
|
||||||
]
|
|
||||||
|
|
||||||
let devicesCtrl: AbortController | null = null
|
let devicesCtrl: AbortController | null = null
|
||||||
async function loadDevices() {
|
async function loadDevices() {
|
||||||
devicesError.value = null
|
devicesError.value = null
|
||||||
@@ -100,10 +84,7 @@ async function loadDevices() {
|
|||||||
devicesCtrl?.abort()
|
devicesCtrl?.abort()
|
||||||
devicesCtrl = new AbortController()
|
devicesCtrl = new AbortController()
|
||||||
const { data } = await api.get('/devices', { signal: devicesCtrl.signal })
|
const { data } = await api.get('/devices', { signal: devicesCtrl.signal })
|
||||||
|
|
||||||
if (!isDevicesResponse(data)) throw new Error('Unexpected devices response')
|
if (!isDevicesResponse(data)) throw new Error('Unexpected devices response')
|
||||||
|
|
||||||
// Transform API -> table shape
|
|
||||||
device_data.value = data.devices.map((d) => ({
|
device_data.value = data.devices.map((d) => ({
|
||||||
guid: d.guid,
|
guid: d.guid,
|
||||||
devicename: d.name,
|
devicename: d.name,
|
||||||
@@ -117,7 +98,7 @@ async function loadDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Trackers
|
// ---------------- Trackers ----------------
|
||||||
type ApiTracker = { guid: string; name: string; users?: ApiDeviceUser[] }
|
type ApiTracker = { guid: string; name: string; users?: ApiDeviceUser[] }
|
||||||
type TrackersResponse = { trackers: ApiTracker[]; offset: number; limit: number; total: number }
|
type TrackersResponse = { trackers: ApiTracker[]; offset: number; limit: number; total: number }
|
||||||
|
|
||||||
@@ -125,29 +106,22 @@ function isTrackersResponse(t: unknown): t is TrackersResponse {
|
|||||||
const x = t as any
|
const x = t as any
|
||||||
return !!x && Array.isArray(x.trackers) &&
|
return !!x && Array.isArray(x.trackers) &&
|
||||||
x.trackers.every((dev: any) =>
|
x.trackers.every((dev: any) =>
|
||||||
dev &&
|
dev && typeof dev.guid === 'string' && typeof dev.name === 'string' &&
|
||||||
typeof dev.guid === 'string' &&
|
(dev.users === undefined ||
|
||||||
typeof dev.name === 'string' &&
|
(Array.isArray(dev.users) && dev.users.every((u: any) => u && typeof u.username === 'string')))
|
||||||
(
|
|
||||||
dev.users === undefined ||
|
|
||||||
(
|
|
||||||
Array.isArray(dev.users) &&
|
|
||||||
dev.users.every((u: any) => u && typeof u.username === 'string')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tracker_columns = [
|
||||||
|
{ accessorKey: 'guid', header: 'GUID' },
|
||||||
|
{ accessorKey: 'devicename', header: 'Tracker' },
|
||||||
|
{ accessorKey: 'assigned_users', header: 'Users' },
|
||||||
|
]
|
||||||
|
|
||||||
const trackers_data = ref<Device[]>([])
|
const trackers_data = ref<Device[]>([])
|
||||||
const trackersLoading = ref(false)
|
const trackersLoading = ref(false)
|
||||||
const trackersError = ref<string | null>(null)
|
const trackersError = ref<string | null>(null)
|
||||||
|
|
||||||
const tracker_columns = [
|
|
||||||
{ accessorKey: 'guid', header: 'GUID' },
|
|
||||||
{ accessorKey: 'devicename', header: 'Tracker' },
|
|
||||||
{ accessorKey: 'assigned_users', header: 'Users' },
|
|
||||||
]
|
|
||||||
|
|
||||||
let treckerCtrl: AbortController | null = null
|
let treckerCtrl: AbortController | null = null
|
||||||
async function loadTrackers() {
|
async function loadTrackers() {
|
||||||
trackersError.value = null
|
trackersError.value = null
|
||||||
@@ -156,10 +130,7 @@ async function loadTrackers() {
|
|||||||
treckerCtrl?.abort()
|
treckerCtrl?.abort()
|
||||||
treckerCtrl = new AbortController()
|
treckerCtrl = new AbortController()
|
||||||
const { data } = await api.get('/trackers', { signal: treckerCtrl.signal })
|
const { data } = await api.get('/trackers', { signal: treckerCtrl.signal })
|
||||||
|
|
||||||
if (!isTrackersResponse(data)) throw new Error('Unexpected trackers response')
|
if (!isTrackersResponse(data)) throw new Error('Unexpected trackers response')
|
||||||
|
|
||||||
// Transform API -> table shape
|
|
||||||
trackers_data.value = data.trackers.map((d) => ({
|
trackers_data.value = data.trackers.map((d) => ({
|
||||||
guid: d.guid,
|
guid: d.guid,
|
||||||
devicename: d.name,
|
devicename: d.name,
|
||||||
@@ -173,47 +144,83 @@ async function loadTrackers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- Immediate UI updates from dropdown dialogs ----------------
|
||||||
|
function handleDeviceUpdated(row: Device, payload: { name: string; userIds: string[] }) {
|
||||||
|
const guid = row.guid
|
||||||
|
const usersStr = usernamesFromIds(payload.userIds)
|
||||||
|
device_data.value = device_data.value.map(d =>
|
||||||
|
d.guid === guid ? { ...d, devicename: payload.name, assigned_users: usersStr } : d
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function handleDeviceDeleted(row: Device) {
|
||||||
|
const guid = row.guid
|
||||||
|
device_data.value = device_data.value.filter(d => d.guid !== guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trackers use the same dropdown to edit, keep these too:
|
||||||
|
function handleTrackerUpdated(row: Device, payload: { name: string; userIds: string[] }) {
|
||||||
|
const guid = row.guid
|
||||||
|
const usersStr = usernamesFromIds(payload.userIds)
|
||||||
|
trackers_data.value = trackers_data.value.map(d =>
|
||||||
|
d.guid === guid ? { ...d, devicename: payload.name, assigned_users: usersStr } : d
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function handleTrackerDeleted(row: Device) {
|
||||||
|
const guid = row.guid
|
||||||
|
trackers_data.value = trackers_data.value.filter(d => d.guid !== guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- lifecycle ----------------
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUsers()
|
loadUsers()
|
||||||
loadDevices()
|
loadDevices()
|
||||||
loadTrackers()
|
loadTrackers()
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
ctrl?.abort()
|
ctrl?.abort()
|
||||||
devicesCtrl?.abort()
|
devicesCtrl?.abort()
|
||||||
treckerCtrl?.abort()
|
treckerCtrl?.abort()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a href="/create">
|
<a href="/create">
|
||||||
<Button>
|
<Button>Create</Button>
|
||||||
Create
|
</a>
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<Tabs default-value="users">
|
<Tabs default-value="users">
|
||||||
<TabsList class="space-x-8">
|
<TabsList class="space-x-8">
|
||||||
<TabsTrigger value="users">
|
<TabsTrigger value="users">Users</TabsTrigger>
|
||||||
Users
|
<TabsTrigger value="devices">Devices</TabsTrigger>
|
||||||
</TabsTrigger>
|
<TabsTrigger value="trackers">Trackers</TabsTrigger>
|
||||||
<TabsTrigger value="devices">
|
</TabsList>
|
||||||
Devices
|
|
||||||
</TabsTrigger>
|
<TabsContent value="users">
|
||||||
<TabsTrigger value="trackers">
|
<DataTableNoCheckbox
|
||||||
Trackers
|
:columns="user_columns"
|
||||||
</TabsTrigger>
|
:data="user_data"
|
||||||
</TabsList>
|
:dropdownComponent="AdminUserDropdonw"
|
||||||
<TabsContent value="users">
|
/>
|
||||||
<DataTableNoCheckbox :columns="user_columns" :data="user_data" :dropdownComponent="AdminUserDropdonw" />
|
</TabsContent>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="devices">
|
<TabsContent value="devices">
|
||||||
<DataTableNoCheckbox :columns="device_columns" :data="device_data"
|
<DataTableNoCheckbox
|
||||||
:dropdownComponent="AdminDeviceDropdown" />
|
:columns="device_columns"
|
||||||
</TabsContent>
|
:data="device_data"
|
||||||
<TabsContent value="trackers">
|
:dropdownComponent="AdminDeviceDropdown"
|
||||||
<DataTableNoCheckbox :columns="tracker_columns" :data="trackers_data"
|
:onRowUpdated="handleDeviceUpdated"
|
||||||
:dropdownComponent="AdminDeviceDropdown" />
|
:onRowDeleted="handleDeviceDeleted"
|
||||||
</TabsContent>
|
/>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
</template>
|
|
||||||
|
<TabsContent value="trackers">
|
||||||
|
<DataTableNoCheckbox
|
||||||
|
:columns="tracker_columns"
|
||||||
|
:data="trackers_data"
|
||||||
|
:dropdownComponent="AdminDeviceDropdown"
|
||||||
|
:onRowUpdated="handleTrackerUpdated"
|
||||||
|
:onRowDeleted="handleTrackerDeleted"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ const props = defineProps<{
|
|||||||
* :dropdownComponent="MyActionsMenu"
|
* :dropdownComponent="MyActionsMenu"
|
||||||
* />
|
* />
|
||||||
*/
|
*/
|
||||||
dropdownComponent?: DefineComponent<{ row: TData }, any, any>
|
// dropdownComponent?: DefineComponent<{ row: TData }, any, any>
|
||||||
|
dropdownComponent?: DefineComponent<{ row: TData } & {
|
||||||
|
onRowUpdated?: (row: TData, payload: any) => void
|
||||||
|
onRowDeleted?: (row: TData) => void
|
||||||
|
}, any, any>
|
||||||
|
onRowUpdated?: (row: TData, payload: any) => void // <-- NEW
|
||||||
|
onRowDeleted?: (row: TData) => void // <-- NEW
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// ——— Table setup ———
|
// ——— Table setup ———
|
||||||
@@ -97,6 +103,8 @@ const table = useVueTable({
|
|||||||
:is="props.dropdownComponent"
|
:is="props.dropdownComponent"
|
||||||
:row="row.original"
|
:row="row.original"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
|
:on-row-updated="props.onRowUpdated"
|
||||||
|
:on-row-deleted="props.onRowDeleted"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function onSave() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>List of certificates</DialogTitle>
|
<DialogTitle>List of certificates</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
List of certificates for device {{ props.device?.guid }}
|
{{ props.device?.guid }}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function onSave() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Tasks</DialogTitle>
|
<DialogTitle>Tasks</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
List of tasks for device {{ props.device?.guid }}
|
{{ props.device?.guid }}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -10,76 +10,111 @@ import {
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { defineProps, defineEmits, ref, watch } from 'vue'
|
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import AssignDevice from './AssignDevice.vue'
|
import AssignDevice from './AssignDevice.vue'
|
||||||
import type { Device, Users } from '@/lib/interfaces'
|
import type { Device, Users } from '@/lib/interfaces'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
|
|
||||||
|
type DeviceWithUsers =
|
||||||
|
& Partial<Device>
|
||||||
|
& { guid?: string; name?: string; devicename?: string }
|
||||||
|
& { users?: Array<{ id: number; username: string; role?: string }> }
|
||||||
|
|
||||||
// 1) runtime props so Vue + TS agree
|
// 1) runtime props so Vue + TS agree
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: { type: Boolean as PropType<boolean>, required: true },
|
||||||
type: Boolean as PropType<boolean>,
|
device: { type: Object as PropType<DeviceWithUsers>, required: true }, // must have guid
|
||||||
required: true,
|
allUsers: { type: Array as PropType<Users[]>, required: false },
|
||||||
},
|
|
||||||
device: { type: Object as PropType<Device>, required: false },
|
|
||||||
allUsers: { type: Array as PropType<Users[]>, required: false },
|
|
||||||
initialUserIds: { type: Array as PropType<string[]>, required: false }, // <- if you have IDs already
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2) two emits: v-model and confirm
|
// 2) two emits: v-model and confirm
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', v: boolean): void
|
(e: 'update:modelValue', v: boolean): void
|
||||||
(e: 'confirm'): void
|
// (e: 'confirm'): void
|
||||||
|
(e: 'updated', payload: { name: string; userIds: string[] }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// ---------- Local form state ----------
|
||||||
|
const guid = computed(() => String(props.device?.guid ?? ''))
|
||||||
|
const originalName = computed(() => (props.device?.devicename ?? props.device?.name ?? ''))
|
||||||
|
const originalIds = computed<string[]>(() =>
|
||||||
|
Array.isArray(props.device?.users) ? props.device!.users.map(u => String(u.id)) : []
|
||||||
|
)
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const selectedUserIds = ref<string[]>([])
|
const selectedUserIds = ref<string[]>([])
|
||||||
|
|
||||||
// helper: map usernames → ids when we only have a string of usernames
|
|
||||||
const usernameToId = (uname: string): string | null => {
|
|
||||||
const id = props.allUsers?.find(u => u.username === uname)?.id
|
|
||||||
return typeof id === 'number' ? String(id) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// when device changes or dialog opens, update local value
|
|
||||||
watch(
|
watch(
|
||||||
() => props.device,
|
() => props.device,
|
||||||
(dev) => {
|
() => {
|
||||||
console.log(dev?.assigned_users)
|
name.value = originalName.value
|
||||||
name.value = dev?.devicename ?? ''
|
selectedUserIds.value = [...originalIds.value]
|
||||||
if (props.initialUserIds && props.initialUserIds.length) {
|
|
||||||
selectedUserIds.value = [...props.initialUserIds]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const usernames = (dev?.assigned_users ?? '')
|
|
||||||
.split(',')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
if (usernames.length && props.allUsers?.length) {
|
|
||||||
selectedUserIds.value = usernames
|
|
||||||
.map(usernameToId)
|
|
||||||
.filter((x): x is string => !!x)
|
|
||||||
} else {
|
|
||||||
selectedUserIds.value = []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
function onSave() {
|
// ---------- Save (conditional API calls) ----------
|
||||||
emit('confirm')
|
const saving = ref(false)
|
||||||
// close the dialog
|
const errorText = ref<string | null>(null)
|
||||||
emit('update:modelValue', false)
|
|
||||||
|
function changedName() {
|
||||||
|
return name.value.trim() !== originalName.value.trim()
|
||||||
|
}
|
||||||
|
function changedUsers() {
|
||||||
|
// compare as sets
|
||||||
|
const a = new Set(originalIds.value)
|
||||||
|
const b = new Set(selectedUserIds.value)
|
||||||
|
if (a.size !== b.size) return true
|
||||||
|
for (const id of a) if (!b.has(id)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave() {
|
||||||
|
if (!guid.value) return
|
||||||
|
errorText.value = null
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const ops: Promise<any>[] = []
|
||||||
|
|
||||||
|
if (changedName()) {
|
||||||
|
ops.push(
|
||||||
|
api.post(`/devices/${encodeURIComponent(guid.value)}/rename`, {
|
||||||
|
name: name.value.trim(),
|
||||||
|
} as { name: string })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedUsers()) {
|
||||||
|
const userIdsNum = selectedUserIds.value
|
||||||
|
.map(v => Number(v))
|
||||||
|
.filter(n => Number.isFinite(n)) as number[]
|
||||||
|
|
||||||
|
ops.push(
|
||||||
|
api.post(`/devices/${encodeURIComponent(guid.value)}/set_users`, {
|
||||||
|
userIds: userIdsNum,
|
||||||
|
} as { userIds: number[] })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ops.length > 0) {
|
||||||
|
await Promise.all(ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('updated', { name: name.value, userIds: [...selectedUserIds.value] })
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
errorText.value = err?.response?.data?.message || 'Failed to save changes.'
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
|
||||||
:open="props.modelValue"
|
|
||||||
@update:open="(v: boolean) => emit('update:modelValue', v)"
|
|
||||||
>
|
|
||||||
<DialogContent class="sm:min-w-[500px]">
|
<DialogContent class="sm:min-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit device</DialogTitle>
|
<DialogTitle>Edit device</DialogTitle>
|
||||||
@@ -91,20 +126,22 @@ function onSave() {
|
|||||||
<div class="grid gap-4 py-4">
|
<div class="grid gap-4 py-4">
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="guid" class="text-right">GUID</Label>
|
<Label for="guid" class="text-right">GUID</Label>
|
||||||
<p id="guid" class="col-span-3"> {{ props.device?.guid }}</p>
|
<p id="guid" class="col-span-3 break-all text-muted-foreground"> {{ props.device?.guid }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="name" class="text-right">Name</Label>
|
<Label for="name" class="text-right">Name</Label>
|
||||||
<Input id="name" class="col-span-3" v-model="name" />
|
<Input id="name" class="col-span-3" v-model="name" :disabled="saving" placeholder="Device name"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="users" class="text-right">Allowed users</Label>
|
<Label for="users" class="text-right">Allowed users</Label>
|
||||||
<AssignDevice id="users" v-model="selectedUserIds" :all-users="props.allUsers" class="col-span-3 w-full"/>
|
<AssignDevice id="users" v-model="selectedUserIds" :all-users="props.allUsers" class="col-span-3 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="errorText" class="text-sm text-red-600 mt-1 col-span-4">{{ errorText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button @click="onSave">Save changes</Button>
|
<Button :disabled="saving" @click="onSave">
|
||||||
|
{{ saving ? 'Saving…' : 'Save changes' }}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user