Skip to content

Commit

Permalink
✨ add columns to the event list
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Sep 7, 2023
1 parent 69b71c1 commit 3009a4c
Show file tree
Hide file tree
Showing 13 changed files with 767 additions and 105 deletions.
8 changes: 6 additions & 2 deletions developer-extension/src/panel/components/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useSettings } from '../hooks/useSettings'
import { DEFAULT_PANEL_TAB, PanelTabs } from '../../common/constants'
import { SettingsTab } from './tabs/settingsTab'
import { InfosTab } from './tabs/infosTab'
import { EventsTab } from './tabs/eventsTab'
import { EventsTab, DEFAULT_COLUMNS } from './tabs/eventsTab'
import { ReplayTab } from './tabs/replayTab'

export function Panel() {
Expand All @@ -21,6 +21,8 @@ export function Panel() {

const { events, filters, setFilters, clear, facetRegistry } = useEvents(settings)

const [columns, setColumns] = useState(DEFAULT_COLUMNS)

const [activeTab, setActiveTab] = useState<string | null>(DEFAULT_PANEL_TAB)
function updateActiveTab(activeTab: string | null) {
setActiveTab(activeTab)
Expand Down Expand Up @@ -60,7 +62,9 @@ export function Panel() {
events={events}
facetRegistry={facetRegistry}
filters={filters}
onFiltered={setFilters}
onFiltersChange={setFilters}
columns={columns}
onColumnsChange={setColumns}
clear={clear}
/>
</Tabs.Panel>
Expand Down
190 changes: 190 additions & 0 deletions developer-extension/src/panel/components/tabs/eventsTab/columnDrag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import type { RefObject } from 'react'
import React, { useState, useEffect } from 'react'
import { Box, Text } from '@mantine/core'
import { BORDER_RADIUS } from '../../../uiUtils'
import type { Coordinates } from './drag'
import { initDrag } from './drag'
import type { EventListColumn } from './columnUtils'
import { getColumnTitle } from './columnUtils'
import { HORIZONTAL_PADDING, VERTICAL_PADDING } from './grid'

/** Number of pixel to determine if the cursor is close enough of a position to trigger an action */
const ACTION_DISTANCE_THRESHOLD = 20

export function ColumnDrag({
headerRowRef,
columns,
onColumnsChange,
}: {
headerRowRef: RefObject<HTMLDivElement>
columns: EventListColumn[]
onColumnsChange: (columns: EventListColumn[]) => void
}) {
const [drag, setDrag] = useState<DragState | null>(null)

useEffect(() => {
if (columns.length > 1) {
const { stop } = initColumnDrag(headerRowRef.current!, setDrag, columns, onColumnsChange)
return stop
}
}, [columns])

return drag && <DragGhost columns={columns} drag={drag} />
}

function DragGhost({ columns, drag }: { columns: EventListColumn[]; drag: DragState }) {
return (
<Box
sx={{
position: 'fixed',
opacity: 0.5,
borderRadius: BORDER_RADIUS,

top: drag.targetRect.top,
height: drag.targetRect.height,
transform: 'translateX(-50%)',
left: drag.position.x,
background: 'grey',

paddingTop: VERTICAL_PADDING,
paddingBottom: VERTICAL_PADDING,
paddingLeft: HORIZONTAL_PADDING,
paddingRight: HORIZONTAL_PADDING,
cursor: 'grabbing',

...(drag.action?.type === 'insert' && {
width: 0,
left: drag.action.place.xPosition,
background: 'green',
color: 'transparent',
paddingRight: 3,
paddingLeft: 3,
}),

...(drag.action?.type === 'delete' && {
top: drag.targetRect.y + (drag.position.y - drag.startPosition.y),
background: 'red',
}),
}}
>
<Text weight="bold">{getColumnTitle(columns[drag.columnIndex])}</Text>
</Box>
)
}

function getClosestCell(target: HTMLElement) {
if (target.closest('button, .mantine-Popover-dropdown')) {
return null
}
return target.closest('[data-header-cell]')
}

interface DragState {
targetRect: DOMRect
startPosition: Coordinates
position: Coordinates
action?: DragAction
moved: boolean
insertPlaces: Place[]
columnIndex: number
}

interface Place {
index: number
xPosition: number
}

type DragAction = { type: 'delete' } | { type: 'insert'; place: Place }

function initColumnDrag(
target: HTMLElement,
onColumnDragStateChanges: (state: DragState | null) => void,
columns: EventListColumn[],
onColumnsChange: (columns: EventListColumn[]) => void
) {
let state: DragState | null = null

return initDrag({
target,

onStart({ target, position }) {
const targetCell = getClosestCell(target)
if (!targetCell) {
return false
}
const siblings = Array.from(targetCell.parentElement!.children)
const columnIndex = siblings.indexOf(targetCell)

state = {
targetRect: targetCell.getBoundingClientRect(),
insertPlaces: siblings.flatMap((sibling, index) => {
if (sibling === targetCell) {
return []
}
return {
xPosition: sibling.getBoundingClientRect()[index < columnIndex ? 'left' : 'right'],
index,
}
}),
startPosition: position,
position,
moved: false,
action: undefined,
columnIndex,
}
onColumnDragStateChanges(state)
},

onMove({ position }) {
if (!state) {
return
}
let action: DragAction | undefined
if (Math.abs(state.startPosition.y - position.y) > ACTION_DISTANCE_THRESHOLD) {
action = { type: 'delete' }
} else {
const insertPlace = state.insertPlaces.find(
({ xPosition }) => Math.abs(position.x - xPosition) < ACTION_DISTANCE_THRESHOLD
)
if (insertPlace) {
action = { type: 'insert', place: insertPlace }
}
}

state = { ...state, action, position, moved: true }
onColumnDragStateChanges(state)
},

onDrop() {
if (!state) {
return
}

if (state.action) {
switch (state.action.type) {
case 'delete': {
const newColumns = columns.slice()
newColumns.splice(state.columnIndex, 1)
onColumnsChange(newColumns)
break
}
case 'insert': {
const newColumns = columns.slice()
const [column] = newColumns.splice(state.columnIndex, 1)
newColumns.splice(state.action.place.index, 0, column)
onColumnsChange(newColumns)
break
}
}
}

state = null
onColumnDragStateChanges(state)
},

onAbort() {
state = null
onColumnDragStateChanges(state)
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type EventListColumn =
| { type: 'date' }
| { type: 'description' }
| { type: 'type' }
| { type: 'field'; path: string }

export const DEFAULT_COLUMNS: EventListColumn[] = [{ type: 'date' }, { type: 'type' }, { type: 'description' }]

export function includesColumn(existingColumns: EventListColumn[], newColumn: EventListColumn) {
return existingColumns.some((column) => {
if (column.type === 'field' && newColumn.type === 'field') {
return column.path === newColumn.path
}
return column.type === newColumn.type
})
}

export function getColumnTitle(column: EventListColumn) {
return column.type === 'date'
? 'Date'
: column.type === 'description'
? 'Description'
: column.type === 'type'
? 'Type'
: column.path
}
121 changes: 121 additions & 0 deletions developer-extension/src/panel/components/tabs/eventsTab/drag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
export interface Coordinates {
x: number
y: number
}

/**
* This is a framework agnostic drag implementation that works as a state machine:
*
* ```
* (init)
* |
* [onStart]
* | \______________
* | \
* <returns true> <returns false>
* | ____ |
* | / \ (end)
* [onMove] )
* | \ \____/
* | \______________
* | \
* <drop in the window> |
* | <stop() called or drop out of the window>
* | |
* [onDrop] [onAbort]
* | _________________/
* |/
* (end)
* ```
*/
export function initDrag({
target,
onStart,
onMove,
onAbort,
onDrop,
}: {
target: HTMLElement
onStart: (event: { target: HTMLElement; position: Coordinates }) => boolean | void
onMove: (event: { position: Coordinates }) => void
onDrop: () => void
onAbort: () => void
}) {
type DragState =
| { isDragging: false }
| {
isDragging: true
removeListeners: () => void
}

let state: DragState = {
isDragging: false,
}

target.addEventListener('pointerdown', onPointerDown, { capture: true })

return {
stop: () => {
endDrag(true)
target.removeEventListener('pointerdown', onPointerDown, { capture: true })
},
}

function onPointerDown(event: PointerEvent) {
if (
state.isDragging ||
event.buttons !== 1 ||
onStart({ target: event.target as HTMLElement, position: { x: event.clientX, y: event.clientY } }) === false
) {
return
}

event.preventDefault()

state = {
isDragging: true,
removeListeners: () => {
removeEventListener('pointerup', onPointerUp, { capture: true })
removeEventListener('pointermove', onPointerMove, { capture: true })
},
}

addEventListener('pointerup', onPointerUp, { capture: true })
addEventListener('pointermove', onPointerMove, { capture: true })
}

function onPointerUp(_event: PointerEvent) {
endDrag(false)
}

function onPointerMove(event: PointerEvent) {
if (!state.isDragging) {
return
}

if (event.buttons !== 1) {
// The user might have released the click outside of the window
endDrag(true)
return
}

onMove({
position: {
x: event.clientX,
y: event.clientY,
},
})
}

function endDrag(abort: boolean) {
if (state.isDragging) {
if (abort) {
onAbort()
} else {
onDrop()
}
state.removeListeners()
state = { isDragging: false }
}
}
}
Loading

0 comments on commit 3009a4c

Please sign in to comment.