Skip to content

Commit

Permalink
Feature/raw events (#27)
Browse files Browse the repository at this point in the history
* Added raw events API

* Moved explorer layout to component

* Moved WS state icon to reusable component

* Fixed event message factory

* Changed raw events page to explorer layout

* Added json viewer for raw events

* Added raw events table

* Added limit select

* Added pause / connect toggle button

* Added placeholders

* Removed connecting spinner

* Moved raw events limit to settings

* Added event type facet

* Fixed linting
  • Loading branch information
danyi1212 committed Aug 17, 2024
1 parent 45d0cf0 commit e92fc6b
Show file tree
Hide file tree
Showing 21 changed files with 485 additions and 152 deletions.
9 changes: 2 additions & 7 deletions frontend/src/components/CeleryStateSync.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { handleEvent, loadInitialState, useStateStore } from "@stores/useStateStore"
import { toWebSocketUri } from "@utils/webSocketUtils"
import React, { useEffect } from "react"
import useWebSocket from "react-use-websocket"

const toWsUri = (path: string): string => {
const location = window.location
const protocol = location.protocol === "https:" ? "wss:" : "ws:"
return `${protocol}//${location.host}/${path}`
}

const CeleryStateSync: React.FC = () => {
const { readyState } = useWebSocket(toWsUri("ws/events"), {
const { readyState } = useWebSocket(toWebSocketUri("ws/events"), {
shouldReconnect: () => true,
onError: (error) => console.error("Error connecting to websockets!", error),
onReconnectStop: (numAttempts) => console.error(`Out of attempts to reconnected websockets (${numAttempts})`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import Slide from "@mui/material/Slide"
import Stack from "@mui/material/Stack"
import Tooltip from "@mui/material/Tooltip"
import Typography from "@mui/material/Typography"
import useSettingsStore from "@stores/useSettingsStore"
import { useStateStore } from "@stores/useStateStore"
import React, { useEffect, useState } from "react"
import { ReadyState } from "react-use-websocket"

Expand Down Expand Up @@ -40,23 +38,26 @@ const statusMeta: Record<ReadyState, Meta> = {
},
}

const WSStatus: React.FC = () => {
const isDemo = useSettingsStore((state) => state.demo)
const status = useStateStore((store) => store.status)
const meta: Meta = isDemo
? {
description: "Demo Mode",
icon: <OfflineBoltIcon color="primary" />,
}
: statusMeta[status]
const demoMeta: Meta = {
description: "Demo Mode",
icon: <OfflineBoltIcon color="primary" />,
}

interface WsStateIconProps {
state: ReadyState
isDemo?: boolean
}

const WsStateIcon: React.FC<WsStateIconProps> = ({ state, isDemo }) => {
const meta: Meta = isDemo ? demoMeta : statusMeta[state]
const [isOpen, setOpen] = useState(true)

useEffect(() => {
setOpen(true)
const token = setTimeout(() => setOpen(false), 1000 * 5)
return () => clearTimeout(token)
}, [status, isDemo])
}, [state, isDemo])

return (
<Stack direction="row" alignItems="center" p={1}>
<Box overflow="hidden">
Expand All @@ -68,4 +69,4 @@ const WSStatus: React.FC = () => {
</Stack>
)
}
export default WSStatus
export default WsStateIcon
31 changes: 31 additions & 0 deletions frontend/src/components/raw_events/LimitSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import FormControl from "@mui/material/FormControl"
import InputLabel from "@mui/material/InputLabel"
import MenuItem from "@mui/material/MenuItem"
import Select from "@mui/material/Select"
import React from "react"

interface LimitSelectProps {
limit: number
setLimit: (limit: number) => void
}

export const LimitSelect: React.FC<LimitSelectProps> = ({ limit, setLimit }) => {
return (
<FormControl size="small">
<InputLabel id="limit-select-label">Limit</InputLabel>
<Select
labelId="limit-select-label"
id="limit-select"
value={limit}
label="Limit"
onChange={(event) => setLimit(event.target.value as number)}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={300}>300</MenuItem>
<MenuItem value={1000}>1,000</MenuItem>
</Select>
</FormControl>
)
}
58 changes: 58 additions & 0 deletions frontend/src/components/raw_events/RawEventRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import TaskAvatar from "@components/task/TaskAvatar"
import { CeleryEvent } from "@hooks/useRawEvents"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
import { useTheme } from "@mui/material"
import Collapse from "@mui/material/Collapse"
import IconButton from "@mui/material/IconButton"
import TableCell from "@mui/material/TableCell"
import TableRow from "@mui/material/TableRow"
import { JsonViewer } from "@textea/json-viewer"
import { format } from "date-fns"
import React from "react"

interface RawEventRowProps {
event: CeleryEvent
}

export const RawEventRow: React.FC<RawEventRowProps> = ({ event }) => {
const [open, setOpen] = React.useState(false)

const theme = useTheme()
return (
<>
<TableRow sx={{ "& > *": { borderBottom: "unset" } }}>
<TableCell>
{event?.uuid ? (
<TaskAvatar taskId={event.uuid.toString()} type={event?.name as string} />
) : (
<TaskAvatar taskId="worker" type={event?.hostname as string} />
)}
</TableCell>
<TableCell>
{event?.timestamp ? format(event.timestamp as number, "hh:mm:ss.SSS") : "Unknown"}
</TableCell>
<TableCell>{(event?.type as string) || "Unknown"}</TableCell>
<TableCell>{(event?.name as string) || (event?.hostname as string) || "Unknown"}</TableCell>
<TableCell>
<IconButton aria-label="Expand raw event" size="small" onClick={() => setOpen(!open)}>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<JsonViewer
theme={theme.palette.mode}
editable={false}
rootName={false}
quotesOnKeys={false}
value={event}
/>
</Collapse>
</TableCell>
</TableRow>
</>
)
}
36 changes: 36 additions & 0 deletions frontend/src/components/raw_events/RawEventsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { RawEventRow } from "@components/raw_events/RawEventRow"
import { CeleryEvent } from "@hooks/useRawEvents"
import Table from "@mui/material/Table"
import TableBody from "@mui/material/TableBody"
import TableCell from "@mui/material/TableCell"
import TableContainer from "@mui/material/TableContainer"
import TableHead from "@mui/material/TableHead"
import TableRow from "@mui/material/TableRow"
import React from "react"

interface RawEventsTableProps {
events: CeleryEvent[]
}

export const RawEventsTable: React.FC<RawEventsTableProps> = ({ events }) => {
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width={60}>Task</TableCell>
<TableCell width={120}>Timestamp</TableCell>
<TableCell width={180}>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell width={120}>Expand</TableCell>
</TableRow>
</TableHead>
<TableBody>
{events.map((event, index) => (
<RawEventRow key={index} event={event} />
))}
</TableBody>
</Table>
</TableContainer>
)
}
21 changes: 21 additions & 0 deletions frontend/src/components/raw_events/ToggleConnect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PauseIcon from "@mui/icons-material/Pause"
import PlayArrowIcon from "@mui/icons-material/PlayArrow"
import IconButton from "@mui/material/IconButton"
import Tooltip from "@mui/material/Tooltip"
import React from "react"

interface ToggleConnectProps {
connect: boolean
setConnect: (connect: boolean) => void
disabled?: boolean
}

export const ToggleConnect: React.FC<ToggleConnectProps> = ({ connect, setConnect, disabled }) => {
return (
<Tooltip title={connect ? "Freeze" : "Connect"}>
<IconButton onClick={() => setConnect(!connect)} disabled={disabled}>
{connect ? <PauseIcon /> : <PlayArrowIcon />}
</IconButton>
</Tooltip>
)
}
25 changes: 25 additions & 0 deletions frontend/src/hooks/useRawEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { toWebSocketUri } from "@utils/webSocketUtils"
import { useState } from "react"
import useWebSocket from "react-use-websocket"

export type CeleryEvent = Record<string, unknown>

export const useRawEvents = (connect: boolean, limit: number) => {
const [events, setEvents] = useState<CeleryEvent[]>([])
const { readyState } = useWebSocket(
toWebSocketUri("ws/raw_events"),
{
shouldReconnect: () => connect,
share: true,
onError: (error) => console.error("Error connecting to websockets!", error),
onReconnectStop: (numAttempts) =>
console.error(`Out of attempts to reconnected websockets (${numAttempts})`),
onMessage: (event) => {
const message = JSON.parse(event.data)
setEvents((state) => [message, ...state.slice(0, limit - 1)])
},
},
connect,
)
return { events, readyState }
}
55 changes: 55 additions & 0 deletions frontend/src/layout/explorer/ExplorerLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"
import Box from "@mui/material/Box"
import Divider from "@mui/material/Divider"
import IconButton from "@mui/material/IconButton"
import Stack from "@mui/material/Stack"
import Toolbar from "@mui/material/Toolbar"
import Tooltip from "@mui/material/Tooltip"
import Typography from "@mui/material/Typography"
import React, { useState } from "react"

interface ExplorerLayoutProps {
facets?: React.ReactNode
actions?: React.ReactNode
children?: React.ReactNode
}

const FACET_WIDTH = 300
export const ExplorerLayout: React.FC<ExplorerLayoutProps> = ({ facets, actions, children }) => {
const [isFacetMenuOpen, setFacetMenuOpen] = useState(true)

return (
<Box display="flex" flexDirection="row">
<Box
width={isFacetMenuOpen ? FACET_WIDTH : 0}
sx={{ transition: (theme) => theme.transitions.create("width"), overflow: "hidden" }}
>
<Toolbar>
<Typography variant="h5">Facets</Typography>
</Toolbar>
<Divider />
{facets}
</Box>
<Box flexGrow={1}>
<Toolbar>
<Tooltip title={isFacetMenuOpen ? "Hide facets" : "Show facets"}>
<IconButton onClick={() => setFacetMenuOpen(!isFacetMenuOpen)}>
{isFacetMenuOpen ? <ArrowBackIosNewIcon /> : <ArrowForwardIosIcon />}
</IconButton>
</Tooltip>
<Box flexGrow={1} />
<Stack
direction="row"
justifyContent="space-between"
spacing={1}
sx={{ justifyContent: "space-between", alignItems: "center" }}
>
{actions}
</Stack>
</Toolbar>
{children}
</Box>
</Box>
)
}
12 changes: 10 additions & 2 deletions frontend/src/layout/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import WsStateIcon from "@components/common/WsStateIcon"
import SearchBox from "@components/search/SearchBox"
import NotificationBadge from "@layout/header/NotificationBadge"
import ThemeSelector from "@layout/header/ThemeSelector"
import WSStatus from "@layout/header/WSStatus"
import { DRAWER_WIDTH, DRAWER_WIDTH_COLLAPSED } from "@layout/menu/Menu"
import GitHubIcon from "@mui/icons-material/GitHub"
import AppBar from "@mui/material/AppBar"
Expand All @@ -11,9 +11,17 @@ import Slide from "@mui/material/Slide"
import Stack from "@mui/material/Stack"
import Toolbar from "@mui/material/Toolbar"
import useScrollTrigger from "@mui/material/useScrollTrigger"
import useSettingsStore from "@stores/useSettingsStore"
import useSettings from "@stores/useSettingsStore"
import { useStateStore } from "@stores/useStateStore"
import React from "react"

const StateWsStatusIcon: React.FC = () => {
const isDemo = useSettingsStore((state) => state.demo)
const status = useStateStore((store) => store.status)
return <WsStateIcon state={status} isDemo={isDemo} />
}

const Header: React.FC = () => {
const trigger = useScrollTrigger({ target: window })
const menuExpanded = useSettings((state) => state.menuExpanded)
Expand All @@ -32,7 +40,7 @@ const Header: React.FC = () => {
<SearchBox />
<Box flexGrow="1" />
<Stack direction="row" spacing={1} justifyContent="space-between" alignItems="center">
<WSStatus />
<StateWsStatusIcon />
<IconButton
component="a"
href=" https://github.com/danyi1212/celery-insights"
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/layout/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import MenuItem, { MenuLink } from "@layout/menu/MenuItem"
import ApiIcon from "@mui/icons-material/Api"
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"
import InboxIcon from "@mui/icons-material/Inbox"
import ManageSearchIcon from "@mui/icons-material/ManageSearch"
import RssFeedIcon from "@mui/icons-material/RssFeed"
import SettingsIcon from "@mui/icons-material/Settings"
import SpaceDashboardOutlinedIcon from "@mui/icons-material/SpaceDashboardOutlined"
import SubjectIcon from "@mui/icons-material/Subject"
import { useMediaQuery, useTheme } from "@mui/material"
import Collapse from "@mui/material/Collapse"
Expand Down Expand Up @@ -50,8 +51,8 @@ const StyledLogoContainer = styled(Link)({

const menuLinks: MenuLink[] = [
{
label: "Home",
icon: <InboxIcon />,
label: "Dashboard",
icon: <SpaceDashboardOutlinedIcon />,
to: "/",
external: false,
},
Expand All @@ -61,6 +62,12 @@ const menuLinks: MenuLink[] = [
to: "/explorer",
external: false,
},
{
label: "Live Events",
icon: <RssFeedIcon />,
to: "/raw_events",
external: false,
},
{
label: "API Explorer",
icon: <ApiIcon />,
Expand Down
Loading

0 comments on commit e92fc6b

Please sign in to comment.