mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-27 18:19:50 +00:00
events headers | event handlers controls
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user