Skip to content

Commit 29089fe

Browse files
committed
[iris] Link aggregated job log lines to originating task
In the job dashboard's aggregated log view, each line now starts with a T<N> link that navigates to the originating task's detail page. Uses LogEntry.key, which the log store already populates with the task path.
1 parent fc78e0c commit 29089fe

1 file changed

Lines changed: 50 additions & 8 deletions

File tree

lib/iris/dashboard/src/components/shared/LogViewer.vue

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { ref, computed, onMounted, watch } from 'vue'
3+
import { RouterLink } from 'vue-router'
34
import { logServiceRpcCall } from '@/composables/useRpc'
45
import { useAutoRefresh } from '@/composables/useAutoRefresh'
56
import type { FetchLogsResponse, LogEntry, TaskAttempt } from '@/types/rpc'
@@ -159,7 +160,40 @@ watch(() => props.workerId, resetAndFetch)
159160
160161
onMounted(resetAndFetch)
161162
162-
const filteredLogs = computed<LogEntry[]>(() => entries.value)
163+
// Job-aggregate mode shows logs from many tasks; render a per-line link to the
164+
// originating task. Single-task mode would link every line to itself, so skip.
165+
const showTaskLinks = computed(() => {
166+
if (!props.taskId) return false
167+
return !/\/\d+$/.test(props.taskId)
168+
})
169+
170+
interface TaskRef {
171+
taskId: string
172+
taskIndex: string
173+
}
174+
175+
function parseTaskFromKey(key: string | undefined): TaskRef | null {
176+
if (!key) return null
177+
const colonIdx = key.lastIndexOf(':')
178+
const taskId = colonIdx > 0 ? key.slice(0, colonIdx) : key
179+
const lastSlash = taskId.lastIndexOf('/')
180+
if (lastSlash < 0) return null
181+
const taskIndex = taskId.slice(lastSlash + 1)
182+
if (!/^\d+$/.test(taskIndex)) return null
183+
return { taskId, taskIndex }
184+
}
185+
186+
interface LogRow {
187+
entry: LogEntry
188+
taskRef: TaskRef | null
189+
}
190+
191+
const logRows = computed<LogRow[]>(() =>
192+
entries.value.map(entry => ({
193+
entry,
194+
taskRef: showTaskLinks.value ? parseTaskFromKey(entry.key) : null,
195+
})),
196+
)
163197
164198
defineExpose({ selectedAttemptId })
165199
</script>
@@ -211,7 +245,7 @@ defineExpose({ selectedAttemptId })
211245
{{ autoRefreshActive ? 'Auto ⟳' : 'Paused' }}
212246
</button>
213247
<span class="ml-auto text-xs text-text-muted font-mono">
214-
{{ filteredLogs.length }} lines
248+
{{ logRows.length }} lines
215249
</span>
216250
</div>
217251

@@ -227,27 +261,35 @@ defineExpose({ selectedAttemptId })
227261
:style="{ maxHeight: maxHeight }"
228262
>
229263
<div
230-
v-if="loading && filteredLogs.length === 0"
264+
v-if="loading && logRows.length === 0"
231265
class="py-12 text-center text-text-muted text-sm"
232266
>
233267
Loading logs...
234268
</div>
235269
<div
236-
v-else-if="filteredLogs.length === 0"
270+
v-else-if="logRows.length === 0"
237271
class="py-12 text-center text-text-muted text-sm"
238272
>
239273
No log entries
240274
</div>
241275
<div
242-
v-for="(entry, i) in filteredLogs"
276+
v-for="(row, i) in logRows"
243277
:key="i"
244278
:class="[
245279
'px-3 py-0.5 font-mono text-xs leading-relaxed hover:bg-surface-sunken',
246-
logLevelClass(entry.level),
280+
logLevelClass(row.entry.level),
247281
]"
248282
>
249-
<span class="text-text-muted mr-2">{{ formatLogTime(timestampMs(entry.timestamp)) }}</span>
250-
<span class="whitespace-pre-wrap break-all">{{ entry.data }}</span>
283+
<RouterLink
284+
v-if="row.taskRef && props.taskId"
285+
:to="`/job/${encodeURIComponent(props.taskId)}/task/${encodeURIComponent(row.taskRef.taskId)}`"
286+
class="text-accent hover:underline mr-2"
287+
:title="row.taskRef.taskId"
288+
>
289+
T{{ row.taskRef.taskIndex }}
290+
</RouterLink>
291+
<span class="text-text-muted mr-2">{{ formatLogTime(timestampMs(row.entry.timestamp)) }}</span>
292+
<span class="whitespace-pre-wrap break-all">{{ row.entry.data }}</span>
251293
</div>
252294
</div>
253295
</div>

0 commit comments

Comments
 (0)