diff --git a/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.SetupRouting.test.tsx b/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.SetupRouting.test.tsx index a65b10422e99..0e868ece9084 100644 --- a/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.SetupRouting.test.tsx +++ b/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.SetupRouting.test.tsx @@ -180,98 +180,139 @@ describe('InputSetupWizard Setup Routing', () => { it('should render the Setup Routing step', async () => { renderWizard(); - const routingStepText = await screen.findByText(/Choose a Destination Stream to route Messages from this Input to./i); + const routingStepText = await screen.findByText(/Select a destination Stream to route messages from this input to./i); expect(routingStepText).toBeInTheDocument(); }); - it('should only show editable existing streams', async () => { - asMock(useStreams).mockReturnValue(useStreamsResult( - [ - { id: 'alohoid', title: 'Aloho', is_editable: true }, - { id: 'moraid', title: 'Mora', is_editable: false }, - ], - )); + describe('Stream Selection', () => { + it('should show the stream select when clicking on choose stream', async () => { + renderWizard(); + const selectStreamButton = await screen.findByRole('button', { + name: /Select Stream/i, + hidden: true, + }); - renderWizard(); + fireEvent.click(selectStreamButton); - const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); + await screen.findByLabelText(/All messages \(Default\)/i); + }); - await selectEvent.openMenu(streamSelect); + it('should only show editable existing streams', async () => { + asMock(useStreams).mockReturnValue(useStreamsResult( + [ + { id: 'alohoid', title: 'Aloho', is_editable: true }, + { id: 'moraid', title: 'Mora', is_editable: false }, + ], + )); - const alohoOption = await screen.findByText(/Aloho/i); - const moraOption = screen.queryByText(/Mora/i); + renderWizard(); + const selectStreamButton = await screen.findByRole('button', { + name: /Select Stream/i, + hidden: true, + }); - expect(alohoOption).toBeInTheDocument(); - expect(moraOption).not.toBeInTheDocument(); - }); + fireEvent.click(selectStreamButton); - it('should not show existing default stream in select', async () => { - asMock(useStreams).mockReturnValue(useStreamsResult( - [ - { id: 'alohoid', title: 'Aloho', is_editable: true, is_default: true }, - { id: 'moraid', title: 'Mora', is_editable: true }, - ], - )); + const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); - renderWizard(); + await selectEvent.openMenu(streamSelect); - const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); + const alohoOption = await screen.findByText(/Aloho/i); + const moraOption = screen.queryByText(/Mora/i); - await selectEvent.openMenu(streamSelect); + expect(alohoOption).toBeInTheDocument(); + expect(moraOption).not.toBeInTheDocument(); + }); - const moraOption = await screen.findByText(/Mora/i); - const alohoOption = screen.queryByText(/Aloho/i); + it('should not show existing default stream in select', async () => { + asMock(useStreams).mockReturnValue(useStreamsResult( + [ + { id: 'alohoid', title: 'Aloho', is_editable: true, is_default: true }, + { id: 'moraid', title: 'Mora', is_editable: true }, + ], + )); - expect(moraOption).toBeInTheDocument(); - expect(alohoOption).not.toBeInTheDocument(); - }); + renderWizard(); - it('should allow the user to select a stream', async () => { - asMock(useStreams).mockReturnValue(useStreamsResult( - [ - { id: 'alohoid', title: 'Aloho', is_editable: true }, - { id: 'moraid', title: 'Mora', is_editable: true }, - ], - )); + const selectStreamButton = await screen.findByRole('button', { + name: /Select Stream/i, + hidden: true, + }); - renderWizard(); + fireEvent.click(selectStreamButton); - const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); + const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); - await selectEvent.openMenu(streamSelect); + await selectEvent.openMenu(streamSelect); - await selectEvent.select(streamSelect, 'Aloho'); - }); + const moraOption = await screen.findByText(/Mora/i); + const alohoOption = screen.queryByText(/Aloho/i); - it('should show a warning if the selected stream has connected pipelines', async () => { - asMock(useStreams).mockReturnValue(useStreamsResult( - [ - { id: 'alohoid', title: 'Aloho', is_editable: true }, - { id: 'moraid', title: 'Mora', is_editable: true }, - ], - )); + expect(moraOption).toBeInTheDocument(); + expect(alohoOption).not.toBeInTheDocument(); + }); - asMock(usePipelinesConnectedStream).mockReturnValue(pipelinesConnectedMock([ - { id: 'pipeline1', title: 'Pipeline1' }, - { id: 'pipeline2', title: 'Pipeline2' }, - ])); + it('should allow the user to select a stream', async () => { + asMock(useStreams).mockReturnValue(useStreamsResult( + [ + { id: 'alohoid', title: 'Aloho', is_editable: true }, + { id: 'moraid', title: 'Mora', is_editable: true }, + ], + )); - renderWizard(); + renderWizard(); + + const selectStreamButton = await screen.findByRole('button', { + name: /Select Stream/i, + hidden: true, + }); + + fireEvent.click(selectStreamButton); + + const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); + + await selectEvent.openMenu(streamSelect); + + await selectEvent.select(streamSelect, 'Aloho'); + }); - const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); + it('should show a warning if the selected stream has connected pipelines', async () => { + asMock(useStreams).mockReturnValue(useStreamsResult( + [ + { id: 'alohoid', title: 'Aloho', is_editable: true }, + { id: 'moraid', title: 'Mora', is_editable: true }, + ], + )); - await selectEvent.openMenu(streamSelect); + asMock(usePipelinesConnectedStream).mockReturnValue(pipelinesConnectedMock([ + { id: 'pipeline1', title: 'Pipeline1' }, + { id: 'pipeline2', title: 'Pipeline2' }, + ])); - await selectEvent.select(streamSelect, 'Aloho'); + renderWizard(); + + const selectStreamButton = await screen.findByRole('button', { + name: /Select Stream/i, + hidden: true, + }); + + fireEvent.click(selectStreamButton); - const warning = await screen.findByText(/The selected stream has existing pipelines/i); - const warningPipeline1 = await screen.findByText(/Pipeline1/i); - const warningPipeline2 = await screen.findByText(/Pipeline2/i); + const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); - expect(warning).toBeInTheDocument(); - expect(warningPipeline1).toBeInTheDocument(); - expect(warningPipeline2).toBeInTheDocument(); + await selectEvent.openMenu(streamSelect); + + await selectEvent.select(streamSelect, 'Aloho'); + + const warning = await screen.findByText(/Pipelines connected to target Stream/i); + const warningPipeline1 = await screen.findByText(/Pipeline1/i); + const warningPipeline2 = await screen.findByText(/Pipeline2/i); + + expect(warning).toBeInTheDocument(); + expect(warningPipeline1).toBeInTheDocument(); + expect(warningPipeline2).toBeInTheDocument(); + }); }); describe('Stream creation', () => { @@ -323,7 +364,7 @@ describe('InputSetupWizard Setup Routing', () => { }); const nextStepButton = await screen.findByRole('button', { - name: /Finish & Start Input/i, + name: /Skip & Start Input/i, hidden: true, }); diff --git a/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.StartInput.test.tsx b/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.StartInput.test.tsx index 61184248d6e2..7cfb3ddf2691 100644 --- a/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.StartInput.test.tsx +++ b/graylog2-web-interface/src/components/inputs/InputSetupWizard/InputSetupWizard.StartInput.test.tsx @@ -184,7 +184,7 @@ const newPipelineConfig = { }; const goToStartInputStep = async () => { - const nextButton = await screen.findByRole('button', { name: /Finish & Start Input/i, hidden: true }); + const nextButton = await screen.findByRole('button', { name: /& Start Input/i, hidden: true }); fireEvent.click(nextButton); }; @@ -265,6 +265,13 @@ describe('InputSetupWizard Start Input', () => { it('should start input when an existing stream is selected', async () => { renderWizard(); + const selectStreamButton = await screen.findByRole('button', { + name: /Select Stream/i, + hidden: true, + }); + + fireEvent.click(selectStreamButton); + const streamSelect = await screen.findByLabelText(/All messages \(Default\)/i); await selectEvent.openMenu(streamSelect); diff --git a/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/SetupRoutingStep.tsx b/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/SetupRoutingStep.tsx index f194b673730c..2c6719703c9a 100644 --- a/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/SetupRoutingStep.tsx +++ b/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/SetupRoutingStep.tsx @@ -19,7 +19,7 @@ import { useEffect, useMemo, useState, useCallback } from 'react'; import styled, { css } from 'styled-components'; import { Alert, Button, Row, Col } from 'components/bootstrap'; -import { Select } from 'components/common'; +import { Select, Tooltip } from 'components/common'; import useInputSetupWizard from 'components/inputs/InputSetupWizard/hooks/useInputSetupWizard'; import useInputSetupWizardSteps from 'components/inputs/InputSetupWizard/hooks/useInputSetupWizardSteps'; import { defaultCompare } from 'logic/DefaultCompare'; @@ -50,9 +50,9 @@ const ExistingStreamCol = styled(Col)(({ theme }) => css` `); const CreateStreamCol = styled(Col)(({ theme }) => css` - border-left: 1px solid ${theme.colors.cards.border}; padding-top: ${theme.spacings.sm}; padding-bottom: ${theme.spacings.md}; + border-right: 1px solid ${theme.colors.cards.border}; `); const ButtonCol = styled(Col)(({ theme }) => css` @@ -62,7 +62,14 @@ const ButtonCol = styled(Col)(({ theme }) => css` margin-top: ${theme.spacings.lg}; `); -const ConntectedPipelinesList = styled.ul` +const StyledTooltip = styled(Tooltip)(({ theme }) => css` + &.mantine-Tooltip-tooltip { + background-color: ${theme.colors.global.background}!important; + font-size: ${theme.fonts.size.small}!important; + } +`); + +const StyledList = styled.ul` list-style-type: disc; padding-left: 20px; `; @@ -89,6 +96,7 @@ const SetupRoutingStep = () => { const { stepsData, setStepsData } = useInputSetupWizardSteps(); const newStream: StreamFormValues = getStepConfigOrData(stepsData, currentStepName, 'newStream'); const [selectedStreamId, setSelectedStreamId] = useState(undefined); + const [showSelectStream, setShowSelectStream] = useState(false); const [showCreateStream, setShowCreateStream] = useState(false); const hasPreviousStep = checkHasPreviousStep(orderedSteps, activeStep); const hasNextStep = checkHasNextStep(orderedSteps, activeStep); @@ -162,6 +170,10 @@ const SetupRoutingStep = () => { setShowCreateStream(true); }; + const handleSelectStream = () => { + setShowSelectStream(true); + }; + const onNextStep = () => { goToNextStep(); }; @@ -171,6 +183,8 @@ const SetupRoutingStep = () => { updateStepConfigOrData(stepsData, currentStepName, defaultStepData, true), ); + setSelectedStreamId(undefined); + setShowSelectStream(false); setShowCreateStream(false); goToPreviousStep(); @@ -189,74 +203,116 @@ const SetupRoutingStep = () => { }; const backButtonText = newStream ? 'Reset' : 'Back'; + const nextButtonText = showCreateStream || showSelectStream ? 'Finish & Start Input' : 'Skip & Start Input'; const showNewStreamSection = newStream || showCreateStream; return ( - - -

- Choose a Destination Stream to route Messages from this Input to. Messages that are not - routed to any Streams will be sent to the "All Messages" Stream. -

-
-
- {selectedStreamId && streamHasConnectedPipelines && ( + {!showNewStreamSection && !showSelectStream && ( + <> - - - The selected Stream has existing Pipelines connected to it: - - {streamPipelinesData.map((pipeline) =>
  • {pipeline.title}
  • )} -
    -
    - + + +
  • + Select a destination Stream to route messages from this input to. +
  • +
  • + We recommend creating a new stream for each new input. This will help categorise your messages into a basic schema. +
  • +
  • + Messages that are not routed to any Stream will be routed to the Default Stream. +
  • +
  • + Pipeline rules can be automatically created and attached to the Default Stream by this Wizard. +
  • +
    +
    - )} - {showNewStreamSection ? ( - - - - Create new Stream - - {newStream ? ( - <> -

    This Input will use a new stream: "{newStream.title}".

    -

    Matches will {!newStream.remove_matches_from_default_stream && ('not ')}be removed from the Default Stream.

    - {getStepConfigOrData(stepsData, currentStepName, 'shouldCreateNewPipeline') && (

    A new Pipeline will be created.

    )} - - ) : ( - - )} - -
    - ) : ( - - - Choose an existing Stream - {!isLoadingStreams && ( - + + + )} + )} + {(hasPreviousStep || hasNextStep || showNewStreamSection) && ( - {(hasPreviousStep || showNewStreamSection) && ()} - {hasNextStep && ()} + {(hasPreviousStep || showNewStreamSection || showSelectStream) && ()} + {hasNextStep && ()} )} diff --git a/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/StartInputStep.tsx b/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/StartInputStep.tsx index 5f548f18ac9a..7958af06ee2c 100644 --- a/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/StartInputStep.tsx +++ b/graylog2-web-interface/src/components/inputs/InputSetupWizard/steps/StartInputStep.tsx @@ -18,7 +18,9 @@ import * as React from 'react'; import { useEffect, useState, useMemo } from 'react'; import styled, { css } from 'styled-components'; import type { UseMutationResult } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import Routes from 'routing/Routes'; import useSetupInputMutations from 'components/inputs/InputSetupWizard/hooks/useSetupInputMutations'; import { InputStatesStore } from 'stores/inputs/InputStatesStore'; import { Button, Row, Col } from 'components/bootstrap'; @@ -54,7 +56,8 @@ const ButtonCol = styled(Col)(({ theme }) => css` export type ProcessingSteps = 'createStream' | 'startStream' | 'createPipeline' | 'setupRouting' | 'deleteStream' | 'deletePipeline' | 'deleteRouting' | 'result'; const StartInputStep = () => { - const { goToPreviousStep, goToNextStep, orderedSteps, activeStep, wizardData, stepsConfig } = useInputSetupWizard(); + const navigateTo = useNavigate(); + const { goToPreviousStep, orderedSteps, activeStep, wizardData, stepsConfig } = useInputSetupWizard(); const { stepsData } = useInputSetupWizardSteps(); const hasPreviousStep = checkHasPreviousStep(orderedSteps, activeStep); const hasNextStep = checkHasNextStep(orderedSteps, activeStep); @@ -234,8 +237,12 @@ const StartInputStep = () => { rollback(); }; - const onNextStep = () => { - goToNextStep(); + const goToInputDiagnosis = () => { + const { input } = wizardData; + + if (!input) return; + + navigateTo(Routes.SYSTEM.INPUT_DIAGNOSIS(input.id)); }; const handleBackClick = () => { @@ -311,7 +318,7 @@ const StartInputStep = () => { if (hasNextStep) { return ( - + ); }