diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx index 156981207e225..98759cd5ceb1d 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx @@ -1348,5 +1348,175 @@ describe('PackagePolicyInputPanel', () => { expect(lastCall?.streams?.[0]?.vars).not.toHaveProperty(USE_APM_VAR_NAME); }); }); + + it('hoists the non-GA release badge up to the input header for input-type packages', async () => { + const betaOtelStreams: RegistryStreamWithDataStream[] = [ + { + ...otelPackageInputStreams[0], + data_stream: { + ...otelPackageInputStreams[0].data_stream, + release: 'beta', + }, + }, + ]; + const otelPolicyInput = { + id: 'input-1', + type: OTEL_COLLECTOR_INPUT_TYPE, + policy_template: 'otel_template', + enabled: true, + streams: [ + { + id: 'otel-stream-1', + data_stream: { type: 'logs', dataset: 'my_otel.data' }, + enabled: true, + vars: {}, + }, + ], + } as NewPackagePolicyInput; + + renderResult = testRenderer.render( + + ); + + await waitFor(() => { + expect(renderResult.getByText('Beta')).toBeInTheDocument(); + // Stream-level toggle should not render for input-type packages, + // which is why the badge is hoisted up to the input header instead. + expect(renderResult.queryByTestId('streamOptions.switch')).not.toBeInTheDocument(); + }); + }); + }); + + describe('Non-GA release badge hoisting', () => { + beforeEach(() => { + jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ + enableVarGroups: true, + } as any); + useAgentlessMock.mockReturnValue({ + isAgentlessEnabled: false, + isAgentlessDefault: false, + isAgentlessAgentPolicy: jest.fn(), + getAgentlessStatusForPackage: jest + .fn() + .mockReturnValue({ isAgentless: false, isDefaultDeploymentMode: false }), + isServerless: false, + isCloud: false, + }); + }); + + const singleBetaStream: RegistryStreamWithDataStream[] = [ + { + input: 'logfile', + title: 'Stream 1', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + required: false, + show_user: true, + }, + ], + description: 'Test stream', + data_stream: { + ...mockPackageInputStreams[0].data_stream, + release: 'beta', + }, + }, + ]; + + const singleStreamPolicyInput = { + ...packagePolicyInput, + streams: [packagePolicyInput.streams[0]], + } as NewPackagePolicyInput; + + it('hoists the release badge to the input header when there is a single stream', async () => { + renderResult = testRenderer.render( + + ); + await waitFor(() => { + expect(renderResult.getByText('Beta')).toBeInTheDocument(); + // Single-stream rows don't render their own toggle, so the badge + // would otherwise float alone - here we expect it at the input header. + expect(renderResult.queryByTestId('streamOptions.switch')).not.toBeInTheDocument(); + }); + }); + + it('renders the release badge at the stream row for multi-stream integrations', async () => { + const multiStreamsWithBeta: RegistryStreamWithDataStream[] = [ + { + input: 'logfile', + title: 'Stream 1', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + required: false, + show_user: true, + }, + ], + description: 'Test stream 1', + data_stream: { + ...mockPackageInputStreams[0].data_stream, + release: 'beta', + }, + }, + { + input: 'logfile', + title: 'Stream 2', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + required: false, + show_user: true, + }, + ], + description: 'Test stream 2', + data_stream: { + ...mockPackageInputStreams[1].data_stream, + }, + }, + ]; + + renderResult = testRenderer.render( + + ); + await waitFor(() => { + expect(renderResult.getByText('Beta')).toBeInTheDocument(); + // Multi-stream integrations show per-stream toggles, so the badge + // stays at the stream row - no need to hoist. + expect(renderResult.getAllByTestId('streamOptions.switch').length).toBeGreaterThan(0); + }); + }); }); }); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx index 59c2ec345cd4f..968b9cf505f12 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, Fragment, memo, useMemo, useCallback } from 'react'; +import React, { useState, memo, useMemo, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -40,7 +40,9 @@ import { DATA_STREAM_USE_APM_VAR, shouldIncludeUseAPMVar, hasDynamicSignalTypes, + mapPackageReleaseToIntegrationCardRelease, } from '../../../../../../../../../common/services'; +import { InlineReleaseBadge } from '../../../../../../components'; import { DATA_STREAM_TYPE_VAR_NAME, USE_APM_VAR_NAME, @@ -234,11 +236,6 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ const errorCount = inputValidationResults && countValidationErrors(inputValidationResults); const hasErrors = forceShowErrors && errorCount; - const hasInputStreams = useMemo( - () => packageInputStreams.length > 0, - [packageInputStreams.length] - ); - const inputStreams = useMemo( () => packageInputStreams @@ -254,6 +251,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ .filter((stream) => Boolean(stream.packagePolicyInputStream)), [packageInputStreamShouldBeVisible, packageInputStreams, packagePolicyInput.streams] ); + const hasInputStreams = useMemo(() => inputStreams.length > 0, [inputStreams.length]); const showTopLevelDescription = inputStreams.length === 1; const dynamicSignalTypes = useMemo( @@ -345,6 +343,27 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ defaultMessage: 'This input is deprecated.', }); + // Whether the individual stream rows will render their own toggle switch. + // When they won't (input-type package or single visible stream), we hoist + // the non-GA release badge up to the input header so it doesn't float alone. + const hasStreamToggle = packageInfo.type !== 'input' && inputStreams.length > 1; + + const inputReleaseBadge = useMemo(() => { + if (hasStreamToggle) return null; + const preReleaseStream = inputStreams.find( + ({ packageInputStream }) => + packageInputStream.data_stream.release && packageInputStream.data_stream.release !== 'ga' + ); + if (!preReleaseStream?.packageInputStream.data_stream.release) return null; + return ( + + ); + }, [hasStreamToggle, inputStreams]); + // Check if any vars or streams in this input are deprecated const hasDeprecatedFeatures = useMemo(() => { const inputVarsDeprecated = (packageInput.vars || []).some((v) => !!v.deprecated); @@ -382,6 +401,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ + {inputReleaseBadge && {inputReleaseBadge}} {migrationTooltip} @@ -407,6 +427,9 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ + {inputReleaseBadge && ( + {inputReleaseBadge} + )} {migrationTooltip} {isDeprecatedInput && ( @@ -505,11 +528,16 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ - {/* Header rule break */} - {isShowingStreams ? : null} + {/* Spacing if we are showing rest of content */} + {isShowingStreams && + hasInputStreams && + ((packageInput.vars && packageInput.vars.length) || !shouldConsolidateAdvancedSections) ? ( + + ) : null} + {/* Input level policy */} {isShowingStreams && packageInput.vars && packageInput.vars.length ? ( - + <> ) : ( - + )} - + ) : null} {/* Per-stream policy */} - {isShowingStreams && !shouldConsolidateAdvancedSections ? ( + {isShowingStreams && hasInputStreams && !shouldConsolidateAdvancedSections ? ( {inputStreams.map(({ packageInputStream, packagePolicyInputStream }, index) => { return ( @@ -546,7 +574,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ data-test-subj="PackagePolicy.InputStreamConfig" packageInfo={packageInfo} packageInputStream={packageInputStream} - totalStreams={inputStreams.length} + hasStreamToggle={hasStreamToggle} packagePolicyInputStream={packagePolicyInputStream!} inputPolicyTemplate={packagePolicyInput.policy_template} showDescriptionColumn={!isSingleInputAndStreams} diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.test.tsx index 2b02760ec79bc..74e44299a349c 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.test.tsx @@ -223,7 +223,7 @@ describe('PackagePolicyInputStreamConfig', () => { updatePackagePolicyInputStream={mockUpdatePackagePolicyInputStream} inputStreamValidationResults={{ vars: {} }} forceShowErrors={false} - totalStreams={2} + hasStreamToggle={true} inputPolicyTemplate={inputPolicyTemplate} /> ); @@ -297,7 +297,7 @@ describe('PackagePolicyInputStreamConfig', () => { updatePackagePolicyInputStream={mockUpdatePackagePolicyInputStream} inputStreamValidationResults={{ vars: {} }} forceShowErrors={false} - totalStreams={2} + hasStreamToggle={true} /> ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx index 5f8a69a7407ed..78323d8e65f5c 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx @@ -84,7 +84,7 @@ interface Props { forceShowErrors?: boolean; isEditPage?: boolean; isUpgrade?: boolean; - totalStreams?: number; + hasStreamToggle?: boolean; showDescriptionColumn?: boolean; varGroupSelections?: Record; /** Parent input's `policy_template`; required for correct composable multi-template matching. */ @@ -101,7 +101,7 @@ export const PackagePolicyInputStreamConfig = memo( forceShowErrors, isEditPage, isUpgrade, - totalStreams, + hasStreamToggle = true, showDescriptionColumn = true, varGroupSelections = {}, inputPolicyTemplate, @@ -126,7 +126,6 @@ export const PackagePolicyInputStreamConfig = memo( !!packagePolicyInputStream.id && packagePolicyInputStream.id === defaultDataStreamId; const isPackagePolicyEdit = !!packagePolicyId; - const shouldShowStreamsToggles = totalStreams ? totalStreams > 1 : true; const customDatasetVar = packagePolicyInputStream.vars?.[DATASET_VAR_NAME]; const customDatasetVarValue = customDatasetVar?.value?.dataset || customDatasetVar?.value; @@ -303,113 +302,115 @@ export const PackagePolicyInputStreamConfig = memo( data-test-subj={`streamOptions.inputStreams.${packageInputStream.data_stream.dataset}`} > - - - - - - {packageInfo.type !== 'input' && shouldShowStreamsToggles && ( - - + {showDescriptionColumn || hasStreamToggle || hasRequiredVarGroupErrors ? ( + + + + + {hasStreamToggle && ( + <> + - { - const enabled = e.target.checked; - updatePackagePolicyInputStream({ - enabled, - }); - }} - /> + + + { + const enabled = e.target.checked; + updatePackagePolicyInputStream({ + enabled, + }); + }} + /> + + {showStreamDeprecationIcon && ( + + + + + + )} + {isUpgrade && + packagePolicyInputStream.migrate_from && + !showStreamDeprecationIcon && ( + + )} + - {showStreamDeprecationIcon && ( + {packageInputStream.data_stream.release && + packageInputStream.data_stream.release !== 'ga' ? ( - - - - - )} - {isUpgrade && - packagePolicyInputStream.migrate_from && - !showStreamDeprecationIcon && ( - - )} + + ) : null} - + {packageInputStream.description ? ( + <> + + + {packageInputStream.description} + + + ) : null} + )} - {packageInputStream.data_stream.release && - packageInputStream.data_stream.release !== 'ga' ? ( - - - - ) : null} - - {packageInfo.type !== 'input' && - packageInputStream.description && - shouldShowStreamsToggles ? ( - <> - - - {packageInputStream.description} - - - ) : null} - {hasRequiredVarGroupErrors && ( - <> - - - + {hasRequiredVarGroupErrors && ( + <> + + + + + } + > + + {Object.entries(inputStreamValidationResults?.required_vars || {}).map( + ([groupName, vars]) => { + return ( + <> + {groupName} +
    + {vars.map(({ name }) => ( +
  • {name}
  • + ))} +
+ + ); + } + )}
- } - > - - {Object.entries(inputStreamValidationResults?.required_vars || {}).map( - ([groupName, vars]) => { - return ( - <> - {groupName} -
    - {vars.map(({ name }) => ( -
  • {name}
  • - ))} -
- - ); - } - )} -
-
- - )} -
-
-
+ + + )} +
+
+
+ ) : null} {/* Stream-level Var Group Selectors */} diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/add_integration_flyout_configure_header.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/add_integration_flyout_configure_header.tsx index cfe10a3102806..f23715748136a 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/add_integration_flyout_configure_header.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/add_integration_flyout_configure_header.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { EuiLink, EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { PackageIcon } from '../../../../../components'; @@ -24,61 +25,52 @@ export const AddIntegrationFlyoutConfigureHeader: React.FC = ({ pkgLabel, integration, }) => { + const theme = useEuiTheme(); return ( <> - - - - - - - - - - - + + + + + + {pkgLabel} + + + + - - - {pkgLabel} - - - - - - - ), - }} - /> - - - - - - + + ), + }} + /> + ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 28464d9490322..e96058745b7d4 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -102,6 +102,7 @@ import { useAgentless } from './hooks/setup_technology'; export const StepsWithLessPadding = styled(EuiSteps)` .euiStep__content { + padding-top: ${(props) => props.theme.eui.euiSizeXS}; padding-bottom: ${(props) => props.theme.eui.euiSizeM}; } diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/add_integration_flyout.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/add_integration_flyout.tsx index 8bd5864a29aa6..5e1522ca266a3 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/add_integration_flyout.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/add_integration_flyout.tsx @@ -147,7 +147,7 @@ export const AddIntegrationFlyout: React.FunctionComponent<{ - +