diff --git a/graylog2-server/src/main/java/org/graylog2/rest/resources/streams/StreamResource.java b/graylog2-server/src/main/java/org/graylog2/rest/resources/streams/StreamResource.java index 12d3d1dff7e0..2a304a066582 100644 --- a/graylog2-server/src/main/java/org/graylog2/rest/resources/streams/StreamResource.java +++ b/graylog2-server/src/main/java/org/graylog2/rest/resources/streams/StreamResource.java @@ -49,6 +49,11 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; import org.bson.types.ObjectId; import org.graylog.grn.GRNTypes; +import org.graylog.plugins.pipelineprocessor.db.PipelineDao; +import org.graylog.plugins.pipelineprocessor.db.PipelineService; +import org.graylog.plugins.pipelineprocessor.db.PipelineStreamConnectionsService; +import org.graylog.plugins.pipelineprocessor.rest.PipelineCompactSource; +import org.graylog.plugins.pipelineprocessor.rest.PipelineConnections; import org.graylog.plugins.views.startpage.recentActivities.RecentActivityService; import org.graylog.security.UserContext; import org.graylog2.audit.AuditEventSender; @@ -103,6 +108,7 @@ import org.joda.time.format.ISODateTimeFormat; import java.net.URI; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -154,6 +160,8 @@ public class StreamResource extends RestResource { private final BulkExecutor bulkStreamDeleteExecutor; private final BulkExecutor bulkStreamStartExecutor; private final BulkExecutor bulkStreamStopExecutor; + private final PipelineStreamConnectionsService pipelineStreamConnectionsService; + private final PipelineService pipelineService; private final DbQueryCreator dbQueryCreator; private final Set streamDeletionGuards; @@ -167,13 +175,17 @@ public StreamResource(StreamService streamService, RecentActivityService recentActivityService, AuditEventSender auditEventSender, MessageFactory messageFactory, - Set streamDeletionGuards) { + Set streamDeletionGuards, + PipelineStreamConnectionsService pipelineStreamConnectionsService, + PipelineService pipelineService) { this.streamService = streamService; this.streamRuleService = streamRuleService; this.streamRouterEngineFactory = streamRouterEngineFactory; this.indexSetRegistry = indexSetRegistry; this.paginatedStreamService = paginatedStreamService; this.messageFactory = messageFactory; + this.pipelineStreamConnectionsService = pipelineStreamConnectionsService; + this.pipelineService = pipelineService; this.dbQueryCreator = new DbQueryCreator(StreamImpl.FIELD_TITLE, attributes); this.recentActivityService = recentActivityService; final SuccessContextCreator successAuditLogContextCreator = (entity, entityClass) -> @@ -596,6 +608,21 @@ public Response cloneStream(@ApiParam(name = "streamId", required = true) @PathP return Response.created(streamUri).entity(result).build(); } + @GET + @Path("/{streamId}/pipelines") + @ApiOperation(value = "Get pipelines associated with a stream") + @Produces(MediaType.APPLICATION_JSON) + public List getConnectedPipelines(@ApiParam(name = "streamId", required = true) @PathParam("streamId") String streamId) throws NotFoundException { + PipelineConnections pipelineConnections = pipelineStreamConnectionsService.load(streamId); + List list = new ArrayList<>(); + + for (String id : pipelineConnections.pipelineIds()) { + PipelineDao pipelineDao = pipelineService.load(id); + list.add(PipelineCompactSource.create(pipelineDao.id(), pipelineDao.title())); + } + return list; + } + @PUT @Path("/indexSet/{indexSetId}") @Timed diff --git a/graylog2-web-interface/src/@types/graylog-web-plugin/index.d.ts b/graylog2-web-interface/src/@types/graylog-web-plugin/index.d.ts index 59250ef87f79..bfbbdf65febb 100644 --- a/graylog2-web-interface/src/@types/graylog-web-plugin/index.d.ts +++ b/graylog2-web-interface/src/@types/graylog-web-plugin/index.d.ts @@ -137,6 +137,15 @@ type DataTiering = { }>, } +interface PluginDataWarehouse { + DataWarehouseStatus: React.ComponentType<{ + stream: { + enabled_status: boolean; + } + }>, + StreamDataWarehouse: React.ComponentType<{}>, +} + type FieldValueProvider = { type: string, displayName: string, @@ -153,12 +162,25 @@ type FieldValueProvider = { key_field?: string, }, } + +interface PluginDataWarehouse { + DataWarehouseStatus: React.ComponentType<{ + stream: { + enabled_status: boolean; + } + }>, + StreamDataWarehouse: React.ComponentType<{}>, + DataWarehouseJobs: React.ComponentType<{}>, +} + declare module 'graylog-web-plugin/plugin' { interface PluginExports { navigation?: Array; + dataWarehouse?: Array dataTiering?: Array defaultNavigation?: Array; navigationItems?: Array; + dataWarehouse?: Array globalNotifications?: Array; fieldValueProviders?:Array; // Global context providers allow to fetch and process data once diff --git a/graylog2-web-interface/src/components/common/Section.tsx b/graylog2-web-interface/src/components/common/Section.tsx index 0beb3c889531..94610bc8ce38 100644 --- a/graylog2-web-interface/src/components/common/Section.tsx +++ b/graylog2-web-interface/src/components/common/Section.tsx @@ -25,26 +25,27 @@ const Container = styled.div(({ theme }) => css` padding: 15px; `); -const Header = styled.div` +const Header = styled.div<{ $alignActionsLeft: boolean; }>(({ theme, $alignActionsLeft }) => css` display: flex; - justify-content: space-between; + justify-content: ${$alignActionsLeft ? 'flex-start' : 'space-between'}; + ${$alignActionsLeft ? `gap: ${theme.spacings.sm};` : 'gap: 5px;'}; align-items: center; margin-bottom: 10px; flex-wrap: wrap; - gap: 5px; -`; +`); type Props = React.PropsWithChildren<{ title: React.ReactNode, actions?: React.ReactNode, + alignActionLeft?: boolean, }> /** * Simple section component. Currently only a "filled" version exists. */ -const Section = ({ title, actions, children }: Props) => ( +const Section = ({ title, actions, children, alignActionLeft }: Props) => ( -
+

{title}

{actions &&
{actions}
}
@@ -54,6 +55,7 @@ const Section = ({ title, actions, children }: Props) => ( Section.defaultProps = { actions: undefined, + alignActionLeft: false, }; export default Section; diff --git a/graylog2-web-interface/src/components/common/Text.tsx b/graylog2-web-interface/src/components/common/Text.tsx new file mode 100644 index 000000000000..58d8d6557868 --- /dev/null +++ b/graylog2-web-interface/src/components/common/Text.tsx @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import type { TextProps } from '@mantine/core'; +import { Text as MantineText } from '@mantine/core'; + +type Props = { + children: string, +} & TextProps; + +const Text = ({ ...props }: Props) => ( + +); + +export default Text; diff --git a/graylog2-web-interface/src/components/common/index.tsx b/graylog2-web-interface/src/components/common/index.tsx index 481dfccf573a..f053c430b8fb 100644 --- a/graylog2-web-interface/src/components/common/index.tsx +++ b/graylog2-web-interface/src/components/common/index.tsx @@ -112,6 +112,7 @@ export { default as TimeUnit } from './TimeUnit'; export { default as TimeUnitInput } from './TimeUnitInput'; export { default as Timestamp } from './Timestamp'; export { default as TimezoneSelect } from './TimezoneSelect'; +export { default as Text } from './Text'; export { default as Tooltip } from './Tooltip'; export { default as TypeAheadDataFilter } from './TypeAheadDataFilter'; export { default as TypeAheadFieldInput } from './TypeAheadFieldInput'; diff --git a/graylog2-web-interface/src/components/streamrules/CreateStreamRuleButton.tsx b/graylog2-web-interface/src/components/streamrules/CreateStreamRuleButton.tsx new file mode 100644 index 000000000000..075eb6e2998a --- /dev/null +++ b/graylog2-web-interface/src/components/streamrules/CreateStreamRuleButton.tsx @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useCallback, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import PropTypes from 'prop-types'; + +import { Button } from 'components/bootstrap'; +import type { BsSize } from 'components/bootstrap/types'; +import type { StyleProps } from 'components/bootstrap/Button'; +import type { StreamRule } from 'stores/streams/StreamsStore'; +import { StreamRulesStore } from 'stores/streams/StreamRulesStore'; +import UserNotification from 'util/UserNotification'; + +import StreamRuleModal from './StreamRuleModal'; + +type Props = { + bsSize?: BsSize, + bsStyle?: StyleProps, + buttonText?: string, + className?: string, + streamId: string, +} + +const CreateStreamRuleButton = ({ bsSize, bsStyle, buttonText, className, streamId }: Props) => { + const [showCreateModal, setShowCreateModal] = useState(false); + const queryClient = useQueryClient(); + const toggleCreateModal = useCallback(() => setShowCreateModal((cur) => !cur), []); + + const onSaveStreamRule = useCallback((_streamRuleId: string, streamRule: StreamRule) => StreamRulesStore.create(streamId, streamRule, () => { + UserNotification.success('Stream rule was created successfully.', 'Success'); + queryClient.invalidateQueries(['stream', streamId]); + }), [streamId, queryClient]); + + return ( + <> + + {showCreateModal && ( + + + )} + {} + + ); +}; + +CreateStreamRuleButton.propTypes = { + buttonText: PropTypes.string, + bsStyle: PropTypes.string, + bsSize: PropTypes.string, + className: PropTypes.string, + streamId: PropTypes.string, +}; + +CreateStreamRuleButton.defaultProps = { + buttonText: 'Create Rule', + bsSize: undefined, + bsStyle: undefined, + className: undefined, + streamId: undefined, +}; + +export default CreateStreamRuleButton; diff --git a/graylog2-web-interface/src/components/streamrules/DetailsStreamRule.tsx b/graylog2-web-interface/src/components/streamrules/DetailsStreamRule.tsx new file mode 100644 index 000000000000..8e6b435c487f --- /dev/null +++ b/graylog2-web-interface/src/components/streamrules/DetailsStreamRule.tsx @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ + +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { useQueryClient } from '@tanstack/react-query'; + +import UserNotification from 'util/UserNotification'; +import { isPermitted } from 'util/PermissionsMixin'; +import HumanReadableStreamRule from 'components/streamrules/HumanReadableStreamRule'; +import { useStore } from 'stores/connect'; +import { ConfirmDialog, Icon } from 'components/common'; +import { Button } from 'components/bootstrap'; +import StreamRuleModal from 'components/streamrules/StreamRuleModal'; +import { StreamRulesInputsActions, StreamRulesInputsStore } from 'stores/inputs/StreamRulesInputsStore'; +import { StreamRulesStore } from 'stores/streams/StreamRulesStore'; +import type { StreamRule as StreamRuleTypeDefinition, Stream, StreamRule } from 'stores/streams/StreamsStore'; + +import useCurrentUser from '../../hooks/useCurrentUser'; + +const ActionButtonsWrap = styled.span` + margin-right: 6px; + float: right; +`; + +type Props = { + matchData?: { + matches: boolean, + rules: { [id: string]: false }, + }, + stream: Stream, + onDelete: (ruleId: string) => void, + onSubmit: (ruleId: string, data: unknown) => void, + streamRule: StreamRuleTypeDefinition +} + +const DetailsStreamRule = ({ stream, streamRule, onSubmit, onDelete }: Props) => { + const { permissions } = useCurrentUser(); + const [showStreamRuleForm, setShowStreamRuleForm] = useState(false); + const [showConfirmDelete, setShowConfirmDelete] = useState(false); + const { inputs } = useStore(StreamRulesInputsStore); + const queryClient = useQueryClient(); + const STREAM_QUERY_KEY = ['stream', stream.id]; + + useEffect(() => { + StreamRulesInputsActions.list(); + }, []); + + const onConfirmDelete = () => { + StreamRulesStore.remove(stream.id, streamRule.id, () => { + if (onDelete) { + onDelete(streamRule.id); + } + + queryClient.invalidateQueries(STREAM_QUERY_KEY); + setShowConfirmDelete(false); + UserNotification.success('Stream rule has been successfully deleted.', 'Success'); + }); + }; + + const _onSubmit = (streamRuleId: string, data: StreamRule) => StreamRulesStore.update(stream.id, streamRuleId, data, () => { + if (onSubmit) { + onSubmit(streamRuleId, data); + } + + queryClient.invalidateQueries(STREAM_QUERY_KEY); + UserNotification.success('Stream rule has been successfully updated.', 'Success'); + }); + + const _formatActionItems = () => ( + + + + + ); + + const actionItems = isPermitted(permissions, [`streams:edit:${stream.id}`]) ? _formatActionItems() : null; + const description = streamRule.description ? {' '}({streamRule.description}) : null; + + return ( + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + {description} + + {actionItems} + {showStreamRuleForm && ( + setShowStreamRuleForm(false)} + title="Edit Stream Rule" + submitButtonText="Update Rule" + submitLoadingText="Updating Rule..." + onSubmit={_onSubmit} /> + )} + {showConfirmDelete && ( + setShowConfirmDelete(false)} + title="Delete stream rule." + btnConfirmText="Ok"> + Do you really want to delete this stream rule? + + )} + + ); +}; + +DetailsStreamRule.propTypes = { + matchData: PropTypes.shape({ + matches: PropTypes.bool, + rules: PropTypes.object, + }), + onDelete: PropTypes.func, + onSubmit: PropTypes.func, + stream: PropTypes.object.isRequired, + streamRule: PropTypes.object.isRequired, +}; + +DetailsStreamRule.defaultProps = { + matchData: {}, + onSubmit: () => {}, + onDelete: () => {}, +}; + +export default DetailsStreamRule; diff --git a/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingDestinations.tsx b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingDestinations.tsx new file mode 100644 index 000000000000..0621ed37a294 --- /dev/null +++ b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingDestinations.tsx @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ + +import * as React from 'react'; +import { PluginStore } from 'graylog-web-plugin/plugin'; + +import { Section } from 'components/common'; + +const StreamDataRoutingDestinations = () => { + const StreamDataWarehouseComponent = PluginStore.exports('dataWarehouse')?.[0]?.StreamDataWarehouse; + + return ( + <> +
+
+
IndexSet Name: Default index set
+
+ +
+ + + ); +}; + +export default StreamDataRoutingDestinations; diff --git a/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingIntake.tsx b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingIntake.tsx new file mode 100644 index 000000000000..ce9ba1298ef9 --- /dev/null +++ b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingIntake.tsx @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import { type Stream } from 'stores/streams/StreamsStore'; +import { Table } from 'components/bootstrap'; +import DetailsStreamRule from 'components/streamrules/DetailsStreamRule'; +import { IfPermitted, Section } from 'components/common'; +import CreateStreamRuleButton from 'components/streamrules/CreateStreamRuleButton'; + +type Props = { + stream: Stream, +} + +export const Headline = styled.h2(({ theme }) => css` + margin-top: ${theme.spacings.sm}; + margin-bottom: ${theme.spacings.xs}; +`); + +const StreamDataRoutingInstake = ({ stream }: Props) => { + const hasStreamRules = !!stream.rules?.length; + + return ( +
+ + + )}> + + + + + + + + {hasStreamRules && stream.rules.map((streamRule) => ( + + ))} + + {!hasStreamRules && ( + + + + )} + +
Rule
No rules defined.
+
+ ); +}; + +export default StreamDataRoutingInstake; diff --git a/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingProcessing.tsx b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingProcessing.tsx new file mode 100644 index 000000000000..3bfbe39508ab --- /dev/null +++ b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDataRoutingProcessing.tsx @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; + +import usePipelinesConnectedStream from 'hooks/usePipelinesConnectedStream'; +import { Table } from 'components/bootstrap'; +import { Link } from 'components/common/router'; +import Routes from 'routing/Routes'; +import { Section } from 'components/common'; + +const StreamDataRoutingProcessing = () => { + const { streamId } = useParams<{streamId: string}>(); + const { data: connectedPipelines, isInitialLoading } = usePipelinesConnectedStream(streamId); + const hasConnectedPipelines = !isInitialLoading && connectedPipelines?.length; + + return ( + <> +
+

Illuminate Processing step

+
+
+ + + + + + + + {hasConnectedPipelines && connectedPipelines.map((pipeline) => ( + + + + ))} + {!hasConnectedPipelines && ( + + + + )} + +
Pipeline
+ {pipeline.title} +
This stream is not connected to any Pipeline.
+
+ + ); +}; + +export default StreamDataRoutingProcessing; diff --git a/graylog2-web-interface/src/components/streams/StreamDetails/StreamDetails.tsx b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDetails.tsx new file mode 100644 index 000000000000..006f485b8015 --- /dev/null +++ b/graylog2-web-interface/src/components/streams/StreamDetails/StreamDetails.tsx @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import { useState } from 'react'; +import { PluginStore } from 'graylog-web-plugin/plugin'; + +import Routes from 'routing/Routes'; +import { + Button, + Col, + DropdownButton, + MenuItem, + Row, + SegmentedControl, +} from 'components/bootstrap'; +import { Icon, IfPermitted } from 'components/common'; +import type { Stream } from 'stores/streams/StreamsStore'; +import SectionGrid from 'components/common/Section/SectionGrid'; + +import StreamDataRoutingIntake from './StreamDataRoutingIntake'; +import StreamDataRoutingProcessing from './StreamDataRoutingProcessing'; +import StreamDataRoutingDestinations from './StreamDataRoutingDestinations'; + +type Props = { + stream: Stream, +}; +const INTAKE_SEGMENT = 'intake'; +const PROCESSING_SEGMENT = 'processing'; +const DESTINATIONS_SEGMENT = 'destinations'; + +const SEGMENTS_DETAILS = [ + { + value: 'intake' as const, + label: 'Intake', + }, + { + value: 'processing' as const, + label: 'Processing', + }, + { + value: 'destinations' as const, + label: 'Destinations', + }, +]; + +type DetailsSegments = 'intake' | 'processing' | 'destinations'; + +const Container = styled.div` + display: flex; + height: 100%; + flex-direction: column; +`; + +const Header = styled.div` + display: flex; + justify-content: space-between; + padding-top: 30px; + margin-bottom: 20px; + gap: 15px; + margin-left: -15px; + margin-right: -15px; +`; + +const LeftCol = styled.div` + display: flex; + gap: 20px; + align-items: center; +`; + +const RightCol = styled.div` + display: flex; + gap: 30px; +`; + +const MainDetailsRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; +`; + +const SegmentContainer = styled(Row)` + flex: 1; +`; + +const FullHeightCol = styled(Col)` + height: 100%; +`; + +const StyledSectionGrid = styled(SectionGrid)` + align-items: center; +`; + +const StreamDetails = ({ stream }: Props) => { + const navigate = useNavigate(); + const [currentSegment, setCurrentSegment] = useState(INTAKE_SEGMENT); + const DataWarehouseJobComponent = PluginStore.exports('dataWarehouse')?.[0]?.DataWarehouseJobs; + + return ( + <> + + +
+ + + +

{stream.title}

+ + + } id="stream-actions" noCaret bsSize="xs"> + {}}>Edit + + +
+ +
+ + + + +

Data Routing

+ + data={SEGMENTS_DETAILS} + value={currentSegment} + onChange={setCurrentSegment} /> + +
+ +
+ + + {currentSegment === INTAKE_SEGMENT && } + {currentSegment === PROCESSING_SEGMENT && } + {currentSegment === DESTINATIONS_SEGMENT && } + + +
+ + ); +}; + +export default StreamDetails; diff --git a/graylog2-web-interface/src/components/streams/StreamDetails/routing-destination/IndexSetDestination.tsx b/graylog2-web-interface/src/components/streams/StreamDetails/routing-destination/IndexSetDestination.tsx new file mode 100644 index 000000000000..e76185d7a40e --- /dev/null +++ b/graylog2-web-interface/src/components/streams/StreamDetails/routing-destination/IndexSetDestination.tsx @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ diff --git a/graylog2-web-interface/src/components/streams/StreamsOverview/ColumnRenderers.tsx b/graylog2-web-interface/src/components/streams/StreamsOverview/ColumnRenderers.tsx index 88e6f54ed7e3..4020cdf286cc 100644 --- a/graylog2-web-interface/src/components/streams/StreamsOverview/ColumnRenderers.tsx +++ b/graylog2-web-interface/src/components/streams/StreamsOverview/ColumnRenderers.tsx @@ -15,35 +15,21 @@ * . */ import * as React from 'react'; -import styled from 'styled-components'; import type { Stream, StreamRule } from 'stores/streams/StreamsStore'; -import { Label } from 'components/bootstrap'; -import { Link } from 'components/common/router'; -import Routes from 'routing/Routes'; import type { ColumnRenderers } from 'components/common/EntityDataTable'; import IndexSetCell from 'components/streams/StreamsOverview/cells/IndexSetCell'; +import TitleCell from 'components/streams/StreamsOverview/cells/TitleCell'; import ThroughputCell from 'components/streams/StreamsOverview/cells/ThroughputCell'; import type { IndexSet } from 'stores/indices/IndexSetsStore'; import StatusCell from './cells/StatusCell'; import StreamRulesCell from './cells/StreamRulesCell'; -const DefaultLabel = styled(Label)` - display: inline-flex; - margin-left: 5px; - vertical-align: inherit; -`; - const customColumnRenderers = (indexSets: Array): ColumnRenderers => ({ attributes: { title: { - renderCell: (title: string, stream) => ( - <> - {title} - {stream.is_default && Default} - - ), + renderCell: (_title: string, stream) => , }, index_set_title: { renderCell: (_index_set_title: string, stream) => , @@ -51,15 +37,15 @@ const customColumnRenderers = (indexSets: Array): ColumnRenderers , - staticWidth: 120, + staticWidth: 130, }, disabled: { renderCell: (_disabled: string, stream) => , - staticWidth: 100, + staticWidth: 130, }, rules: { renderCell: (_rules: StreamRule[], stream) => , - staticWidth: 70, + staticWidth: 130, }, }, }); diff --git a/graylog2-web-interface/src/components/streams/StreamsOverview/Constants.ts b/graylog2-web-interface/src/components/streams/StreamsOverview/Constants.ts index ff6c3c0cc7df..ead9946f9548 100644 --- a/graylog2-web-interface/src/components/streams/StreamsOverview/Constants.ts +++ b/graylog2-web-interface/src/components/streams/StreamsOverview/Constants.ts @@ -21,8 +21,8 @@ export const ENTITY_TABLE_ID = 'streams'; export const DEFAULT_LAYOUT = { pageSize: 20, sort: { attributeId: 'title', direction: 'asc' } as Sort, - displayedColumns: ['title', 'description', 'index_set_title', 'rules', 'throughput', 'disabled'], - columnsOrder: ['title', 'description', 'index_set_title', 'rules', 'throughput', 'disabled', 'created_at'], + displayedColumns: ['title', 'index_set_title', 'rules', 'throughput', 'disabled'], + columnsOrder: ['title', 'index_set_title', 'rules', 'throughput', 'disabled', 'created_at'], }; export const ADDITIONAL_ATTRIBUTES = [ diff --git a/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx b/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx index 19c97738fd13..0c28eb3361c2 100644 --- a/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx +++ b/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx @@ -18,7 +18,7 @@ import * as React from 'react'; import { useState, useCallback } from 'react'; import { ShareButton, IfPermitted, HoverForHelp } from 'components/common'; -import { ButtonToolbar, MenuItem } from 'components/bootstrap'; +import { Button, ButtonToolbar, MenuItem } from 'components/bootstrap'; import type { Stream, StreamRule } from 'stores/streams/StreamsStore'; import StreamsStore from 'stores/streams/StreamsStore'; import Routes from 'routing/Routes'; @@ -34,6 +34,7 @@ import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import useSelectedEntities from 'components/common/EntityDataTable/hooks/useSelectedEntities'; import { MoreActions } from 'components/common/EntityDataTable'; +import { LinkContainer } from 'components/common/router'; import StreamModal from '../StreamModal'; @@ -155,6 +156,9 @@ const StreamActions = ({ return ( + + + . + */ +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import Routes from 'routing/Routes'; +import { Link } from 'components/common/router'; +import type { Stream } from 'stores/streams/StreamsStore'; +import { Label } from 'components/bootstrap'; +import { Text } from 'components/common'; + +type Props = { + stream: Stream, +}; +const DefaultLabel = styled(Label)` + display: inline-flex; + margin-left: 5px; + vertical-align: inherit; +`; + +const StyledText = styled(Text)(({ theme }) => css` + color: ${theme.colors.gray[50]}; +`); + +const TitleCell = ({ stream }: Props) => ( + <> + {stream.title} + {stream.is_default && Default} + {stream.description} + +); + +export default TitleCell; diff --git a/graylog2-web-interface/src/components/streams/hooks/useStream.ts b/graylog2-web-interface/src/components/streams/hooks/useStream.ts index 4358ca02345d..5eb979cadab8 100644 --- a/graylog2-web-interface/src/components/streams/hooks/useStream.ts +++ b/graylog2-web-interface/src/components/streams/hooks/useStream.ts @@ -35,7 +35,7 @@ const useStream = (streamId: string, { enabled } = { enabled: true }): { isError, } => { const { data, refetch, isFetching, isError } = useQuery( - ['streams', streamId], + ['stream', streamId], () => fetchStream(streamId), { onError: (errorThrown) => { diff --git a/graylog2-web-interface/src/hooks/usePipelinesConnectedStream.ts b/graylog2-web-interface/src/hooks/usePipelinesConnectedStream.ts new file mode 100644 index 000000000000..a2016f46a8ff --- /dev/null +++ b/graylog2-web-interface/src/hooks/usePipelinesConnectedStream.ts @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import { useQuery } from '@tanstack/react-query'; + +import { qualifyUrl } from 'util/URLUtils'; +import fetch from 'logic/rest/FetchProvider'; +import ApiRoutes from 'routing/ApiRoutes'; +import type FetchError from 'logic/errors/FetchError'; +import type { PipelineType } from 'stores/pipelines/PipelinesStore'; + +export type StreamConnectedPipelines = Array> + +const fetchPipelinesConnectedStream = (streamId: string) => fetch('GET', qualifyUrl(ApiRoutes.StreamsApiController.stream_connected_pipelines(streamId).url)); + +const usePipelinesConnectedStream = (streamId: string): { + data: StreamConnectedPipelines, + refetch: () => void, + isInitialLoading: boolean, + error: FetchError, +} => { + const { data, refetch, isInitialLoading, error } = useQuery( + ['stream', 'pipelines', streamId], + () => fetchPipelinesConnectedStream(streamId), + { + notifyOnChangeProps: ['data', 'error'], + }, + ); + + return ({ + data, + refetch, + isInitialLoading, + error, + }); +}; + +export default usePipelinesConnectedStream; diff --git a/graylog2-web-interface/src/hooks/useStream.ts b/graylog2-web-interface/src/hooks/useStream.ts new file mode 100644 index 000000000000..ba2e43e2ba71 --- /dev/null +++ b/graylog2-web-interface/src/hooks/useStream.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import { useQuery } from '@tanstack/react-query'; + +import UserNotification from 'util/UserNotification'; +import fetch from 'logic/rest/FetchProvider'; +import { qualifyUrl } from 'util/URLUtils'; +import ApiRoutes from 'routing/ApiRoutes'; +import type { Stream } from 'stores/streams/StreamsStore'; +import type FetchError from 'logic/errors/FetchError'; + +const fetchStream = (streamId: string) => fetch('GET', qualifyUrl(ApiRoutes.StreamsApiController.get(streamId).url)); + +const useStream = (streamId: string): { + data: Stream, + refetch: () => void, + isInitialLoading: boolean, + error: FetchError, +} => { + const { data, refetch, isInitialLoading, error } = useQuery( + ['stream', streamId], + () => fetchStream(streamId), + { + onError: (errorThrown) => { + UserNotification.error(`Loading stream failed with status: ${errorThrown}`, + 'Could not load stream.'); + }, + notifyOnChangeProps: ['data', 'error'], + }, + ); + + return ({ + data, + refetch, + isInitialLoading, + error, + }); +}; + +export default useStream; diff --git a/graylog2-web-interface/src/pages/StreamDetailsPage.tsx b/graylog2-web-interface/src/pages/StreamDetailsPage.tsx new file mode 100644 index 000000000000..ee6cb4ff5c19 --- /dev/null +++ b/graylog2-web-interface/src/pages/StreamDetailsPage.tsx @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; + +import { Spinner } from 'components/common'; +import StreamDetails from 'components/streams/StreamDetails/StreamDetails'; +import useStream from 'components/streams/hooks/useStream'; + +const StreamDetailsPage = () => { + const { streamId } = useParams<{ streamId: string }>(); + const { data: stream, isFetching } = useStream(streamId); + + if (isFetching) { + return ; + } + + return ( + + ); +}; + +export default StreamDetailsPage; diff --git a/graylog2-web-interface/src/pages/index.jsx b/graylog2-web-interface/src/pages/index.jsx index 051a02437e9d..b42504ddf02a 100644 --- a/graylog2-web-interface/src/pages/index.jsx +++ b/graylog2-web-interface/src/pages/index.jsx @@ -96,6 +96,7 @@ const StartPage = loadAsync(() => import('./StartPage')); const StreamEditPage = loadAsync(() => import('./StreamEditPage')); const StreamOutputsPage = loadAsync(() => import('./StreamOutputsPage')); const StreamsPage = loadAsync(() => import('./StreamsPage')); +const StreamDetailsPage = loadAsync(() => import('./StreamDetailsPage')); const SystemOutputsPage = loadAsync(() => import('./SystemOutputsPage')); const SystemOverviewPage = loadAsync(() => import('./SystemOverviewPage')); const SystemLogsPage = loadAsync(() => import('./SystemLogsPage')); @@ -187,6 +188,7 @@ export { SimulatorPage, StartPage, StreamEditPage, + StreamDetailsPage, StreamOutputsPage, StreamsPage, SystemOutputsPage, diff --git a/graylog2-web-interface/src/routing/ApiRoutes.ts b/graylog2-web-interface/src/routing/ApiRoutes.ts index b27f234ec03e..dd1f626157f7 100644 --- a/graylog2-web-interface/src/routing/ApiRoutes.ts +++ b/graylog2-web-interface/src/routing/ApiRoutes.ts @@ -253,6 +253,7 @@ const ApiRoutes = { delete: (streamId: string) => ({ url: `/streams/${streamId}` }), pause: (streamId: string) => ({ url: `/streams/${streamId}/pause` }), resume: (streamId: string) => ({ url: `/streams/${streamId}/resume` }), + stream_connected_pipelines: (streamId: string) => ({ url: `/streams/${streamId}/pipelines` }), testMatch: (streamId: string) => ({ url: `/streams/${streamId}/testMatch` }), }, StreamOutputsApiController: { diff --git a/graylog2-web-interface/src/routing/AppRouter.tsx b/graylog2-web-interface/src/routing/AppRouter.tsx index 6f4bc083903f..fc73652b180e 100644 --- a/graylog2-web-interface/src/routing/AppRouter.tsx +++ b/graylog2-web-interface/src/routing/AppRouter.tsx @@ -95,6 +95,7 @@ import { StreamEditPage, StreamOutputsPage, StreamsPage, + StreamDetailsPage, SystemOutputsPage, SystemOverviewPage, ThreadDumpPage, @@ -172,6 +173,7 @@ const AppRouter = () => { { path: RoutePaths.message_show(':index', ':messageId'), element: }, { path: RoutePaths.WELCOME, element: }, { path: RoutePaths.STREAMS, element: }, + { path: RoutePaths.stream_view(':streamId'), element: }, { path: RoutePaths.stream_edit(':streamId'), element: }, !isCloud && { path: RoutePaths.stream_outputs(':streamId'), element: }, diff --git a/graylog2-web-interface/src/routing/Routes.ts b/graylog2-web-interface/src/routing/Routes.ts index f60b6d1a0f53..9a69447fa95a 100644 --- a/graylog2-web-interface/src/routing/Routes.ts +++ b/graylog2-web-interface/src/routing/Routes.ts @@ -259,6 +259,7 @@ const Routes = { }, search: (query: string, timeRange: RoutesTimeRange, resolution?: number) => Routes._common_search_url(Routes.SEARCH, query, timeRange, resolution), message_show: (index: string, messageId: string) => `/messages/${index}/${messageId}`, + stream_view: (streamId: string) => `/streams/${streamId}/view`, stream_edit: (streamId: string) => `/streams/${streamId}/edit`, stream_edit_example: (streamId: string, index: string, messageId: string) => `${Routes.stream_edit(streamId)}?index=${index}&message_id=${messageId}`, stream_outputs: (streamId: string) => `/streams/${streamId}/outputs`,