From 50bcad7c3d079dcd38a8c18284c71c922761be1e Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 13 Sep 2023 09:53:58 -0700 Subject: [PATCH] feat: Style timeline page and add filter panel --- console/client/index.html | 3 +- console/client/src/App.tsx | 8 +- console/client/src/components/PageHeader.tsx | 21 +++ .../client/src/features/graph/GraphPage.tsx | 3 + .../src/features/modules/ModulesPage.tsx | 14 ++ .../client/src/features/timeline/Timeline.tsx | 45 ++----- .../src/features/timeline/TimelinePage.tsx | 21 +++ .../timeline/filters/TimelineFilterPanel.tsx | 127 ++++++++++++++++++ .../timeline/filters/TimelineTimeControls.tsx | 114 ++++++++++++++++ console/client/src/layout/Layout.tsx | 2 +- console/client/src/layout/Navigation.tsx | 12 +- console/client/src/utils/date.utils.ts | 4 +- console/client/src/utils/style.utils.ts | 5 +- console/client/tailwind.config.js | 6 +- 14 files changed, 332 insertions(+), 53 deletions(-) create mode 100644 console/client/src/components/PageHeader.tsx create mode 100644 console/client/src/features/modules/ModulesPage.tsx create mode 100644 console/client/src/features/timeline/TimelinePage.tsx create mode 100644 console/client/src/features/timeline/filters/TimelineFilterPanel.tsx create mode 100644 console/client/src/features/timeline/filters/TimelineTimeControls.tsx diff --git a/console/client/index.html b/console/client/index.html index e89b502a2c..2406df2c88 100644 --- a/console/client/index.html +++ b/console/client/index.html @@ -5,9 +5,10 @@ FTL + - +
diff --git a/console/client/src/App.tsx b/console/client/src/App.tsx index 98379e0345..078ad058c9 100644 --- a/console/client/src/App.tsx +++ b/console/client/src/App.tsx @@ -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' @@ -11,8 +11,8 @@ export const App = () => { }> } /> - } /> - } /> + } /> + } /> } /> diff --git a/console/client/src/components/PageHeader.tsx b/console/client/src/components/PageHeader.tsx new file mode 100644 index 0000000000..62ee3ce6c8 --- /dev/null +++ b/console/client/src/components/PageHeader.tsx @@ -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 ( +
+
+ {icon} + {title} +
+ {children} +
+ ) +} diff --git a/console/client/src/features/graph/GraphPage.tsx b/console/client/src/features/graph/GraphPage.tsx index ded19c419d..563f46c184 100644 --- a/console/client/src/features/graph/GraphPage.tsx +++ b/console/client/src/features/graph/GraphPage.tsx @@ -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' @@ -21,6 +23,7 @@ export const GraphPage = () => { return ( <> + } title='Graph' />
{ + return ( + <> +
+ } title='Modules' /> + +
+ + ) +} diff --git a/console/client/src/features/timeline/Timeline.tsx b/console/client/src/features/timeline/Timeline.tsx index a7662b4c4a..14ffab6e58 100644 --- a/console/client/src/features/timeline/Timeline.tsx +++ b/console/client/src/features/timeline/Timeline.tsx @@ -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([]) const [selectedEntry, setSelectedEntry] = React.useState(null) - const [selectedEventTypes, setSelectedEventTypes] = React.useState(['log', 'call', 'deployment']) - const [selectedLogLevels, setSelectedLogLevels] = React.useState([1, 5, 9, 13, 17]) - const [selectedTimeRange, setSelectedTimeRange] = React.useState('1h') + const [selectedEventTypes] = React.useState(['log', 'call', 'deployment']) + const [selectedLogLevels] = React.useState([1, 5, 9, 13, 17]) + const [selectedTimeRange] = React.useState('24h') React.useEffect(() => { const abortController = new AbortController() @@ -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') { @@ -107,19 +86,11 @@ export const Timeline = () => { }) return ( -
- +
- +
- + handleEntryClicked(entry)} @@ -139,7 +110,7 @@ export const Timeline = () => { -
Date @@ -131,7 +102,7 @@ export const Timeline = () => { {filteredEntries.map((entry) => (
+ {formatTimestampShort(entry.timeStamp)} diff --git a/console/client/src/features/timeline/TimelinePage.tsx b/console/client/src/features/timeline/TimelinePage.tsx new file mode 100644 index 0000000000..11c01ff140 --- /dev/null +++ b/console/client/src/features/timeline/TimelinePage.tsx @@ -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 ( + <> + } title='Events'> + + +
+ +
+ +
+
+ + ) +} diff --git a/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx b/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx new file mode 100644 index 0000000000..2ec705cd3b --- /dev/null +++ b/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx @@ -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 = { + 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(Object.keys(EVENT_TYPES)) + const [selectedModules, setSelectedModules] = React.useState([]) + + 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 ( +
+
+
+ + {({ open }) => ( + <> + + Event types + + + +
+ Event types +
+ {Object.keys(EVENT_TYPES).map((key) => ( +
+
+ handleTypeChanged(key, e.target.checked)} + className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer' + /> +
+
+ +
+
+ ))} +
+
+
+ + )} +
+ + {({ open }) => ( + <> + + Modules + + + +
+ Modules +
+ {modules.modules.map((module) => ( +
+
+ handleModuleChanged(module.name, e.target.checked)} + className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer' + /> +
+
+ +
+
+ ))} +
+
+
+ + )} +
+
+
+
+ ) +} diff --git a/console/client/src/features/timeline/filters/TimelineTimeControls.tsx b/console/client/src/features/timeline/filters/TimelineTimeControls.tsx new file mode 100644 index 0000000000..858c52818b --- /dev/null +++ b/console/client/src/features/timeline/filters/TimelineTimeControls.tsx @@ -0,0 +1,114 @@ +import { Listbox, Transition } from '@headlessui/react' +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline' +import { NavigateBefore, NavigateNext, PlayArrow } from '@mui/icons-material' +import React, { Fragment } from 'react' +import { bgColor, borderColor, classNames, panelColor, textColor } from '../../../utils' + +interface TimeRange { + label: string + value: number +} + +export const TIME_RANGES: Record = { + tail: { label: 'Live tail', value: 0 }, + '5m': { label: 'Past 5 minutes', value: 5 * 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 = () => { + const [selected, setSelected] = React.useState(TIME_RANGES['tail']) + + return ( + <> +
+ + {({ open }) => ( + <> +
+ + {selected.label} + + + + + + + {Object.keys(TIME_RANGES).map((key) => { + const timeRange = TIME_RANGES[key] + return ( + + classNames( + active ? 'bg-indigo-600 text-white' : `${textColor}`, + 'relative cursor-pointer select-none py-2 pl-3 pr-9', + ) + } + value={timeRange} + > + {({ selected, active }) => ( + <> + + {timeRange.label} + + + {selected ? ( + + + ) : null} + + )} + + ) + })} + + +
+ + )} +
+ + + + + +
+ + ) +} diff --git a/console/client/src/layout/Layout.tsx b/console/client/src/layout/Layout.tsx index ec31c9eab8..780bb3f56f 100644 --- a/console/client/src/layout/Layout.tsx +++ b/console/client/src/layout/Layout.tsx @@ -8,7 +8,7 @@ export const Layout = () => {
-
+
diff --git a/console/client/src/layout/Navigation.tsx b/console/client/src/layout/Navigation.tsx index 54ab8c345b..e96051a2b0 100644 --- a/console/client/src/layout/Navigation.tsx +++ b/console/client/src/layout/Navigation.tsx @@ -1,4 +1,4 @@ -import { Schema, Timeline, ViewModuleSharp } from '@mui/icons-material' +import { Schema, Timeline, ViewModuleRounded } from '@mui/icons-material' import { useContext } from 'react' import { Link, NavLink } from 'react-router-dom' import { DarkModeSwitch } from '../components/DarkModeSwitch' @@ -7,7 +7,7 @@ import { classNames } from '../utils' const navigation = [ { name: 'Events', href: '/events', icon: Timeline }, - { name: 'Modules', href: '/modules', icon: ViewModuleSharp }, + { name: 'Modules', href: '/modules', icon: ViewModuleRounded }, { name: 'Graph', href: '/graph', icon: Schema }, ] @@ -17,8 +17,8 @@ export const Navigation = () => { return (