diff --git a/frontend/src/components/misc/kowl-json-view.test.tsx b/frontend/src/components/misc/kowl-json-view.test.tsx
index 9460626f23..c49e6a999f 100644
--- a/frontend/src/components/misc/kowl-json-view.test.tsx
+++ b/frontend/src/components/misc/kowl-json-view.test.tsx
@@ -161,4 +161,24 @@ describe('KowlJsonView', () => {
expect(editorLayoutSpy).toHaveBeenLastCalledWith({ width: 800, height: 480 });
});
+
+ test('escapeLatin1 re-escapes Latin-1 code points so copy-paste recovers the original byte', async () => {
+ render();
+
+ await waitFor(() => {
+ const value = editorPropsSpy.mock.lastCall?.[0].value as string;
+ expect(value).toContain('\\u00db');
+ expect(value).not.toContain('Û');
+ });
+ });
+
+ test('without escapeLatin1, Latin-1 glyphs pass through to the editor unchanged', async () => {
+ render();
+
+ await waitFor(() => {
+ const value = editorPropsSpy.mock.lastCall?.[0].value as string;
+ expect(value).toContain('Û');
+ expect(value).not.toContain('\\u00db');
+ });
+ });
});
diff --git a/frontend/src/components/misc/kowl-json-view.tsx b/frontend/src/components/misc/kowl-json-view.tsx
index c6df2f1ff1..8abda24b35 100644
--- a/frontend/src/components/misc/kowl-json-view.tsx
+++ b/frontend/src/components/misc/kowl-json-view.tsx
@@ -49,15 +49,26 @@ const READ_ONLY_EDITOR_OPTIONS = {
scrollBeyondLastLine: false,
} as const;
-export const KowlJsonView = (props: { srcObj: object | string | null | undefined; style?: CSSProperties }) => {
+export const KowlJsonView = (props: {
+ srcObj: object | string | null | undefined;
+ style?: CSSProperties;
+ // escapeLatin1 re-escapes code points in 0x80-0xFF as \u00XX so bytes from
+ // Avro's JSON bytes encoding survive copy-paste out of the viewer. Without
+ // this, JSON.stringify emits the code point as a literal glyph and the
+ // clipboard receives the UTF-8 encoding, not the original byte.
+ escapeLatin1?: boolean;
+}) => {
const containerRef = useRef(null);
const editorRef = useRef(null);
const frameRef = useRef(null);
const lastSizeRef = useRef({ width: 0, height: 0 });
- const str = useMemo(
- () => (typeof props.srcObj === 'string' ? props.srcObj : JSON.stringify(props.srcObj, undefined, 4)),
- [props.srcObj]
- );
+ const str = useMemo(() => {
+ const raw = typeof props.srcObj === 'string' ? props.srcObj : JSON.stringify(props.srcObj, undefined, 4);
+ if (!props.escapeLatin1) {
+ return raw;
+ }
+ return raw.replace(/[\u0080-\u00ff]/g, (c) => `\\u00${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
+ }, [props.srcObj, props.escapeLatin1]);
const scheduleLayout = useCallback(() => {
if (frameRef.current !== null) {
diff --git a/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx b/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx
index 78418637b1..a270f0e48e 100644
--- a/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx
+++ b/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx
@@ -182,7 +182,10 @@ export const PayloadComponent = (p: { payload: Payload; loadLargeMessage: () =>
);
}
if (renderData.type === 'json') {
- return ;
+ // Avro JSON encodes bytes fields as \u00XX escape sequences. Re-escape
+ // Latin-1 code points in the viewer so copy-paste yields the original
+ // bytes rather than their UTF-8 encoding.
+ return ;
}
return Error in RenderExpandedMessage: {renderData.content};
};