Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-14480.toml
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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(<EventDefinitionFormContainer action="create" eventDefinition={mockAggregationEventDefinition} />);

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -49,6 +49,7 @@ const ShareForm = ({ onChange, eventDefinition }: Props) => {
entityType="event_definition"
entityTitle=""
dependenciesGRN={[...streamDependenciesGRN, ...notificationDependenciesGRN]}
defaultSharePayload={eventDefinition?.share_request}
/>
</Col>
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import * as mockImmutable from 'immutable';
import { render, waitFor, screen } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';

Expand All @@ -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 },
}),
),
Expand Down Expand Up @@ -90,7 +91,40 @@ describe('EntityCreateShareFormGroup', () => {
render(<SUT />);

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(<SUT defaultSharePayload={{ selected_grantee_capabilities }} />);

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(<SUT defaultSharePayload={{ selected_grantee_capabilities }} dependenciesGRN={dependenciesGRN} />);

await waitFor(() => {
expect(EntityShareDomain.prepare).toHaveBeenCalledWith(mockEntity.entityType, '', mockEntity.entityId, {
selected_grantee_capabilities,
prepare_request: dependenciesGRN,
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GRN> = 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);
Expand Down Expand Up @@ -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 */
<PluggableEntityShareFormGroup
entityType={entityType}
onChange={handleAdditionalFormChange}
Expand Down
Loading