Skip to content

Commit

Permalink
Implementing external value actions for field values. (#11061)
Browse files Browse the repository at this point in the history
* Create `MessageEventsContext` and `MessageEventsProvider` and configure
provider for `Search`.`

* Display summary for messages in message table based on related event type.

* Defining type definitions for security content in plugin store.

* Create test for `MessageEventsProvider`.

* Fixing error which

* Replacing enzyme with react testing library in `MessageTableEntry.test`.

* Updating `MessageEventContext` type definition.

* Add test for summary in `MessageTableEntry".

* Smaller improvements.

* Replacing usage of `securityContent` plugin store section with `messageEventTypes` and `externalValueActions` sections.

* Removing not yet needed logic for external value actions.

* Fixing linter warning.

* Fixing comoponent name.

* Removing not yet needed external value action plugin store type.

* Change usage of message event types, because they are now
Immutable.Map's

* Add attributes `requiredFields` and `optionalFields` for `MessageEventType` type definition.

* Do not use ImmutableJS for `MessageEventTypeContext` because it does not
provide a huge benefit.

* Displaying message summary template on summery hover.

* Define message summary color based on message event type category.

* Add missing import.

* Creating context, provider and types for external value actions.

* Adding `ExternalValueActionsProvider` for `Search`.

* Addling logic for `ExternalValueActionsProvider` to receive actions for a field.

* Displaying external value actions in value actions dropdown.

* Creating tests for `ExternalValueActionsProvider`.

* Unifying format of internal and external value actions.

* Display icon for external links in field value action menu.

* Removing dropdown menu title for external actions

* Creating simple tests for `Action.tsx`.

* Creating one central context for value and field actions, which replaces `ExternalValueActionsContext`

* Removing not needed array flatten.

* Creating separate component for `ActionDropdown` to ensure we are only
iterating through all field and value actions when opening action dropdown.

* Fixing `FieldAndValueActionsProvider.test`
  • Loading branch information
linuspahl committed Jul 27, 2021
1 parent f38f175 commit 71088bc
Show file tree
Hide file tree
Showing 12 changed files with 619 additions and 126 deletions.
103 changes: 53 additions & 50 deletions graylog2-web-interface/src/views/components/Search.tsx
Expand Up @@ -63,6 +63,7 @@ import WidgetFocusContext from 'views/components/contexts/WidgetFocusContext';
import SearchExecutionState from 'views/logic/search/SearchExecutionState';
import { RefluxActions } from 'stores/StoreTypes';
import MessageEventTypesProvider from 'views/components/contexts/MessageEventTypesProvider';
import FieldAndValueActionsProvider from 'views/components/contexts/FieldAndValueActionsProvider';

const GridContainer = styled.div<{ interactive: boolean }>(({ interactive }) => {
return interactive ? css`
Expand Down Expand Up @@ -194,58 +195,60 @@ const Search = ({ location }: Props) => {

return (
<MessageEventTypesProvider>
<WidgetFocusProvider>
<WidgetFocusContext.Consumer>
{({ focusedWidget: { focusing: focusingWidget, editing: editingWidget } = { focusing: false, editing: false } }) => (
<CurrentViewTypeProvider>
<IfInteractive>
<IfDashboard>
<WindowLeaveMessage />
</IfDashboard>
</IfInteractive>
<InteractiveContext.Consumer>
{(interactive) => (
<SearchPageLayoutProvider>
<DefaultFieldTypesProvider>
<ViewAdditionalContextProvider>
<HighlightingRulesProvider>
<GridContainer id="main-row" interactive={interactive}>
<IfInteractive>
<ConnectedSidebar>
<FieldsOverview />
</ConnectedSidebar>
</IfInteractive>
<SearchArea>
<FieldAndValueActionsProvider>
<WidgetFocusProvider>
<WidgetFocusContext.Consumer>
{({ focusedWidget: { focusing: focusingWidget, editing: editingWidget } = { focusing: false, editing: false } }) => (
<CurrentViewTypeProvider>
<IfInteractive>
<IfDashboard>
<WindowLeaveMessage />
</IfDashboard>
</IfInteractive>
<InteractiveContext.Consumer>
{(interactive) => (
<SearchPageLayoutProvider>
<DefaultFieldTypesProvider>
<ViewAdditionalContextProvider>
<HighlightingRulesProvider>
<GridContainer id="main-row" interactive={interactive}>
<IfInteractive>
<HeaderElements />
<IfDashboard>
{!editingWidget && <DashboardSearchBarWithStatus onExecute={refreshIfNotUndeclared} />}
</IfDashboard>
<IfSearch>
<SearchBarWithStatus onExecute={refreshIfNotUndeclared} />
</IfSearch>

<QueryBarElements />

<IfDashboard>
{!focusingWidget && <QueryBar />}
</IfDashboard>
<ConnectedSidebar>
<FieldsOverview />
</ConnectedSidebar>
</IfInteractive>
<HighlightMessageInQuery>
<SearchResult hasErrors={hasErrors} />
</HighlightMessageInQuery>
</SearchArea>
</GridContainer>
</HighlightingRulesProvider>
</ViewAdditionalContextProvider>
</DefaultFieldTypesProvider>
</SearchPageLayoutProvider>
)}
</InteractiveContext.Consumer>
</CurrentViewTypeProvider>
)}
</WidgetFocusContext.Consumer>
</WidgetFocusProvider>
<SearchArea>
<IfInteractive>
<HeaderElements />
<IfDashboard>
{!editingWidget && <DashboardSearchBarWithStatus onExecute={refreshIfNotUndeclared} />}
</IfDashboard>
<IfSearch>
<SearchBarWithStatus onExecute={refreshIfNotUndeclared} />
</IfSearch>

<QueryBarElements />

<IfDashboard>
{!focusingWidget && <QueryBar />}
</IfDashboard>
</IfInteractive>
<HighlightMessageInQuery>
<SearchResult hasErrors={hasErrors} />
</HighlightMessageInQuery>
</SearchArea>
</GridContainer>
</HighlightingRulesProvider>
</ViewAdditionalContextProvider>
</DefaultFieldTypesProvider>
</SearchPageLayoutProvider>
)}
</InteractiveContext.Consumer>
</CurrentViewTypeProvider>
)}
</WidgetFocusContext.Consumer>
</WidgetFocusProvider>
</FieldAndValueActionsProvider>
</MessageEventTypesProvider>
);
};
Expand Down
116 changes: 116 additions & 0 deletions graylog2-web-interface/src/views/components/actions/Action.test.tsx
@@ -0,0 +1,116 @@
/*
* 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 { render, screen } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';
import { createSimpleExternalValueAction } from 'fixtures/externalValueActions';

import FieldAndValueActionsContext, { FieldAndValueActionsContextType } from 'views/components/contexts/FieldAndValueActionsContext';
import FieldType from 'views/logic/fieldtypes/FieldType';

import Action from './Action';

jest.mock('views/logic/usePluginEntities', () => jest.fn(() => []));

describe('Action', () => {
const exampleHandlerArgs = {
queryId: 'query-id',
field: 'field1',
value: 'field-value',
type: new FieldType('string', [], []),
contexts: {},
};

type Props = Partial<React.ComponentProps<typeof Action>> & {
fieldActions?: FieldAndValueActionsContextType['fieldActions'],
valueActions?: FieldAndValueActionsContextType['valueActions'],
}

const SimpleAction = ({
children = 'The dropdown header',
handlerArgs = exampleHandlerArgs,
menuContainer = undefined,
type = 'field',
fieldActions = { internal: undefined },
valueActions = { internal: undefined, external: undefined },
}: Props) => {
return (
<FieldAndValueActionsContext.Provider value={{ fieldActions, valueActions }}>
<Action element={() => <div>Open Actions Menu</div>}
handlerArgs={handlerArgs}
menuContainer={menuContainer}
type={type}>
{children}
</Action>
</FieldAndValueActionsContext.Provider>
);
};

const openDropdown = async (headerTitle = 'The dropdown header') => {
const dropdownToggle = screen.getByText('Open Actions Menu');
userEvent.click(dropdownToggle);
await screen.findByText(headerTitle);
};

it('should render dropdown header', async () => {
render(<SimpleAction>The dropdown header</SimpleAction>);
await openDropdown('The dropdown header');

expect(screen.getByText('The dropdown header')).toBeInTheDocument();
});

it('should work with internal field actions', async () => {
const mockActionHandler = jest.fn();
const fieldActions = {
internal: [
{
type: 'aggregate',
title: 'Show top values',
handler: mockActionHandler,
isEnabled: () => true,
resetFocus: true,
},
],
};

render(<SimpleAction type="field" fieldActions={fieldActions} />);

await openDropdown();

const actionMenuItem = screen.getByText('Show top values');
userEvent.click(actionMenuItem);

expect(mockActionHandler).toHaveBeenCalledTimes(1);
});

it('should work with external value actions', async () => {
const mockActionHandler = jest.fn();
const simpleExternalAction = createSimpleExternalValueAction({ handler: mockActionHandler, title: 'External value action' });
const valueActions = { external: [simpleExternalAction], internal: undefined };

render(
<SimpleAction type="value" valueActions={valueActions} />,
);

await openDropdown();

const actionMenuItem = screen.getByText('External value action');
userEvent.click(actionMenuItem);

expect(mockActionHandler).toHaveBeenCalledTimes(1);
});
});
80 changes: 12 additions & 68 deletions graylog2-web-interface/src/views/components/actions/Action.tsx
Expand Up @@ -15,13 +15,11 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import styled from 'styled-components';
import { useCallback, useContext, useState } from 'react';
import { useCallback, useState } from 'react';

import { MenuItem } from 'components/graylog';
import usePluginEntities from 'views/logic/usePluginEntities';
import { ActionDefinition, ActionHandlerArguments, createHandlerFor } from 'views/components/actions/ActionHandler';
import WidgetFocusContext from 'views/components/contexts/WidgetFocusContext';
import { ActionHandlerArguments } from 'views/components/actions/ActionHandler';

import ActionDropdown from './ActionDropdown';

import OverlayDropdown from '../OverlayDropdown';

Expand All @@ -33,64 +31,12 @@ type Props = {
type: 'field' | 'value',
};

const DropdownHeader = styled.span`
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
margin-bottom: 5px;
font-weight: 600;
`;
const StyledListItem = styled.li`
margin-bottom: 10px;
list-style: none;
`;

const Action = ({ type, handlerArgs, menuContainer, element: Element, children }: Props) => {
const { unsetWidgetFocusing } = useContext(WidgetFocusContext);
const [open, setOpen] = useState(false);
const [overflowingComponents, setOverflowingComponents] = useState({});

const _onMenuToggle = useCallback(() => setOpen(!open), [open]);
const actions = usePluginEntities(`${type}Actions`);
const overflowingComponentsValues: Array<React.ReactNode> = Object.values(overflowingComponents);

const menuItems = actions
.filter((action: ActionDefinition) => {
const { isHidden = () => false } = action;

return !isHidden(handlerArgs);
})
.map((action: ActionDefinition) => {
const setActionComponents = (fn) => {
setOverflowingComponents(fn(overflowingComponents));
};

const handler = createHandlerFor(action, setActionComponents);

const onSelect = () => {
const { resetFocus = false } = action;

if (resetFocus) {
unsetWidgetFocusing();
}

_onMenuToggle();
handler(handlerArgs);
};

const { isEnabled = () => true } = action;
const actionDisabled = !isEnabled(handlerArgs);

const { field } = handlerArgs;

return (
<MenuItem key={`${type}-action-${action.type}`}
disabled={actionDisabled}
eventKey={{ action: type, field }}
onSelect={onSelect}>{action.title}
</MenuItem>
);
});

const element = <Element active={open} />;

return (
Expand All @@ -100,15 +46,13 @@ const Action = ({ type, handlerArgs, menuContainer, element: Element, children }
placement="right"
onToggle={_onMenuToggle}
menuContainer={menuContainer}>
<StyledListItem>
<DropdownHeader>
{children}
</DropdownHeader>
</StyledListItem>

<MenuItem divider />
<MenuItem header>Actions</MenuItem>
{menuItems}
<ActionDropdown handlerArgs={handlerArgs}
type={type}
setOverflowingComponents={setOverflowingComponents}
onMenuToggle={_onMenuToggle}
overflowingComponents={overflowingComponents}>
{children}
</ActionDropdown>
</OverlayDropdown>
{overflowingComponentsValues}
</>
Expand Down

0 comments on commit 71088bc

Please sign in to comment.