diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.spec.ts new file mode 100644 index 00000000000..dab5a2c98f4 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; + +import { InstanceEvent } from '@/views/journal/InstanceEvent'; + +import { deduplicateInstanceEvents } from './deduplicate-events'; + +const createEvent = (instance: string, version: number) => + new InstanceEvent({ + instance, + version, + type: 'REGISTERED', + timestamp: '2024-01-01T10:00:00Z', + registration: { name: instance }, + }); + +describe('deduplicateInstanceEvents', () => { + it('removes events with identical instance and version', () => { + const events = [ + createEvent('instance-1', 1), + createEvent('instance-1', 1), + createEvent('instance-2', 3), + createEvent('instance-2', 3), + createEvent('instance-3', 2), + ]; + + const result = deduplicateInstanceEvents(events); + + expect(result).toHaveLength(3); + expect(result.map((event) => event.key)).toEqual([ + 'instance-1-1', + 'instance-2-3', + 'instance-3-2', + ]); + }); + + it('preserves the order of the first occurrences', () => { + const events = [ + createEvent('instance-1', 2), + createEvent('instance-2', 1), + createEvent('instance-1', 2), + createEvent('instance-3', 4), + ]; + + const result = deduplicateInstanceEvents(events); + + expect(result.map((event) => event.key)).toEqual([ + 'instance-1-2', + 'instance-2-1', + 'instance-3-4', + ]); + }); +}); diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.ts b/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.ts new file mode 100644 index 00000000000..ac23e4d5d1c --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.ts @@ -0,0 +1,12 @@ +import { InstanceEvent } from '@/views/journal/InstanceEvent'; + +export function deduplicateInstanceEvents(events: InstanceEvent[]) { + const seen = new Set(); + return events.filter((event) => { + if (seen.has(event.key)) { + return false; + } + seen.add(event.key); + return true; + }); +} diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/journal/index.vue b/spring-boot-admin-server-ui/src/main/frontend/views/journal/index.vue index 19bd97580f8..5d6510ef9ea 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/journal/index.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/journal/index.vue @@ -46,8 +46,9 @@ import { useDateTimeFormatter } from '@/composables/useDateTimeFormatter'; import subscribing from '@/mixins/subscribing'; import Instance from '@/services/instance'; import { compareBy } from '@/utils/collections'; +import { InstanceEvent } from '@/views/journal/InstanceEvent'; +import { deduplicateInstanceEvents } from '@/views/journal/deduplicate-events'; import { - InstanceEvent, InstanceEventType, } from '@/views/journal/InstanceEvent'; import JournalTable from '@/views/journal/JournalTable.vue'; @@ -67,6 +68,7 @@ export default { data: () => ({ Event, events: [], + seenEventKeys: new Set(), listOffset: 0, showPayload: {}, pageSize: 25, @@ -104,7 +106,9 @@ export default { .reverse() .map((e) => new InstanceEvent(e)); - this.events = Object.freeze(events); + const deduplicated = deduplicateInstanceEvents(events); + this.seenEventKeys = new Set(deduplicated.map((event) => event.key)); + this.events = Object.freeze(deduplicated); this.error = null; } catch (error) { console.warn('Fetching events failed:', error); @@ -119,10 +123,12 @@ export default { return Instance.getEventStream().subscribe({ next: (message) => { this.error = null; - this.events = Object.freeze([ - new InstanceEvent(message.data), - ...this.events, - ]); + const incomingEvent = new InstanceEvent(message.data); + if (this.seenEventKeys.has(incomingEvent.key)) { + return; + } + this.seenEventKeys.add(incomingEvent.key); + this.events = Object.freeze([incomingEvent, ...this.events]); this.listOffset += 1; }, error: (error) => {