Skip to content

Commit

Permalink
feat: shareable event urls (#429)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman authored Oct 2, 2023
1 parent e008520 commit 5b807ac
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 23 deletions.
4 changes: 2 additions & 2 deletions backend/controller/internal/dal/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,10 @@ func (d *DAL) QueryEvents(ctx context.Context, limit int, filters ...EventFilter
q += fmt.Sprintf(" AND time_stamp >= $%d::TIMESTAMPTZ", param(filter.newerThan))
}
if filter.idHigherThan != 0 {
q += fmt.Sprintf(" AND id >= $%d::BIGINT", param(filter.idHigherThan))
q += fmt.Sprintf(" AND e.id >= $%d::BIGINT", param(filter.idHigherThan))
}
if filter.idLowerThan != 0 {
q += fmt.Sprintf(" AND id <= $%d::BIGINT", param(filter.idLowerThan))
q += fmt.Sprintf(" AND e.id <= $%d::BIGINT", param(filter.idLowerThan))
}
if filter.deployments != nil {
q += fmt.Sprintf(` AND d.name = ANY($%d::TEXT[])`, param(filter.deployments))
Expand Down
37 changes: 23 additions & 14 deletions console/client/src/features/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Timestamp } from '@bufbuild/protobuf'
import React from 'react'
import { useSearchParams } from 'react-router-dom'
import { Event, EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts'
import { SidePanelContext } from '../../providers/side-panel-provider.tsx'
import { getEvents, streamEvents, timeFilter } from '../../services/console.service.ts'
import { eventIdFilter, getEvents, streamEvents, timeFilter } from '../../services/console.service.ts'
import { formatTimestampShort } from '../../utils/date.utils.ts'
import { panelColor } from '../../utils/style.utils.ts'
import { TimelineCall } from './TimelineCall.tsx'
Expand All @@ -24,25 +25,38 @@ interface Props {
const maxTimelineEntries = 1000

export const Timeline = ({ timeSettings, filters }: Props) => {
const [searchParams, setSearchParams] = useSearchParams()
const { openPanel, closePanel, isOpen } = React.useContext(SidePanelContext)
const [entries, setEntries] = React.useState<Event[]>([])
const [selectedEntry, setSelectedEntry] = React.useState<Event | null>(null)
const [selectedEventTypes] = React.useState<string[]>(['log', 'call', 'deploymentCreated', 'deploymentUpdated'])
const [selectedLogLevels] = React.useState<number[]>([1, 5, 9, 13, 17])

React.useEffect(() => {
const eventId = searchParams.get('id')
const abortController = new AbortController()
abortController.signal

const fetchEvents = async () => {
let eventFilters = filters
if (timeSettings.newerThan || timeSettings.olderThan) {
eventFilters = [timeFilter(timeSettings.olderThan, timeSettings.newerThan), ...filters]
}

if (eventId) {
const id = BigInt(eventId)
eventFilters = [eventIdFilter({ higherThan: id }), ...filters]
}
const events = await getEvents({ filters: eventFilters })
setEntries(events)

if (eventId) {
const entry = events.find((event) => event.id.toString() === eventId)
if (entry) {
handleEntryClicked(entry)
}
}
}

if (timeSettings.isTailing && !timeSettings.isPaused) {
if (timeSettings.isTailing && !timeSettings.isPaused && !eventId) {
setEntries([])
streamEvents({
abortControllerSignal: abortController.signal,
Expand Down Expand Up @@ -71,6 +85,9 @@ export const Timeline = ({ timeSettings, filters }: Props) => {
if (selectedEntry === entry) {
setSelectedEntry(null)
closePanel()
const newParams = new URLSearchParams(searchParams.toString())
newParams.delete('id')
setSearchParams(newParams)
return
}

Expand All @@ -91,17 +108,9 @@ export const Timeline = ({ timeSettings, filters }: Props) => {
break
}
setSelectedEntry(entry)
setSearchParams({ ...Object.fromEntries(searchParams.entries()), id: entry.id.toString() })
}

const filteredEntries = entries.filter((entry) => {
const isActive = selectedEventTypes.includes(entry.entry?.case ?? '')
if (entry.entry.case === 'log') {
return isActive && selectedLogLevels.includes(entry.entry.value.logLevel)
}

return isActive
})

return (
<div className='border border-gray-100 dark:border-slate-700 rounded m-2'>
<div className='overflow-x-hidden'>
Expand All @@ -116,7 +125,7 @@ export const Timeline = ({ timeSettings, filters }: Props) => {
</tr>
</thead>
<tbody>
{filteredEntries.map((entry) => (
{entries.map((entry) => (
<tr
key={entry.id.toString()}
className={`flex border-b border-gray-100 dark:border-slate-700 text-xs font-roboto-mono ${
Expand Down
24 changes: 22 additions & 2 deletions console/client/src/features/timeline/TimelinePage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { ListBulletIcon } from '@heroicons/react/24/outline'
import React from 'react'
import { useSearchParams } from 'react-router-dom'
import { PageHeader } from '../../components/PageHeader'
import { EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb'
import { Timeline } from './Timeline'
import { TimelineFilterPanel } from './filters/TimelineFilterPanel'
import { TimeSettings, TimelineTimeControls } from './filters/TimelineTimeControls'
import { TIME_RANGES, TimeSettings, TimelineTimeControls } from './filters/TimelineTimeControls'

export const TimelinePage = () => {
const [searchParams] = useSearchParams()
const [timeSettings, setTimeSettings] = React.useState<TimeSettings>({ isTailing: true, isPaused: false })
const [filters, setFilters] = React.useState<EventsQuery_Filter[]>([])
const [selectedTimeRange, setSelectedTimeRange] = React.useState(TIME_RANGES['tail'])
const [isTimelinePaused, setIsTimelinePaused] = React.useState(false)

React.useEffect(() => {
if (searchParams.get('id')) {
// if we're loading a specific event, we don't want to tail.
setSelectedTimeRange(TIME_RANGES['5m'])
setIsTimelinePaused(true)
} else {
// Reset to initial state if there's no 'id' query parameter
setSelectedTimeRange(TIME_RANGES['tail'])
setIsTimelinePaused(false)
}
}, [searchParams])

const handleTimeSettingsChanged = (settings: TimeSettings) => {
setTimeSettings(settings)
Expand All @@ -21,7 +37,11 @@ export const TimelinePage = () => {
return (
<>
<PageHeader icon={<ListBulletIcon />} title='Events'>
<TimelineTimeControls onTimeSettingsChange={handleTimeSettingsChanged} />
<TimelineTimeControls
selectedTimeRange={selectedTimeRange}
isTimelinePaused={isTimelinePaused}
onTimeSettingsChange={handleTimeSettingsChanged}
/>
</PageHeader>
<div className='flex' style={{ height: 'calc(100% - 44px)' }}>
<div className='sticky top-0 flex-none overflow-y-auto'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,22 @@ export interface TimeSettings {

interface Props {
onTimeSettingsChange: (settings: TimeSettings) => void
selectedTimeRange: TimeRange
isTimelinePaused: boolean
}

export const TimelineTimeControls = ({ onTimeSettingsChange }: Props) => {
const [selected, setSelected] = React.useState(TIME_RANGES['tail'])
const [isPaused, setIsPaused] = React.useState(false)
export const TimelineTimeControls = ({ onTimeSettingsChange, selectedTimeRange, isTimelinePaused }: Props) => {
const [selected, setSelected] = React.useState(selectedTimeRange)
const [isPaused, setIsPaused] = React.useState(isTimelinePaused)
const [newerThan, setNewerThan] = React.useState<Timestamp | undefined>()

const isTailing = selected.value === TIME_RANGES['tail'].value

React.useEffect(() => {
setSelected(selectedTimeRange)
setIsPaused(isTimelinePaused)
}, [selectedTimeRange, isTimelinePaused])

React.useEffect(() => {
if (isTailing) {
onTimeSettingsChange({ isTailing, isPaused })
Expand Down
1 change: 0 additions & 1 deletion console/client/src/features/verbs/VerbPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export const VerbPage = () => {
if (modules) {
const module = modules.modules.find((module) => module.name === moduleName?.toLocaleLowerCase())
setModule(module)
console.log(module)
const verb = module?.verbs.find((verb) => verb.verb?.name.toLocaleLowerCase() === verbName?.toLocaleLowerCase())
setVerb(verb)
}
Expand Down
20 changes: 19 additions & 1 deletion console/client/src/services/console.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EventsQuery_DeploymentFilter,
EventsQuery_EventTypeFilter,
EventsQuery_Filter,
EventsQuery_IDFilter,
EventsQuery_LogLevelFilter,
EventsQuery_Order,
EventsQuery_RequestFilter,
Expand Down Expand Up @@ -92,6 +93,23 @@ export const timeFilter = (olderThan: Timestamp | undefined, newerThan: Timestam
return filter
}

interface IDFilterParams {
lowerThan?: bigint
higherThan?: bigint
}

export const eventIdFilter = ({ lowerThan, higherThan }: IDFilterParams): EventsQuery_Filter => {
const filter = new EventsQuery_Filter()
const idFilter = new EventsQuery_IDFilter()
idFilter.lowerThan = lowerThan
idFilter.higherThan = higherThan
filter.filter = {
case: 'id',
value: idFilter,
}
return filter
}

export const getRequestCalls = async (requestKey: string): Promise<CallEvent[]> => {
const allEvents = await getEvents({
filters: [requestKeysFilter([requestKey]), eventTypesFilter([EventType.CALL])],
Expand All @@ -118,7 +136,7 @@ interface GetEventsParams {

export const getEvents = async ({
limit = 1000,
order = EventsQuery_Order.ASC,
order = EventsQuery_Order.DESC,
filters = [],
}: GetEventsParams): Promise<Event[]> => {
const response = await client.getEvents({ filters, limit, order })
Expand Down

0 comments on commit 5b807ac

Please sign in to comment.