created new dialogs for device
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
48
management-ui/src/customcompometns/DeviceTasksDialog.vue
Normal file
48
management-ui/src/customcompometns/DeviceTasksDialog.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user