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

Issue:
Graylog Web Interface does not check if its version matches the server's
<!--- Provide a general summary of the issue in the Title above -->

## 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 -->

The Graylog Web UI periodically checks if its version matches the server's. If there is a version mismatch, it should cease operation.

This is also necessary due to our asynchronous loading of components. The asset names for parts of the web ui are (necessarily) hardcoded and differ (intentionally, for cache busting) between versions. 

## 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 -->

If a web ui is still running while the server is updated in the background, it will try to load old assets and throw an error. 

## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->

It should show a helpful message instead, telling the user to do a refresh and if the problem persists, notify the admin.

## 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.
2.
3.
4.

## 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:
* Elasticsearch Version:
* MongoDB Version:
* Operating System:
* Browser version:



Discussion / hints:
Refs #5587.
This issue has been making any version of our graylog3 deploy on k8s deployed via helm unusable anytime we redeploy. The issue can't be resolved via browser cache either :(

Only thing we've found to mitigate is to erase all the volumes, uninstall and reinstall.
This is still an issue on 5.1.0

Fix patch:
diff --git a/changelog/unreleased/issue-22152.toml b/changelog/unreleased/issue-22152.toml
new file mode 100644
index 000000000000..de724d3014bf
--- /dev/null
+++ b/changelog/unreleased/issue-22152.toml
@@ -0,0 +1,5 @@
+type = "f"
+message = "Suggest a reload when Graylog server version changed."
+
+pulls = ["22236"]
+issues = ["22152"]
diff --git a/graylog2-web-interface/src/pages/LoggedInPage.tsx b/graylog2-web-interface/src/pages/LoggedInPage.tsx
index 6b55bee83e7e..cfe6a627b57e 100644
--- a/graylog2-web-interface/src/pages/LoggedInPage.tsx
+++ b/graylog2-web-interface/src/pages/LoggedInPage.tsx
@@ -23,6 +23,7 @@ import DefaultQueryClientProvider from 'contexts/DefaultQueryClientProvider';
 import TelemetryProvider from 'logic/telemetry/TelemetryProvider';
 import NodesProvider from 'contexts/NodesProvider';
 import InputsProvider from 'contexts/InputsProvider';
+import SuggestReloadIfVersionChanged from 'routing/SuggestReloadIfVersionChanged';
 
 const LoggedInPage = () => (
   <DefaultQueryClientProvider>
@@ -32,6 +33,7 @@ const LoggedInPage = () => (
           <NodesProvider>
             <InputsProvider>
               <AppRouter />
+              <SuggestReloadIfVersionChanged />
             </InputsProvider>
           </NodesProvider>
         </StreamsProvider>
diff --git a/graylog2-web-interface/src/routing/SuggestReloadIfVersionChanged.tsx b/graylog2-web-interface/src/routing/SuggestReloadIfVersionChanged.tsx
new file mode 100644
index 000000000000..7ed96ac8be5a
--- /dev/null
+++ b/graylog2-web-interface/src/routing/SuggestReloadIfVersionChanged.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 Graylog, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ */
+import * as React from 'react';
+import { Dialog } from '@mantine/core';
+import { useEffect, useState } from 'react';
+import styled from 'styled-components';
+
+import Button from 'components/bootstrap/Button';
+import useServerVersion from 'routing/useServerVersion';
+
+type Props = {
+  reload?: () => void;
+};
+const ButtonContainer = styled.div`
+  display: flex;
+  justify-content: flex-end;
+`;
+const defaultReload = () => window.location.reload();
+const SuggestReloadIfVersionChanged = ({ reload = defaultReload }: Props) => {
+  const [versionChanged, setVersionChanged] = useState<boolean>(false);
+  const [dismissed, setDismissed] = useState<boolean>(false);
+  const [version, setVersion] = useState<string>();
+
+  const newVersion = useServerVersion();
+
+  useEffect(() => {
+    if (!newVersion) {
+      return;
+    }
+    if (!version) {
+      setVersion(newVersion);
+    }
+
+    if (version && newVersion !== version) {
+      setVersionChanged(true);
+    }
+  }, [newVersion, version]);
+
+  return versionChanged && !dismissed ? (
+    <Dialog
+      opened
+      withCloseButton
+      onClose={() => setDismissed(true)}
+      size="lg"
+      radius="md"
+      position={{ top: 55, right: 20 }}>
+      <p>
+        <strong>Graylog Version Changed</strong>
+      </p>
+      <p>
+        Your Graylog version has changed from <strong>{version}</strong> to <strong>{newVersion}</strong>.
+      </p>
+      <p>Please save your work and reload the page to avoid running into errors.</p>
+      <ButtonContainer>
+        <Button onClick={reload}>Reload now</Button>
+      </ButtonContainer>
+    </Dialog>
+  ) : null;
+};
+
+export default SuggestReloadIfVersionChanged;
diff --git a/graylog2-web-interface/src/routing/useServerVersion.ts b/graylog2-web-interface/src/routing/useServerVersion.ts
new file mode 100644
index 000000000000..273d989d0d9a
--- /dev/null
+++ b/graylog2-web-interface/src/routing/useServerVersion.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 Graylog, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ */
+import { useStore } from 'stores/connect';
+import { ServerAvailabilityStore } from 'stores/sessions/ServerAvailabilityStore';
+
+const useServerVersion = () => useStore(ServerAvailabilityStore, (state) => state.version);
+
+export default useServerVersion;
diff --git a/graylog2-web-interface/src/stores/sessions/ServerAvailabilityStore.ts b/graylog2-web-interface/src/stores/sessions/ServerAvailabilityStore.ts
index 50d2d986eb8a..3b405e27ccc1 100644
--- a/graylog2-web-interface/src/stores/sessions/ServerAvailabilityStore.ts
+++ b/graylog2-web-interface/src/stores/sessions/ServerAvailabilityStore.ts
@@ -38,44 +38,61 @@ export type ServerError = {
 };
 export type ServerAvailabilityStoreState = {
   server: { up: true } | { up: false; error: ServerError };
+  version: string;
 };
 
 const ping = (url: string) =>
-  window.fetch(url, {
-    method: 'GET',
-    headers: {
-      Accept: 'application/json',
-      'X-Graylog-No-Session-Extension': 'true',
-    },
-  });
+  window
+    .fetch(url, {
+      method: 'GET',
+      headers: {
+        Accept: 'application/json',
+        'X-Graylog-No-Session-Extension': 'true',
+      },
+    })
+    .then<StatusResponse>((response) => response.json());
 
+type StatusResponse = {
+  version: string;
+};
 export const ServerAvailabilityStore = singletonStore('core.ServerAvailability', () =>
   Reflux.createStore<ServerAvailabilityStoreState>({
     listenables: [ServerAvailabilityActions],
     server: { up: true },
+    version: undefined,
     init() {
       this.ping();
     },
     getInitialState() {
-      return { server: this.server };
+      return { server: this.server, version: this.version };
     },
     ping() {
       return ping(URLUtils.qualifyUrl(ApiRoutes.ping().url)).then(
-        () => ServerAvailabilityActions.reportSuccess(),
+        (response: StatusResponse) => {
+          if (response?.version !== this.version) {
+            this.version = response?.version;
+            this._trigger();
+          }
+
+          return ServerAvailabilityActions.reportSuccess();
+        },
         (error) => ServerAvailabilityActions.reportError(error),
       );
     },
     reportError(error) {
       if (this.server.up) {
         this.server = { up: false, error: error };
-        this.trigger({ server: this.server });
+        this._trigger();
       }
     },
     reportSuccess() {
       if (!this.server.up) {
         this.server = { up: true };
-        this.trigger({ server: this.server });
+        this._trigger();
       }
     },
+    _trigger() {
+      this.trigger({ server: this.server, version: this.version });
+    },
   }),
 );


Test patch:
diff --git a/graylog2-web-interface/src/routing/SuggestReloadIfVersionChanged.test.tsx b/graylog2-web-interface/src/routing/SuggestReloadIfVersionChanged.test.tsx
new file mode 100644
index 000000000000..0ace316a6002
--- /dev/null
+++ b/graylog2-web-interface/src/routing/SuggestReloadIfVersionChanged.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 Graylog, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ */
+import * as React from 'react';
+import { render, screen } from 'wrappedTestingLibrary';
+import userEvent from '@testing-library/user-event';
+
+import asMock from 'helpers/mocking/AsMock';
+import useServerVersion from 'routing/useServerVersion';
+
+import SuggestReloadIfVersionChanged from './SuggestReloadIfVersionChanged';
+
+jest.mock('./useServerVersion', () => jest.fn());
+
+describe('SuggestReloadIfVersionChanged', () => {
+  beforeEach(() => {
+    asMock(useServerVersion).mockReturnValue('6.1.0');
+  });
+
+  it('shows dialog when version changes, reloads on click', async () => {
+    const reload = jest.fn();
+    const { rerender } = render(<SuggestReloadIfVersionChanged reload={reload} />);
+
+    expect(screen.queryByText(/Your Graylog version has changed/i)).not.toBeInTheDocument();
+
+    asMock(useServerVersion).mockReturnValue('6.2.0');
+
+    rerender(<SuggestReloadIfVersionChanged reload={reload} />);
+
+    await screen.findByText(/Your Graylog version has changed/i);
+
+    const reloadButton = await screen.findByRole('button', { name: 'Reload now' });
+
+    userEvent.click(reloadButton);
+
+    expect(reload).toHaveBeenCalled();
+  });
+});