created new dialogs for device

This commit is contained in:
tdv
2025-10-07 18:32:48 +03:00
parent 79dbd98ca6
commit 22469ac206
6 changed files with 182 additions and 56 deletions

View File

@@ -6,6 +6,8 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import EditDeviceDialog from './EditDeviceDialog.vue' import EditDeviceDialog from './EditDeviceDialog.vue'
import DeleteDeviceDialog from './DeleteDeviceDialog.vue' import DeleteDeviceDialog from './DeleteDeviceDialog.vue'
import DeviceCertificateDialog from './DeviceCertificateDialog.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 } from '@/lib/interfaces'
// import { api } from '@/lib/api' // import { api } from '@/lib/api'
@@ -14,6 +16,8 @@ const props = defineProps<{ row: Device }>() // ← accept full row
const isEditOpen = ref(false) const isEditOpen = ref(false)
const isDeleteOpen = ref(false) const isDeleteOpen = ref(false)
const isTasksOpen = ref(false)
const itCertsOpen = ref(false)
function onDeleteConfirmed() { function onDeleteConfirmed() {
// await api.delete(`/devices/${encodeURIComponent(props.row.guid)}`) // await api.delete(`/devices/${encodeURIComponent(props.row.guid)}`)
@@ -22,6 +26,14 @@ function onDeleteConfirmed() {
function onEditConfirm() { function onEditConfirm() {
isEditOpen.value = false isEditOpen.value = false
} }
function onTaskConfirm() {
isTasksOpen.value = false
}
function onCertsConfirm() {
itCertsOpen.value = false
}
</script> </script>
<template> <template>
@@ -33,10 +45,14 @@ function onEditConfirm() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[160px]"> <DropdownMenuContent align="end" class="w-[160px]">
<DropdownMenuItem @click.prevent="isEditOpen = true">Rename</DropdownMenuItem> <DropdownMenuItem @click.prevent="isEditOpen = true">Rename</DropdownMenuItem>
<DropdownMenuItem @click.prevent="isTasksOpen = true">Tasks</DropdownMenuItem>
<DropdownMenuItem @click.prevent="itCertsOpen = true">Certificates</DropdownMenuItem>
<DropdownMenuItem @click.prevent="isDeleteOpen = true">Delete</DropdownMenuItem> <DropdownMenuItem @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" @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" />
<DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" />
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useFilter } from 'reka-ui' import { useFilter } from 'reka-ui'
import { computed, ref, onMounted, onBeforeUnmount } from 'vue' import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { import {
Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup, Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup,
ComboboxInput, ComboboxItem, ComboboxList ComboboxInput, ComboboxItem, ComboboxList
@@ -11,63 +11,52 @@ import {
import type { Users } from '@/lib/interfaces' import type { Users } from '@/lib/interfaces'
import { api } from '@/lib/api' import { api } from '@/lib/api'
// ——— v-model plumbing ———
const props = defineProps<{ const props = defineProps<{
modelValue: string[] // selected user IDs (strings) modelValue: string[] // selected user IDs
}>() allUsers?: Users[] // OPTIONAL: full users list provided by parent
const emit = defineEmits<{
(e: 'update:modelValue', v: string[]): void
}>() }>()
const emit = defineEmits<{ (e:'update:modelValue', v:string[]):void }>()
// computed proxy to v-model // computed proxy
const selected = computed<string[]>({ const selected = computed<string[]>({
get: () => props.modelValue ?? [], get: () => props.modelValue ?? [],
set: (v) => emit('update:modelValue', v), set: (v) => emit('update:modelValue', v),
}) })
// ——— local state ———
const open = ref(false) const open = ref(false)
const searchTerm = ref('') const searchTerm = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const allUsers = ref<Users[]>([]) /** SOURCE of truth for users used by the combobox */
const internalUsers = ref<Users[]>([]) // used only when parent didn't pass allUsers
const usingExternal = computed(() => Array.isArray(props.allUsers))
const effectiveUsers = computed<Users[]>(() => usingExternal.value ? (props.allUsers as Users[]) : internalUsers.value)
// self-fetch only when parent didn't provide allUsers
let ctrl: AbortController | null = null let ctrl: AbortController | null = null
async function loadUsers() { async function loadUsers() {
error.value = null if (usingExternal.value) return
loading.value = true
try { try {
ctrl?.abort() ctrl?.abort()
ctrl = new AbortController() ctrl = new AbortController()
const { data } = await api.get<Users[]>('/users', { signal: ctrl.signal }) const { data } = await api.get<Users[]>('/users', { signal: ctrl.signal })
if (!Array.isArray(data)) throw new Error('Unexpected response') if (Array.isArray(data)) {
allUsers.value = data.filter( internalUsers.value = data.filter((u:any) => typeof u?.id === 'number' && typeof u?.username === 'string')
(u: any) => typeof u?.id === 'number' && typeof u?.username === 'string'
)
} catch (e: any) {
if (e?.name === 'CanceledError' || e?.message === 'canceled') return
error.value = 'Failed to load users.'
} finally {
loading.value = false
} }
} catch { /* ignore */ }
} }
onMounted(loadUsers) onMounted(loadUsers)
onBeforeUnmount(() => ctrl?.abort()) onBeforeUnmount(() => ctrl?.abort())
type UserOption = { value: string; label: string } type UserOption = { value: string; label: string }
const users = computed<UserOption[]>(() => const users = computed<UserOption[]>(() =>
allUsers.value.map(u => ({ value: String(u.id), label: u.username })) effectiveUsers.value.map(u => ({ value: String(u.id), label: u.username }))
) )
const { contains } = useFilter({ sensitivity: 'base' }) const { contains } = useFilter({ sensitivity: 'base' })
const filteredUsers = computed(() => { const filteredUsers = computed(() => {
const selectedSet = new Set(selected.value) const selectedSet = new Set(selected.value)
const options = users.value.filter(o => !selectedSet.has(o.value)) const options = users.value.filter(o => !selectedSet.has(o.value))
return searchTerm.value return searchTerm.value ? options.filter(o => contains(o.label, searchTerm.value)) : options
? options.filter(o => contains(o.label, searchTerm.value))
: options
}) })
function userLabelById(id: string) { function userLabelById(id: string) {
@@ -77,21 +66,20 @@ function userLabelById(id: string) {
function onSelect(ev: CustomEvent) { function onSelect(ev: CustomEvent) {
const val = String((ev as any).detail?.value ?? '') const val = String((ev as any).detail?.value ?? '')
if (!val) return if (!val) return
if (!selected.value.includes(val)) { if (!selected.value.includes(val)) selected.value = [...selected.value, val]
// assign a new array so v-model updates properly
selected.value = [...selected.value, val]
}
searchTerm.value = '' searchTerm.value = ''
if (filteredUsers.value.length === 0) { if (filteredUsers.value.length === 0) open.value = false
open.value = false
}
} }
// When parent switches between external/internal data at runtime, refetch if needed.
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
</script> </script>
<template> <template>
<Combobox v-model="selected" v-model:open="open" :ignore-filter="true"> <Combobox v-model="selected" v-model:open="open" :ignore-filter="true">
<ComboboxAnchor as-child> <ComboboxAnchor as-child>
<TagsInput v-model="selected" class="px-2 gap-2 w-90"> <div class="relative w-full">
<TagsInput v-model="selected" class="w-full px-2 gap-2">
<div class="flex gap-2 flex-wrap items-center"> <div class="flex gap-2 flex-wrap items-center">
<TagsInputItem v-for="id in selected" :key="id" :value="id"> <TagsInputItem v-for="id in selected" :key="id" :value="id">
<span class="px-1">{{ userLabelById(id) }}</span> <span class="px-1">{{ userLabelById(id) }}</span>
@@ -102,23 +90,19 @@ function onSelect(ev: CustomEvent) {
<ComboboxInput v-model="searchTerm" as-child> <ComboboxInput v-model="searchTerm" as-child>
<TagsInputInput <TagsInputInput
placeholder="User..." placeholder="User..."
class="min-w-[300px] w-full p-0 border-none focus-visible:ring-0 h-auto" class="min-w-[200px] w-full p-0 border-none focus-visible:ring-0 h-auto"
@keydown.enter.prevent @keydown.enter.prevent
/> />
</ComboboxInput> </ComboboxInput>
</TagsInput> </TagsInput>
<!-- match input width --> <ComboboxList class="w-[--reka-popper-anchor-width] w-full min-w-[350px]">
<ComboboxList
class="w-[--reka-popper-anchor-width] min-w-[350px]"
>
<ComboboxEmpty> <ComboboxEmpty>
<span v-if="loading">Loading</span> <span v-if="!usingExternal && !effectiveUsers.length">Loading</span>
<span v-else-if="error">{{ error }}</span> <span v-else-if="!users.length">No users found.</span>
<span v-else>No users found.</span>
</ComboboxEmpty> </ComboboxEmpty>
<ComboboxGroup v-if="!loading && !error && filteredUsers.length"> <ComboboxGroup v-if="filteredUsers.length">
<ComboboxItem <ComboboxItem
v-for="usr in filteredUsers" v-for="usr in filteredUsers"
:key="usr.value" :key="usr.value"
@@ -129,6 +113,7 @@ function onSelect(ev: CustomEvent) {
</ComboboxItem> </ComboboxItem>
</ComboboxGroup> </ComboboxGroup>
</ComboboxList> </ComboboxList>
</div>
</ComboboxAnchor> </ComboboxAnchor>
</Combobox> </Combobox>
</template> </template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import type { Device } from '@/lib/interfaces';
import type { PropType } from 'vue';
const props = defineProps({
modelValue: {
type: Boolean as PropType<boolean>,
required: true,
},
device: { type: Object as PropType<Device>, required: false },
})
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(e: 'confirm'): void
}>()
function onSave() {
emit('confirm')
// close the dialog
emit('update:modelValue', false)
}
</script>
<template>
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
<DialogContent class="sm:min-w-[800px]">
<DialogHeader>
<DialogTitle>List of certificates</DialogTitle>
<DialogDescription>
List of certificates for device {{ props.device?.guid }}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button @click="onSave">Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@@ -32,7 +32,7 @@ const usrIDs = selectedUserIds.value
Dashboard Dashboard
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent class="flex-1 space-y-6"> <CardContent class="flex-1 grid gap-4 py-4">
<div class="grid gap-5"> <div class="grid gap-5">
<div class="grid space-y-2 grid-cols-4 items-center"> <div class="grid space-y-2 grid-cols-4 items-center">
<Label for="users">Allowed users</Label> <Label for="users">Allowed users</Label>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import type { Device } from '@/lib/interfaces';
import type { PropType } from 'vue';
const props = defineProps({
modelValue: {
type: Boolean as PropType<boolean>,
required: true,
},
device: { type: Object as PropType<Device>, required: false },
})
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(e: 'confirm'): void
}>()
function onSave() {
emit('confirm')
// close the dialog
emit('update:modelValue', false)
}
</script>
<template>
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
<DialogContent class="sm:min-w-[800px]">
<DialogHeader>
<DialogTitle>Tasks</DialogTitle>
<DialogDescription>
List of tasks for device {{ props.device?.guid }}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button @click="onSave">Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@@ -13,7 +13,7 @@ import { Label } from '@/components/ui/label'
import { defineProps, defineEmits, ref, watch } from 'vue' import { defineProps, defineEmits, ref, watch } 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 } from '@/lib/interfaces' import type { Device, Users } from '@/lib/interfaces'
// 1) runtime props so Vue + TS agree // 1) runtime props so Vue + TS agree
const props = defineProps({ const props = defineProps({
@@ -22,6 +22,8 @@ const props = defineProps({
required: true, required: true,
}, },
device: { type: Object as PropType<Device>, 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
@@ -31,11 +33,37 @@ const emit = defineEmits<{
}>() }>()
const name = ref('') const name = ref('')
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 // when device changes or dialog opens, update local value
watch( watch(
() => props.device, () => props.device,
(dev) => { (dev) => {
console.log(dev?.assigned_users)
name.value = dev?.devicename ?? '' name.value = dev?.devicename ?? ''
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 }
) )
@@ -52,7 +80,7 @@ function onSave() {
:open="props.modelValue" :open="props.modelValue"
@update:open="(v: boolean) => emit('update:modelValue', v)" @update:open="(v: boolean) => emit('update:modelValue', v)"
> >
<DialogContent class="sm:max-w-[425px]"> <DialogContent class="sm:min-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit device</DialogTitle> <DialogTitle>Edit device</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -71,7 +99,7 @@ function onSave() {
</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"/> <AssignDevice id="users" v-model="selectedUserIds" :all-users="props.allUsers" class="col-span-3 w-full"/>
</div> </div>
</div> </div>