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

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useFilter } from 'reka-ui'
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'
import {
Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup,
ComboboxInput, ComboboxItem, ComboboxList
@@ -11,63 +11,52 @@ import {
import type { Users } from '@/lib/interfaces'
import { api } from '@/lib/api'
// ——— v-model plumbing ———
const props = defineProps<{
modelValue: string[] // selected user IDs (strings)
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: string[]): void
modelValue: string[] // selected user IDs
allUsers?: Users[] // OPTIONAL: full users list provided by parent
}>()
const emit = defineEmits<{ (e:'update:modelValue', v:string[]):void }>()
// computed proxy to v-model
// computed proxy
const selected = computed<string[]>({
get: () => props.modelValue ?? [],
set: (v) => emit('update:modelValue', v),
})
// ——— local state ———
const open = ref(false)
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
async function loadUsers() {
error.value = null
loading.value = true
if (usingExternal.value) return
try {
ctrl?.abort()
ctrl = new AbortController()
const { data } = await api.get<Users[]>('/users', { signal: ctrl.signal })
if (!Array.isArray(data)) throw new Error('Unexpected response')
allUsers.value = data.filter(
(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
}
if (Array.isArray(data)) {
internalUsers.value = data.filter((u:any) => typeof u?.id === 'number' && typeof u?.username === 'string')
}
} catch { /* ignore */ }
}
onMounted(loadUsers)
onBeforeUnmount(() => ctrl?.abort())
type UserOption = { value: string; label: string }
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 filteredUsers = computed(() => {
const selectedSet = new Set(selected.value)
const options = users.value.filter(o => !selectedSet.has(o.value))
return searchTerm.value
? options.filter(o => contains(o.label, searchTerm.value))
: options
return searchTerm.value ? options.filter(o => contains(o.label, searchTerm.value)) : options
})
function userLabelById(id: string) {
@@ -77,21 +66,20 @@ function userLabelById(id: string) {
function onSelect(ev: CustomEvent) {
const val = String((ev as any).detail?.value ?? '')
if (!val) return
if (!selected.value.includes(val)) {
// assign a new array so v-model updates properly
selected.value = [...selected.value, val]
}
if (!selected.value.includes(val)) selected.value = [...selected.value, val]
searchTerm.value = ''
if (filteredUsers.value.length === 0) {
open.value = false
}
if (filteredUsers.value.length === 0) open.value = false
}
// When parent switches between external/internal data at runtime, refetch if needed.
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
</script>
<template>
<Combobox v-model="selected" v-model:open="open" :ignore-filter="true">
<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">
<TagsInputItem v-for="id in selected" :key="id" :value="id">
<span class="px-1">{{ userLabelById(id) }}</span>
@@ -99,26 +87,22 @@ function onSelect(ev: CustomEvent) {
</TagsInputItem>
</div>
<ComboboxInput v-model="searchTerm" as-child>
<TagsInputInput
placeholder="User..."
class="min-w-[300px] w-full p-0 border-none focus-visible:ring-0 h-auto"
@keydown.enter.prevent
/>
</ComboboxInput>
<ComboboxInput v-model="searchTerm" as-child>
<TagsInputInput
placeholder="User..."
class="min-w-[200px] w-full p-0 border-none focus-visible:ring-0 h-auto"
@keydown.enter.prevent
/>
</ComboboxInput>
</TagsInput>
<!-- match input width -->
<ComboboxList
class="w-[--reka-popper-anchor-width] min-w-[350px]"
>
<ComboboxList class="w-[--reka-popper-anchor-width] w-full min-w-[350px]">
<ComboboxEmpty>
<span v-if="loading">Loading</span>
<span v-else-if="error">{{ error }}</span>
<span v-else>No users found.</span>
<span v-if="!usingExternal && !effectiveUsers.length">Loading</span>
<span v-else-if="!users.length">No users found.</span>
</ComboboxEmpty>
<ComboboxGroup v-if="!loading && !error && filteredUsers.length">
<ComboboxGroup v-if="filteredUsers.length">
<ComboboxItem
v-for="usr in filteredUsers"
:key="usr.value"
@@ -129,6 +113,7 @@ function onSelect(ev: CustomEvent) {
</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</div>
</ComboboxAnchor>
</Combobox>
</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
</CardTitle>
</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 space-y-2 grid-cols-4 items-center">
<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 type { PropType } from '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
const props = defineProps({
@@ -22,6 +22,8 @@ const props = defineProps({
required: true,
},
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
@@ -31,11 +33,37 @@ const emit = defineEmits<{
}>()
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
watch(
() => props.device,
(dev) => {
console.log(dev?.assigned_users)
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 }
)
@@ -52,7 +80,7 @@ function onSave() {
:open="props.modelValue"
@update:open="(v: boolean) => emit('update:modelValue', v)"
>
<DialogContent class="sm:max-w-[425px]">
<DialogContent class="sm:min-w-[500px]">
<DialogHeader>
<DialogTitle>Edit device</DialogTitle>
<DialogDescription>
@@ -71,7 +99,7 @@ function onSave() {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<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>