Repository: Graylog2/graylog2-server (language: java)

Issue:
Dashboards perform a refresh of all tab/page contents when moving/resizing a widget
<!--- Provide a general summary of the issue in the Title above -->
Dashboards perform a refresh of all tab/page contents when moving/resizing a widget. Depending on the time range and search query, and query result, this can be very expensive.

## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
Widgets are not refresh when moved or resized.

## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
Widgets are refreshed when moved or resized.

## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->

## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1. Create a dashboard and add at least one widget. It is helpful to add a message table widget so you can visibly see the contents change
2. Move or resize a widget
3. Observe that the widget's contents changes

## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->

* Graylog Version: 6.1.8
* Java Version: Bundled
* OpenSearch Version: 2.15.0
* MongoDB Version: 7.0.18
* Operating System: Ubuntu Server 22.04 LTS
* Browser version: Google Chrome Version 134.0.6998.166 (Official Build) (arm64)

Please let me know if there are any questions. Thanks!


Discussion / hints:

Fix patch:
diff --git a/changelog/unreleased/issue-22153.toml b/changelog/unreleased/issue-22153.toml
new file mode 100644
index 000000000000..5fc57d08b5e6
--- /dev/null
+++ b/changelog/unreleased/issue-22153.toml
@@ -0,0 +1,5 @@
+type = "f"
+message = "Fix comparison of messages widget to avoid unnecessary refresh."
+
+pulls = ["22161"]
+issues = ["22153"]
diff --git a/graylog2-web-interface/src/views/logic/aggregationbuilder/AggregationWidget.ts b/graylog2-web-interface/src/views/logic/aggregationbuilder/AggregationWidget.ts
index 24aa2ba07cdd..cfa2c8214c52 100644
--- a/graylog2-web-interface/src/views/logic/aggregationbuilder/AggregationWidget.ts
+++ b/graylog2-web-interface/src/views/logic/aggregationbuilder/AggregationWidget.ts
@@ -24,7 +24,7 @@ import type { QueryString } from 'views/logic/queries/types';
 
 import AggregationWidgetConfig from './AggregationWidgetConfig';
 
-import Widget from '../widgets/Widget';
+import Widget, { widgetAttributesForComparison } from '../widgets/Widget';
 
 export default class AggregationWidget extends Widget {
   constructor(
@@ -73,9 +73,7 @@ export default class AggregationWidget extends Widget {
 
   equals(other: any) {
     if (other instanceof AggregationWidget) {
-      return ['id', 'config', 'filter', 'timerange', 'query', 'streams', 'stream_categories', 'filters'].every((key) =>
-        isDeepEqual(this[key], other[key]),
-      );
+      return widgetAttributesForComparison.every((key) => isDeepEqual(this[key], other[key]));
     }
 
     return false;
@@ -83,9 +81,7 @@ export default class AggregationWidget extends Widget {
 
   equalsForSearch(other: any) {
     if (other instanceof AggregationWidget) {
-      return ['id', 'config', 'filter', 'timerange', 'query', 'streams', 'stream_categories', 'filters'].every((key) =>
-        isEqualForSearch(this[key], other[key]),
-      );
+      return widgetAttributesForComparison.every((key) => isEqualForSearch(this[key], other[key]));
     }
 
     return false;
diff --git a/graylog2-web-interface/src/views/logic/widgets/MessagesWidget.ts b/graylog2-web-interface/src/views/logic/widgets/MessagesWidget.ts
index 5d6280b35aa5..62980beaf8b1 100644
--- a/graylog2-web-interface/src/views/logic/widgets/MessagesWidget.ts
+++ b/graylog2-web-interface/src/views/logic/widgets/MessagesWidget.ts
@@ -21,7 +21,7 @@ import isEqualForSearch from 'views/stores/isEqualForSearch';
 import type { FiltersType } from 'views/types';
 import type { QueryString } from 'views/logic/queries/types';
 
-import Widget from './Widget';
+import Widget, { widgetAttributesForComparison } from './Widget';
 import MessagesWidgetConfig from './MessagesWidgetConfig';
 import type { WidgetState } from './Widget';
 
@@ -67,9 +67,7 @@ export default class MessagesWidget extends Widget {
 
   equals(other: any) {
     if (other instanceof MessagesWidget) {
-      return ['id', 'config', 'filter', 'timerange', 'query', 'streams', 'stream_categories', 'filters'].every((key) =>
-        isDeepEqual(this._value[key], other[key]),
-      );
+      return widgetAttributesForComparison.every((key) => isDeepEqual(this[key], other[key]));
     }
 
     return false;
@@ -81,9 +79,7 @@ export default class MessagesWidget extends Widget {
 
   equalsForSearch(other: any) {
     if (other instanceof MessagesWidget) {
-      return ['id', 'config', 'filter', 'timerange', 'query', 'streams', 'stream_categories', 'filters'].every((key) =>
-        isEqualForSearch(this._value[key], other[key]),
-      );
+      return widgetAttributesForComparison.every((key) => isEqualForSearch(this[key], other[key]));
     }
 
     return false;
diff --git a/graylog2-web-interface/src/views/logic/widgets/Widget.ts b/graylog2-web-interface/src/views/logic/widgets/Widget.ts
index 6621ad165d9f..5f21885ba744 100644
--- a/graylog2-web-interface/src/views/logic/widgets/Widget.ts
+++ b/graylog2-web-interface/src/views/logic/widgets/Widget.ts
@@ -36,9 +36,9 @@ export type WidgetState = {
   stream_categories: Array<string>;
 };
 
-type DeserializesWidgets = {
-  fromJSON: (value) => Widget;
-};
+interface DeserializesWidgets {
+  fromJSON: (value: any) => Widget;
+}
 
 const isNullish = (o: any) => o === null || o === undefined;
 
@@ -266,6 +266,17 @@ class Builder {
   }
 }
 
+export const widgetAttributesForComparison: Array<keyof Widget> = [
+  'id',
+  'config',
+  'filter',
+  'timerange',
+  'query',
+  'streams',
+  'streamCategories',
+  'filters',
+];
+
 Widget.Builder = Builder;
 
 const SingletonWidget = singleton('views.logic.widgets.Widget', () => Widget);
diff --git a/graylog2-web-interface/src/views/logic/widgets/events/EventsWidget.ts b/graylog2-web-interface/src/views/logic/widgets/events/EventsWidget.ts
index 53bb3d98a489..df46b1bbf390 100644
--- a/graylog2-web-interface/src/views/logic/widgets/events/EventsWidget.ts
+++ b/graylog2-web-interface/src/views/logic/widgets/events/EventsWidget.ts
@@ -16,7 +16,7 @@
  */
 import { Map } from 'immutable';
 
-import Widget from 'views/logic/widgets/Widget';
+import Widget, { widgetAttributesForComparison } from 'views/logic/widgets/Widget';
 import type { WidgetState } from 'views/logic/widgets/Widget';
 import isDeepEqual from 'stores/isDeepEqual';
 import isEqualForSearch from 'views/stores/isEqualForSearch';
@@ -61,9 +61,7 @@ export default class EventsWidget extends Widget {
 
   equals(other: any) {
     if (other instanceof EventsWidget) {
-      return ['id', 'config', 'filter', 'timerange', 'query', 'streams', 'stream_categories', 'filters'].every((key) =>
-        isDeepEqual(this._value[key], other[key]),
-      );
+      return widgetAttributesForComparison.every((key) => isDeepEqual(this._value[key], other[key]));
     }
 
     return false;
@@ -71,9 +69,7 @@ export default class EventsWidget extends Widget {
 
   equalsForSearch(other: any) {
     if (other instanceof EventsWidget) {
-      return ['id', 'config', 'filter', 'timerange', 'query', 'streams', 'stream_categories', 'filters'].every((key) =>
-        isEqualForSearch(this._value[key], other[key]),
-      );
+      return widgetAttributesForComparison.every((key) => isEqualForSearch(this._value[key], other[key]));
     }
 
     return false;
diff --git a/graylog2-web-interface/src/views/stores/isEqualForSearch.ts b/graylog2-web-interface/src/views/stores/isEqualForSearch.ts
index 2f6f0895567a..0dfde61d0a02 100644
--- a/graylog2-web-interface/src/views/stores/isEqualForSearch.ts
+++ b/graylog2-web-interface/src/views/stores/isEqualForSearch.ts
@@ -17,12 +17,12 @@
 import isEqualWith from 'lodash/isEqualWith';
 import isFunction from 'lodash/isFunction';
 
-const hasFn = (obj, fn) => obj && obj[fn] && isFunction(obj[fn]);
-const hasEquals = (obj) => hasFn(obj, 'equals');
-const hasEqualsForSearch = (obj) => hasFn(obj, 'equalsForSearch');
-const isImmutable = (obj) => hasFn(obj, 'toJS');
+const hasFn = (obj: any, fn: string) => obj && obj[fn] && isFunction(obj[fn]);
+const hasEquals = (obj: any) => hasFn(obj, 'equals');
+const hasEqualsForSearch = (obj: any) => hasFn(obj, 'equalsForSearch');
+const isImmutable = (obj: any) => hasFn(obj, 'toJS');
 
-const _isEqual = (first, second) => {
+const _isEqual = (first: any, second: any) => {
   if (hasEqualsForSearch(first)) {
     return first.equalsForSearch(second);
   }


Test patch: