From 1c27c3beabd2135f8ac3003e3519f195cc567403 Mon Sep 17 00:00:00 2001 From: Maksym Yadlovskyi Date: Tue, 19 Nov 2024 14:18:51 +0100 Subject: [PATCH] Events page refactoring with entity data table usage (#20831) --- changelog/unreleased/issue-20881.toml | 5 + .../src/components/events/Constants.ts | 132 ++++++++ .../components/events/EventsEntityTable.tsx | 54 ++++ .../src/components/events/ExpandedSection.tsx | 49 +++ .../events/FilterValueRenderers.tsx | 27 ++ .../events/events/ColumnRenderers.tsx | 176 +++++++++++ .../components/events/events/EventActions.tsx | 47 +++ .../components/events/events/EventDetails.tsx | 131 -------- .../events/events/EventDetailsTable.tsx | 56 ++++ .../components/events/events/EventFields.tsx | 6 +- .../src/components/events/events/Events.tsx | 298 ------------------ .../events/events/EventsContainer.tsx | 200 ------------ .../events/events/EventsSearchBar.css | 41 --- .../events/events/EventsSearchBar.tsx | 112 ------- .../events/events/hooks/useEventAction.tsx | 37 +++ .../events/hooks/usePluggableEventActions.tsx | 45 +++ .../events/hooks/useTableComponents.tsx | 48 +++ .../src/components/events/events/types.ts | 5 + .../src/components/events/fetchEvents.ts | 87 +++++ .../navigation/NotificationBadge.tsx | 2 +- .../src/pages/EventsPage.tsx | 47 ++- .../events/EventsList/DefaultDetails.tsx | 54 ++++ .../events/EventsList/EventDetails.test.tsx | 3 +- .../events/EventsList/EventDetails.tsx | 4 +- graylog2-web-interface/src/views/types.ts | 21 +- 25 files changed, 864 insertions(+), 823 deletions(-) create mode 100644 changelog/unreleased/issue-20881.toml create mode 100644 graylog2-web-interface/src/components/events/Constants.ts create mode 100644 graylog2-web-interface/src/components/events/EventsEntityTable.tsx create mode 100644 graylog2-web-interface/src/components/events/ExpandedSection.tsx create mode 100644 graylog2-web-interface/src/components/events/FilterValueRenderers.tsx create mode 100644 graylog2-web-interface/src/components/events/events/ColumnRenderers.tsx create mode 100644 graylog2-web-interface/src/components/events/events/EventActions.tsx delete mode 100644 graylog2-web-interface/src/components/events/events/EventDetails.tsx create mode 100644 graylog2-web-interface/src/components/events/events/EventDetailsTable.tsx delete mode 100644 graylog2-web-interface/src/components/events/events/Events.tsx delete mode 100644 graylog2-web-interface/src/components/events/events/EventsContainer.tsx delete mode 100644 graylog2-web-interface/src/components/events/events/EventsSearchBar.css delete mode 100644 graylog2-web-interface/src/components/events/events/EventsSearchBar.tsx create mode 100644 graylog2-web-interface/src/components/events/events/hooks/useEventAction.tsx create mode 100644 graylog2-web-interface/src/components/events/events/hooks/usePluggableEventActions.tsx create mode 100644 graylog2-web-interface/src/components/events/events/hooks/useTableComponents.tsx create mode 100644 graylog2-web-interface/src/components/events/fetchEvents.ts create mode 100644 graylog2-web-interface/src/views/components/widgets/events/EventsList/DefaultDetails.tsx diff --git a/changelog/unreleased/issue-20881.toml b/changelog/unreleased/issue-20881.toml new file mode 100644 index 000000000000..64c5a84a6bd5 --- /dev/null +++ b/changelog/unreleased/issue-20881.toml @@ -0,0 +1,5 @@ +type = "a" +message = "Implement common entity data table with sorting and filtering for events page" + +issues = ["20881"] +pulls = ["20831", "graylog-plugin-enterprise#9020"] diff --git a/graylog2-web-interface/src/components/events/Constants.ts b/graylog2-web-interface/src/components/events/Constants.ts new file mode 100644 index 000000000000..2f5841d50ffe --- /dev/null +++ b/graylog2-web-interface/src/components/events/Constants.ts @@ -0,0 +1,132 @@ +/* + * 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 type { Sort, Attribute } from 'stores/PaginationTypes'; + +export const EVENTS_ENTITY_TABLE_ID = 'events'; + +export const detailsAttributes: Array = [ + { + id: 'id', + title: 'ID', + type: 'STRING', + sortable: true, + }, + { + id: 'priority', + title: 'Priority', + type: 'STRING', + sortable: true, + searchable: false, + }, + { + id: 'timestamp', + title: 'Timestamp', + type: 'DATE', + sortable: true, + filterable: true, + }, + { + id: 'event_definition_id', + title: 'Event Definition', + type: 'STRING', + sortable: false, + searchable: false, + }, + { + id: 'event_definition_type', + title: 'Event Definition Type', + type: 'STRING', + sortable: true, + }, + { + id: 'remediation_steps', + title: 'Remediation Steps', + sortable: false, + }, + { + id: 'timerange_start', + title: 'Aggregation time range', + }, + { + id: 'key', + title: 'Key', + type: 'STRING', + sortable: true, + searchable: false, + }, + { + id: 'fields', + title: 'Additional Fields', + type: 'STRING', + sortable: false, + }, + { + id: 'group_by_fields', + title: 'Group-By Fields', + sortable: false, + }, +]; +export const additionalAttributes: Array = [ + { + id: 'message', + title: 'Description', + type: 'STRING', + sortable: false, + searchable: false, + }, + { + id: 'alert', + title: 'Type', + type: 'BOOLEAN', + sortable: true, + filterable: true, + filter_options: [{ value: 'false', title: 'Event' }, { value: 'true', title: 'Alert' }], + }, + ...detailsAttributes, +]; + +export const eventsTableElements = { + defaultLayout: { + entityTableId: EVENTS_ENTITY_TABLE_ID, + defaultPageSize: 20, + defaultSort: { attributeId: 'timestamp', direction: 'desc' } as Sort, + defaultDisplayedAttributes: [ + 'priority', + 'message', + 'key', + 'alert', + 'event_definition_id', + 'event_definition_type', + 'timestamp', + ], + }, + columnOrder: [ + 'message', + 'id', + 'priority', + 'key', + 'alert', + 'event_definition_id', + 'event_definition_type', + 'timestamp', + 'fields', + 'group_by_fields', + 'remediation_steps', + 'timerange_start', + ], +}; diff --git a/graylog2-web-interface/src/components/events/EventsEntityTable.tsx b/graylog2-web-interface/src/components/events/EventsEntityTable.tsx new file mode 100644 index 000000000000..ebfcb883bac8 --- /dev/null +++ b/graylog2-web-interface/src/components/events/EventsEntityTable.tsx @@ -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 React, { useCallback } from 'react'; + +import useTableElements from 'components/events/events/hooks/useTableComponents'; +import { eventsTableElements } from 'components/events/Constants'; +import PaginatedEntityTable from 'components/common/PaginatedEntityTable'; +import FilterValueRenderers from 'components/events/FilterValueRenderers'; +import fetchEvents, { keyFn } from 'components/events/fetchEvents'; +import type { SearchParams } from 'stores/PaginationTypes'; +import type { Event, EventsAdditionalData } from 'components/events/events/types'; +import useQuery from 'routing/useQuery'; +import useColumnRenderers from 'components/events/events/ColumnRenderers'; + +import QueryHelper from '../common/QueryHelper'; + +const EventsEntityTable = () => { + const { stream_id: streamId } = useQuery(); + + const columnRenderers = useColumnRenderers(); + const _fetchEvents = useCallback((searchParams: SearchParams) => fetchEvents(searchParams, streamId as string), [streamId]); + const { entityActions, expandedSections } = useTableElements({ defaultLayout: eventsTableElements.defaultLayout }); + + return ( + humanName="events" + columnsOrder={eventsTableElements.columnOrder} + queryHelpComponent={} + entityActions={entityActions} + tableLayout={eventsTableElements.defaultLayout} + fetchEntities={_fetchEvents} + keyFn={keyFn} + actionsCellWidth={110} + expandedSectionsRenderer={expandedSections} + entityAttributesAreCamelCase={false} + filterValueRenderers={FilterValueRenderers} + columnRenderers={columnRenderers} /> + ); +}; + +export default EventsEntityTable; diff --git a/graylog2-web-interface/src/components/events/ExpandedSection.tsx b/graylog2-web-interface/src/components/events/ExpandedSection.tsx new file mode 100644 index 000000000000..47c6b6683d3b --- /dev/null +++ b/graylog2-web-interface/src/components/events/ExpandedSection.tsx @@ -0,0 +1,49 @@ +/* + * 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, { useMemo } from 'react'; + +import useTableLayout from 'components/common/EntityDataTable/hooks/useTableLayout'; +import { useTableFetchContext } from 'components/common/PaginatedEntityTable'; +import type { Attribute } from 'stores/PaginationTypes'; +import type { Event, EventsAdditionalData } from 'components/events/events/types'; +import useMetaDataContext from 'components/common/EntityDataTable/hooks/useMetaDataContext'; +import EventDetailsTable from 'components/events/events/EventDetailsTable'; + +type Props = { + defaultLayout: Parameters[0], + event: Event, +} + +const ExpandedSection = ({ defaultLayout, event }: Props) => { + const { meta } = useMetaDataContext(); + const { layoutConfig: { displayedAttributes }, isInitialLoading } = useTableLayout(defaultLayout); + const { attributes } = useTableFetchContext(); + + const nonDisplayedAttributes = useMemo(() => { + if (isInitialLoading) return []; + + const displayedAttributesSet = new Set(displayedAttributes); + + return attributes.filter(({ id }) => !displayedAttributesSet.has(id)).map(({ id, title }: Attribute) => ({ id, title })); + }, [attributes, displayedAttributes, isInitialLoading]); + + if (!nonDisplayedAttributes.length) return No further details; + + return ; +}; + +export default ExpandedSection; diff --git a/graylog2-web-interface/src/components/events/FilterValueRenderers.tsx b/graylog2-web-interface/src/components/events/FilterValueRenderers.tsx new file mode 100644 index 000000000000..c5d3f0b02c7e --- /dev/null +++ b/graylog2-web-interface/src/components/events/FilterValueRenderers.tsx @@ -0,0 +1,27 @@ +/* + * 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 EventTypeLabel from 'components/events/events/EventTypeLabel'; + +const FilterValueRenderers = { + alert: (value: 'true' | 'false') => ( + + ), +}; + +export default FilterValueRenderers; diff --git a/graylog2-web-interface/src/components/events/events/ColumnRenderers.tsx b/graylog2-web-interface/src/components/events/events/ColumnRenderers.tsx new file mode 100644 index 000000000000..08bf27c5a4a2 --- /dev/null +++ b/graylog2-web-interface/src/components/events/events/ColumnRenderers.tsx @@ -0,0 +1,176 @@ +/* + * 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 { useMemo } from 'react'; +import styled from 'styled-components'; +import isEmpty from 'lodash/isEmpty'; + +import { isPermitted } from 'util/PermissionsMixin'; +import type { ColumnRenderers } from 'components/common/EntityDataTable'; +import EventTypeLabel from 'components/events/events/EventTypeLabel'; +import { Link } from 'components/common/router'; +import Routes from 'routing/Routes'; +import type { Event, EventsAdditionalData } from 'components/events/events/types'; +import PriorityName from 'components/events/events/PriorityName'; +import usePluginEntities from 'hooks/usePluginEntities'; +import EventFields from 'components/events/events/EventFields'; +import { MarkdownPreview } from 'components/common/MarkdownEditor'; +import useExpandedSections from 'components/common/EntityDataTable/hooks/useExpandedSections'; +import { Timestamp } from 'components/common'; +import useCurrentUser from 'hooks/useCurrentUser'; + +const EventDefinitionRenderer = ({ eventDefinitionId, meta }: { eventDefinitionId: string, meta: EventsAdditionalData }) => { + const { permissions } = useCurrentUser(); + const { context: eventsContext } = meta; + const eventDefinitionContext = eventsContext?.event_definitions?.[eventDefinitionId]; + + if (!eventDefinitionContext) { + return {eventDefinitionId}; + } + + if (isPermitted(permissions, `eventdefinitions:edit:${eventDefinitionContext.id}`)) { + return ( + + {eventDefinitionContext.title} + + ); + } + + return <>{eventDefinitionContext.title}; +}; + +const EventDefinitionTypeRenderer = ({ type }: { type: string }) => { + const eventDefinitionTypes = usePluginEntities('eventDefinitionTypes'); + const plugin = useMemo(() => { + if (!type) { + return null; + } + + return eventDefinitionTypes.find((edt) => edt.type === type); + }, [eventDefinitionTypes, type]); + + return <>{(plugin && plugin.displayName) || type}; +}; + +const FieldsRenderer = ({ fields }: { fields: { [fieldName: string]: string } }) => ( + isEmpty(fields) + ? No additional Fields added to this Event. + : +); + +const GroupByFieldsRenderer = ({ groupByFields }: {groupByFields: Record }) => ( + isEmpty(groupByFields) + ? No group-by fields on this Event. + : +); + +const RemediationStepRenderer = ({ eventDefinitionId, meta }: { eventDefinitionId: string, meta: EventsAdditionalData }) => { + const { context: eventsContext } = meta; + const eventDefinitionContext = eventsContext?.event_definitions?.[eventDefinitionId]; + + return ( + eventDefinitionContext?.remediation_steps ? ( + + ) : ( + No remediation steps + ) + ); +}; + +const StyledDiv = styled.div` + cursor: pointer; + &:hover { + text-decoration: underline; + } +`; + +const MessageRenderer = ({ message, eventId }: { message: string, eventId: string }) => { + const { toggleSection } = useExpandedSections(); + + const toggleExtraSection = () => toggleSection(eventId, 'restFieldsExpandedSection'); + + return {message}; +}; + +const TimeRangeRenderer = ({ eventData }: { eventData: Event}) => (eventData.timerange_start && eventData.timerange_end ? ( +
+ +  —  + +
+) : ( + No time range +)); + +const customColumnRenderers = (): ColumnRenderers => ({ + attributes: { + message: { + minWidth: 300, + width: 0.7, + renderCell: (_message: string, event) => , + }, + key: { + renderCell: (_key: string) => {_key || No Key set for this Event.}, + staticWidth: 200, + }, + id: { + staticWidth: 300, + }, + alert: { + renderCell: (_alert: boolean) => , + staticWidth: 100, + }, + event_definition_id: { + minWidth: 300, + width: 0.3, + renderCell: (_eventDefinitionId: string, _, __, meta: EventsAdditionalData) => , + }, + priority: { + renderCell: (_priority: number) => , + staticWidth: 100, + }, + event_definition_type: { + renderCell: (_type: string) => , + staticWidth: 200, + }, + fields: { + renderCell: (_fields: Record) => , + staticWidth: 400, + }, + group_by_fields: { + renderCell: (groupByFields: Record) => , + staticWidth: 400, + }, + remediation_steps: { + renderCell: (_, event: Event, __, meta: EventsAdditionalData) => , + width: 0.3, + }, + timerange_start: { + renderCell: (_, event: Event) => , + staticWidth: 320, + }, + }, +}); + +const useColumnRenderers = () => useMemo(customColumnRenderers, []); + +export default useColumnRenderers; diff --git a/graylog2-web-interface/src/components/events/events/EventActions.tsx b/graylog2-web-interface/src/components/events/events/EventActions.tsx new file mode 100644 index 000000000000..b158931b0add --- /dev/null +++ b/graylog2-web-interface/src/components/events/events/EventActions.tsx @@ -0,0 +1,47 @@ +/* + * 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 { ButtonToolbar, Button } from 'components/bootstrap'; +import { MoreActions } from 'components/common/EntityDataTable'; +import type { Event } from 'components/events/events/types'; +import useExpandedSections from 'components/common/EntityDataTable/hooks/useExpandedSections'; +import useEventAction from 'components/events/events/hooks/useEventAction'; + +const EventActions = ({ event }: { event: Event }) => { + const { moreActions, pluggableActionModals } = useEventAction(event); + const { toggleSection } = useExpandedSections(); + const toggleExtraSection = () => toggleSection(event.id, 'restFieldsExpandedSection'); + + return ( + <> + + + { + moreActions.length ? ( + + {moreActions} + + ) : null + } + + {pluggableActionModals} + + ); +}; + +export default EventActions; diff --git a/graylog2-web-interface/src/components/events/events/EventDetails.tsx b/graylog2-web-interface/src/components/events/events/EventDetails.tsx deleted file mode 100644 index 2f088993ae83..000000000000 --- a/graylog2-web-interface/src/components/events/events/EventDetails.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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, { useMemo } from 'react'; -import isEmpty from 'lodash/isEmpty'; - -import usePluginEntities from 'hooks/usePluginEntities'; -import { Col, Row } from 'components/bootstrap'; -import { Timestamp } from 'components/common'; -import { MarkdownPreview } from 'components/common/MarkdownEditor'; -import type { Event, EventDefinitionContext } from 'components/events/events/types'; -import EventFields from 'components/events/events/EventFields'; -import EventDefinitionLink from 'components/event-definitions/event-definitions/EventDefinitionLink'; -import LinkToReplaySearch from 'components/event-definitions/replay-search/LinkToReplaySearch'; -import PriorityName from 'components/events/events/PriorityName'; - -export const usePluggableEventActions = (eventId: string) => { - const pluggableEventActions = usePluginEntities('views.components.eventActions'); - - return pluggableEventActions.filter( - (perspective) => (perspective.useCondition ? !!perspective.useCondition() : true), - ).map( - ({ component: PluggableEventAction, key }) => ( - - ), - ); -}; - -type Props = { - event: Event, - eventDefinitionContext: EventDefinitionContext, -}; - -const EventDetails = ({ event, eventDefinitionContext }: Props) => { - const eventDefinitionTypes = usePluginEntities('eventDefinitionTypes'); - const pluggableActions = usePluggableEventActions(event.id); - - const plugin = useMemo(() => { - if (event.event_definition_type === undefined) { - return null; - } - - return eventDefinitionTypes.find((edt) => edt.type === event.event_definition_type); - }, [event, eventDefinitionTypes]); - - return ( - - -
-
ID
-
{event.id}
-
Priority
-
- -
-
Timestamp
-
-
-
Event Definition
-
- -   - ({(plugin && plugin.displayName) || event.event_definition_type}) -
-
Remediation Steps
-
- {eventDefinitionContext?.remediation_steps ? ( - - ) : ( - No remediation steps - )} -
- {!event.event_definition_type.startsWith('system-notifications') && ( - <> -
Actions
- {event.replay_info && ( -
- -
- )} - {pluggableActions} - - )} -
- - -
- {event.timerange_start && event.timerange_end && ( - <> -
Aggregation time range
-
- -  —  - -
- - )} -
Event Key
-
{event.key || 'No Key set for this Event.'}
-
Additional Fields
- {isEmpty(event.fields) - ?
No additional Fields added to this Event.
- : } -
Group-By Fields
- {isEmpty(event.group_by_fields) - ?
No group-by fields on this Event.
- : } -
- -
- ); -}; - -export default EventDetails; diff --git a/graylog2-web-interface/src/components/events/events/EventDetailsTable.tsx b/graylog2-web-interface/src/components/events/events/EventDetailsTable.tsx new file mode 100644 index 000000000000..b07840061cfd --- /dev/null +++ b/graylog2-web-interface/src/components/events/events/EventDetailsTable.tsx @@ -0,0 +1,56 @@ +/* + * 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 from 'react'; +import { styled } from 'styled-components'; + +import { Table } from 'components/bootstrap'; +import type { Event, EventsAdditionalData } from 'components/events/events/types'; +import useColumnRenderers from 'components/events/events/ColumnRenderers'; + +const TD = styled.td` + white-space: nowrap; +`; + +type Props = { + attributesList: Array<{ id: string, title: string}>, + event: Event, + meta: EventsAdditionalData, +} + +const EventDetailsTable = ({ event, attributesList, meta }: Props) => { + const { attributes: attributesRenderers } = useColumnRenderers(); + + return ( + + + {attributesList.map((attribute) => { + const renderCell = attributesRenderers[attribute.id]?.renderCell; + const value = event[attribute.id]; + + return ( + + + + + ); + })} + +
{attribute.title}{renderCell ? renderCell(value, event, attribute, meta) : value}
+ ); +}; + +export default EventDetailsTable; diff --git a/graylog2-web-interface/src/components/events/events/EventFields.tsx b/graylog2-web-interface/src/components/events/events/EventFields.tsx index af1e68a7d201..ea1b667ab0d9 100644 --- a/graylog2-web-interface/src/components/events/events/EventFields.tsx +++ b/graylog2-web-interface/src/components/events/events/EventFields.tsx @@ -24,11 +24,11 @@ const EventFields = ({ fields }: Props) => { const fieldNames = Object.keys(fields); return ( -
    + <> {fieldNames.map((fieldName) => ( -
  • {fieldName} {fields[fieldName]}
  • +
    {fieldName} {fields[fieldName]}
    ))} -
+ ); }; diff --git a/graylog2-web-interface/src/components/events/events/Events.tsx b/graylog2-web-interface/src/components/events/events/Events.tsx deleted file mode 100644 index dee55ee3dd06..000000000000 --- a/graylog2-web-interface/src/components/events/events/Events.tsx +++ /dev/null @@ -1,298 +0,0 @@ -/* - * 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 from 'react'; -import capitalize from 'lodash/capitalize'; -import without from 'lodash/without'; -import styled, { css } from 'styled-components'; - -import { Link, LinkContainer } from 'components/common/router'; -import { OverlayTrigger, EmptyEntity, NoSearchResult, NoEntitiesExist, IfPermitted, PaginatedList, Timestamp, Icon } from 'components/common'; -import { Col, Row, Table, Button } from 'components/bootstrap'; -import withPaginationQueryParameter from 'components/common/withPaginationQueryParameter'; -import Routes from 'routing/Routes'; -import EventDefinitionPriorityEnum from 'logic/alerts/EventDefinitionPriorityEnum'; -import { isPermitted } from 'util/PermissionsMixin'; -import type User from 'logic/users/User'; - -import EventsSearchBar from './EventsSearchBar'; -import EventDetails from './EventDetails'; -import EventTypeLabel from './EventTypeLabel'; - -const HEADERS = ['Description', 'Key', 'Type', 'Event Definition', 'Timestamp']; - -const ExpandedTR = styled.tr(({ theme }) => css` - > td { - border-top: 1px solid ${theme.colors.gray[80]} !important; - padding: 10px 8px 8px 35px !important; - } - - dd { - margin-bottom: 0.25em; - } - - dl { - > dl, - > ul { - padding-left: 1.5em; - } - } - - ul { - list-style-type: disc; - } -`); - -const EventsTbody = styled.tbody<{ expanded: boolean }>(({ expanded, theme }) => css` - border-left: ${expanded ? `3px solid ${theme.colors.variant.light.info}` : ''}; - border-collapse: ${expanded ? 'separate' : 'collapse'}; -`); - -const CollapsibleTr = styled.tr` - cursor: pointer; -`; - -const EventsTable = styled(Table)(({ theme }) => css` - tr { - &:hover { - background-color: ${theme.colors.gray[90]}; - } - - &${ExpandedTR} { - &:hover { - background-color: ${theme.colors.global.contentBackground}; - } - } - } -`); - -const EventsIcon = styled(Icon)(({ theme }) => css` - font-size: ${theme.fonts.size.large}; - vertical-align: top; -`); - -const EventListContainer = styled.div` - margin-top: -50px; -`; - -export const PAGE_SIZES = [10, 25, 50, 100]; -export const EVENTS_MAX_OFFSET_LIMIT = 10000; - -const priorityFormatter = (_eventId, priority) => { - const priorityName = capitalize(EventDefinitionPriorityEnum.properties[priority].name); - let style; - - switch (priority) { - case EventDefinitionPriorityEnum.LOW: - style = 'text-muted'; - break; - case EventDefinitionPriorityEnum.HIGH: - style = 'text-danger'; - break; - default: - style = 'text-info'; - } - - const tooltip = <>{priorityName} Priority; - - return ( - - - - ); -}; - -const renderEmptyContent = () => ( - - - -

- Create Event Definitions that are able to search, aggregate or correlate Messages and other - Events, allowing you to record significant Events in Graylog and alert on them. -

- - - - - -
- -
-); - -type EventsProps = { - events: any[]; - parameters: any; - currentUser: User; - totalEvents: number; - totalEventDefinitions: number; - context: any; - onPageChange: (...args: any[]) => void; - onQueryChange: (...args: any[]) => void; - onAlertFilterChange: (...args: any[]) => (...args: any[]) => void; - onTimeRangeChange: (...args: any[]) => void; - onSearchReload: (...args: any[]) => void; - paginationQueryParameter: any; -}; - -class Events extends React.Component { - constructor(props) { - super(props); - - this.state = { - expanded: [], - }; - } - - expandRow = (eventId) => () => { - const { expanded } = this.state; - const nextExpanded = expanded.includes(eventId) ? without(expanded, eventId) : expanded.concat([eventId]); - - this.setState({ expanded: nextExpanded }); - }; - - renderLinkToEventDefinition = (event, eventDefinitionContext) => { - const { currentUser } = this.props; - - if (!eventDefinitionContext) { - return {event.event_definition_id}; - } - - return isPermitted(currentUser.permissions, - `eventdefinitions:edit:${eventDefinitionContext.id}`) - ? {eventDefinitionContext.title} - : eventDefinitionContext.title; - }; - - renderEvent = (event) => { - const { context } = this.props; - const { expanded } = this.state; - const eventDefinitionContext = context.event_definitions[event.event_definition_id]; - - return ( - - - - {priorityFormatter(event.id, event.priority)} -   - {event.message} - - {event.key || none} - - - {this.renderLinkToEventDefinition(event, eventDefinitionContext)} - - - - {expanded.includes(event.id) && ( - - - - - - )} - - ); - }; - - render() { - const { - events, - parameters, - totalEvents, - totalEventDefinitions, - onPageChange, - onQueryChange, - onAlertFilterChange, - onTimeRangeChange, - onSearchReload, - paginationQueryParameter, - } = this.props; - - const eventList = events.map((e) => e.event); - - if (totalEventDefinitions === 0) { - return renderEmptyContent(); - } - - const { query, filter: { alerts: filter } } = parameters; - const excludedFile = filter === 'exclude' ? 'Events' : 'Alerts & Events'; - const entity = (filter === 'only' ? 'Alerts' : excludedFile); - const offsetLimitError = paginationQueryParameter.page * paginationQueryParameter.pageSize > EVENTS_MAX_OFFSET_LIMIT; - - const emptyListComponent = query ? ( - - No {entity} found for the current search criteria. - - ) : ( - - No {entity} exist. - - ); - - const offsetLimitErrorComponent = ( - - - - - Unfortunately we can only fetch Events with an Offset (page number * rows per page) less than or equal to: [10000]. - Please use more advanced methods (Search Field and Date Filter) in order to get distant chunks of results. - - - - - ); - - return ( - - - - {(eventList.length === 0 && !offsetLimitError) ? ( - emptyListComponent - ) : ( - - - - - - {HEADERS.map((header) => {header})} - - - {offsetLimitError ? ( - offsetLimitErrorComponent - ) : ( - eventList.map(this.renderEvent) - )} - - - - )} - - - ); - } -} - -export default withPaginationQueryParameter(Events); diff --git a/graylog2-web-interface/src/components/events/events/EventsContainer.tsx b/graylog2-web-interface/src/components/events/events/EventsContainer.tsx deleted file mode 100644 index 2707d9918043..000000000000 --- a/graylog2-web-interface/src/components/events/events/EventsContainer.tsx +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 from 'react'; -import isObject from 'lodash/isObject'; -import debounce from 'lodash/debounce'; - -import { Spinner } from 'components/common'; -import UserNotification from 'util/UserNotification'; -import connect from 'stores/connect'; -import Store from 'logic/local-storage/Store'; -import { CurrentUserStore } from 'stores/users/CurrentUserStore'; -import { EventDefinitionsActions, EventDefinitionsStore } from 'stores/event-definitions/EventDefinitionsStore'; -import { EventsActions, EventsStore } from 'stores/events/EventsStore'; -import type { PaginationProps } from 'components/common/withPaginationQueryParameter'; -import withPaginationQueryParameter from 'components/common/withPaginationQueryParameter'; -import type { TimeRange } from 'views/logic/queries/Query'; - -import Events, { PAGE_SIZES, EVENTS_MAX_OFFSET_LIMIT } from './Events'; - -const LOCAL_STORAGE_ITEM = 'events-last-search'; -const SEARCH_DEBOUNCE_THRESHOLD = 500; - -type FetchEventsParams = { - page?: number, - pageSize: number, - query?: string, - filter?: { alerts: string }, - timerange?: TimeRange, -} - -const fetchEvents = ({ page, pageSize, query, filter, timerange }: FetchEventsParams) => { - Store.set(LOCAL_STORAGE_ITEM, { filter: filter, timerange: timerange }); - - return EventsActions.search({ - query: query, - page: page, - pageSize: pageSize, - filter: filter, - timerange: timerange, - }).catch((error) => { - UserNotification.error(`Fetching alerts failed with status: ${error}`); - }); -}; - -const fetchEventDefinitions = () => EventDefinitionsActions.listPaginated({}); - -type EventsContainerProps = PaginationProps & { - events: any; - eventDefinitions: any; - currentUser: any; - streamId?: string; -}; - -class EventsContainer extends React.Component { - static defaultProps = { - streamId: '', - }; - - componentDidMount() { - const { streamId } = this.props; - const { page, pageSize } = this.props.paginationQueryParameter; - const lastSearch: FetchEventsParams = Store.get(LOCAL_STORAGE_ITEM) || {}; - - const params: FetchEventsParams = { page, pageSize }; - - if (streamId) { - params.query = `source_streams:${streamId}`; - } - - if (lastSearch && isObject(lastSearch)) { - params.filter = lastSearch.filter; - params.timerange = lastSearch.timerange; - } - - fetchEvents(params); - fetchEventDefinitions(); - } - - handlePageChange = (nextPage, nextPageSize) => { - if (nextPage * nextPageSize <= EVENTS_MAX_OFFSET_LIMIT) { - const { events } = this.props; - - fetchEvents({ - page: nextPage, - pageSize: nextPageSize, - query: events.parameters.query, - filter: events.parameters.filter, - timerange: events.parameters.timerange, - }); - } - }; - - handleQueryChange = debounce((nextQuery, callback = () => {}) => { - const { events } = this.props; - const { resetPage, pageSize } = this.props.paginationQueryParameter; - - resetPage(); - - const promise = fetchEvents({ - query: nextQuery, - pageSize, - filter: events.parameters.filter, - timerange: events.parameters.timerange, - }); - - promise.finally(callback); - }, SEARCH_DEBOUNCE_THRESHOLD); - - handleAlertFilterChange = (nextAlertFilter) => () => { - const { events } = this.props; - const { resetPage, pageSize } = this.props.paginationQueryParameter; - - resetPage(); - - fetchEvents({ - query: events.parameters.query, - pageSize: pageSize, - filter: { alerts: nextAlertFilter }, - timerange: events.parameters.timerange, - }); - }; - - handleTimeRangeChange = (timeRangeType, range) => { - const { events } = this.props; - const { resetPage, pageSize } = this.props.paginationQueryParameter; - - resetPage(); - - fetchEvents({ - query: events.parameters.query, - pageSize, - filter: events.parameters.filter, - timerange: { type: timeRangeType, range: range }, - }); - }; - - handleSearchReload = (callback = () => {}) => { - const { events } = this.props; - const { resetPage, pageSize } = this.props.paginationQueryParameter; - - resetPage(); - - const promise = fetchEvents({ - query: events.parameters.query, - pageSize, - filter: events.parameters.filter, - timerange: events.parameters.timerange, - }); - - promise.finally(callback); - }; - - render() { - const { events, eventDefinitions, currentUser } = this.props; - const isLoading = !events.events || !eventDefinitions.eventDefinitions; - - if (isLoading) { - return ; - } - - return ( - - ); - } -} - -export default connect(withPaginationQueryParameter(EventsContainer, { pageSizes: PAGE_SIZES }), { - events: EventsStore, - eventDefinitions: EventDefinitionsStore, - currentUser: CurrentUserStore, -}, ({ currentUser, ...otherProps }) => ({ - ...otherProps, - currentUser: currentUser.currentUser, -})); diff --git a/graylog2-web-interface/src/components/events/events/EventsSearchBar.css b/graylog2-web-interface/src/components/events/events/EventsSearchBar.css deleted file mode 100644 index f33470306cfb..000000000000 --- a/graylog2-web-interface/src/components/events/events/EventsSearchBar.css +++ /dev/null @@ -1,41 +0,0 @@ -:local(.eventsSearchBar) { - display: flex; - flex-flow: column nowrap; - margin-bottom: 15px; -} - -@media all and (max-width: 768px) { - :local(.eventsSearchBar) > div { - flex-flow: column nowrap; - } -} - -:local(.eventsSearchBar) > div { - display: flex; - flex-flow: row nowrap; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 10px; -} - -:local(.eventsSearchBar) .form-group { - margin-bottom: 0; -} - -:local(.searchForm) { - flex: 0 1 45%; - margin-right: 10px; -} - -:local(.searchForm) .form-inline { - display: flex; - flex-flow: row nowrap; -} - -:local(.searchForm) .form-group:first-child { - flex: 1 100%; -} - -:local(.searchForm) .form-group:not(:first-child) { - margin-right: 5px; -} diff --git a/graylog2-web-interface/src/components/events/events/EventsSearchBar.tsx b/graylog2-web-interface/src/components/events/events/EventsSearchBar.tsx deleted file mode 100644 index 58dfd3ddc78b..000000000000 --- a/graylog2-web-interface/src/components/events/events/EventsSearchBar.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 from 'react'; -import moment from 'moment'; -import max from 'lodash/max'; - -import { ButtonGroup, Button } from 'components/bootstrap'; -import { SearchForm, TimeUnitInput, Icon } from 'components/common'; -import { extractDurationAndUnit } from 'components/common/TimeUnitInput'; - -import styles from './EventsSearchBar.css'; - -const TIME_UNITS = ['DAYS', 'HOURS', 'MINUTES', 'SECONDS']; - -type EventsSearchBarProps = { - parameters: any; - onQueryChange: (...args: any[]) => void; - onAlertFilterChange: (...args: any[]) => (...args: any[]) => void; - onTimeRangeChange: (...args: any[]) => void; - onSearchReload: (...args: any[]) => void; -}; - -class EventsSearchBar extends React.Component { - constructor(props) { - super(props); - - const timerangeDuration = extractDurationAndUnit(props.parameters.timerange.range * 1000, TIME_UNITS); - - this.state = { - isReloadingResults: false, - timeRangeDuration: timerangeDuration.duration, - timeRangeUnit: timerangeDuration.unit, - }; - } - - updateSearchTimeRange = (nextValue, nextUnit) => { - const { onTimeRangeChange } = this.props; - const durationInSeconds = moment.duration(max([nextValue, 1]), nextUnit).asSeconds(); - - onTimeRangeChange('relative', durationInSeconds); - this.setState({ timeRangeDuration: nextValue, timeRangeUnit: nextUnit }); - }; - - resetLoadingState = () => { - this.setState({ isReloadingResults: false }); - }; - - handleSearchReload = () => { - this.setState({ isReloadingResults: true }); - const { onSearchReload } = this.props; - - onSearchReload(this.resetLoadingState); - }; - - render() { - const { parameters, onQueryChange, onAlertFilterChange } = this.props; - const { isReloadingResults, timeRangeUnit, timeRangeDuration } = this.state; - - const filterAlerts = parameters.filter.alerts; - - return ( -
-
-
- - - -
- - -
-
- - - - - -
-
- ); - } -} - -export default EventsSearchBar; diff --git a/graylog2-web-interface/src/components/events/events/hooks/useEventAction.tsx b/graylog2-web-interface/src/components/events/events/hooks/useEventAction.tsx new file mode 100644 index 000000000000..a812ec495443 --- /dev/null +++ b/graylog2-web-interface/src/components/events/events/hooks/useEventAction.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 React, { useMemo } from 'react'; + +import usePluggableEventActions from 'components/events/events/hooks/usePluggableEventActions'; +import { MenuItem } from 'components/bootstrap'; +import LinkToReplaySearch from 'components/event-definitions/replay-search/LinkToReplaySearch'; + +const useEventAction = (event) => { + const { actions: pluggableActions, actionModals: pluggableActionModals } = usePluggableEventActions(event.id); + const hasReplayInfo = !!event.replay_info; + const isNotSystemEvent = !event.event_definition_type.startsWith('system-notifications'); + + const moreActions = useMemo(() => [ + hasReplayInfo ? : null, + pluggableActions.length && hasReplayInfo && isNotSystemEvent ? : null, + pluggableActions.length && isNotSystemEvent ? pluggableActions : null, + ].filter(Boolean), [event.id, hasReplayInfo, isNotSystemEvent, pluggableActions]); + + return { moreActions, pluggableActionModals }; +}; + +export default useEventAction; diff --git a/graylog2-web-interface/src/components/events/events/hooks/usePluggableEventActions.tsx b/graylog2-web-interface/src/components/events/events/hooks/usePluggableEventActions.tsx new file mode 100644 index 000000000000..1a3ea23c2e21 --- /dev/null +++ b/graylog2-web-interface/src/components/events/events/hooks/usePluggableEventActions.tsx @@ -0,0 +1,45 @@ +/* + * 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, { useRef } from 'react'; + +import usePluginEntities from 'hooks/usePluginEntities'; +import type { EventActionComponentProps } from 'views/types'; + +const usePluggableEventActions = (eventId: string) => { + const modalRefs = useRef({}); + const pluggableActions = usePluginEntities('views.components.eventActions'); + const availableActions = pluggableActions.filter( + (perspective) => (perspective.useCondition ? !!perspective.useCondition() : true), + ); + const actions = availableActions.map(({ component: PluggableEventAction, key }: { component: React.ComponentType, key: string }) => ( + modalRefs.current[key]} /> + )); + + const actionModals = availableActions + .filter(({ modal }) => !!modal) + .map(({ modal: ActionModal, key }) => ( + { modalRefs.current[key] = r; }} /> + )); + + return ({ actions, actionModals }); +}; + +export default usePluggableEventActions; diff --git a/graylog2-web-interface/src/components/events/events/hooks/useTableComponents.tsx b/graylog2-web-interface/src/components/events/events/hooks/useTableComponents.tsx new file mode 100644 index 000000000000..999c4fa54015 --- /dev/null +++ b/graylog2-web-interface/src/components/events/events/hooks/useTableComponents.tsx @@ -0,0 +1,48 @@ +/* + * 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, { useCallback, useMemo } from 'react'; + +import EventActions from 'components/events/events/EventActions'; +import type { Event } from 'components/events/events/types'; +import ExpandedSection from 'components/events/ExpandedSection'; +import type useTableLayout from 'components/common/EntityDataTable/hooks/useTableLayout'; + +const useTableElements = ({ defaultLayout }: { + defaultLayout: Parameters[0], +}) => { + const entityActions = useCallback((event: Event) => ( + + ), []); + + const renderExpandedRules = useCallback((event: Event) => ( + + ), [defaultLayout]); + + const expandedSections = useMemo(() => ({ + restFieldsExpandedSection: { + title: 'Details', + content: renderExpandedRules, + }, + }), [renderExpandedRules]); + + return { + entityActions, + expandedSections, + }; +}; + +export default useTableElements; diff --git a/graylog2-web-interface/src/components/events/events/types.ts b/graylog2-web-interface/src/components/events/events/types.ts index 485e460d779f..61891efba685 100644 --- a/graylog2-web-interface/src/components/events/events/types.ts +++ b/graylog2-web-interface/src/components/events/events/types.ts @@ -45,3 +45,8 @@ export type EventDefinitionContext = { remediation_steps?: string, description?: string, }; + +export type EventDefinitionContexts = { [eventDefinitionId: string]: EventDefinitionContext }; +export type EventsAdditionalData = { + context: { event_definitions?: EventDefinitionContexts, streams?: EventDefinitionContexts }, +} diff --git a/graylog2-web-interface/src/components/events/fetchEvents.ts b/graylog2-web-interface/src/components/events/fetchEvents.ts new file mode 100644 index 000000000000..2e14c1059f8c --- /dev/null +++ b/graylog2-web-interface/src/components/events/fetchEvents.ts @@ -0,0 +1,87 @@ +/* + * 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 moment from 'moment'; + +import * as URLUtils from 'util/URLUtils'; +import { adjustFormat } from 'util/DateTime'; +import type { SearchParams } from 'stores/PaginationTypes'; +import fetch from 'logic/rest/FetchProvider'; +import type { PaginatedResponse } from 'components/common/PaginatedEntityTable/useFetchEntities'; +import type { Event, EventsAdditionalData } from 'components/events/events/types'; +import { additionalAttributes } from 'components/events/Constants'; +import { extractRangeFromString } from 'components/common/EntityFilters/helpers/timeRange'; +import type { UrlQueryFilters } from 'components/common/EntityFilters/types'; + +const url = URLUtils.qualifyUrl('/events/search'); + +type FiltersResult = { filter: { alerts?: string }, timerange?: { from?: string, to?: string, type: string, range?: number}}; + +const parseFilters = (filters: UrlQueryFilters) => { + const result: FiltersResult = { filter: {} }; + + if (filters.get('timestamp')?.[0]) { + const [from, to] = extractRangeFromString(filters.get('timestamp')[0]); + + result.timerange = from + ? { from, to: to || adjustFormat(moment().utc(), 'internal'), type: 'absolute' } + : { type: 'relative', range: 0 }; + } else { + result.timerange = { type: 'relative', range: 0 }; + } + + switch (filters?.get('alert')?.[0]) { + case 'true': + result.filter.alerts = 'only'; + break; + case 'false': + result.filter.alerts = 'exclude'; + break; + default: + result.filter.alerts = 'include'; + } + + return result; +}; + +const getConcatenatedQuery = (query: string, streamId: string) => { + if (!streamId) return query; + + if (streamId && !query) return `source_streams:${streamId}`; + + return `(${query}) AND source_streams:${streamId}`; +}; + +export const keyFn = (searchParams: SearchParams) => ['events', 'search', searchParams]; + +const fetchEvents = (searchParams: SearchParams, streamId: string): Promise> => fetch('POST', url, { + query: getConcatenatedQuery(searchParams.query, streamId), + page: searchParams.page, + per_page: searchParams.pageSize, + sort_by: searchParams.sort.attributeId, + sort_direction: searchParams.sort.direction, + ...parseFilters(searchParams.filters), +}).then(({ events, total_events, parameters, context }) => ({ + attributes: additionalAttributes, + list: events.map(({ event }) => event), + pagination: { total: total_events, page: parameters.page, per_page: parameters.per_page, count: events.length }, + meta: { + context, + }, +})); + +export default fetchEvents; diff --git a/graylog2-web-interface/src/components/navigation/NotificationBadge.tsx b/graylog2-web-interface/src/components/navigation/NotificationBadge.tsx index c9820565a0f1..8c7d7345b066 100644 --- a/graylog2-web-interface/src/components/navigation/NotificationBadge.tsx +++ b/graylog2-web-interface/src/components/navigation/NotificationBadge.tsx @@ -62,7 +62,7 @@ const NotificationBadge = () => { - {total} + {total} diff --git a/graylog2-web-interface/src/pages/EventsPage.tsx b/graylog2-web-interface/src/pages/EventsPage.tsx index 0553843d56b3..203e4ee6238c 100644 --- a/graylog2-web-interface/src/pages/EventsPage.tsx +++ b/graylog2-web-interface/src/pages/EventsPage.tsx @@ -18,35 +18,30 @@ import React from 'react'; import { Col, Row } from 'components/bootstrap'; import { DocumentTitle, PageHeader } from 'components/common'; -import EventsContainer from 'components/events/events/EventsContainer'; import DocsHelper from 'util/DocsHelper'; import EventsPageNavigation from 'components/events/EventsPageNavigation'; -import useQuery from 'routing/useQuery'; +import EventsEntityTable from 'components/events/EventsEntityTable'; -const EventsPage = () => { - const { stream_id: streamId } = useQuery(); +const EventsPage = () => ( + + + + + Define Events through different conditions. Add Notifications to Events that require your attention + to create Alerts. + + - return ( - - - - - Define Events through different conditions. Add Notifications to Events that require your attention - to create Alerts. - - - - - - - - - - ); -}; + + + + + + +); export default EventsPage; diff --git a/graylog2-web-interface/src/views/components/widgets/events/EventsList/DefaultDetails.tsx b/graylog2-web-interface/src/views/components/widgets/events/EventsList/DefaultDetails.tsx new file mode 100644 index 000000000000..52bf1675b2d4 --- /dev/null +++ b/graylog2-web-interface/src/views/components/widgets/events/EventsList/DefaultDetails.tsx @@ -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 React, { useMemo } from 'react'; + +import type { Event, EventDefinitionContext } from 'components/events/events/types'; +import EventDetailsTable from 'components/events/events/EventDetailsTable'; +import { detailsAttributes } from 'components/events/Constants'; +import DropdownButton from 'components/bootstrap/DropdownButton'; +import useEventAction from 'components/events/events/hooks/useEventAction'; + +type Props = { + event: Event, + eventDefinitionContext: EventDefinitionContext, +}; + +const attributesList = detailsAttributes.map(({ id, title }) => ({ id, title })); + +const ActionsWrapper = ({ children }) => ( + + {children} + +); + +const DefaultDetails = ({ event, eventDefinitionContext }: Props) => { + const { moreActions, pluggableActionModals } = useEventAction(event); + const meta = useMemo(() => ({ context: { event_definitions: { [event.event_definition_id]: eventDefinitionContext } } }), [event.event_definition_id, eventDefinitionContext]); + + return ( + <> + + + {moreActions} + + {pluggableActionModals} + + ); +}; + +export default DefaultDetails; diff --git a/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.test.tsx b/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.test.tsx index a3457d031824..f7d9aee40949 100644 --- a/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.test.tsx +++ b/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.test.tsx @@ -22,7 +22,7 @@ import useCurrentUser from 'hooks/useCurrentUser'; import { adminUser, alice } from 'fixtures/users'; import usePluginEntities from 'hooks/usePluginEntities'; import useEventById from 'hooks/useEventById'; -import { mockEventData } from 'helpers/mocking/EventAndEventDefinitions_mock'; +import { mockEventData, mockEventDefinitionTwoAggregations } from 'helpers/mocking/EventAndEventDefinitions_mock'; import useEventDefinition from 'components/events/events/hooks/useEventDefinition'; import EventDetails from './EventDetails'; @@ -61,6 +61,7 @@ describe('EventDetails', () => { }); it('should render default event details', async () => { + asMock(useEventDefinition).mockReturnValue({ data: mockEventDefinitionTwoAggregations, isFetching: false, isInitialLoading: false }); render(); await waitFor(() => expect(useEventDefinition).toHaveBeenCalledWith('event-definition-id-1', true)); diff --git a/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.tsx b/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.tsx index 402e448d3788..ec91ab5b3eb7 100644 --- a/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.tsx +++ b/graylog2-web-interface/src/views/components/widgets/events/EventsList/EventDetails.tsx @@ -17,12 +17,12 @@ import * as React from 'react'; +import { isPermitted } from 'util/PermissionsMixin'; import usePluginEntities from 'hooks/usePluginEntities'; import useEventById from 'hooks/useEventById'; import useEventDefinition from 'components/events/events/hooks/useEventDefinition'; import { Spinner } from 'components/common'; -import DefaultDetails from 'components/events/events/EventDetails'; -import { isPermitted } from 'util/PermissionsMixin'; +import DefaultDetails from 'views/components/widgets/events/EventsList/DefaultDetails'; import useCurrentUser from 'hooks/useCurrentUser'; export const usePluggableEventDetails = (eventId: string) => { diff --git a/graylog2-web-interface/src/views/types.ts b/graylog2-web-interface/src/views/types.ts index 9c1720fd57b2..04ad375cab93 100644 --- a/graylog2-web-interface/src/views/types.ts +++ b/graylog2-web-interface/src/views/types.ts @@ -14,11 +14,13 @@ * along with this program. If not, see * . */ + import type React from 'react'; import type * as Immutable from 'immutable'; import type { FormikErrors } from 'formik'; import type { Reducer, AnyAction } from '@reduxjs/toolkit'; +import type { ExportPayload } from 'util/MessagesExportUtils'; import type { IconName } from 'components/common/Icon'; import type Widget from 'views/logic/widgets/Widget'; import type { ActionDefinition } from 'views/components/actions/ActionHandler'; @@ -59,7 +61,6 @@ import type { UndoRedoState } from 'views/logic/slices/undoRedoSlice'; import type { SearchExecutors } from 'views/logic/slices/searchExecutionSlice'; import type { JobIds } from 'views/stores/SearchJobs'; import type { FilterComponents, Attributes } from 'views/components/widgets/overview-configuration/filters/types'; -import type { ExportPayload } from 'util/MessagesExportUtils'; export type ArrayElement = ArrayType extends readonly (infer ElementType)[] ? ElementType : never; @@ -310,6 +311,13 @@ type DashboardAction = { useCondition?: () => boolean, } +type EventAction = { + useCondition: () => boolean, + modal?: React.ComponentType>, + component: React.ComponentType, + key: string, +} + type EventWidgetAction = { key: string, component: React.ComponentType>, @@ -322,8 +330,9 @@ type AssetInformation = { key: string, } -type EventActionComponentProps = { +export type EventActionComponentProps = { eventId: string, + modalRef: () => unknown, } type MessageActionComponentProps = { @@ -474,12 +483,8 @@ declare module 'graylog-web-plugin/plugin' { valueActions?: Array; 'views.completers'?: Array; 'views.components.assetInformationActions'?: Array; - 'views.components.dashboardActions'?: Array>; - 'views.components.eventActions'?: Array<{ - useCondition: () => boolean, - component: React.ComponentType, - key: string, - }>; + 'views.components.dashboardActions'?: Array> + 'views.components.eventActions'?: Array; 'views.components.widgets.messageTable.previewOptions'?: Array; 'views.components.widgets.messageTable.messageRowOverride'?: Array>; 'views.components.widgets.messageDetails.contextProviders'?: Array>>;