Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/raw events #27

Merged
merged 14 commits into from
Nov 18, 2023
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