Merge pull request #155 from PRO-Robotech/feature/dev

events headers | event handlers controls
This commit is contained in:
typescreep
2025-10-30 22:27:57 +03:00
committed by GitHub
4 changed files with 109 additions and 29 deletions

8
package-lock.json generated
View File

@@ -11,7 +11,7 @@
"@ant-design/icons": "5.6.0",
"@monaco-editor/react": "4.6.0",
"@originjs/vite-plugin-federation": "1.3.6",
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.149",
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.150",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",
@@ -2804,9 +2804,9 @@
}
},
"node_modules/@prorobotech/openapi-k8s-toolkit": {
"version": "0.0.1-alpha.149",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.149.tgz",
"integrity": "sha512-qRgbcBXSaxgayoRH9vziaoglno7jSWjkuOqBQ/CsHvuEDBZ7Tnbe1lzrfWHOuRngfFt1ulCHYfoiLmi9Ou5OUQ==",
"version": "0.0.1-alpha.150",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.150.tgz",
"integrity": "sha512-WkrZDN4XNHA5p/Vtcj4vE+yNHu1vyG6ezX2QCrE77TxmCTRfTSJM69Pvh6AllqWwoL7kDeUKG66Zbh+TiDm+vg==",
"license": "MIT",
"dependencies": {
"@monaco-editor/react": "4.6.0",

View File

@@ -20,7 +20,7 @@
"@ant-design/icons": "5.6.0",
"@monaco-editor/react": "4.6.0",
"@originjs/vite-plugin-federation": "1.3.6",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.149",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.150",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-lines-per-function */
// ------------------------------------------------------------
// Simple, self-contained React component implementing:
// - WebSocket connection to your events endpoint
@@ -9,7 +10,8 @@
// ------------------------------------------------------------
import React, { FC, useCallback, useEffect, useReducer, useRef, useState } from 'react'
import { theme as antdtheme } from 'antd'
import { theme as antdtheme, Flex, Tooltip } from 'antd'
import { ResumeCircleIcon, PauseCircleIcon, LockedIcon, UnlockedIcon } from '@prorobotech/openapi-k8s-toolkit'
import { TScrollMsg, TServerFrame } from './types'
import { eventKey } from './utils'
import { reducer } from './reducer'
@@ -25,6 +27,23 @@ type TEventsProps = {
export const Events: FC<TEventsProps> = ({ wsUrl, pageSize = 50, height }) => {
const { token } = antdtheme.useToken()
// pause behaviour
const [isPaused, setIsPaused] = useState(false)
const pausedRef = useRef(isPaused)
useEffect(() => {
pausedRef.current = isPaused
}, [isPaused])
// ignore REMOVE signal
const [isRemoveIgnored, setIsRemoveIgnored] = useState(true)
const removeIgnoredRef = useRef(isRemoveIgnored)
useEffect(() => {
removeIgnoredRef.current = isRemoveIgnored
}, [isRemoveIgnored])
// Reducer-backed store of events
const [state, dispatch] = useReducer(reducer, { order: [], byKey: {} })
@@ -107,15 +126,17 @@ export const Events: FC<TEventsProps> = ({ wsUrl, pageSize = 50, height }) => {
return
}
if (frame.type === 'ADDED' || frame.type === 'MODIFIED') {
// Live update: insert or replace
dispatch({ type: 'UPSERT', item: frame.item })
return
}
if (!pausedRef.current) {
if (frame.type === 'ADDED' || frame.type === 'MODIFIED') {
// Live update: insert or replace
dispatch({ type: 'UPSERT', item: frame.item })
return
}
if (frame.type === 'DELETED') {
// Live delete
dispatch({ type: 'REMOVE', key: eventKey(frame.item) })
if (!removeIgnoredRef.current && frame.type === 'DELETED') {
// Live delete
dispatch({ type: 'REMOVE', key: eventKey(frame.item) })
}
}
}, [])
@@ -209,14 +230,45 @@ export const Events: FC<TEventsProps> = ({ wsUrl, pageSize = 50, height }) => {
return (
<Styled.Root $maxHeight={height || 640}>
<Styled.Header>
<Styled.Status>
{connStatus === 'connecting' && 'Connecting…'}
{connStatus === 'open' && 'Live'}
{connStatus === 'closed' && 'Reconnecting…'}
{typeof total === 'number' ? ` · ${total} items` : ''}
</Styled.Status>
{hasMore ? <span>Scroll to load older events</span> : <span>No more events.</span>}
{lastError && <span aria-live="polite">· {lastError}</span>}
<Styled.HeaderLeftSide>
<Flex justify="start" align="center" gap={10}>
<Styled.CursorPointerDiv
onClick={() => {
if (isPaused) {
setIsPaused(false)
} else {
setIsPaused(true)
}
}}
>
{isPaused ? <ResumeCircleIcon /> : <PauseCircleIcon />}
</Styled.CursorPointerDiv>
<Styled.StatusText>
{isPaused && 'Streaming paused'}
{!isPaused && connStatus === 'connecting' && 'Connecting…'}
{!isPaused && connStatus === 'open' && 'Streaming events...'}
{!isPaused && connStatus === 'closed' && 'Reconnecting…'}
</Styled.StatusText>
</Flex>
</Styled.HeaderLeftSide>
<Styled.HeaderRightSide $colorTextDescription={token.colorTextDescription}>
{!hasMore && <div>No more events · </div>}
{typeof total === 'number' ? <div>Loaded {total} events</div> : ''}
{lastError && <span aria-live="polite"> · {lastError}</span>}
<Tooltip
title={
<div>
<div>{isRemoveIgnored ? 'Handle REMOVE signals' : 'Ignore REMOVE signals'}</div>
<Flex justify="end">Locked means ignore</Flex>
</div>
}
placement="left"
>
<Styled.CursorPointerDiv onClick={() => setIsRemoveIgnored(!isRemoveIgnored)}>
{isRemoveIgnored ? <LockedIcon size={16} /> : <UnlockedIcon size={16} />}
</Styled.CursorPointerDiv>
</Tooltip>
</Styled.HeaderRightSide>
</Styled.Header>
{/* Scrollable list of event rows */}

View File

@@ -15,16 +15,41 @@ const Root = styled.div<TRootProps>`
position: relative;
`
const Header = styled.header`
padding: 12px 16px;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
align-self: stretch;
margin-bottom: 16px;
padding-left: 19px;
`
const Status = styled.span`
font-size: 12px;
color: #6b7280;
const HeaderLeftSide = styled.div`
display: flex;
align-items: center;
gap: 10px;
flex: 1 0 0;
`
const CursorPointerDiv = styled.div`
cursor: pointer;
user-select: none;
`
const StatusText = styled.div`
font-size: 16px;
line-height: 24px; /* 150% */
`
type THeaderRightSideProps = {
$colorTextDescription: string
}
const HeaderRightSide = styled.div<THeaderRightSideProps>`
display: flex;
gap: 4px;
text-align: right;
color: ${({ $colorTextDescription }) => $colorTextDescription};
`
const List = styled.div`
@@ -65,7 +90,10 @@ const Sentinel = styled.div`
export const Styled = {
Root,
Header,
Status,
HeaderLeftSide,
CursorPointerDiv,
StatusText,
HeaderRightSide,
Timeline,
List,
Sentinel,