Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
export let workflow: WorkflowExecution;
export let startTime: string | Timestamp;

let currentTime = Date.now();
export let currentTime = Date.now();

const rightNow = () => {
currentTime = Date.now();
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/lines-and-dots/svg/dot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
type Props = {
point: [number, number];
category?: string;
classification?: string;
classification?: string | null;
r?: number;
icon?: TimelineIconName;
strokeWidth?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ describe('category → dot icon selection', () => {
it('paused state overrides to "pause" icon (second dot)', () => {
const pauseTime = '2024-01-01T00:00:10Z';
// index !== 0 && pauseTime → 'pause'
const index = 1;
const index: number = 1;
const icon = pauseTime && index !== 0 ? 'pause' : 'activity';
expect(icon).toBe('pause');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@
</script>

{#await getWorkflowAndEventHistory() then { workflow, history }}
<div class="cursor-pointer overflow-auto {className}">
<TimelineGraph
{viewportHeight}
{workflow}
groups={createGroups(workflow, history)}
readOnly
/>
</div>
{#if workflow}
<div class="cursor-pointer overflow-auto {className}">
<TimelineGraph
{viewportHeight}
{workflow}
groups={createGroups(workflow, history)}
readOnly
/>
</div>
{/if}
{/await}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
if (group?.pendingActivity) {
if (group.pendingActivity.paused) {
status = translate('workflows.paused');
} else if (group.pendingActivity.attempt > 1) {
} else if ((group.pendingActivity.attempt ?? 0) > 1) {
status = translate('events.event-classification.retrying');
} else {
status = translate('events.event-classification.pending');
Expand Down Expand Up @@ -114,15 +114,17 @@
<div class="surface-primary p-4">
<div class="font-medium leading-4 text-secondary">Child Workflow</div>
{#key group.eventList.length}
<GraphWidget
{namespace}
workflowId={childWorkflowStartedEvent.attributes.workflowExecution
.workflowId}
runId={childWorkflowStartedEvent.attributes.workflowExecution
.runId}
viewportHeight={320}
class="surface-primary overflow-x-hidden border-t border-subtle"
/>
{#if childWorkflowStartedEvent.attributes.workflowExecution?.workflowId}
<GraphWidget
{namespace}
workflowId={childWorkflowStartedEvent.attributes
.workflowExecution.workflowId}
runId={childWorkflowStartedEvent.attributes.workflowExecution
.runId ?? undefined}
viewportHeight={320}
class="surface-primary overflow-x-hidden border-t border-subtle"
/>
{/if}
{/key}
</div>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@
import { formatDistanceAbbreviated } from '$lib/utilities/format-time';

import { TimelineConfig } from '../constants';
import Line from '../svg/line.svelte';

import Line from './line.svelte';
import type { TimelineScale } from './timeline-scale.svelte';

type Props = {
x1: number;
x2: number;
gutter: number;
timelineHeight: number;
startTime: string | Timestamp;
duration: number;
scale: TimelineScale;
};
let {
x1 = 0,
x2 = 1000,
gutter = 0,
timelineHeight = 1000,
startTime,
duration,
scale,
}: Props = $props();

const { radius } = TimelineConfig;
const ticks = 20;

const distance = $derived(x2 - x1);
const tickDistance = $derived(distance / ticks);

const startMs = $derived(scale.unproject(x1 - gutter));
const endMs = $derived(scale.unproject(x2 - gutter));
const includeMilliseconds = $derived((endMs - startMs) / ticks < 1000);
</script>

<Line
Expand All @@ -46,10 +53,8 @@
>
{formatDistanceAbbreviated({
start: startTime,
end: new Date(
new Date(startTime.toString()).getTime() + (duration / ticks) * i,
),
includeMilliseconds: duration / ticks < 1000,
end: new Date(scale.unproject(tickX - gutter)),
includeMilliseconds,
})}
</text>
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<script lang="ts">
import Icon from '$lib/holocene/icon/icon.svelte';
import Tooltip from '$lib/holocene/tooltip.svelte';
import { translate } from '$lib/i18n/translate';
import { formatDistanceAbbreviated } from '$lib/utilities/format-time';

import { TimelineConfig } from '../constants';

import type { TimelineScale } from './timeline-scale.svelte';

type Props = {
scale: TimelineScale;
timelineHeight: number;
readOnly?: boolean;
onToggle: (segmentKey: string) => void;
};
let { scale, timelineHeight, readOnly = false, onToggle }: Props = $props();

const { radius, height } = TimelineConfig;

const AXIS_STROKE_WIDTH = radius / 2;

const ZIGZAG_HALF_WIDTH = 4;

const HIT_HALF_WIDTH = Math.max(radius, 12);
const HIT_WIDTH = HIT_HALF_WIDTH * 2;

const collapsibleSegments = $derived(
scale.segments.filter((s) => s.isCollapsible),
);

const zigzagPath = (xStart: number, xEnd: number, height: number) => {
const step = 8;
let d = `M ${xStart} 0`;
let y = 0;
let toRight = true;
while (y < height) {
const nextY = Math.min(y + step, height);
d += ` L ${toRight ? xEnd : xStart} ${nextY}`;
toRight = !toRight;
y = nextY;
}
return d;
};

let activeSegmentKey = $state<string | null>(null);

const handleToggle = (segmentKey: string) => {
if (readOnly) return;
onToggle(segmentKey);
};

const handleKeydown = (e: KeyboardEvent, segmentKey: string) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleToggle(segmentKey);
}
};
</script>

{#each collapsibleSegments as seg (seg.key)}
{@const labelX = (seg.startPx + seg.endPx) / 2}
{@const labelY = timelineHeight + radius * 2}
{@const distance = formatDistanceAbbreviated({
start: new Date(seg.startTimeMs),
end: new Date(seg.endTimeMs),
})}
{#if seg.isCollapsed}
{@const half = Math.min(ZIGZAG_HALF_WIDTH, (seg.endPx - seg.startPx) / 4)}
<path
class="zigzag"
d={zigzagPath(labelX - half, labelX + half, timelineHeight)}
fill="none"
stroke-width="0.5"
stroke-dasharray="2"
/>
{@render marker(labelX, height, {
text: translate('workflows.show-idle-time-segment', {
distance,
}),
show: activeSegmentKey === seg.key,
})}
{@render marker(labelX, timelineHeight)}
<text
class="zigzag-label"
font-size="10"
transform="rotate(90, {labelX}, {labelY})"
x={labelX - radius}
y={labelY + 3}
>
{distance}
</text>
{/if}
{#if !readOnly}
<rect
role="button"
tabindex="0"
aria-pressed={seg.isCollapsed}
aria-label={translate('workflows.hide-idle-time-segment', {
distance,
})}
class="toggle-handle"
x={seg.isCollapsed ? labelX - HIT_HALF_WIDTH : seg.startPx}
y={0}
width={seg.isCollapsed ? HIT_WIDTH : seg.endPx - seg.startPx}
height={seg.isCollapsed
? timelineHeight + AXIS_STROKE_WIDTH * 2
: timelineHeight - AXIS_STROKE_WIDTH / 2}
onmouseenter={() => (activeSegmentKey = seg.key)}
onmouseleave={() => (activeSegmentKey = null)}
onfocus={() => (activeSegmentKey = seg.key)}
onblur={() => (activeSegmentKey = null)}
onclick={() => handleToggle(seg.key)}
onkeydown={(e) => handleKeydown(e, seg.key)}
/>
{/if}
{/each}

{#snippet marker(
cx: number,
cy: number,
tooltip?: {
text?: string;
show?: boolean;
},
)}
{@const iconSize = radius * 2}
<rect
class="marker"
x={cx - HIT_HALF_WIDTH}
y={cy - radius}
width={HIT_WIDTH}
height={radius * 2}
/>
<foreignObject
x={cx - iconSize / 2}
y={cy - iconSize / 2}
width={iconSize}
height={iconSize}
>
<Tooltip
text={tooltip?.text}
usePortal
right
portalOffset={{ x: 8 }}
show={tooltip?.show ?? false}
>
<Icon
class="text-secondary"
name="timeline-collapse"
width={iconSize}
height={iconSize}
/>
</Tooltip>
</foreignObject>
{/snippet}

<style lang="postcss">
.zigzag {
stroke: rgb(var(--color-text-secondary));
}

.zigzag-label {
fill: rgb(var(--color-text-secondary));
}

.marker {
fill: rgb(var(--color-surface-primary));
}

.toggle-handle {
fill: currentColor;
cursor: pointer;
opacity: 0;
outline: none;
transition: opacity 0.1s ease-in-out;
}

.toggle-handle:hover,
.toggle-handle:focus-visible {
opacity: 0.2;
}
</style>
Loading
Loading