Skip to content

Commit

Permalink
Merge pull request #253 from PrefectHQ/horizontal-waterfall-layouts
Browse files Browse the repository at this point in the history
Allow user to set the view to trace or dependency
  • Loading branch information
pleek91 authored Oct 19, 2023
2 parents 0edbdce + 06bb9ed commit fbae4d9
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 45 deletions.
2 changes: 2 additions & 0 deletions src/components/RunGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
<div class="run-graph__actions">
<p-button title="Recenter Timeline" icon="Target" flat @click="() => centerViewport({ animate: true })" />
<p-button title="View Timeline in Fullscreen" icon="ArrowsPointingOutIcon" flat @click="toggleFullscreen" />
<RunGraphSettings />
</div>
</div>
</template>

<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import RunGraphSettings from '@/components/RunGraphSettings.vue'
import { RunGraphProps } from '@/models/RunGraph'
import { ViewportDateRange } from '@/models/viewport'
import { start, stop, centerViewport } from '@/objects'
Expand Down
100 changes: 100 additions & 0 deletions src/components/RunGraphSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<template>
<p-pop-over
class="run-graph-settings"
auto-close
:placement="placement"
>
<template #target="{ toggle }">
<p-button
aria-label="Run Graph Options"
icon="CogIcon"
flat
@click="toggle"
/>
</template>

<p-overflow-menu class="run-graph-settings__menu">
<p-label label="View">
<p-radio-group v-model="horizontal" :options="horizontalOptions">
<template #label="{ option }">
{{ option.label }}
</template>
</p-radio-group>
</p-label>
<p-label label="Layout">
<p-radio-group v-model="vertical" :options="verticalOptions" disabled>
<template #label="{ option }">
{{ option.label }}
</template>
</p-radio-group>
</p-label>
</p-overflow-menu>
</p-pop-over>
</template>

<script lang="ts" setup>
import { PButton, positions, PPopOver } from '@prefecthq/prefect-design'
import { computed } from 'vue'
import { HorizontalMode, VerticalMode } from '@/models/layout'
import { layout, setHorizontalMode, setVerticalMode } from '@/objects/layout'
type Option<T extends string> = {
value: T,
label: string,
}
const placement = [positions.topRight, positions.bottomRight, positions.topLeft, positions.bottomLeft]
const horizontalOptions: Option<HorizontalMode>[] = [
{
value: 'dependency',
label: 'Dependency',
},
{
value: 'trace',
label: 'Trace',
},
]
const horizontal = computed({
get() {
return layout.horizontal
},
set(value) {
setHorizontalMode(value)
},
})
const verticalOptions: Option<VerticalMode>[] = [
{
value: 'waterfall',
label: 'Waterfall',
},
{
value: 'nearest-parent',
label: 'Nearest Parent',
},
]
const vertical = computed({
get() {
return layout.vertical
},
set(value) {
setVerticalMode(value)
},
})
</script>

<style>
.run-graph-settings {
display: inline-block;
}
.run-graph-settings__menu {
display: grid;
grid-template-columns: 1fr;
gap: theme('spacing.4');
padding: theme('spacing.2');
}
</style>
25 changes: 18 additions & 7 deletions src/factories/box.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { differenceInMilliseconds, millisecondsInSecond } from 'date-fns'
import { Graphics } from 'pixi.js'
import { DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts'
import { DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts'
import { RunGraphNode } from '@/models/RunGraph'
import { waitForConfig } from '@/objects/config'
import { layout } from '@/objects/layout'

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function nodeBoxFactory() {
Expand All @@ -12,21 +13,31 @@ export async function nodeBoxFactory() {
async function render(node: RunGraphNode): Promise<Graphics> {
const { background } = config.styles.node(node)

const right = node.start_time
const left = node.end_time ?? new Date()
const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond
const boxWidth = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS
const boxHeight = config.styles.nodeHeight - config.styles.nodeMargin * 2
const width = getWidth(node)
const height = config.styles.nodeHeight - config.styles.nodeMargin * 2

box.clear()
box.lineStyle(1, 0x0, 1, 2)
box.beginFill(background)
box.drawRoundedRect(0, 0, boxWidth, boxHeight, 4)
box.drawRoundedRect(0, 0, width, height, 4)
box.endFill()

return await box
}

function getWidth(node: RunGraphNode): number {
if (layout.horizontal === 'trace') {
const right = node.start_time
const left = node.end_time ?? new Date()
const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond
const width = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS

return width
}

return DEFAULT_LINEAR_COLUMN_SIZE_PIXELS
}

return {
box,
render,
Expand Down
49 changes: 32 additions & 17 deletions src/factories/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Container } from 'pixi.js'
import { DEFAULT_NODES_CONTAINER_NAME, DEFAULT_POLL_INTERVAL } from '@/consts'
import { NodeContainerFactory, nodeContainerFactory } from '@/factories/node'
import { offsetsFactory } from '@/factories/offsets'
import { HorizontalPositionSettings } from '@/factories/position'
import { horizontalSettingsFactory } from '@/factories/settings'
import { NodeLayoutRequest, NodeLayoutResponse, Pixels } from '@/models/layout'
import { RunGraphNode, RunGraphNodes } from '@/models/RunGraph'
import { NodeLayoutResponse, NodeWidths, Pixels } from '@/models/layout'
import { RunGraphData, RunGraphNode } from '@/models/RunGraph'
import { waitForConfig } from '@/objects/config'
import { emitter } from '@/objects/events'
import { exhaustive } from '@/utilities/exhaustive'
import { WorkerLayoutMessage, WorkerMessage, layoutWorkerFactory } from '@/workers/runGraph'

Expand All @@ -20,40 +20,55 @@ export async function nodesContainerFactory(runId: string) {
const config = await waitForConfig()
const rows = await offsetsFactory()

let settings: HorizontalPositionSettings
let data: RunGraphData | null = null
let layout: NodeLayoutResponse = new Map()
let interval: ReturnType<typeof setInterval> | undefined = undefined

container.name = DEFAULT_NODES_CONTAINER_NAME

emitter.on('layoutUpdated', () => renderNodes())

async function render(): Promise<void> {
const data = await config.fetch(runId)
if (data === null) {
await fetch()
}

settings = horizontalSettingsFactory(data.start_time)
if (data === null) {
throw new Error('Data was null after fetch')
}

await renderNodes()
}

await renderNodes(data.nodes)
async function fetch(): Promise<void> {
clearInterval(interval)

data = await config.fetch(runId)

if (!data.end_time) {
setTimeout(() => render(), DEFAULT_POLL_INTERVAL)
interval = setTimeout(() => fetch(), DEFAULT_POLL_INTERVAL)
}
}

async function renderNodes(nodes: RunGraphNodes): Promise<void> {
const request: NodeLayoutRequest = new Map()
async function renderNodes(): Promise<void> {
if (data === null) {
return
}

const widths: NodeWidths = new Map()

for (const [nodeId, node] of nodes) {
for (const [nodeId, node] of data.nodes) {
// eslint-disable-next-line no-await-in-loop
const { width } = await renderNode(node)

request.set(nodeId, {
node,
width,
})
widths.set(nodeId, width)
}

worker.postMessage({
type: 'layout',
nodes: request,
settings,
data,
widths,
settings: horizontalSettingsFactory(data.start_time),
})
}

Expand Down
8 changes: 4 additions & 4 deletions src/factories/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ export type HorizontalPositionSettings = {
startTime: Date,
timeSpan: number,
timeSpanPixels: number,
dagColumnSize: number,
dependencyColumnSize: number,
}

export type HorizontalScale = ReturnType<typeof horizontalScaleFactory>

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function horizontalScaleFactory(settings: HorizontalPositionSettings) {
if (settings.mode === 'time') {
if (settings.mode === 'trace') {
return getTimeScale(settings)
}

Expand All @@ -36,6 +36,6 @@ function getTimeScale({ startTime, timeSpan, timeSpanPixels }: HorizontalPositio
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function getLinearScale({ dagColumnSize }: HorizontalPositionSettings) {
return scaleLinear().domain([0, 1]).range([0, dagColumnSize])
function getLinearScale({ dependencyColumnSize }: HorizontalPositionSettings) {
return scaleLinear().domain([0, 1]).range([0, dependencyColumnSize])
}
2 changes: 1 addition & 1 deletion src/factories/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export function horizontalSettingsFactory(startTime: Date): HorizontalPositionSe
startTime,
timeSpan: DEFAULT_TIME_COLUMN_SPAN_SECONDS,
timeSpanPixels: DEFAULT_TIME_COLUMN_SIZE_PIXELS,
dagColumnSize: DEFAULT_LINEAR_COLUMN_SIZE_PIXELS,
dependencyColumnSize: DEFAULT_LINEAR_COLUMN_SIZE_PIXELS,
}
}
10 changes: 3 additions & 7 deletions src/models/layout.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { RunGraphNode } from '@/models/RunGraph'

export type Pixels = { x: number, y: number }
export type VerticalMode = 'waterfall' | 'nearest-parent'
export type HorizontalMode = 'time' | 'dat'
export type HorizontalMode = 'trace' | 'dependency'

export type LayoutMode = {
horizontal: HorizontalMode,
vertical: VerticalMode,
}

export type NodeLayoutRequest = Map<string, {
node: RunGraphNode,
width: number,
}>
export type NodeWidths = Map<string, number>

export type NodeLayoutResponse = Map<string, {
x: number,
Expand Down
19 changes: 16 additions & 3 deletions src/objects/layout.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { reactive } from 'vue'
import { HorizontalMode, LayoutMode, VerticalMode } from '@/models/layout'
import { emitter } from '@/objects/events'

export const layout: LayoutMode = {
horizontal: 'time',
export const layout: LayoutMode = reactive({
horizontal: 'trace',
vertical: 'waterfall',
}
})

export function setLayoutMode({ horizontal, vertical }: LayoutMode): void {
if (layout.horizontal === horizontal && layout.vertical === vertical) {
return
}

layout.horizontal = horizontal
layout.vertical = vertical

emitter.emit('layoutUpdated', layout)
}

export function setHorizontalMode(mode: HorizontalMode): void {
if (layout.horizontal === mode) {
return
}

layout.horizontal = mode

emitter.emit('layoutUpdated', layout)
}

export function setVerticalMode(mode: VerticalMode): void {
if (layout.vertical === mode) {
return
}

layout.vertical = mode

emitter.emit('layoutUpdated', layout)
Expand Down
6 changes: 4 additions & 2 deletions src/workers/runGraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HorizontalPositionSettings } from '@/factories/position'
import { NodeLayoutRequest, NodeLayoutResponse } from '@/models/layout'
import { NodeLayoutResponse, NodeWidths } from '@/models/layout'
import { RunGraphData } from '@/models/RunGraph'

// eslint-disable-next-line import/default
import RunGraphWorker from '@/workers/runGraph.worker?worker'
Expand All @@ -9,7 +10,8 @@ export type WorkerMessage = WorkerLayoutMessage

export type ClientLayoutMessage = {
type: 'layout',
nodes: NodeLayoutRequest,
data: RunGraphData,
widths: NodeWidths,
settings: HorizontalPositionSettings,
}

Expand Down
Loading

0 comments on commit fbae4d9

Please sign in to comment.