From e72ebf4d7f759e9d93376ca27003e4a9ef65dead Mon Sep 17 00:00:00 2001 From: teja2 Date: Mon, 24 Nov 2025 16:31:09 -0500 Subject: [PATCH 1/4] fix-4858 dedeuplicate --- .../views/journal/deduplicate-events.spec.ts | 67 +++++++++++++++++++ .../views/journal/deduplicate-events.ts | 27 ++++++++ .../src/main/frontend/views/journal/index.vue | 16 +++-- 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.spec.ts create mode 100644 spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.ts 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..3188953a5f2 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.spec.ts @@ -0,0 +1,67 @@ +/*! + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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..57ff32d67f0 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/journal/deduplicate-events.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 dd1eaa6af71..b5be1136380 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 @@ -47,6 +47,7 @@ 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 JournalTable from '@/views/journal/JournalTable.vue'; export default { @@ -64,6 +65,7 @@ export default { data: () => ({ Event, events: [], + seenEventKeys: new Set(), listOffset: 0, showPayload: {}, pageSize: 25, @@ -101,7 +103,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); @@ -116,10 +120,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) => { From b6f7bf2146a04cadc725e67ea2bb4678004130ee Mon Sep 17 00:00:00 2001 From: teja2 Date: Mon, 24 Nov 2025 16:33:01 -0500 Subject: [PATCH 2/4] removing comments --- .../views/journal/deduplicate-events.spec.ts | 15 --------------- .../frontend/views/journal/deduplicate-events.ts | 15 --------------- 2 files changed, 30 deletions(-) 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 index 3188953a5f2..dab5a2c98f4 100644 --- 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 @@ -1,18 +1,3 @@ -/*! - * Copyright 2014-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ import { describe, expect, it } from 'vitest'; import { InstanceEvent } from '@/views/journal/InstanceEvent'; 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 index 57ff32d67f0..ac23e4d5d1c 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * Copyright 2014-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ import { InstanceEvent } from '@/views/journal/InstanceEvent'; export function deduplicateInstanceEvents(events: InstanceEvent[]) { From d86b76a18d8025492efcefda5fb76d5a0b753a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Ko=CC=88ninger?= Date: Tue, 10 Feb 2026 11:38:20 +0100 Subject: [PATCH 3/4] fix: enhance deduplication logic for instance events to include type and timestamp --- .../frontend/views/journal/InstanceEvent.ts | 2 +- .../views/journal/deduplicate-events.spec.ts | 37 ++++++++++++------- .../views/journal/deduplicate-events.ts | 18 +++++++++ .../src/main/frontend/views/journal/index.vue | 1 + 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/journal/InstanceEvent.ts b/spring-boot-admin-server-ui/src/main/frontend/views/journal/InstanceEvent.ts index a7c43607f25..d1960f53c7e 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/journal/InstanceEvent.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/views/journal/InstanceEvent.ts @@ -38,7 +38,7 @@ export class InstanceEvent implements IInstanceEvent { } get key() { - return `${this.instance}-${this.version}`; + return `${this.instance}-${this.version}-${this.type}-${this.timestamp.getTime()}`; } } 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 index dab5a2c98f4..e83ab07fd48 100644 --- 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 @@ -1,35 +1,46 @@ import { describe, expect, it } from 'vitest'; -import { InstanceEvent } from '@/views/journal/InstanceEvent'; - import { deduplicateInstanceEvents } from './deduplicate-events'; -const createEvent = (instance: string, version: number) => +import { + InstanceEvent, + InstanceEventType, +} from '@/views/journal/InstanceEvent'; + +const createEvent = ( + instance: string, + version: number, + type = InstanceEventType.REGISTERED, +) => new InstanceEvent({ instance, version, - type: 'REGISTERED', + type, timestamp: '2024-01-01T10:00:00Z', registration: { name: instance }, }); describe('deduplicateInstanceEvents', () => { - it('removes events with identical instance and version', () => { + it('removes events with identical instance, type and version', () => { const events = [ createEvent('instance-1', 1), - createEvent('instance-1', 1), + createEvent('instance-1', 1, InstanceEventType.DEREGISTERED), createEvent('instance-2', 3), createEvent('instance-2', 3), createEvent('instance-3', 2), + createEvent('instance-3', 2, InstanceEventType.INFO_CHANGED), + createEvent('instance-3', 2, InstanceEventType.INFO_CHANGED), ]; const result = deduplicateInstanceEvents(events); - expect(result).toHaveLength(3); + expect(result).toHaveLength(5); expect(result.map((event) => event.key)).toEqual([ - 'instance-1-1', - 'instance-2-3', - 'instance-3-2', + 'instance-1-1-REGISTERED-1704103200000', + 'instance-1-1-DEREGISTERED-1704103200000', + 'instance-2-3-REGISTERED-1704103200000', + 'instance-3-2-REGISTERED-1704103200000', + 'instance-3-2-INFO_CHANGED-1704103200000', ]); }); @@ -44,9 +55,9 @@ describe('deduplicateInstanceEvents', () => { const result = deduplicateInstanceEvents(events); expect(result.map((event) => event.key)).toEqual([ - 'instance-1-2', - 'instance-2-1', - 'instance-3-4', + 'instance-1-2-REGISTERED-1704103200000', + 'instance-2-1-REGISTERED-1704103200000', + 'instance-3-4-REGISTERED-1704103200000', ]); }); }); 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 index ac23e4d5d1c..71b37b505cd 100644 --- 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 @@ -1,5 +1,23 @@ import { InstanceEvent } from '@/views/journal/InstanceEvent'; +/** + * Removes duplicate instance events from an array based on their key property. + * + * This function filters an array of InstanceEvent objects, keeping only the first occurrence + * of each unique event key. Subsequent events with the same key are filtered out. + * + * @param events - Array of InstanceEvent objects to deduplicate + * @returns A new array containing only unique events (by key), preserving the order of first occurrence + * + * @example + * const events = [ + * { key: 'event1', ... }, + * { key: 'event2', ... }, + * { key: 'event1', ... } // duplicate + * ]; + * const unique = deduplicateInstanceEvents(events); + * // Returns first two events only + */ export function deduplicateInstanceEvents(events: InstanceEvent[]) { const seen = new Set(); return events.filter((event) => { 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 5d6510ef9ea..f8b3abba844 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 @@ -109,6 +109,7 @@ export default { const deduplicated = deduplicateInstanceEvents(events); this.seenEventKeys = new Set(deduplicated.map((event) => event.key)); this.events = Object.freeze(deduplicated); + this.listOffset = events.length - deduplicated.length; this.error = null; } catch (error) { console.warn('Fetching events failed:', error); From 6215c2d37d0633d615f23244171959d32dcc05a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Ko=CC=88ninger?= Date: Tue, 10 Feb 2026 12:00:54 +0100 Subject: [PATCH 4/4] fix: reorder imports for deduplication logic in index.vue --- .../src/main/frontend/views/journal/index.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 f8b3abba844..51af1b241c6 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 @@ -47,11 +47,9 @@ 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 { - InstanceEventType, -} from '@/views/journal/InstanceEvent'; +import { InstanceEventType } from '@/views/journal/InstanceEvent'; import JournalTable from '@/views/journal/JournalTable.vue'; +import { deduplicateInstanceEvents } from '@/views/journal/deduplicate-events'; export default { components: { JournalTable, SbaAlert },