+
+
+
+ {item.scenario}
+
+ {item.feature &&
{item.feature}
}
+
+
+
+
- Status
+ - {statusName(item.status)}
+
+
+
- Start
+ - {formatTime(item.start)}
+
+
+
- End
+ - {formatTime(item.end)}
+
+
+
- Duration
+ - {formatExecutionDuration(new Date(item.start), new Date(item.end))}
+
+
+
- Worker
+ - {item.groupLabel}
+
+
+
+
+ )
+}
+
+function formatTime(time: number): string {
+ const d = new Date(time)
+ const formattedTime = `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}:${d.getMilliseconds()}`
+ return formattedTime
+}
+
+function bucketItems(items: TimelineItem[], minDuration: number) {
+ const result: number[][] = []
+
+ let i = 0
+ while (i < items.length) {
+ const bucket: number[] = []
+ let bucketDuration = 0
+
+ do {
+ bucket.push(i)
+ bucketDuration += items[i].end - items[i].start + 1
+ i++
+ } while (i < items.length && bucketDuration < minDuration)
+
+ if (bucketDuration >= minDuration) {
+ result.push(bucket)
+ } else {
+ // Try merging with left
+ if (result.length > 0) {
+ result[result.length - 1].push(...bucket)
+ } else {
+ // No adjacent bucket exist
+ result.push(bucket)
+ }
+ }
+ }
+
+ return result
+}
diff --git a/src/components/app/index.ts b/src/components/app/index.ts
index 890c590d..b6399b7b 100644
--- a/src/components/app/index.ts
+++ b/src/components/app/index.ts
@@ -11,4 +11,5 @@ export * from './Report.js'
export * from './SearchBar.js'
export * from './StatusesSummary.js'
export * from './TestRunHooks.js'
+export * from './Timeline.js'
export * from './UrlSearchProvider.js'
diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts
new file mode 100644
index 00000000..90fa5331
--- /dev/null
+++ b/src/hooks/useTimelineData.ts
@@ -0,0 +1,127 @@
+import {
+ type PickleTag,
+ type TestCaseStarted,
+ TestStepResultStatus,
+ TimeConversion,
+} from '@cucumber/messages'
+import { useMemo } from 'react'
+import { useFilteredTestCases } from './useFilteredTestCases.js'
+import { useQueries } from './useQueries.js'
+
+export interface TimelineItem {
+ readonly id: string
+ readonly groupId: string
+ readonly groupLabel: string
+ readonly feature: string
+ readonly scenario: string
+ readonly tags: readonly PickleTag[]
+ readonly status: TestStepResultStatus
+ readonly start: number
+ readonly end: number
+ readonly testCaseStarted: TestCaseStarted
+}
+
+export interface TimelineGroup {
+ readonly id: string
+ readonly label: string
+ readonly items: TimelineItem[]
+}
+
+export interface TimelineData {
+ readonly groups: readonly TimelineGroup[]
+ readonly fullStart: number
+ readonly fullEnd: number
+}
+
+const UNASSIGNED_GROUP_ID = ''
+
+export function useTimelineData(): TimelineData {
+ const { cucumberQuery } = useQueries()
+
+ const finishedTestCases = useFilteredTestCases()
+ return useMemo(() => {
+ const groupMap: Record