Skip to content

Commit 7e9c2d2

Browse files
ousmaneogally47
andauthored
Fix Output creation on the Stream details page (#25552)
* fix Stream details output creation * add changlog * fix linter --------- Co-authored-by: Mohamed OULD HOCINE <106236152+gally47@users.noreply.github.com>
1 parent af0fb89 commit 7e9c2d2

File tree

7 files changed

+204
-9
lines changed

7 files changed

+204
-9
lines changed

changelog/unreleased/pr-25552.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type = "fixed"
2+
message = "Fix creating output from Data Routing page for Stream results in empty output configuration."
3+
4+
issues = ["23793"]
5+
pulls = ["25552"]

graylog2-web-interface/src/components/outputs/OutputsComponent.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import StreamsStore from 'stores/streams/StreamsStore';
2626
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
2727
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
2828
import { isPermitted } from 'util/PermissionsMixin';
29-
import useAvailableOutputTypes from 'components/streams/useAvailableOutputTypes';
29+
import useAvailableOutputTypes, { getOutputTypeDefinition } from 'components/streams/useAvailableOutputTypes';
3030
import useOutputs from 'hooks/useOutputs';
3131
import useStreamOutputs from 'hooks/useStreamOutputs';
3232
import useOutputMutations from 'hooks/useOutputMutations';
@@ -72,8 +72,10 @@ const OutputsComponent = ({ streamId = undefined, permissions }: Props) => {
7272

7373
const getTypeDefinition = useCallback(
7474
(typeName: string, callback: (def: any) => void) => {
75-
if (types?.[typeName]) {
76-
callback(types[typeName]);
75+
const typeDefinition = getOutputTypeDefinition(types, typeName);
76+
77+
if (typeDefinition) {
78+
callback(typeDefinition);
7779
}
7880
},
7981
[types],

graylog2-web-interface/src/components/streams/StreamDetails/routing-destination/AddOutputButton.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
3030
import useStreamOutputMutation from 'hooks/useStreamOutputMutations';
3131
import type {
3232
AvailableOutputRequestedConfiguration,
33+
AvailableOutputSummary,
3334
AvailableOutputTypes,
3435
} from 'components/streams/useAvailableOutputTypes';
3536
import { Icon } from 'components/common';
3637

3738
type Props = {
3839
stream: Stream;
39-
getTypeDefinition: (type: string) => AvailableOutputRequestedConfiguration;
40+
getTypeDefinition: (
41+
type: string,
42+
callback?: (available: AvailableOutputSummary) => void,
43+
) => AvailableOutputRequestedConfiguration | undefined;
4044
availableOutputTypes: AvailableOutputTypes;
4145
assignableOutputs: Array<Output>;
4246
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
import * as React from 'react';
18+
import { render } from 'wrappedTestingLibrary';
19+
20+
import type { ConfigurationField } from 'components/configurationforms';
21+
import { asMock } from 'helpers/mocking';
22+
import useOutputs from 'hooks/useOutputs';
23+
import useStreamOutputs from 'hooks/useStreamOutputs';
24+
import useAvailableOutputTypes from 'components/streams/useAvailableOutputTypes';
25+
import type { AvailableOutputTypes } from 'components/streams/useAvailableOutputTypes';
26+
import AddOutputButton from 'components/streams/StreamDetails/routing-destination/AddOutputButton';
27+
import OutputsList from 'components/streams/StreamDetails/routing-destination/OutputsList';
28+
29+
import DestinationOutputs from './DestinationOutputs';
30+
31+
jest.mock('hooks/useOutputs');
32+
jest.mock('hooks/useStreamOutputs');
33+
jest.mock('components/streams/useAvailableOutputTypes', () => {
34+
const actual = jest.requireActual('components/streams/useAvailableOutputTypes');
35+
36+
return {
37+
__esModule: true,
38+
...actual,
39+
default: jest.fn(),
40+
};
41+
});
42+
jest.mock('components/streams/StreamDetails/routing-destination/AddOutputButton', () => jest.fn(() => <div>add output button</div>));
43+
jest.mock('components/streams/StreamDetails/routing-destination/OutputsList', () => jest.fn(() => <div>outputs list</div>));
44+
45+
describe('DestinationOutputs', () => {
46+
const streamOutput = { id: 'output-id', title: 'Existing output', type: 'enterprise-output', configuration: {} };
47+
const hostField: ConfigurationField = {
48+
type: 'text',
49+
human_name: 'Host',
50+
additional_info: {},
51+
attributes: [],
52+
default_value: '',
53+
description: 'Host to connect to',
54+
is_encrypted: false,
55+
is_optional: false,
56+
position: 0,
57+
};
58+
const availableOutputTypes: AvailableOutputTypes = {
59+
'enterprise-output': {
60+
type: 'enterprise-output',
61+
name: 'Enterprise output',
62+
human_name: 'Enterprise output',
63+
link_to_docs: '',
64+
requested_configuration: {
65+
host: hostField,
66+
},
67+
},
68+
};
69+
70+
beforeEach(() => {
71+
jest.clearAllMocks();
72+
73+
asMock(useStreamOutputs).mockReturnValue({
74+
data: { outputs: [streamOutput], total: 1 },
75+
refetch: jest.fn(),
76+
isInitialLoading: false,
77+
isError: false,
78+
});
79+
asMock(useOutputs).mockReturnValue({
80+
data: { outputs: [streamOutput], total: 1 },
81+
refetch: jest.fn(),
82+
isInitialLoading: false,
83+
});
84+
asMock(useAvailableOutputTypes).mockReturnValue({
85+
data: availableOutputTypes,
86+
refetch: jest.fn(),
87+
isInitialLoading: false,
88+
});
89+
});
90+
91+
it('uses flat available output types map to resolve requested configuration for create and edit paths', () => {
92+
render(<DestinationOutputs stream={{ id: 'stream-1' } as any} />);
93+
94+
const addOutputButtonProps = asMock(AddOutputButton).mock.calls[0][0];
95+
const callback = jest.fn();
96+
const requestedConfiguration = addOutputButtonProps.getTypeDefinition('enterprise-output', callback);
97+
98+
expect(addOutputButtonProps.availableOutputTypes).toEqual(availableOutputTypes);
99+
expect(callback).toHaveBeenCalledWith(availableOutputTypes['enterprise-output']);
100+
expect(requestedConfiguration).toEqual(availableOutputTypes['enterprise-output'].requested_configuration);
101+
102+
const outputsListProps = asMock(OutputsList).mock.calls[0][0];
103+
expect(outputsListProps.getTypeDefinition('enterprise-output')).toEqual(
104+
availableOutputTypes['enterprise-output'].requested_configuration,
105+
);
106+
});
107+
});

graylog2-web-interface/src/components/streams/StreamDetails/routing-destination/DestinationOutputs.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import { IfPermitted, Section, Spinner } from 'components/common';
2121
import type { Stream } from 'stores/streams/StreamsStore';
2222
import useStreamOutputs from 'hooks/useStreamOutputs';
2323
import type { AvailableOutputSummary } from 'components/streams/useAvailableOutputTypes';
24-
import useAvailableOutputTypes from 'components/streams/useAvailableOutputTypes';
24+
import useAvailableOutputTypes, {
25+
getOutputTypeDefinition,
26+
getRequestedOutputConfiguration,
27+
} from 'components/streams/useAvailableOutputTypes';
2528
import SectionCountLabel from 'components/streams/StreamDetails/SectionCountLabel';
2629
import AddOutputButton from 'components/streams/StreamDetails/routing-destination/AddOutputButton';
2730
import OutputsList from 'components/streams/StreamDetails/routing-destination/OutputsList';
@@ -37,13 +40,13 @@ const DestinationOutputs = ({ stream }: Props) => {
3740
const { data: availableOutputTypes, isInitialLoading: isLoadingOutputTypes } = useAvailableOutputTypes();
3841

3942
const getTypeDefinition = (type: string, callback?: (available: AvailableOutputSummary) => void) => {
40-
const definitition = availableOutputTypes.types[type];
43+
const definition = getOutputTypeDefinition(availableOutputTypes, type);
4144

42-
if (callback && definitition) {
43-
callback(definitition);
45+
if (callback && definition) {
46+
callback(definition);
4447
}
4548

46-
return definitition?.requested_configuration;
49+
return getRequestedOutputConfiguration(availableOutputTypes, type);
4750
};
4851

4952
if (isInitialLoading || isLoadingOutput || isLoadingOutputTypes) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
18+
import {
19+
getOutputTypeDefinition,
20+
getRequestedOutputConfiguration,
21+
type AvailableOutputTypes,
22+
} from 'components/streams/useAvailableOutputTypes';
23+
import type { ConfigurationField } from 'components/configurationforms';
24+
25+
describe('useAvailableOutputTypes helpers', () => {
26+
const hostField: ConfigurationField = {
27+
type: 'text',
28+
human_name: 'Host',
29+
additional_info: {},
30+
attributes: [],
31+
default_value: '',
32+
description: 'Host to connect to',
33+
is_encrypted: false,
34+
is_optional: false,
35+
position: 0,
36+
};
37+
38+
const outputTypes: AvailableOutputTypes = {
39+
'enterprise-output': {
40+
type: 'enterprise-output',
41+
name: 'Enterprise output',
42+
human_name: 'Enterprise output',
43+
link_to_docs: '',
44+
requested_configuration: {
45+
host: hostField,
46+
},
47+
},
48+
};
49+
50+
it('returns output definition for a known output type', () => {
51+
expect(getOutputTypeDefinition(outputTypes, 'enterprise-output')).toEqual(outputTypes['enterprise-output']);
52+
});
53+
54+
it('returns requested configuration from the flat output types map', () => {
55+
expect(getRequestedOutputConfiguration(outputTypes, 'enterprise-output')).toEqual(
56+
outputTypes['enterprise-output'].requested_configuration,
57+
);
58+
});
59+
60+
it('returns undefined for unknown type or missing map', () => {
61+
expect(getOutputTypeDefinition(outputTypes, 'missing-output')).toBeUndefined();
62+
expect(getRequestedOutputConfiguration(undefined, 'enterprise-output')).toBeUndefined();
63+
});
64+
});

graylog2-web-interface/src/components/streams/useAvailableOutputTypes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export type AvailableOutputTypes = {
4040
[_key: string]: AvailableOutputSummary;
4141
};
4242

43+
export const getOutputTypeDefinition = (
44+
outputTypes: AvailableOutputTypes | undefined,
45+
outputType: string,
46+
): AvailableOutputSummary | undefined => outputTypes?.[outputType];
47+
48+
export const getRequestedOutputConfiguration = (
49+
outputTypes: AvailableOutputTypes | undefined,
50+
outputType: string,
51+
): AvailableOutputRequestedConfiguration | undefined => getOutputTypeDefinition(outputTypes, outputType)?.requested_configuration;
52+
4353
export const fetchOutputsTypes = () => {
4454
const url = qualifyUrl(ApiRoutes.OutputsApiController.availableTypes().url);
4555

0 commit comments

Comments
 (0)