From 04ad9654812f6e0c03bfc376b3a4c636a26fe257 Mon Sep 17 00:00:00 2001 From: gchuf Date: Thu, 2 Jul 2026 11:45:01 +0200 Subject: [PATCH] ARTEMIS-6040 Add navigation between messages Assisted-by: Claude --- .../src/messages/MessageView.tsx | 22 ++++- .../src/messages/MessagesTable.tsx | 91 +++++++++++++++--- .../src/queues/QueuesView.tsx | 95 +++++++++++++++---- 3 files changed, 171 insertions(+), 37 deletions(-) diff --git a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessageView.tsx b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessageView.tsx index a6ea84c4..3d3b7223 100644 --- a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessageView.tsx +++ b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessageView.tsx @@ -22,7 +22,13 @@ import { artemisService } from '../artemis-service'; export type MessageProps = { currentMessage: Message, - back?: Function + back?: Function, + onNext?: () => void, + onPrevious?: () => void, + hasNext?: boolean, + hasPrevious?: boolean, + currentIndex?: number, + totalCount?: number } export type Message = { @@ -52,7 +58,7 @@ export type Message = { export const MessageView: React.FunctionComponent = props => { - const [currentMessage] = useState(props.currentMessage); + const currentMessage = props.currentMessage; const [messageBody, setMessageBody] = useState(""); const [messageTextMode, setMessageTextMode] = useState(""); @@ -126,7 +132,9 @@ export const MessageView: React.FunctionComponent = props => { return ( <> - Message ID: {currentMessage.messageID} + + Message ID: {currentMessage.messageID} ({(props.currentIndex ?? 0) + 1}/{props.totalCount}) + Displaying Body as : {messageTextMode} Headers @@ -229,6 +237,12 @@ export const MessageView: React.FunctionComponent = props => { <> } + {(props.onPrevious || props.onNext) && + <> + + + + } ) } @@ -237,7 +251,7 @@ function getProps(properties: any, type:string): React.ReactNode { if(properties) { return Object.keys(properties).map((key, index) => { return ( - + {key} {"" + properties[key]} {type} diff --git a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessagesTable.tsx b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessagesTable.tsx index caf438b9..f352e543 100644 --- a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessagesTable.tsx +++ b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/messages/MessagesTable.tsx @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useEffect, useImperativeHandle, useRef, useState, forwardRef } from 'react' import { Column } from '../table/ArtemisTable'; import { artemisService } from '../artemis-service'; import { Toolbar, ToolbarContent, ToolbarItem, Text, SearchInput, Button, PaginationVariant, Pagination, DataList, DataListCell, DataListCheck, DataListItem, DataListItemCells, DataListItemRow, Modal, TextContent, Icon, ModalVariant } from '@patternfly/react-core'; @@ -34,17 +34,60 @@ export type MessageProps = { address: string, queue: string, routingType: string, - selectMessage?: Function, + selectMessage?: (message: any, globalIndex: number, totalCount: number) => void, + onTotalCountChange?: (count: number) => void, back?: Function } +export type MessagesTableHandle = { + fetchMessageAt: (globalIndex: number) => Promise<{ message: Message | null; totalCount: number }>; +} + +export const MessagesTable = forwardRef((props, ref) => { + useImperativeHandle(ref, () => ({ fetchMessageAt })); + + const fetchMessageAt = async (globalIndex: number): Promise<{ message: Message | null; totalCount: number }> => { + const targetPage = Math.floor(globalIndex / perPage) + 1; + const indexInPage = globalIndex % perPage; + + if (targetPage === page) { + return { message: rowsRef.current[indexInPage] ?? null, totalCount: resultsSizeRef.current }; + } + const cached = pageCacheRef.current.get(targetPage); + if (cached) { + return { message: cached[indexInPage] ?? null, totalCount: resultsSizeRef.current }; + } -export const MessagesTable: React.FunctionComponent = props => { + try { + const brokerObjectname = await artemisService.getBrokerObjectName(); + const queueMBean: string = createQueueObjectName(brokerObjectname, props.address, props.routingType, props.queue); + const response: any = await artemisService.getMessages(queueMBean, targetPage, perPage, filter); + const data = response?.data ?? []; + const totalCount = response?.count ?? resultsSizeRef.current; + + if (totalCount !== resultsSizeRef.current) { + // count shifted (e.g. messages deleted elsewhere) — cached pages are no longer trustworthy + pageCacheRef.current.clear(); + resultsSizeRef.current = totalCount; + setresultsSize(totalCount); + props.onTotalCountChange?.(totalCount); + } else { + pageCacheRef.current.set(targetPage, data); + } + return { message: (data[indexInPage] as Message) ?? null, totalCount }; + } catch (error) { + eventService.notify({ type: 'warning', message: jolokiaService.errorMessage(error) }); + return { message: null, totalCount: resultsSizeRef.current }; + } + }; const messageView = (row: any) => { - if (props.selectMessage) { props.selectMessage(row); } + const currentRows = rowsRef.current; + const indexInPage = currentRows.findIndex((r: any) => r.messageID === row.messageID); + const globalIndex = (page - 1) * perPage + indexInPage; + if (props.selectMessage) { props.selectMessage(row, globalIndex, resultsSizeRef.current); } } const allColumns: Column[] = [ @@ -64,7 +107,7 @@ export const MessagesTable: React.FunctionComponent = props => { const [filter, setFilter] = useState(""); const [inputValue, setInputValue] = useState(''); const [page, setPage] = useState(1); - const [rows, setRows] = useState([]) + const [rows, setRows] = useState([]) const [perPage, setPerPage] = useState(10); const [columns, setColumns] = useState(allColumns); const [columnsLoaded, setColumnsLoaded] = useState(false); @@ -77,25 +120,41 @@ export const MessagesTable: React.FunctionComponent = props => { const [showCopyMessagesModal, setShowCopyMessagesModal] = useState(false); const [showRetryMessagesModal, setShowRetryMessagesModal] = useState(false); const [showResendModal, setShowResendModal] = useState(false); - const [ resendMessage, setResendMessage] = useState(); + const [resendMessage, setResendMessage] = useState(); const { brokerNode } = useContext(ArtemisContext); + const rowsRef = useRef([]); + const resultsSizeRef = useRef(0); + const pageCacheRef = useRef>(new Map()); + + useEffect(() => { + rowsRef.current = rows; + }, [rows]); + useEffect(() => { + resultsSizeRef.current = resultsSize; + }, [resultsSize]); + useEffect(() => { + pageCacheRef.current.clear(); + }, [props.address, props.routingType, props.queue, perPage, filter]); + useEffect(() => { + const listMessages = async (): Promise => { + const brokerObjectname = await artemisService.getBrokerObjectName(); + const queueMBean: string = createQueueObjectName(brokerObjectname, props.address, props.routingType, props.queue); + const response = await artemisService.getMessages(queueMBean, page, perPage, filter); + return response; + } const listData = async () => { listMessages().then((data) => { setRows(data.data); setresultsSize(data.count); + props.onTotalCountChange?.(data.count); }).catch((error) => { eventService.notify({type: 'warning', message: jolokiaService.errorMessage(error) }) }) } - const listMessages = async (): Promise => { - const brokerObjectname = await artemisService.getBrokerObjectName(); - const queueMBean: string = createQueueObjectName(brokerObjectname, props.address, props.routingType, props.queue); - const response = await artemisService.getMessages(queueMBean, page, perPage, filter); - return response; - } + setPerPage(artemisPreferencesService.loadTablePageSize(columnStorage.messages)); if (!columnsLoaded) { const updatedColumns: Column[] = artemisPreferencesService.loadColumnPreferences(columnStorage.messages, allColumns); @@ -103,7 +162,6 @@ export const MessagesTable: React.FunctionComponent = props => { setColumnsLoaded(true); } listData(); - }, [props.address, props.routingType, props.queue, page, perPage, filter, selectedMessages]) const handleSetPage = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => { @@ -131,6 +189,8 @@ export const MessagesTable: React.FunctionComponent = props => { }; const getRowActions = (row: any): IAction[] => { + const indexInPage = rows.findIndex((r: any) => r.messageID === row.messageID); + const globalIndex = (page - 1) * perPage + indexInPage; return [ { title: 'Delete', @@ -144,7 +204,7 @@ export const MessagesTable: React.FunctionComponent = props => { title: 'View', id: 'message-dropdown-view', onClick: () => { - if (props.selectMessage) { props.selectMessage(row); } + if (props.selectMessage) { props.selectMessage(row, globalIndex, resultsSizeRef.current); } } }, { @@ -392,6 +452,7 @@ export const MessagesTable: React.FunctionComponent = props => { selectAllMessages(isSelecting), isSelected: areAllMessagesSelected @@ -627,4 +688,4 @@ export const MessagesTable: React.FunctionComponent = props => { ); -} +}) diff --git a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/queues/QueuesView.tsx b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/queues/QueuesView.tsx index 7923705b..d1621017 100644 --- a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/queues/QueuesView.tsx +++ b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/queues/QueuesView.tsx @@ -14,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Navigate } from '../views/ArtemisTabView'; import { QueuesTable } from './QueuesTable'; -import { MessagesTable } from '../messages/MessagesTable'; +import { MessagesTable, MessagesTableHandle } from '../messages/MessagesTable'; import { Filter } from '../table/ArtemisTable'; import { Button, Modal, ModalVariant, TextContent, Title, Text, Icon, TextVariants, TextList, TextListItem, TextListItemVariants, TextListVariants } from '@patternfly/react-core'; import { Message, MessageView } from '../messages/MessageView'; @@ -42,10 +42,6 @@ export type Queue = { routingType: string } - - - - export const QueuesView: React.FunctionComponent = navigate => { const initialMessage: Message = { messageID: '', @@ -62,10 +58,19 @@ export const QueuesView: React.FunctionComponent = navigate => { userID: '' }; - const [ activeTabKey, setActiveTabKey ] = useState(0); - const [ currentQueue, setCurrentQueue ] = useState({name: "", address: "", routingType: "ANYCAST"}); - const [ currentMessage, setCurrentMessage ] = useState(initialMessage); - const [ showHelpModal, setShowHelpModal ] = useState(false); + const [activeTabKey, setActiveTabKey] = useState(0); + const [currentQueue, setCurrentQueue] = useState({name: "", address: "", routingType: "ANYCAST"}); + const [currentMessage, setCurrentMessage] = useState(initialMessage); + const [showHelpModal, setShowHelpModal] = useState(false); + + const [currentIndex, setCurrentIndex] = useState(0); + const [totalMessages, setTotalMessages] = useState(0); + const messagesTableRef = useRef(null); + + const handleTotalCountChange = (count: number) => { + setTotalMessages(count); + setCurrentIndex(prev => (prev >= count ? Math.max(0, count - 1) : prev)); + }; const selectQueue = (queue: string, address: string, routingType: string) => { setCurrentQueue({ @@ -76,13 +81,58 @@ export const QueuesView: React.FunctionComponent = navigate => { setActiveTabKey(1); } - const selectMessage = (message: Message) => { + const selectMessage = (message: Message, globalIndex: number, totalCount: number) => { setCurrentMessage(message); + setCurrentIndex(globalIndex); + setTotalMessages(totalCount); setActiveTabKey(2); } + const nextMessage = async () => { + const newIndex = currentIndex + 1; + if (!messagesTableRef.current || newIndex >= totalMessages) return; + const result = await messagesTableRef.current.fetchMessageAt(newIndex); + setTotalMessages(result.totalCount); + if (result.message) { + setCurrentIndex(newIndex); + setCurrentMessage(result.message); + } else { + // that message is gone (deleted) — try the new last valid index instead + const clamped = Math.min(newIndex, result.totalCount - 1); + if (clamped >= 0 && clamped !== currentIndex) { + const retry = await messagesTableRef.current.fetchMessageAt(clamped); + if (retry.message) { + setCurrentIndex(clamped); + setCurrentMessage(retry.message); + } + setTotalMessages(retry.totalCount); + } + } + } + + const previousMessage = async () => { + const newIndex = currentIndex - 1; + if (!messagesTableRef.current || newIndex < 0) return; + const result = await messagesTableRef.current.fetchMessageAt(newIndex); + setTotalMessages(result.totalCount); + if (result.message) { + setCurrentIndex(newIndex); + setCurrentMessage(result.message); + } else { + const clamped = Math.max(0, Math.min(newIndex, result.totalCount - 1)); + if (clamped !== currentIndex) { + const retry = await messagesTableRef.current.fetchMessageAt(clamped); + if (retry.message) { + setCurrentIndex(clamped); + setCurrentMessage(retry.message); + } + setTotalMessages(retry.totalCount); + } + } + } + const back = (tab: number) => { - setActiveTabKey(tab); + setActiveTabKey(tab); } return ( @@ -90,16 +140,25 @@ export const QueuesView: React.FunctionComponent = navigate => { {activeTabKey === 0 && } - {activeTabKey === 1 && - <> - Browsing {currentQueue.name + ' '} <Icon status="info"><HelpIcon onClick={() => setShowHelpModal(true)}/></Icon> - - + {activeTabKey !== 0 && +
+ Browsing {currentQueue.name + ' '} <Icon status="info"><HelpIcon onClick={() => setShowHelpModal(true)}/></Icon> + +
} {activeTabKey === 2 && <> Viewing Message on {currentQueue.name} - + 0} + currentIndex={currentIndex} + totalCount={totalMessages} + /> }