Skip to content

Commit 2939a91

Browse files
committed
feat: refactor admin view and improve UI components for better readability and usability
1 parent e2189f1 commit 2939a91

6 files changed

Lines changed: 392 additions & 184 deletions

File tree

packages/frontend/src/App.vue

Lines changed: 6 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const addServerVisible = ref(false)
3030
const createProjectVisible = ref(false)
3131
const manageProjectUsersVisible = ref(false)
3232
const serverPopover = ref()
33-
const userPopover = ref()
3433
const projectMenu = ref()
3534
const editingServer = ref<ServerProfile | null>(null)
3635
const editingProject = ref<ProjectRecord | null>(null)
@@ -353,51 +352,6 @@ const openManageProjectUsersDialog = () => {
353352
354353
manageProjectUsersVisible.value = true
355354
}
356-
357-
const authButtonTooltip = computed(() => {
358-
if (!authStore.status.enabled) {
359-
return null
360-
}
361-
362-
if (authStore.currentUser) {
363-
return authStore.currentUser.email
364-
}
365-
366-
return authStore.requiresOnboarding ? 'Create the first admin user' : 'Sign in'
367-
})
368-
369-
const toggleUserPopover = async (event: Event) => {
370-
if (!authStore.status.enabled) {
371-
return
372-
}
373-
374-
if (!authStore.currentUser) {
375-
await router.push({ name: authStore.requiresOnboarding ? 'onboarding' : 'login' })
376-
return
377-
}
378-
379-
userPopover.value?.toggle(event)
380-
}
381-
382-
const goToAdmin = async () => {
383-
userPopover.value?.hide()
384-
await router.push({ name: 'admin' })
385-
}
386-
387-
const logout = async () => {
388-
userPopover.value?.hide()
389-
390-
try {
391-
await authStore.logout()
392-
} catch (error) {
393-
toast.add({
394-
severity: 'error',
395-
summary: 'Logout failed',
396-
detail: getErrorMessage(error, 'The session could not be closed cleanly.'),
397-
life: 3200,
398-
})
399-
}
400-
}
401355
</script>
402356

403357
<template>
@@ -407,7 +361,7 @@ const logout = async () => {
407361
<div
408362
class="grid h-full"
409363
:style="{
410-
gridTemplateColumns: `76px ${sideBarWidth}px 1fr`,
364+
gridTemplateColumns: `76px ${route.meta?.hideSidebar ? '' : `${sideBarWidth}px`} 1fr`,
411365
}"
412366
>
413367
<div class="p-2 border-r border-neutral-200 dark:border-neutral-800 app-left-rail">
@@ -455,51 +409,6 @@ const logout = async () => {
455409
</div>
456410

457411
<div class="p-1 mx-auto flex flex-col items-center gap-2">
458-
<Button
459-
v-if="authStore.status.enabled"
460-
v-tooltip.right="authButtonTooltip || undefined"
461-
:icon="`ti ${authStore.currentUser ? 'ti-user-circle' : 'ti-lock'}`"
462-
severity="secondary"
463-
rounded
464-
text
465-
size="large"
466-
class="border border-primary-500/20"
467-
@click="toggleUserPopover"
468-
/>
469-
470-
<Popover v-if="authStore.status.enabled && authStore.currentUser" ref="userPopover">
471-
<div class="w-[18rem] flex flex-col gap-3">
472-
<div>
473-
<div class="font-semibold">{{ authStore.currentUser.name }}</div>
474-
<div class="text-sm opacity-70">{{ authStore.currentUser.email }}</div>
475-
</div>
476-
477-
<div
478-
class="rounded-xl border border-neutral-200 dark:border-neutral-800 px-3 py-2 text-xs opacity-70"
479-
>
480-
{{ workspaceStore.currentServer?.name || 'Current server' }}
481-
</div>
482-
483-
<Button size="small"
484-
v-if="authStore.hasPermission(adminPermissionTargets('access', 'read'))"
485-
label="Admin page"
486-
icon="ti ti-shield"
487-
severity="secondary"
488-
text
489-
class="justify-start"
490-
@click="goToAdmin"
491-
/>
492-
<Button size="small"
493-
label="Sign out"
494-
icon="ti ti-logout-2"
495-
severity="secondary"
496-
text
497-
class="justify-start"
498-
@click="logout"
499-
/>
500-
</div>
501-
</Popover>
502-
503412
<Button
504413
v-tooltip.right="serverTooltip()"
505414
:icon="`ti ${getServerIcon(workspaceStore.currentServer?.kind)}`"
@@ -536,7 +445,8 @@ const logout = async () => {
536445
v-if="workspaceStore.currentServerId === server.id"
537446
class="ti ti-check text-sm"
538447
/>
539-
<Button size="small"
448+
<Button
449+
size="small"
540450
icon="ti ti-edit"
541451
text
542452
rounded
@@ -545,7 +455,8 @@ const logout = async () => {
545455
:disabled="isManagedLocalServer(server)"
546456
@click.stop="openEditServerDialog(server)"
547457
/>
548-
<Button size="small"
458+
<Button
459+
size="small"
549460
icon="ti ti-trash"
550461
text
551462
rounded
@@ -576,7 +487,7 @@ const logout = async () => {
576487
</div>
577488
</div>
578489

579-
<SourcesSidebar v-model:sidebar-width="sideBarWidth" />
490+
<SourcesSidebar v-if="!route.meta?.hideSidebar" v-model:sidebar-width="sideBarWidth" />
580491

581492
<main class="h-full w-full overflow-hidden">
582493
<RouterView />

packages/frontend/src/components/table/ExtendedDataTable.vue

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ type CellPosition = {
1212
}
1313
1414
const CELL_PREVIEW_TEXT_LIMIT = 150
15+
const DRAG_SCROLL_THRESHOLD = 48
16+
const DRAG_SCROLL_STEP = 20
1517
1618
const props = withDefaults(
1719
defineProps<{
@@ -38,6 +40,9 @@ const editingCell = ref<CellPosition | null>(null)
3840
const editingValue = ref<unknown>('')
3941
const isDragging = ref(false)
4042
const contextCell = ref<CellPosition | null>(null)
43+
const dragPointer = ref<{ x: number; y: number } | null>(null)
44+
45+
let dragScrollFrame = 0
4146
4247
const createRowId = () =>
4348
typeof crypto !== 'undefined' && 'randomUUID' in crypto
@@ -143,6 +148,21 @@ const focusGrid = () => {
143148
gridContainer.value?.focus()
144149
}
145150
151+
const getCellElement = (cell: CellPosition) =>
152+
gridContainer.value?.querySelector<HTMLTableCellElement>(
153+
`td[data-cell-row="${cell.row}"][data-cell-column="${cell.column}"]`,
154+
) ?? null
155+
156+
const ensureCellVisible = (cell: CellPosition) => {
157+
nextTick(() => {
158+
const targetCell = getCellElement(cell)
159+
targetCell?.scrollIntoView({
160+
block: 'nearest',
161+
inline: 'nearest',
162+
})
163+
})
164+
}
165+
146166
const restoreGridFocusAfterEdit = () => {
147167
nextTick(() => {
148168
requestAnimationFrame(() => {
@@ -165,7 +185,10 @@ const finishPendingEdit = (nextCell?: CellPosition) => {
165185
commitEditing()
166186
}
167187
168-
const setSelection = (cell: CellPosition, options?: { extend?: boolean }) => {
188+
const setSelection = (
189+
cell: CellPosition,
190+
options?: { extend?: boolean; ensureVisible?: boolean },
191+
) => {
169192
if (!hasRows.value) return
170193
171194
const nextCell = clampCell(cell)
@@ -179,6 +202,10 @@ const setSelection = (cell: CellPosition, options?: { extend?: boolean }) => {
179202
}
180203
181204
focusGrid()
205+
206+
if (options?.ensureVisible !== false) {
207+
ensureCellVisible(nextCell)
208+
}
182209
}
183210
184211
const selectAll = () => {
@@ -543,10 +570,94 @@ const moveSelection = (deltaRow: number, deltaColumn: number, extend: boolean) =
543570
row: baseCell.row + deltaRow,
544571
column: baseCell.column + deltaColumn,
545572
},
546-
{ extend },
573+
{ extend, ensureVisible: true },
547574
)
548575
}
549576
577+
const updateDragSelectionFromPointer = (clientX: number, clientY: number) => {
578+
if (!isDragging.value || !anchorCell.value || !gridContainer.value) {
579+
return
580+
}
581+
582+
const containerRect = gridContainer.value.getBoundingClientRect()
583+
const probeX = Math.min(Math.max(clientX, containerRect.left + 2), containerRect.right - 2)
584+
const probeY = Math.min(Math.max(clientY, containerRect.top + 2), containerRect.bottom - 2)
585+
const element = document.elementFromPoint(probeX, probeY)
586+
const cell = element?.closest?.('td[data-cell-row][data-cell-column]') as HTMLTableCellElement | null
587+
588+
if (!cell) {
589+
return
590+
}
591+
592+
const row = Number(cell.dataset.cellRow)
593+
const column = Number(cell.dataset.cellColumn)
594+
595+
if (Number.isNaN(row) || Number.isNaN(column)) {
596+
return
597+
}
598+
599+
focusCell.value = clampCell({ row, column })
600+
}
601+
602+
const stopDragAutoScroll = () => {
603+
if (dragScrollFrame) {
604+
cancelAnimationFrame(dragScrollFrame)
605+
dragScrollFrame = 0
606+
}
607+
}
608+
609+
const runDragAutoScroll = () => {
610+
if (!isDragging.value || !dragPointer.value || !gridContainer.value) {
611+
dragScrollFrame = 0
612+
return
613+
}
614+
615+
const container = gridContainer.value
616+
const rect = container.getBoundingClientRect()
617+
let deltaX = 0
618+
let deltaY = 0
619+
620+
if (dragPointer.value.x < rect.left + DRAG_SCROLL_THRESHOLD) {
621+
deltaX = -Math.ceil(
622+
((rect.left + DRAG_SCROLL_THRESHOLD - dragPointer.value.x) / DRAG_SCROLL_THRESHOLD) *
623+
DRAG_SCROLL_STEP,
624+
)
625+
} else if (dragPointer.value.x > rect.right - DRAG_SCROLL_THRESHOLD) {
626+
deltaX = Math.ceil(
627+
((dragPointer.value.x - (rect.right - DRAG_SCROLL_THRESHOLD)) / DRAG_SCROLL_THRESHOLD) *
628+
DRAG_SCROLL_STEP,
629+
)
630+
}
631+
632+
if (dragPointer.value.y < rect.top + DRAG_SCROLL_THRESHOLD) {
633+
deltaY = -Math.ceil(
634+
((rect.top + DRAG_SCROLL_THRESHOLD - dragPointer.value.y) / DRAG_SCROLL_THRESHOLD) *
635+
DRAG_SCROLL_STEP,
636+
)
637+
} else if (dragPointer.value.y > rect.bottom - DRAG_SCROLL_THRESHOLD) {
638+
deltaY = Math.ceil(
639+
((dragPointer.value.y - (rect.bottom - DRAG_SCROLL_THRESHOLD)) / DRAG_SCROLL_THRESHOLD) *
640+
DRAG_SCROLL_STEP,
641+
)
642+
}
643+
644+
if (deltaX || deltaY) {
645+
container.scrollLeft += deltaX
646+
container.scrollTop += deltaY
647+
updateDragSelectionFromPointer(dragPointer.value.x, dragPointer.value.y)
648+
}
649+
650+
dragScrollFrame = requestAnimationFrame(runDragAutoScroll)
651+
}
652+
653+
const startDragAutoScroll = () => {
654+
if (dragScrollFrame) {
655+
return
656+
}
657+
658+
dragScrollFrame = requestAnimationFrame(runDragAutoScroll)
659+
}
660+
550661
const onGridKeyDown = (event: KeyboardEvent) => {
551662
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement)
552663
return
@@ -628,7 +739,12 @@ const onCellMouseDown = (event: MouseEvent, rowIndex: number, columnIndex: numbe
628739
629740
event.preventDefault()
630741
isDragging.value = true
631-
setSelection({ row: rowIndex, column: columnIndex }, { extend: event.shiftKey })
742+
dragPointer.value = { x: event.clientX, y: event.clientY }
743+
setSelection(
744+
{ row: rowIndex, column: columnIndex },
745+
{ extend: event.shiftKey, ensureVisible: true },
746+
)
747+
startDragAutoScroll()
632748
}
633749
634750
const onCellMouseEnter = (_event: MouseEvent, rowIndex: number, columnIndex: number) => {
@@ -638,8 +754,20 @@ const onCellMouseEnter = (_event: MouseEvent, rowIndex: number, columnIndex: num
638754
focusCell.value = clampCell({ row: rowIndex, column: columnIndex })
639755
}
640756
757+
const onDocumentMouseMove = (event: MouseEvent) => {
758+
if (!isDragging.value) {
759+
return
760+
}
761+
762+
dragPointer.value = { x: event.clientX, y: event.clientY }
763+
updateDragSelectionFromPointer(event.clientX, event.clientY)
764+
startDragAutoScroll()
765+
}
766+
641767
const onDocumentMouseUp = () => {
642768
isDragging.value = false
769+
dragPointer.value = null
770+
stopDragAutoScroll()
643771
}
644772
645773
const showContextMenu = (event: MouseEvent, rowIndex?: number, columnIndex?: number) => {
@@ -712,10 +840,13 @@ const contextMenuItems = computed(() => [
712840
])
713841
714842
onMounted(() => {
843+
window.addEventListener('mousemove', onDocumentMouseMove)
715844
window.addEventListener('mouseup', onDocumentMouseUp)
716845
})
717846
718847
onUnmounted(() => {
848+
stopDragAutoScroll()
849+
window.removeEventListener('mousemove', onDocumentMouseMove)
719850
window.removeEventListener('mouseup', onDocumentMouseUp)
720851
})
721852
@@ -814,6 +945,8 @@ defineExpose({
814945
<td
815946
v-for="(column, columnIndex) of columns"
816947
:key="column.field"
948+
:data-cell-row="rowIndex"
949+
:data-cell-column="columnIndex"
817950
class="border border-neutral-200 dark:border-neutral-800 mono text-xs align-top"
818951
:class="{
819952
'bg-primary-500/18 ring-inset ring-1 ring-primary-500/30': isCellSelected(

0 commit comments

Comments
 (0)