-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding "Replay Search" bulk action for alerts/events. (#21262)
* Initial setup of "Replay Search" bulk action, using state for event ids. * Adding bulk endpoint for events. * Adding reducer to handle state for selected events/status. * Splitting up components. * Styling improvements. * Improving behavior on hover. * Using type parameter to access state in `CreateProfile`. * Show replayed search when event is selected. * Adjusting type to include `message` field. * Using common hooks instead of asking query client directly. * Adding bulk actions & close buttons. * Showing green checkmark when event is checked. * Improving user messages. * Disabling URL synchronization, fixing unnecessary history change. * Adding changelog snippet. * Adding license headers. * Fixing up import. * Adjusting test. * Adding unique key, adding test case. * Adding license headers. * Change caption when event is marked. * Improving button and headline styling. * Redirecting back to original page when closing dialog. * Fixing color of trashcan icon.
- Loading branch information
1 parent
ce32b08
commit 534c3eb
Showing
29 changed files
with
953 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
type = "a" | ||
message = "Adding 'Replay Search' Bulk action to alerts & events." | ||
|
||
pulls = ["21262"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
graylog2-web-interface/src/components/events/bulk-replay/BulkEventReplay.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
/* | ||
* 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 | ||
* <http://www.mongodb.com/licensing/server-side-public-license>. | ||
*/ | ||
import * as React from 'react'; | ||
import { useState } from 'react'; | ||
import styled, { css } from 'styled-components'; | ||
|
||
import EventListItem from 'components/events/bulk-replay/EventListItem'; | ||
import useSelectedEvents from 'components/events/bulk-replay/useSelectedEvents'; | ||
import ReplaySearch from 'components/events/bulk-replay/ReplaySearch'; | ||
import type { Event } from 'components/events/events/types'; | ||
import Button from 'components/bootstrap/Button'; | ||
import DropdownButton from 'components/bootstrap/DropdownButton'; | ||
import useEventBulkActions from 'components/events/events/hooks/useEventBulkActions'; | ||
import Center from 'components/common/Center'; | ||
import ButtonToolbar from 'components/bootstrap/ButtonToolbar'; | ||
|
||
const Container = styled.div` | ||
display: flex; | ||
height: 100%; | ||
`; | ||
|
||
const EventsListSidebar = styled.div(({ theme }) => css` | ||
display: flex; | ||
flex-direction: column; | ||
flex-shrink: 0; | ||
position: relative; | ||
width: 20vw; | ||
height: 100%; | ||
top: 0; | ||
left: 0; | ||
overflow: auto; | ||
padding: 5px 10px; | ||
background: ${theme.colors.global.contentBackground}; | ||
border-right: none; | ||
box-shadow: 3px 3px 3px ${theme.colors.global.navigationBoxShadow}; | ||
z-index: 1030; | ||
`); | ||
|
||
const ReplayedSearchContainer = styled.div` | ||
width: 100%; | ||
overflow: auto; | ||
padding: 5px; | ||
`; | ||
|
||
const StyledList = styled.ul` | ||
flex-grow: 1; | ||
padding-inline-start: 0; | ||
margin-top: 20px; | ||
`; | ||
|
||
const ActionsBar = styled(ButtonToolbar)` | ||
align-self: flex-end; | ||
display: flex; | ||
justify-content: flex-end; | ||
align-items: end; | ||
gap: 0.25em; | ||
`; | ||
|
||
type Props = { | ||
initialEventIds: Array<string>; | ||
events: { [eventId: string]: { event: Event } }; | ||
onClose: () => void; | ||
} | ||
|
||
type RemainingBulkActionsProps = { | ||
events: Event[]; | ||
completed: boolean; | ||
} | ||
|
||
const RemainingBulkActions = ({ completed, events }: RemainingBulkActionsProps) => { | ||
const { actions, pluggableActionModals } = useEventBulkActions(events); | ||
|
||
return ( | ||
<> | ||
<DropdownButton title="Bulk actions" | ||
bsStyle={completed ? 'success' : 'default'} | ||
id="bulk-actions-dropdown" | ||
disabled={!events?.length}> | ||
{actions} | ||
</DropdownButton> | ||
{pluggableActionModals} | ||
</> | ||
); | ||
}; | ||
|
||
const ReplayedSearch = ({ total, completed, selectedEvent }: React.PropsWithChildren<{ | ||
total: number; | ||
completed: number; | ||
selectedEvent: { event: Event } | undefined; | ||
}>) => { | ||
if (total === 0) { | ||
return ( | ||
<Center> | ||
You have removed all events from the list. You can now return back by clicking the “Close” button. | ||
</Center> | ||
); | ||
} | ||
|
||
if (!selectedEvent && total === completed) { | ||
return ( | ||
<Center> | ||
You are done investigating all events. You can now select a bulk action to apply to all remaining events, or close the page to return to the events list. | ||
</Center> | ||
); | ||
} | ||
|
||
if (!selectedEvent) { | ||
return ( | ||
<Center> | ||
You have no event selected. Please select an event from the list to replay its search. | ||
</Center> | ||
); | ||
} | ||
|
||
return <ReplaySearch key={`replaying-search-for-event-${selectedEvent.event.id}`} event={selectedEvent.event} />; | ||
}; | ||
|
||
const Headline = styled.h2` | ||
margin-bottom: 10px; | ||
`; | ||
|
||
const BulkEventReplay = ({ initialEventIds, events: _events, onClose }: Props) => { | ||
const [events] = useState<Props['events']>(_events); | ||
const { eventIds, selectedId, removeItem, selectItem, markItemAsDone } = useSelectedEvents(initialEventIds); | ||
const selectedEvent = events?.[selectedId]; | ||
const total = eventIds.length; | ||
const completed = eventIds.filter((event) => event.status === 'DONE').length; | ||
const remainingEvents = eventIds.map((eventId) => events[eventId.id]?.event); | ||
|
||
return ( | ||
<Container> | ||
<EventsListSidebar> | ||
<Headline>Replay Search</Headline> | ||
<p> | ||
The following list contains all of the events/alerts you selected in the previous step, allowing you to | ||
investigate the replayed search for each of them. | ||
</p> | ||
<i>Investigation of {completed}/{total} events completed.</i> | ||
<StyledList> | ||
{eventIds.map(({ id: eventId, status }) => ( | ||
<EventListItem key={`bulk-replay-search-item-${eventId}`} | ||
event={events?.[eventId]?.event} | ||
selected={eventId === selectedId} | ||
done={status === 'DONE'} | ||
removeItem={removeItem} | ||
onClick={() => selectItem(eventId)} | ||
markItemAsDone={markItemAsDone} /> | ||
))} | ||
</StyledList> | ||
<ActionsBar> | ||
<RemainingBulkActions events={remainingEvents} completed={total > 0 && total === completed} /> | ||
<Button onClick={onClose}>Close</Button> | ||
</ActionsBar> | ||
</EventsListSidebar> | ||
<ReplayedSearchContainer> | ||
<ReplayedSearch total={total} completed={completed} selectedEvent={selectedEvent} /> | ||
</ReplayedSearchContainer> | ||
</Container> | ||
); | ||
}; | ||
|
||
export default BulkEventReplay; |
98 changes: 98 additions & 0 deletions
98
graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* 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 | ||
* <http://www.mongodb.com/licensing/server-side-public-license>. | ||
*/ | ||
import * as React from 'react'; | ||
import { useCallback } from 'react'; | ||
import styled, { css } from 'styled-components'; | ||
|
||
import IconButton from 'components/common/IconButton'; | ||
import ButtonGroup from 'components/bootstrap/ButtonGroup'; | ||
import type { Event } from 'components/events/events/types'; | ||
|
||
type EventListItemProps = { | ||
event: Event, | ||
done: boolean, | ||
selected: boolean, | ||
onClick: () => void, | ||
removeItem: (id: string) => void, | ||
markItemAsDone: (id: string) => void, | ||
} | ||
|
||
type StyledItemProps = { | ||
$selected: boolean; | ||
} | ||
const StyledItem = styled.li<StyledItemProps>(({ theme, $selected }) => css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
height: 30px; | ||
background-color: ${$selected ? theme.colors.background.secondaryNav : 'transparent'}; | ||
cursor: pointer; | ||
&:hover { | ||
background-color: ${theme.colors.background.body}; | ||
} | ||
`); | ||
|
||
type SummaryProps = { | ||
$done: boolean; | ||
} | ||
|
||
const Summary = styled.span<SummaryProps>(({ theme, $done }) => css` | ||
margin: 10px; | ||
color: ${$done ? theme.colors.global.textSecondary : theme.colors.global.textDefault}; | ||
white-space: nowrap; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
&:hover { | ||
text-decoration: underline; | ||
} | ||
`); | ||
|
||
const CompletedButton = styled(IconButton)<{ $done: boolean }>(({ theme, $done }) => css` | ||
color: ${$done ? theme.colors.variant.success : theme.colors.gray[60]}; | ||
`); | ||
|
||
const EventListItem = ({ done, event, onClick, selected, removeItem, markItemAsDone }: EventListItemProps) => { | ||
const _removeItem = useCallback((e: React.MouseEvent<HTMLButtonElement>) => { | ||
e.stopPropagation(); | ||
|
||
return removeItem(event.id); | ||
}, [event?.id, removeItem]); | ||
const _markItemAsDone = useCallback((e: React.MouseEvent<HTMLButtonElement>) => { | ||
e.stopPropagation(); | ||
|
||
return markItemAsDone(event.id); | ||
}, [event?.id, markItemAsDone]); | ||
|
||
return ( | ||
<StyledItem key={`event-replay-list-${event?.id}`} $selected={selected} onClick={onClick}> | ||
<Summary $done={done}>{event?.message ?? <i>Unknown</i>}</Summary> | ||
|
||
<ButtonGroup> | ||
<IconButton onClick={_removeItem} title={`Remove event "${event?.id}" from list`} name="delete" /> | ||
<CompletedButton onClick={_markItemAsDone} | ||
title={`Mark event "${event?.id}" as ${done ? 'not' : ''} investigated`} | ||
name="verified" | ||
iconType={done ? 'solid' : 'regular'} | ||
$done={done} /> | ||
</ButtonGroup> | ||
</StyledItem> | ||
); | ||
}; | ||
|
||
export default EventListItem; |
Oops, something went wrong.