diff --git a/changelog/unreleased/issue-14480.toml b/changelog/unreleased/issue-14480.toml
new file mode 100644
index 000000000000..d675f701450d
--- /dev/null
+++ b/changelog/unreleased/issue-14480.toml
@@ -0,0 +1,5 @@
+type = "fixed"
+message = "Keep the share settings of an event definition populated when navigating between wizard steps."
+
+issues = ["Graylog2/graylog-plugin-enterprise#14480"]
+pulls = ["26990"]
diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDefinitionFormContainer.test.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDefinitionFormContainer.test.tsx
index 390a1f0ee297..7161d4432772 100644
--- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDefinitionFormContainer.test.tsx
+++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDefinitionFormContainer.test.tsx
@@ -16,10 +16,14 @@
*/
import * as React from 'react';
+import * as Immutable from 'immutable';
import { render, screen } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';
import { defaultUser as mockDefaultUser } from 'defaultMockValues';
+import selectEvent from 'helpers/selectEvent';
+import EntityShareDomain from 'domainActions/permissions/EntityShareDomain';
+import { createEntityShareState, everyone, viewer } from 'fixtures/entityShareState';
import useLocation from 'routing/useLocation';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import { asMock } from 'helpers/mocking';
@@ -198,6 +202,14 @@ jest.mock('logic/telemetry/withTelemetry', () => ({
),
}));
+jest.mock('domainActions/permissions/EntityShareDomain', () => ({
+ __esModule: true,
+ default: { prepare: jest.fn(() => Promise.resolve()), update: jest.fn(() => Promise.resolve()) },
+}));
+
+// Rendered once the form is dirty and relies on `useBlocker`, which needs a data router.
+jest.mock('components/common/ConfirmLeaveDialog', () => () => null);
+
jest.mock('components/event-definitions/hooks/useEventDefinitionConfigFromLocalStorage');
jest.mock('routing/useLocation');
jest.mock('logic/telemetry/useSendTelemetry');
@@ -358,4 +370,31 @@ describe('EventDefinitionFormContainer', () => {
expect(screen.getByRole('button', { name: /update event definition/i })).toBeVisible();
});
+
+ it('keeps the selected collaborators when navigating away from and back to the Share step', async () => {
+ // Mirrors the server: the prepared state reflects exactly the capabilities that were sent.
+ asMock(EntityShareDomain.prepare).mockImplementation((_type, _title, _grn, payload?: any) =>
+ Promise.resolve(
+ createEntityShareState
+ .toBuilder()
+ .selectedGranteeCapabilities(payload?.selected_grantee_capabilities ?? Immutable.Map())
+ .build(),
+ ),
+ );
+
+ render();
+
+ await userEvent.click(await screen.findByRole('button', { name: /^share$/i }));
+
+ await selectEvent.chooseOption('Search for users and teams', everyone.title);
+ await selectEvent.chooseOption('Select a capability', viewer.title);
+ await userEvent.click(await screen.findByRole('button', { name: /add collaborator/i }));
+
+ expect(await screen.findByText(everyone.title)).toBeVisible();
+
+ await userEvent.click(await screen.findByRole('button', { name: /^summary$/i }));
+ await userEvent.click(await screen.findByRole('button', { name: /^share$/i }));
+
+ expect(await screen.findByText(everyone.title)).toBeVisible();
+ });
});
diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareForm.tsx
index ba9d5a4bfb64..af008ecadba9 100644
--- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareForm.tsx
+++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/ShareForm.tsx
@@ -26,7 +26,7 @@ import type { EventDefinition } from '../event-definitions-types';
type Props = {
onChange: (name: string, value: EntitySharePayload) => void;
- eventDefinition: EventDefinition;
+ eventDefinition: EventDefinition & { share_request?: EntitySharePayload };
};
const ShareForm = ({ onChange, eventDefinition }: Props) => {
@@ -49,6 +49,7 @@ const ShareForm = ({ onChange, eventDefinition }: Props) => {
entityType="event_definition"
entityTitle=""
dependenciesGRN={[...streamDependenciesGRN, ...notificationDependenciesGRN]}
+ defaultSharePayload={eventDefinition?.share_request}
/>
diff --git a/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.test.tsx b/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.test.tsx
index f0341ea2bb1b..6f3a9950e1f2 100644
--- a/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.test.tsx
+++ b/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.test.tsx
@@ -15,6 +15,7 @@
* .
*/
import * as React from 'react';
+import * as mockImmutable from 'immutable';
import { render, waitFor, screen } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';
@@ -34,7 +35,7 @@ jest.mock('domainActions/permissions/EntityShareDomain', () => ({
update: jest.fn(() => Promise.resolve()),
loadUserSharesPaginated: jest.fn(() =>
Promise.resolve({
- list: require('immutable').List(),
+ list: mockImmutable.List(),
pagination: { page: 1, perPage: 10, query: '', total: 0, count: 0 },
}),
),
@@ -90,7 +91,40 @@ describe('EntityCreateShareFormGroup', () => {
render();
await waitFor(() => {
- expect(EntityShareDomain.prepare).toHaveBeenCalledWith(mockEntity.entityType, '', mockEntity.entityId, {});
+ expect(EntityShareDomain.prepare).toHaveBeenCalledWith(mockEntity.entityType, '', mockEntity.entityId, {
+ prepare_request: null,
+ });
+ });
+ });
+
+ it('restores a previously made selection from the default share payload', async () => {
+ const selected_grantee_capabilities = createEntityShareState.selectedGranteeCapabilities.merge({
+ [everyone.id]: viewer.id,
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(EntityShareDomain.prepare).toHaveBeenCalledWith(mockEntity.entityType, '', mockEntity.entityId, {
+ selected_grantee_capabilities,
+ prepare_request: null,
+ });
+ });
+ });
+
+ it('re-runs the dependency check when restoring a selection', async () => {
+ const selected_grantee_capabilities = createEntityShareState.selectedGranteeCapabilities.merge({
+ [everyone.id]: viewer.id,
+ });
+ const dependenciesGRN = ['grn::::stream:stream-id'];
+
+ render();
+
+ await waitFor(() => {
+ expect(EntityShareDomain.prepare).toHaveBeenCalledWith(mockEntity.entityType, '', mockEntity.entityId, {
+ selected_grantee_capabilities,
+ prepare_request: dependenciesGRN,
+ });
});
});
diff --git a/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.tsx b/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.tsx
index 80f3eaf69916..762b668658e4 100644
--- a/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.tsx
+++ b/graylog2-web-interface/src/components/permissions/EntityCreateShareFormGroup.tsx
@@ -86,13 +86,21 @@ const EntityCreateShareFormGroup = ({
);
const PluggableEntityShareFormGroup = usePluggableEntityShareFormGroup();
+ // Serialized because callers build `dependenciesGRN` inline, so its identity changes every render.
+ const dependenciesKey = JSON.stringify(dependenciesGRN ?? []);
+
useEffect(() => {
const { selected_collections: _, ...rest } = defaultSharePayload ?? {};
+ const dependencies: Array = JSON.parse(dependenciesKey);
+ // Restoring a selection has to re-run the dependency check, or the missing dependency
+ // warnings shown before would silently disappear.
+ const prepare_request =
+ (rest.selected_grantee_capabilities?.size ?? 0) > 0 && dependencies.length > 0 ? dependencies : null;
- EntityShareDomain.prepare(entityType, entityTitle, entityGRN, rest).then((state) => {
+ EntityShareDomain.prepare(entityType, entityTitle, entityGRN, { ...rest, prepare_request }).then((state) => {
setEntityShareState(entityGRN, state);
});
- }, [entityType, entityTitle, entityGRN, defaultSharePayload, setEntityShareState]);
+ }, [entityType, entityTitle, entityGRN, defaultSharePayload, dependenciesKey, setEntityShareState]);
const resetSelection = () => {
setDisableSubmit(false);
@@ -202,6 +210,8 @@ const EntityCreateShareFormGroup = ({
availableGrantees={entityShareState.availableGrantees}
/>
{PluggableEntityShareFormGroup && (
+ /* Resolved from the plugin store at render time and cannot be hoisted. */
+ /* eslint-disable-next-line react-hooks/static-components */