Skip to content

Commit ecce874

Browse files
committed
feat(explorer): direct detail navigation for related resources + parent breadcrumbs
- viewRelatedItem() now passes resourceId query param so clicking a related resource (e.g. datastream from system detail) opens its Detail tab directly instead of the generic list - ResourceExplorerPage reads resourceId and forwards to ResourcePanel - ResourcePanel watches initialResourceId prop and auto-selects the resource on mount - Added parent navigation bar in ResourceDetail: extracts cross-reference fields (system@id, datastream@id, controlstream@id) and renders clickable breadcrumb buttons to navigate upward through the hierarchy (observation -> datastream -> system, command -> control stream -> system)
1 parent f99c644 commit ecce874

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

demo/src/components/ResourceDetail.vue

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function getItemName(item: any): string {
111111
return item?.properties?.name || item?.properties?.title || item?.name || item?.title || ''
112112
}
113113
114-
/** Click a related item → load its detail (if same type) or navigate */
114+
/** Click a related item → navigate directly to its detail view */
115115
function viewRelatedItem(link: RelatedResourceLink, item: any) {
116116
const id = getItemId(item)
117117
if (id === '') return
@@ -121,13 +121,14 @@ function viewRelatedItem(link: RelatedResourceLink, item: any) {
121121
manualId.value = ''
122122
fetchDetail(id)
123123
} else {
124-
// Different type — navigate to that type's explorer with the item selected
124+
// Different type — navigate directly to that item's detail view
125125
router.push({
126126
path: `/explore/${link.childType}`,
127127
query: {
128128
parentType: props.resourceType,
129129
parentId: String(detail.value?.id || props.resourceId),
130130
relation: link.relation,
131+
resourceId: id,
131132
},
132133
})
133134
}
@@ -152,6 +153,62 @@ function toggleRelation(relation: string) {
152153
state.expanded = !state.expanded
153154
}
154155
156+
// ========================================
157+
// Parent navigation (observation → datastream → system, etc.)
158+
// ========================================
159+
160+
interface ParentLink {
161+
label: string
162+
resourceType: string
163+
resourceId: string
164+
icon: string
165+
}
166+
167+
/** Extract navigable parent references from the raw detail JSON cross-reference fields */
168+
const parentLinks = computed<ParentLink[]>(() => {
169+
if (!detail.value) return []
170+
const links: ParentLink[] = []
171+
const raw = detail.value
172+
173+
// system@id (present on datastreams, controlStreams)
174+
if (typeof raw['system@id'] === 'string') {
175+
links.push({ label: 'System', resourceType: 'systems', resourceId: raw['system@id'], icon: 'pi pi-server' })
176+
} else if (raw['system@link']?.uid) {
177+
// Some servers use system@link with href containing the ID
178+
const match = raw['system@link']?.href?.match(/systems\/([^/?]+)/)
179+
if (match) links.push({ label: 'System', resourceType: 'systems', resourceId: match[1], icon: 'pi pi-server' })
180+
}
181+
182+
// datastream@id (present on observations)
183+
if (typeof raw['datastream@id'] === 'string') {
184+
links.push({ label: 'Datastream', resourceType: 'datastreams', resourceId: raw['datastream@id'], icon: 'pi pi-chart-line' })
185+
}
186+
187+
// controlstream@id (present on commands)
188+
if (typeof raw['controlstream@id'] === 'string') {
189+
links.push({ label: 'Control Stream', resourceType: 'controlStreams', resourceId: raw['controlstream@id'], icon: 'pi pi-sliders-h' })
190+
}
191+
192+
// command@id (present on commandStatuses — future-proofing)
193+
if (typeof raw['command@id'] === 'string') {
194+
links.push({ label: 'Command', resourceType: 'commands', resourceId: raw['command@id'], icon: 'pi pi-send' })
195+
}
196+
197+
// deployment@id (present on deployed systems)
198+
if (typeof raw['deployment@id'] === 'string') {
199+
links.push({ label: 'Deployment', resourceType: 'deployments', resourceId: raw['deployment@id'], icon: 'pi pi-map' })
200+
}
201+
202+
return links
203+
})
204+
205+
function navigateToParent(parent: ParentLink) {
206+
router.push({
207+
path: `/explore/${parent.resourceType}`,
208+
query: { resourceId: parent.resourceId },
209+
})
210+
}
211+
155212
async function fetchDetail(id?: string) {
156213
const useId = id || manualId.value || props.resourceId
157214
if (!useId) return
@@ -217,6 +274,23 @@ watch(
217274
</div>
218275

219276
<template v-if="detail">
277+
<!-- Parent navigation breadcrumbs -->
278+
<div v-if="parentLinks.length > 0" class="parent-nav">
279+
<i class="pi pi-arrow-up parent-nav-icon"></i>
280+
<span class="parent-nav-label">Parent:</span>
281+
<button
282+
v-for="parent in parentLinks"
283+
:key="parent.resourceType"
284+
class="parent-link"
285+
@click="navigateToParent(parent)"
286+
>
287+
<i :class="parent.icon"></i>
288+
{{ parent.label }}
289+
<code>{{ parent.resourceId }}</code>
290+
<i class="pi pi-arrow-up-right parent-link-arrow"></i>
291+
</button>
292+
</div>
293+
220294
<!-- Inline related resource panels in a grid -->
221295
<div v-if="allRelations.length > 0 && (detail?.id || props.resourceId)" class="relations-grid">
222296
<div
@@ -342,6 +416,15 @@ watch(
342416
.browse-all-link { display: block; width: 100%; padding: 0.3rem 0.65rem; border: none; background: transparent; color: #0369a1; font-size: 0.75rem; font-weight: 600; cursor: pointer; text-align: left; }
343417
.browse-all-link:hover { background: #e0f2fe; }
344418
419+
/* Parent navigation bar */
420+
.parent-nav { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.65rem; background: #fefce8; border: 1px solid #fde68a; border-radius: 6px; flex-wrap: wrap; }
421+
.parent-nav-icon { font-size: 0.8rem; color: #ca8a04; }
422+
.parent-nav-label { font-size: 0.78rem; font-weight: 600; color: #92400e; white-space: nowrap; }
423+
.parent-link { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.2rem 0.5rem; border: 1px solid #fde68a; border-radius: 4px; background: #fffbeb; color: #92400e; font-size: 0.78rem; font-weight: 600; cursor: pointer; transition: all 0.15s; white-space: nowrap; }
424+
.parent-link:hover { background: #fef3c7; border-color: #f59e0b; }
425+
.parent-link code { font-size: 0.72rem; background: rgba(0,0,0,0.05); padding: 0.05rem 0.25rem; border-radius: 2px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
426+
.parent-link-arrow { font-size: 0.6rem; color: #d97706; opacity: 0.6; }
427+
345428
.diagram-details { margin-top: 0.25rem; }
346429
.diagram-summary { cursor: pointer; font-size: 0.8rem; font-weight: 600; color: #0369a1; display: flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0; user-select: none; }
347430
.diagram-summary:hover { color: #0284c7; }

demo/src/components/ResourcePanel.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed } from 'vue'
2+
import { ref, computed, watch } from 'vue'
33
import { getResourceType } from '../state'
44
import Tabs from 'primevue/tabs'
55
import TabList from 'primevue/tablist'
@@ -17,13 +17,27 @@ const props = defineProps<{
1717
parentType?: string | null
1818
parentId?: string | null
1919
parentRelation?: string | null
20+
initialResourceId?: string | null
2021
}>()
2122
2223
const rtInfo = computed(() => getResourceType(props.resourceType))
2324
const activeTab = ref(0)
2425
const selectedResourceId = ref<string | null>(null)
2526
const selectedResource = ref<any>(null)
2627
28+
// Auto-select a resource and show its Detail tab when navigated with a direct resourceId
29+
watch(
30+
() => props.initialResourceId,
31+
(id) => {
32+
if (id) {
33+
selectedResourceId.value = id
34+
selectedResource.value = null // will be fetched by ResourceDetail
35+
activeTab.value = 1
36+
}
37+
},
38+
{ immediate: true }
39+
)
40+
2741
function viewDetail(resource: any) {
2842
// Extract ID from GeoJSON feature or flat object
2943
const id = resource?.id || resource?.properties?.id || resource?.['@id'] || ''

demo/src/pages/ResourceExplorerPage.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const parentType = computed(() => (route.query.parentType as string) || null)
1919
const parentId = computed(() => (route.query.parentId as string) || null)
2020
const parentRelation = computed(() => (route.query.relation as string) || null)
2121
22+
/** Direct-link to a specific resource's detail view (e.g., from clicking a related item) */
23+
const initialResourceId = computed(() => (route.query.resourceId as string) || null)
24+
2225
/** True when viewing a nested/sub-resource list */
2326
const isNested = computed(() => !!(parentType.value && parentId.value && parentRelation.value))
2427
@@ -98,7 +101,8 @@ function selectType(key: string) {
98101
:parent-type="parentType"
99102
:parent-id="parentId"
100103
:parent-relation="parentRelation"
101-
:key="activeType + (parentId || '') + (parentRelation || '')"
104+
:initial-resource-id="initialResourceId"
105+
:key="activeType + (parentId || '') + (parentRelation || '') + (initialResourceId || '')"
102106
/>
103107
</main>
104108
</div>

0 commit comments

Comments
 (0)