Skip to content

Commit

Permalink
feat: start hooking up time controls (#390)
Browse files Browse the repository at this point in the history
This isn't fully functional yet, but it had a few bug fixes and got some
of it hooked up.

You can select a time range and use the forward and backward buttons to
page through time ranges.
But, tailing doesn't work (it just gets the 100 latest items) and there
are definitely some other weird ux things I need to update.

<img width="1380" alt="Screenshot 2023-09-15 at 4 14 42 PM"
src="https://github.com/TBD54566975/ftl/assets/51647/f7ba8185-44ee-423a-af7a-f8b7d2ee3bbf">
<img width="1380" alt="Screenshot 2023-09-15 at 4 14 46 PM"
src="https://github.com/TBD54566975/ftl/assets/51647/6a8f35c7-f4e3-4e62-b281-93d41a6e509f">

<img width="1380" alt="Screenshot 2023-09-15 at 4 14 38 PM"
src="https://github.com/TBD54566975/ftl/assets/51647/95651b43-037b-4794-9f1e-d7c7df564cc6">
  • Loading branch information
wesbillman authored Sep 15, 2023
1 parent 949616a commit 87345f8
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 128 deletions.
2 changes: 1 addition & 1 deletion backend/controller/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func eventsQueryProtoToDAL(pb *pbconsole.EventsQuery) ([]dal.EventFilter, error)
if filter.Time.OlderThan != nil {
olderThan = filter.Time.OlderThan.AsTime()
}
query = append(query, dal.FilterTimeRange(newerThan, olderThan))
query = append(query, dal.FilterTimeRange(olderThan, newerThan))

case *pbconsole.EventsQuery_Filter_Id:
var lowerThan, higherThan int64
Expand Down
45 changes: 17 additions & 28 deletions console/client/src/features/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Timestamp } from '@bufbuild/protobuf'
import React from 'react'
import { useClient } from '../../hooks/use-client.ts'
import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect.ts'
import { Event } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts'
import { Event, EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts'
import { SidePanelContext } from '../../providers/side-panel-provider.tsx'
import { getEvents } from '../../services/console.service.ts'
import { getEvents, 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 @@ -14,41 +12,32 @@ import { TimelineLog } from './TimelineLog.tsx'
import { TimelineCallDetails } from './details/TimelineCallDetails.tsx'
import { TimelineDeploymentDetails } from './details/TimelineDeploymentDetails.tsx'
import { TimelineLogDetails } from './details/TimelineLogDetails.tsx'
import { TIME_RANGES } from './filters/TimeFilter.tsx'
import { TimeSettings } from './filters/TimelineTimeControls.tsx'

export const Timeline = () => {
const client = useClient(ConsoleService)
interface Props {
timeSettings: TimeSettings
filters: EventsQuery_Filter[]
}

export const Timeline = ({ timeSettings, filters }: Props) => {
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', 'deployment'])
const [selectedLogLevels] = React.useState<number[]>([1, 5, 9, 13, 17])
const [selectedTimeRange] = React.useState('24h')

React.useEffect(() => {
const abortController = new AbortController()

const streamTimeline = async () => {
setEntries((_) => [])
const events = await getEvents()
console.log(events)
const afterTime = new Date(Date.now() - TIME_RANGES[selectedTimeRange].value)

for await (const response of client.streamEvents(
{ afterTime: Timestamp.fromDate(afterTime) },
{ signal: abortController.signal },
)) {
if (response.event != null) {
setEntries((prevEntries) => [response.event!, ...prevEntries])
}
const fetchEvents = async () => {
let eventFilters = filters
if (timeSettings.newerThan || timeSettings.olderThan) {
eventFilters = [timeFilter(timeSettings.olderThan, timeSettings.newerThan), ...filters]
}
const events = await getEvents(eventFilters)
setEntries(events)
}

streamTimeline()
return () => {
abortController.abort()
}
}, [client, selectedTimeRange])
fetchEvents()
}, [filters, timeSettings])

React.useEffect(() => {
if (!isOpen) {
Expand Down
21 changes: 17 additions & 4 deletions console/client/src/features/timeline/TimelinePage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { ListBulletIcon } from '@heroicons/react/24/outline'
import React from 'react'
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 { TimelineTimeControls } from './filters/TimelineTimeControls'
import { TimeSettings, TimelineTimeControls } from './filters/TimelineTimeControls'

export const TimelinePage = () => {
const [timeSettings, setTimeSettings] = React.useState<TimeSettings>({ isTailing: true, isPaused: false })
const [filters, setFilters] = React.useState<EventsQuery_Filter[]>([])

const handleTimeSettingsChanged = (settings: TimeSettings) => {
setTimeSettings(settings)
}

const handleFiltersChanged = (filters: EventsQuery_Filter[]) => {
setFilters(filters)
}

return (
<>
<PageHeader icon={<ListBulletIcon />} title='Events'>
<TimelineTimeControls />
<TimelineTimeControls onTimeSettingsChange={handleTimeSettingsChanged} />
</PageHeader>
<div className='flex h-full'>
<TimelineFilterPanel />
<TimelineFilterPanel onFiltersChanged={handleFiltersChanged} />
<div className='flex-grow'>
<Timeline />
<Timeline timeSettings={timeSettings} filters={filters} />
</div>
</div>
</>
Expand Down
69 changes: 0 additions & 69 deletions console/client/src/features/timeline/filters/TimelineFilterBar.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PhoneIcon, RocketLaunchIcon } from '@heroicons/react/24/outline'
import React from 'react'
import { LogLevel } from '../../../protos/xyz/block/ftl/v1/console/console_pb'
import { EventsQuery_Filter, LogLevel } from '../../../protos/xyz/block/ftl/v1/console/console_pb'
import { modulesContext } from '../../../providers/modules-provider'
import { textColor } from '../../../utils'
import { LogLevelBadgeSmall } from '../../logs/LogLevelBadgeSmall'
Expand All @@ -27,7 +27,11 @@ const LOG_LEVELS: Record<number, string> = {
17: 'Error',
}

export const TimelineFilterPanel = () => {
interface Props {
onFiltersChanged: (filters: EventsQuery_Filter[]) => void
}

export const TimelineFilterPanel = ({ onFiltersChanged }: Props) => {
const modules = React.useContext(modulesContext)
const [selectedEventTypes, setSelectedEventTypes] = React.useState<string[]>(Object.keys(EVENT_TYPES))
const [selectedModules, setSelectedModules] = React.useState<string[]>([])
Expand All @@ -39,6 +43,10 @@ export const TimelineFilterPanel = () => {
}
}, [modules])

React.useEffect(() => {
onFiltersChanged([])
}, [selectedEventTypes, setSelectedLogLevel, selectedModules])

const handleTypeChanged = (eventType: string, checked: boolean) => {
if (checked) {
setSelectedEventTypes((prev) => [...prev, eventType])
Expand Down
142 changes: 119 additions & 23 deletions console/client/src/features/timeline/filters/TimelineTimeControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Timestamp } from '@bufbuild/protobuf'
import { Listbox, Transition } from '@headlessui/react'
import { BackwardIcon, CheckIcon, ChevronUpDownIcon, ForwardIcon, PlayIcon } from '@heroicons/react/24/outline'
import {
BackwardIcon,
CheckIcon,
ChevronUpDownIcon,
ForwardIcon,
PauseIcon,
PlayIcon,
} from '@heroicons/react/24/outline'
import React, { Fragment } from 'react'
import { bgColor, borderColor, classNames, panelColor, textColor } from '../../../utils'
import { bgColor, borderColor, classNames, formatTimestampShort, panelColor, textColor } from '../../../utils'

interface TimeRange {
label: string
Expand All @@ -11,18 +19,93 @@ interface TimeRange {
export const TIME_RANGES: Record<string, TimeRange> = {
tail: { label: 'Live tail', value: 0 },
'5m': { label: 'Past 5 minutes', value: 5 * 60 * 1000 },
'15m': { label: 'Past 15 minutes', value: 15 * 60 * 1000 },
'30m': { label: 'Past 30 minutes', value: 30 * 60 * 1000 },
'1h': { label: 'Past 1 hour', value: 60 * 60 * 1000 },
'24h': { label: 'Past 24 hours', value: 24 * 60 * 60 * 1000 },
}

export const TimelineTimeControls = () => {
export interface TimeSettings {
isTailing: boolean
isPaused: boolean
olderThan?: Timestamp
newerThan?: Timestamp
}

interface Props {
onTimeSettingsChange: (settings: TimeSettings) => void
}

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

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

React.useEffect(() => {
if (isTailing) {
onTimeSettingsChange({ isTailing, isPaused })
return
}

if (newerThan) {
const startTime = (newerThan.toDate() ?? new Date()).getTime()
const olderThanDate = new Date(startTime + selected.value)

onTimeSettingsChange({
isTailing,
isPaused,
olderThan: Timestamp.fromDate(olderThanDate),
newerThan: newerThan,
})
}
}, [selected, isPaused, newerThan])

const handleRangeChanged = (range: TimeRange) => {
setSelected(range)
if (!newerThan) {
const newerThanDate = new Date(new Date().getTime() - range.value)
setNewerThan(Timestamp.fromDate(newerThanDate))
}
if (range.value === TIME_RANGES['tail'].value) {
setNewerThan(undefined)
}
}

const handleTimeBackward = () => {
if (!newerThan) {
return
}
const newerThanDate = new Date(newerThan.toDate().getTime() - selected.value)
setNewerThan(Timestamp.fromDate(newerThanDate))
}

const handleTimeForward = () => {
if (!newerThan) {
return
}
const newerThanTime = newerThan.toDate().getTime()
const newerThanDate = new Date(newerThanTime + selected.value)
const maxNewTime = new Date().getTime() - selected.value
if (newerThanDate.getTime() > maxNewTime) {
setNewerThan(Timestamp.fromDate(new Date(maxNewTime)))
} else {
setNewerThan(Timestamp.fromDate(newerThanDate))
}
}

const olderThan = newerThan ? Timestamp.fromDate(new Date(newerThan.toDate().getTime() - selected.value)) : undefined
return (
<>
<div className='flex items-center h-6'>
<Listbox value={selected} onChange={setSelected}>
{newerThan && (
<span className='text-xs font-roboto-mono mr-2 text-gray-400'>
{formatTimestampShort(olderThan)} - {formatTimestampShort(newerThan)}
</span>
)}

<Listbox value={selected} onChange={handleRangeChanged}>
{({ open }) => (
<>
<div className='relative w-40 mr-2 -mt-0.5 items-center'>
Expand Down Expand Up @@ -87,26 +170,39 @@ export const TimelineTimeControls = () => {
</>
)}
</Listbox>
<span className={`isolate inline-flex rounded-md shadow-sm h-6 ${textColor} ${bgColor}`}>
<button
type='button'
className={`relative inline-flex items-center rounded-l-md px-3 text-sm font-semibold ring-1 ring-inset ${borderColor} hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-10`}
>
<BackwardIcon className='w-4 h-4' />
</button>
<button
type='button'
className={`relative -ml-px inline-flex items-center px-3 text-sm font-semibold ring-1 ring-inset ${borderColor} hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-10`}
>
<PlayIcon className='w-4 h-4' />
</button>
<button
type='button'
className={`relative -ml-px inline-flex items-center rounded-r-md px-3 text-sm font-semibold ring-1 ring-inset ${borderColor} hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-10`}
{isTailing && (
<span
className={`isolate inline-flex rounded-md shadow-sm h-6 ${textColor} ${
isPaused ? bgColor : 'bg-indigo-600 text-white'
} `}
>
<ForwardIcon className='w-4 h-4' />
</button>
</span>
<button
type='button'
onClick={() => setIsPaused(!isPaused)}
className={`relative inline-flex items-center rounded-md px-3 text-sm font-semibold ring-1 ring-inset ${borderColor} hover:bg-gray-50 dark:hover:bg-indigo-700 focus:z-10`}
>
{isPaused ? <PlayIcon className='w-4 h-4' /> : <PauseIcon className='w-4 h-4' />}
</button>
</span>
)}
{!isTailing && (
<span className={`isolate inline-flex rounded-md shadow-sm h-6 ${textColor} ${bgColor}`}>
<button
type='button'
onClick={handleTimeBackward}
className={`relative inline-flex items-center rounded-l-md px-3 text-sm font-semibold ring-1 ring-inset ${borderColor} hover:bg-gray-50 dark:hover:bg-indigo-700 focus:z-10`}
>
<BackwardIcon className='w-4 h-4' />
</button>
<button
type='button'
onClick={handleTimeForward}
className={`relative -ml-px inline-flex items-center rounded-r-md px-3 text-sm font-semibold ring-1 ring-inset ${borderColor} hover:bg-gray-50 dark:hover:bg-indigo-700 focus:z-10`}
>
<ForwardIcon className='w-4 h-4' />
</button>
</span>
)}
</div>
</>
)
Expand Down
Loading

0 comments on commit 87345f8

Please sign in to comment.