Implement breadcrumb navigation.
This commit is contained in:
		| @@ -5,7 +5,7 @@ | ||||
|     <ProfileView v-if="store.currentView === 'profile'" /> | ||||
|     <DeviceLinkView v-if="store.currentView === 'device-link'" /> | ||||
|     <ResetView v-if="store.currentView === 'reset'" /> | ||||
|   <PermissionDeniedView v-if="store.currentView === 'permission-denied'" /> | ||||
|     <PermissionDeniedView v-if="store.currentView === 'permission-denied'" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @@ -44,9 +44,9 @@ onMounted(async () => { | ||||
|   if (reset) { | ||||
|     store.resetToken = reset | ||||
|     // Remove query param to avoid lingering in history / clipboard | ||||
|   const targetPath = '/auth/' | ||||
|   const currentPath = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/' | ||||
|   history.replaceState(null, '', currentPath.startsWith('/auth') ? '/auth/' : targetPath) | ||||
|     const targetPath = '/auth/' | ||||
|     const currentPath = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/' | ||||
|     history.replaceState(null, '', currentPath.startsWith('/auth') ? '/auth/' : targetPath) | ||||
|   } | ||||
|   try { | ||||
|     await store.loadUserInfo() | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <script setup> | ||||
| import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue' | ||||
| import Breadcrumbs from '@/components/Breadcrumbs.vue' | ||||
| import CredentialList from '@/components/CredentialList.vue' | ||||
| import UserBasicInfo from '@/components/UserBasicInfo.vue' | ||||
| import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue' | ||||
| @@ -318,6 +319,27 @@ const pageHeading = computed(() => { | ||||
|   return (authStore.settings?.rp_name || 'Passkey') + ' Admin' | ||||
| }) | ||||
|  | ||||
| // Breadcrumb entries for admin app. | ||||
| const breadcrumbEntries = computed(() => { | ||||
|   const entries = [ | ||||
|     { label: 'Auth', href: '/auth/' }, | ||||
|     { label: 'Admin', href: '/auth/admin/' } | ||||
|   ] | ||||
|   // Determine organization for user view if selectedOrg not explicitly chosen. | ||||
|   let orgForUser = null | ||||
|   if (selectedUser.value) { | ||||
|     orgForUser = orgs.value.find(o => o.uuid === selectedUser.value.org_uuid) || null | ||||
|   } | ||||
|   const orgToShow = selectedOrg.value || orgForUser | ||||
|   if (orgToShow) { | ||||
|     entries.push({ label: orgToShow.display_name, href: `#org/${orgToShow.uuid}` }) | ||||
|   } | ||||
|   if (selectedUser.value) { | ||||
|     entries.push({ label: selectedUser.value.display_name || 'User', href: `#user/${selectedUser.value.uuid}` }) | ||||
|   } | ||||
|   return entries | ||||
| }) | ||||
|  | ||||
| watch(selectedUser, async (u) => { | ||||
|   if (!u) { userDetail.value = null; return } | ||||
|   try { | ||||
| @@ -432,17 +454,8 @@ async function submitDialog() { | ||||
|  | ||||
| <template> | ||||
|   <div class="container"> | ||||
|     <h1> | ||||
|       {{ pageHeading }} | ||||
|       <a href="/auth/" class="back-link" title="Back to User App">User</a> | ||||
|       <a | ||||
|         v-if="info?.is_global_admin && (selectedOrg || selectedUser)" | ||||
|         @click.prevent="goOverview" | ||||
|         href="#overview" | ||||
|         class="nav-link" | ||||
|         title="Back to overview" | ||||
|       >Overview</a> | ||||
|     </h1> | ||||
|     <h1>{{ pageHeading }}</h1> | ||||
|     <Breadcrumbs :entries="breadcrumbEntries" /> | ||||
|     <div v-if="loading">Loading…</div> | ||||
|     <div v-else-if="error" class="error">{{ error }}</div> | ||||
|     <div v-else> | ||||
| @@ -454,9 +467,8 @@ async function submitDialog() { | ||||
|       </div> | ||||
|       <div v-else> | ||||
|  | ||||
|   <!-- Removed user-specific info (current org, effective permissions, admin flags) --> | ||||
|  | ||||
|         <!-- Overview Page --> | ||||
|          | ||||
|          | ||||
|   <div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card"> | ||||
|           <h2>Organizations</h2> | ||||
|           <div class="actions"> | ||||
| @@ -484,8 +496,6 @@ async function submitDialog() { | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|  | ||||
|         <!-- User Detail Page --> | ||||
|         <div v-if="selectedUser" class="card user-detail"> | ||||
|           <UserBasicInfo | ||||
|             v-if="userDetail && !userDetail.error" | ||||
| @@ -519,7 +529,7 @@ async function submitDialog() { | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Organization Detail Page --> | ||||
|          | ||||
|         <div v-else-if="selectedOrg" class="card"> | ||||
|           <h2 class="org-title" :title="selectedOrg.uuid"> | ||||
|             <span class="org-name">{{ selectedOrg.display_name }}</span> | ||||
| @@ -533,7 +543,7 @@ async function submitDialog() { | ||||
|                 class="perm-matrix-grid" | ||||
|                 :style="{ gridTemplateColumns: 'minmax(180px, 1fr) ' + selectedOrg.roles.map(()=> '2.2rem').join(' ') + ' 2.2rem' }" | ||||
|               > | ||||
|                 <!-- Headers --> | ||||
|                  | ||||
|                 <div class="grid-head perm-head">Permission</div> | ||||
|                 <div | ||||
|                   v-for="r in selectedOrg.roles" | ||||
| @@ -545,7 +555,7 @@ async function submitDialog() { | ||||
|                 </div> | ||||
|                 <div class="grid-head role-head add-role-head" title="Add role" @click="createRole(selectedOrg)" role="button">➕</div> | ||||
|  | ||||
|                 <!-- Data Rows --> | ||||
|                  | ||||
|                 <template v-for="pid in selectedOrg.permissions" :key="pid"> | ||||
|                   <div class="perm-name" :title="pid">{{ permissionDisplayName(pid) }}</div> | ||||
|                   <div | ||||
| @@ -583,14 +593,14 @@ async function submitDialog() { | ||||
|                 </div> | ||||
|               </div> | ||||
|               <template v-if="r.users.length > 0"> | ||||
|         <ul class="user-list"> | ||||
|                 <ul class="user-list"> | ||||
|                   <li | ||||
|                     v-for="u in r.users" | ||||
|                     :key="u.uuid" | ||||
|                     class="user-chip" | ||||
|                     draggable="true" | ||||
|                     @dragstart="e => onUserDragStart(e, u, selectedOrg.uuid)" | ||||
|           @click="openUser(u)" | ||||
|                     @click="openUser(u)" | ||||
|                     :title="u.uuid" | ||||
|                   > | ||||
|                     <span class="name">{{ u.display_name }}</span> | ||||
| @@ -606,7 +616,7 @@ async function submitDialog() { | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|   <div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card"> | ||||
|         <div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card"> | ||||
|           <h2>All Permissions</h2> | ||||
|           <div class="actions"> | ||||
|             <button v-if="!showCreatePermission" @click="showCreatePermission = true">+ Create Permission</button> | ||||
|   | ||||
							
								
								
									
										42
									
								
								frontend/src/components/Breadcrumbs.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								frontend/src/components/Breadcrumbs.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| <script setup> | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| // Props: | ||||
| // entries: Array<{ label:string, href:string }> | ||||
| // showHome: include leading home icon (defaults true) | ||||
| // homeHref: home link target (default '/') | ||||
| const props = defineProps({ | ||||
|   entries: { type: Array, default: () => [] }, | ||||
|   showHome: { type: Boolean, default: true }, | ||||
|   homeHref: { type: String, default: '/' } | ||||
| }) | ||||
|  | ||||
| const crumbs = computed(() => { | ||||
|   const base = props.showHome ? [{ label: '🏠', href: props.homeHref }] : [] | ||||
|   return [...base, ...props.entries] | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <nav class="breadcrumbs" aria-label="Breadcrumb" v-if="crumbs.length"> | ||||
|     <ol> | ||||
|       <li v-for="(c, idx) in crumbs" :key="idx"> | ||||
|         <a :href="c.href">{{ c.label }}</a> | ||||
|         <span v-if="idx < crumbs.length - 1" class="sep"> — </span> | ||||
|       </li> | ||||
|     </ol> | ||||
|   </nav> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| .breadcrumbs { margin: .25rem 0 .5rem; line-height:1.2; } | ||||
| .breadcrumbs ol { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; } | ||||
| .breadcrumbs li { display: inline-flex; align-items: center; } | ||||
| .breadcrumbs a { text-decoration: none; color: #0366d6; padding: 0 .15rem; border-radius:4px; } | ||||
| .breadcrumbs a:hover, .breadcrumbs a:focus { text-decoration: underline; } | ||||
| .breadcrumbs .sep { color: #888; margin: 0 .1rem; } | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   .breadcrumbs a { color: #4ea3ff; } | ||||
|   .breadcrumbs .sep { color: #aaa; } | ||||
| } | ||||
| </style> | ||||
| @@ -17,6 +17,7 @@ | ||||
|  | ||||
| <script setup> | ||||
| import { useAuthStore } from '@/stores/auth' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| const authStore = useAuthStore() | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| <template> | ||||
|   <div class="container"> | ||||
|     <div class="view active"> | ||||
|   <h1>👋 Welcome! <a v-if="isAdmin" href="/auth/admin/" class="admin-link" title="Admin Console">Admin</a></h1> | ||||
|       <h1>👋 Welcome!</h1> | ||||
|       <Breadcrumbs :entries="[{ label: 'Auth', href: '/auth/' }, ...(isAdmin ? [{ label: 'Admin', href: '/auth/admin/' }] : [])]" /> | ||||
|       <UserBasicInfo | ||||
|         v-if="authStore.userInfo?.user" | ||||
|         :name="authStore.userInfo.user.user_name" | ||||
| @@ -9,7 +10,7 @@ | ||||
|         :created-at="authStore.userInfo.user.created_at" | ||||
|         :last-seen="authStore.userInfo.user.last_seen" | ||||
|         :loading="authStore.isLoading" | ||||
|   update-endpoint="/auth/api/user/display-name" | ||||
|         update-endpoint="/auth/api/user/display-name" | ||||
|         @saved="authStore.loadUserInfo()" | ||||
|       /> | ||||
|  | ||||
| @@ -80,6 +81,7 @@ | ||||
|  | ||||
| <script setup> | ||||
| import { ref, onMounted, onUnmounted, computed } from 'vue' | ||||
| import Breadcrumbs from '@/components/Breadcrumbs.vue' | ||||
| import { useAuthStore } from '@/stores/auth' | ||||
| import { formatDate } from '@/utils/helpers' | ||||
| import passkey from '@/utils/passkey' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko