This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new f597fecc2 fix(audit): let the timeline target be activated from the
keyboard (#4580)
f597fecc2 is described below
commit f597fecc2442d061ab02f2e0fd69bf6bcd342559
Author: Zhao Jianing <[email protected]>
AuthorDate: Mon Sep 21 21:03:15 2026 +0800
fix(audit): let the timeline target be activated from the keyboard (#4580)
The audit timeline's target cell in `web/src/pages/ops/audit.tsx` was
already marked up as interactive — `role="button"`, `tabIndex={0}` and an
`aria-label` were all there — so a keyboard user could Tab to it and assistive
tech announced it as a button, but Enter and Space did nothing: `onClick` was
the only handler. That cell is the sole entry point to the resource operation
timeline, so the timeline was unreachable without a pointer (WCAG 2.1.1
Keyboard).
The `setTimelineResource` call is lifted into an `openTimeline` closure
shared by `onClick` and a new `onKeyDown` that fires on Enter or Space and
calls `preventDefault()`, so Space does not also scroll the page. `alerts.tsx`
and `MainLayout.tsx` already paired the two handlers, which leaves no
`role="button"` node in the web app that cannot be activated from the keyboard.
The added test drives `fireEvent.keyDown` and asserts the resulting
`listAuditRecords` arguments, so extracting t [...]
---
web/src/pages/ops/__tests__/AuditPage.test.tsx | 21 ++++++++++++++++++++-
web/src/pages/ops/audit.tsx | 20 +++++++++++++-------
2 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/web/src/pages/ops/__tests__/AuditPage.test.tsx
b/web/src/pages/ops/__tests__/AuditPage.test.tsx
index ee0bea6fa..4aa7c1d85 100644
--- a/web/src/pages/ops/__tests__/AuditPage.test.tsx
+++ b/web/src/pages/ops/__tests__/AuditPage.test.tsx
@@ -16,7 +16,7 @@
*/
import { App } from 'antd';
-import { act, render, screen, waitFor } from '@testing-library/react';
+import { act, fireEvent, render, screen, waitFor } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import type React from 'react';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from
'vitest';
@@ -204,6 +204,25 @@ describe('Audit page', () => {
);
});
+ it('opens a resource timeline from the keyboard on a non-empty audit
target', async () => {
+ renderWithProviders(<AuditPage />);
+
+ const target = await screen.findByRole('button', { name: '查看 topic-a
操作时间线' });
+ target.focus();
+ fireEvent.keyDown(target, { key: 'Enter' });
+
+ expect(await screen.findByText('资源操作时间线')).toBeInTheDocument();
+ await waitFor(() =>
+ expect(opsService.listAuditRecords).toHaveBeenCalledWith({
+ page: 1,
+ pageSize: 20,
+ resourceType: 'TOPIC',
+ target: 'topic-a',
+ clusterId: 'prod-cn',
+ }),
+ );
+ });
+
it('does not offer a timeline action for an empty audit target', async () =>
{
vi.mocked(opsService.listAuditRecords).mockResolvedValueOnce({
items: [
diff --git a/web/src/pages/ops/audit.tsx b/web/src/pages/ops/audit.tsx
index 930933f7d..d33fd79ca 100644
--- a/web/src/pages/ops/audit.tsx
+++ b/web/src/pages/ops/audit.tsx
@@ -362,6 +362,12 @@ const AuditPage: React.FC = () => {
render: (_: string, record) => {
const target = record.target;
if (!target?.trim()) return <Text type="secondary">-</Text>;
+ const openTimeline = () =>
+ setTimelineResource({
+ resourceType: record.resourceType,
+ target,
+ clusterId: record.clusterId || null,
+ });
return (
<Tooltip title={describeAuditRecord(record, t)}>
<Text
@@ -370,13 +376,13 @@ const AuditPage: React.FC = () => {
tabIndex={0}
aria-label={t('audit.timelineView', { target })}
style={{ fontSize: 14, cursor: 'pointer' }}
- onClick={() =>
- setTimelineResource({
- resourceType: record.resourceType,
- target,
- clusterId: record.clusterId || null,
- })
- }
+ onClick={openTimeline}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ openTimeline();
+ }
+ }}
>
{target}
</Text>