Skip to content

Commit

Permalink
feat: Style timeline page and add filter panel (#380)
Browse files Browse the repository at this point in the history
<img width="1582" alt="Screenshot 2023-09-13 at 9 52 39 AM"
src="https://github.com/TBD54566975/ftl/assets/51647/0fa4ac8a-c34b-4d31-bd4a-8b93e5b7fe1e">
<img width="1582" alt="Screenshot 2023-09-13 at 9 52 42 AM"
src="https://github.com/TBD54566975/ftl/assets/51647/fef61b76-30bb-494a-863d-8ae7cc563be6">
  • Loading branch information
wesbillman authored Sep 13, 2023
1 parent 79a1025 commit 4b3b74e
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 53 deletions.
3 changes: 2 additions & 1 deletion console/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FTL</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet" />
</head>

<body class="bg-slate-200 dark:bg-slate-800">
<body class="bg-white dark:bg-slate-800">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
8 changes: 4 additions & 4 deletions console/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { GraphPage } from './features/graph/GraphPage.tsx'
import { ModulesList } from './features/modules/ModulesList.tsx'
import { Timeline } from './features/timeline/Timeline.tsx'
import { ModulesPage } from './features/modules/ModulesPage.tsx'
import { TimelinePage } from './features/timeline/TimelinePage.tsx'
import { Layout } from './layout/Layout.tsx'
import { bgColor, textColor } from './utils/style.utils.ts'

Expand All @@ -11,8 +11,8 @@ export const App = () => {
<Routes>
<Route path='/' element={<Layout />}>
<Route path='/' element={<Navigate to='events' replace />} />
<Route path='events' element={<Timeline />} />
<Route path='modules' element={<ModulesList />} />
<Route path='events' element={<TimelinePage />} />
<Route path='modules' element={<ModulesPage />} />
<Route path='graph' element={<GraphPage />} />
</Route>
</Routes>
Expand Down
21 changes: 21 additions & 0 deletions console/client/src/components/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { panelColor } from '../utils'

interface Props {
icon?: React.ReactNode
title?: string
children?: React.ReactNode
}

export const PageHeader = ({ icon, title, children }: Props) => {
return (
<div
className={`sticky top-0 z-10 ${panelColor} shadow dark:shadow-md flex justify-between items-center py-2 px-4 text-gray-70`}
>
<div className='flex items-center'>
<span className='-mt-0.5 text-indigo-500 pr-1'>{icon}</span>
<span className='text-lg'>{title}</span>
</div>
{children}
</div>
)
}
3 changes: 3 additions & 0 deletions console/client/src/features/graph/GraphPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Schema } from '@mui/icons-material'
import { useContext, useEffect } from 'react'
import ReactFlow, { Controls, MiniMap, useEdgesState, useNodesState } from 'reactflow'
import 'reactflow/dist/style.css'
import { PageHeader } from '../../components/PageHeader'
import { modulesContext } from '../../providers/modules-provider'
import { GroupNode } from './GroupNode'
import { VerbNode } from './VerbNode'
Expand All @@ -21,6 +23,7 @@ export const GraphPage = () => {

return (
<>
<PageHeader icon={<Schema />} title='Graph' />
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
Expand Down
14 changes: 14 additions & 0 deletions console/client/src/features/modules/ModulesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ViewModuleRounded } from '@mui/icons-material'
import { PageHeader } from '../../components/PageHeader'
import { ModulesList } from './ModulesList'

export const ModulesPage = () => {
return (
<>
<div className='w-full m-0'>
<PageHeader icon={<ViewModuleRounded />} title='Modules' />
<ModulesList />
</div>
</>
)
}
45 changes: 8 additions & 37 deletions console/client/src/features/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ 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 { TimelineFilterBar } from './filters/TimelineFilterBar.tsx'

export const Timeline = () => {
const client = useClient(ConsoleService)
const { openPanel, closePanel, isOpen } = React.useContext(SidePanelContext)
const [entries, setEntries] = React.useState<TimelineEvent[]>([])
const [selectedEntry, setSelectedEntry] = React.useState<TimelineEvent | null>(null)
const [selectedEventTypes, setSelectedEventTypes] = React.useState<string[]>(['log', 'call', 'deployment'])
const [selectedLogLevels, setSelectedLogLevels] = React.useState<number[]>([1, 5, 9, 13, 17])
const [selectedTimeRange, setSelectedTimeRange] = React.useState('1h')
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()
Expand Down Expand Up @@ -77,26 +76,6 @@ export const Timeline = () => {
setSelectedEntry(entry)
}

const handleEventTypesChanged = (eventType: string, checked: boolean) => {
if (checked) {
setSelectedEventTypes((prev) => [...prev, eventType])
} else {
setSelectedEventTypes((prev) => prev.filter((filter) => filter !== eventType))
}
}

const handleLogLevelsChanged = (logLevel: number, checked: boolean) => {
if (checked) {
setSelectedLogLevels((prev) => [...prev, logLevel])
} else {
setSelectedLogLevels((prev) => prev.filter((filter) => filter !== logLevel))
}
}

const handleTimeRangeChanged = (key: string) => {
setSelectedTimeRange(key)
}

const filteredEntries = entries.filter((entry) => {
const isActive = selectedEventTypes.includes(entry.entry?.case ?? '')
if (entry.entry.case === 'log') {
Expand All @@ -107,19 +86,11 @@ export const Timeline = () => {
})

return (
<div className='m-0'>
<TimelineFilterBar
selectedEventTypes={selectedEventTypes}
onEventTypesChanged={handleEventTypesChanged}
selectedLogLevels={selectedLogLevels}
onLogLevelsChanged={handleLogLevelsChanged}
selectedTimeRange={selectedTimeRange}
onSelectedTimeRangeChanged={handleTimeRangeChanged}
/>
<div className='border border-gray-100 dark:border-slate-700 rounded m-2'>
<div className='overflow-x-hidden'>
<table className={`w-full table-fixed text-gray-800 dark:text-gray-300`}>
<table className={`w-full table-fixed text-gray-600 dark:text-gray-300`}>
<thead>
<tr className='flex text-xs font-semibold'>
<tr className='flex text-xs'>
<th className='p-1 text-left border-b w-8 border-gray-100 dark:border-slate-700 flex-none'></th>
<th className='p-1 text-left border-b w-40 border-gray-100 dark:border-slate-700 flex-none'>Date</th>
<th className='p-1 text-left border-b border-gray-100 dark:border-slate-700 flex-grow flex-shrink'>
Expand All @@ -131,15 +102,15 @@ export const Timeline = () => {
{filteredEntries.map((entry) => (
<tr
key={entry.id.toString()}
className={`flex border-b border-gray-100 dark:border-slate-700 text-xs font-mono ${
className={`flex border-b border-gray-100 dark:border-slate-700 text-xs font-roboto-mono ${
selectedEntry?.id === entry.id ? 'bg-indigo-50 dark:bg-slate-800' : panelColor
} relative flex cursor-pointer hover:bg-indigo-50 dark:hover:bg-slate-800`}
onClick={() => handleEntryClicked(entry)}
>
<td className='w-8 flex-none flex items-center justify-center'>
<TimelineIcon entry={entry} />
</td>
<td className='p-1 w-40 items-center flex-none text-gray-500 dark:text-gray-400'>
<td className='p-1 w-40 items-center flex-none text-gray-400 dark:text-gray-400'>
{formatTimestampShort(entry.timeStamp)}
</td>
<td className='p-1 flex-grow truncate'>
Expand Down
21 changes: 21 additions & 0 deletions console/client/src/features/timeline/TimelinePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Timeline as TimelineIcon } from '@mui/icons-material'
import { PageHeader } from '../../components/PageHeader'
import { Timeline } from './Timeline'
import { TimelineFilterPanel } from './filters/TimelineFilterPanel'
import { TimelineTimeControls } from './filters/TimelineTimeControls'

export const TimelinePage = () => {
return (
<>
<PageHeader icon={<TimelineIcon />} title='Events'>
<TimelineTimeControls />
</PageHeader>
<div className='flex h-full'>
<TimelineFilterPanel />
<div className='flex-grow'>
<Timeline />
</div>
</div>
</>
)
}
127 changes: 127 additions & 0 deletions console/client/src/features/timeline/filters/TimelineFilterPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Disclosure } from '@headlessui/react'
import { ChevronUpIcon } from '@heroicons/react/20/solid'
import React from 'react'
import { modulesContext } from '../../../providers/modules-provider'
import { textColor } from '../../../utils'

const EVENT_TYPES: Record<string, string> = {
call: 'Call',
log: 'Log',
deployment: 'Deployment',
}

const headerStyles = 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'

export const TimelineFilterPanel = () => {
const modules = React.useContext(modulesContext)
const [selectedEventTypes, setSelectedEventTypes] = React.useState<string[]>(Object.keys(EVENT_TYPES))
const [selectedModules, setSelectedModules] = React.useState<string[]>([])

React.useEffect(() => {
if (selectedModules.length === 0) {
setSelectedModules(modules.modules.map((module) => module.name))
}
console.log(modules)
}, [modules])

const handleTypeChanged = (eventType: string, checked: boolean) => {
if (checked) {
setSelectedEventTypes((prev) => [...prev, eventType])
} else {
setSelectedEventTypes((prev) => prev.filter((filter) => filter !== eventType))
}
}

const handleModuleChanged = (moduleName: string, checked: boolean) => {
if (checked) {
setSelectedModules((prev) => [...prev, moduleName])
} else {
setSelectedModules((prev) => prev.filter((filter) => filter !== moduleName))
}
}

return (
<div className='flex-shrink-0 w-52'>
<div className='w-full'>
<div className='mx-auto w-full max-w-md p-2'>
<Disclosure defaultOpen={true}>
{({ open }) => (
<>
<Disclosure.Button
className={`flex w-full justify-between rounded-md ${headerStyles} py-1 px-2 text-left text-sm font-medium ${textColor} focus:outline-none focus-visible:ring focus-visible:ring-gray-500 focus-visible:ring-opacity-75`}
>
<span>Event types</span>
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 text-gray-500`} />
</Disclosure.Button>
<Disclosure.Panel className={`px-2 py-2 text-sm ${textColor}`}>
<fieldset>
<legend className='sr-only'>Event types</legend>
<div className='space-y-0.5'>
{Object.keys(EVENT_TYPES).map((key) => (
<div key={key} className='relative flex items-start'>
<div className='flex h-6 items-center'>
<input
id={`event-type-${key}`}
name={`event-type-${key}`}
type='checkbox'
checked={selectedEventTypes.includes(key)}
onChange={(e) => handleTypeChanged(key, e.target.checked)}
className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer'
/>
</div>
<div className='ml-2 text-sm leading-6'>
<label htmlFor={`event-type-${key}`} className={`${textColor}`}>
{EVENT_TYPES[key]}
</label>
</div>
</div>
))}
</div>
</fieldset>
</Disclosure.Panel>
</>
)}
</Disclosure>
<Disclosure as='div' className='mt-2' defaultOpen={true}>
{({ open }) => (
<>
<Disclosure.Button
className={`flex w-full justify-between rounded-md ${headerStyles} py-1 px-2 text-left text-sm font-medium ${textColor} hover:bg-gray-200 focus:outline-none focus-visible:ring focus-visible:ring-gray-500 focus-visible:ring-opacity-75`}
>
<span>Modules</span>
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 text-gray-500`} />
</Disclosure.Button>
<Disclosure.Panel className='px-2 py-2 text-sm text-gray-500'>
<fieldset>
<legend className='sr-only'>Modules</legend>
<div className='space-y-0.5'>
{modules.modules.map((module) => (
<div key={module.name} className='relative flex items-start'>
<div className='flex h-6 items-center'>
<input
id={`module-${module}`}
name={`module-${module}`}
type='checkbox'
checked={selectedModules.includes(module.name)}
onChange={(e) => handleModuleChanged(module.name, e.target.checked)}
className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer'
/>
</div>
<div className='ml-2 text-sm leading-6'>
<label htmlFor={`module-${module.name}`} className={`${textColor}`}>
{module.name}
</label>
</div>
</div>
))}
</div>
</fieldset>
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
</div>
</div>
)
}
Loading

0 comments on commit 4b3b74e

Please sign in to comment.