diff --git a/package-lock.json b/package-lock.json index dd1b003..e784628 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 19f5dc2..c79b935 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/organisms/Events/Events.tsx b/src/components/organisms/Events/Events.tsx index cbcb9ce..350fba8 100644 --- a/src/components/organisms/Events/Events.tsx +++ b/src/components/organisms/Events/Events.tsx @@ -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 = ({ 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 = ({ 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 = ({ wsUrl, pageSize = 50, height }) => { return ( - - {connStatus === 'connecting' && 'Connecting…'} - {connStatus === 'open' && 'Live'} - {connStatus === 'closed' && 'Reconnecting…'} - {typeof total === 'number' ? ` · ${total} items` : ''} - - {hasMore ? Scroll to load older events… : No more events.} - {lastError && · {lastError}} + + + { + if (isPaused) { + setIsPaused(false) + } else { + setIsPaused(true) + } + }} + > + {isPaused ? : } + + + {isPaused && 'Streaming paused'} + {!isPaused && connStatus === 'connecting' && 'Connecting…'} + {!isPaused && connStatus === 'open' && 'Streaming events...'} + {!isPaused && connStatus === 'closed' && 'Reconnecting…'} + + + + + {!hasMore &&
No more events ·
} + {typeof total === 'number' ?
Loaded {total} events
: ''} + {lastError && · {lastError}} + +
{isRemoveIgnored ? 'Handle REMOVE signals' : 'Ignore REMOVE signals'}
+ Locked means ignore + + } + placement="left" + > + setIsRemoveIgnored(!isRemoveIgnored)}> + {isRemoveIgnored ? : } + +
+
{/* Scrollable list of event rows */} diff --git a/src/components/organisms/Events/styled.ts b/src/components/organisms/Events/styled.ts index f6dae34..1ac1e29 100644 --- a/src/components/organisms/Events/styled.ts +++ b/src/components/organisms/Events/styled.ts @@ -15,16 +15,41 @@ const Root = styled.div` 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` + 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,