Skip to content

Commit 99084a3

Browse files
jvorcakclaude
andcommitted
fix(messages): default the topic message list to newest-first ordering
The default topic message view (non-continuous pagination, "Newest" / last-50) rendered rows in the backend's stream order. The listMessages stream isn't globally timestamp-sorted across partitions, so messages were not shown newest-first as users expect. `ListMessagesRequest` has no sort-order field, so ordering has to be imposed client-side. Rather than a bespoke sort of the row list, set a sensible default for the existing `sort` URL param so the table's own sorted row model does the work: - DEFAULT_SORTING is now [timestamp desc, offset desc] (offset isn't global across partitions, so timestamp is the primary key and offset is a stable tiebreak). - getSorting() treats an empty/unset sort as "use the default", so it also applies for returning users whose persisted settings still hold the old empty sort. - The Timestamp/Offset column headers now show the sort direction, and the sort is shareable/persisted via the URL. Continuous-pagination mode disables header sorting, so the explicit newest-first sort is kept for its "Newest" fetch (now with the same offset tiebreak for determinism). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7ee42a0 commit 99084a3

2 files changed

Lines changed: 24 additions & 7 deletions

File tree

frontend/src/components/pages/topics/Tab.Messages/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ import { isServerless } from '../../../../config';
8989
import { useQueryStateWithCallback } from '../../../../hooks/use-query-state-with-callback';
9090
import { PayloadEncoding } from '../../../../protogen/redpanda/api/console/v1alpha1/common_pb';
9191
import { appGlobal } from '../../../../state/app-global';
92-
import { useTopicSettingsStore } from '../../../../stores/topic-settings-store';
92+
import { DEFAULT_SORTING, useTopicSettingsStore } from '../../../../stores/topic-settings-store';
9393
import { IsDev } from '../../../../utils/env';
9494
import { sanitizeString, wrapFilterFragment } from '../../../../utils/filter-helper';
9595
import { trimSlidingWindow } from '../../../../utils/message-table-helpers';
@@ -472,7 +472,7 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
472472
getDefaultValue: () => getSorting(props.topic.topicName),
473473
},
474474
'sort',
475-
sortingParser.withDefault([])
475+
sortingParser.withDefault(DEFAULT_SORTING)
476476
);
477477

478478
// Continuous pagination toggle state
@@ -572,11 +572,16 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
572572
})
573573
: messages;
574574

575-
// For continuous pagination, just use the filtered messages directly
576-
// We don't use placeholders as they cause page content to shift
575+
// Continuous pagination disables header sorting, so the table's sorted row
576+
// model can't impose newest-first there. Force it for the "Newest" fetch only
577+
// (timestamp desc, then offset desc as a cross-partition tiebreak). Other
578+
// origins keep the streamed order.
579+
// In the normal paginated view the table sorts client-side via the default
580+
// `sorting` state (DEFAULT_SORTING = timestamp desc, offset desc), so we pass
581+
// the rows through untouched here.
577582
const filteredMessages =
578583
continuousPaginationEnabled && startOffset === PartitionOffsetOrigin.EndMinusResults
579-
? [...baseFilteredMessages].sort((a, b) => b.timestamp - a.timestamp)
584+
? [...baseFilteredMessages].sort((a, b) => b.timestamp - a.timestamp || b.offset - a.offset)
580585
: baseFilteredMessages;
581586

582587
// Convert @computed activePreviewTags to useMemo

frontend/src/stores/topic-settings-store.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,15 @@ export type TopicSettingsStore = {
111111
clearAllSettings: () => void;
112112
};
113113

114-
const DEFAULT_SORTING: SortingState = [];
114+
// Default display order for the message list: newest-first by timestamp, with
115+
// offset descending as a cross-partition tiebreak (offset isn't global across
116+
// partitions, so timestamp is the primary key). The backend's listMessages
117+
// stream isn't globally timestamp-sorted across partitions, so this default
118+
// drives the table's client-side sort to give users a sensible newest-first view.
119+
export const DEFAULT_SORTING: SortingState = [
120+
{ id: 'timestamp', desc: true },
121+
{ id: 'offset', desc: true },
122+
];
115123

116124
const DEFAULT_SEARCH_PARAMS: TopicSearchParams = {
117125
offsetOrigin: -1,
@@ -214,7 +222,11 @@ export const useTopicSettingsStore = create<TopicSettingsStore>()(
214222

215223
getSorting: (topicName: string) => {
216224
const topic = get().perTopicSettings.find((t) => t.topicName === topicName);
217-
return topic?.searchParams.sorting ?? DEFAULT_SORTING;
225+
const sorting = topic?.searchParams.sorting;
226+
// Treat an empty/unset sort as "use the newest-first default" so the
227+
// default applies on every fresh load, including returning users whose
228+
// persisted settings still hold the old empty sort.
229+
return sorting && sorting.length > 0 ? sorting : DEFAULT_SORTING;
218230
},
219231

220232
setSearchParams: (topicName: string, searchParams: Partial<TopicSearchParams>) => {

0 commit comments

Comments
 (0)