mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Delete CellActions from UI actions plugin (#149522)
## Summary This PR only deletes the component from the UI action plugin. @semd has already added the component to a new package here https://github.com/elastic/kibana/pull/149057 Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c4a0ed2c19
commit
482d1ced82
26 changed files with 0 additions and 1596 deletions
|
@ -42,7 +42,6 @@ const STORYBOOKS = [
|
|||
'security_solution',
|
||||
'shared_ux',
|
||||
'triggers_actions_ui',
|
||||
'ui_actions',
|
||||
'ui_actions_enhanced',
|
||||
'language_documentation_popover',
|
||||
'unified_search',
|
||||
|
|
|
@ -47,6 +47,5 @@ export const storybookAliases = {
|
|||
threat_intelligence: 'x-pack/plugins/threat_intelligence/.storybook',
|
||||
triggers_actions_ui: 'x-pack/plugins/triggers_actions_ui/.storybook',
|
||||
ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook',
|
||||
ui_actions: 'src/plugins/ui_actions/.storybook',
|
||||
unified_search: 'src/plugins/unified_search/.storybook',
|
||||
};
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const defaultConfig = require('@kbn/storybook').defaultConfig;
|
||||
|
||||
module.exports = {
|
||||
...defaultConfig,
|
||||
stories: ['../**/*.stories.tsx'],
|
||||
reactOptions: {
|
||||
strictMode: true,
|
||||
},
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
This package provides a uniform interface for displaying UI actions for a cell.
|
||||
For the `CellActions` component to work, it must be wrapped by `CellActionsContextProvider`. Ideally, the wrapper should stay on the top of the rendering tree.
|
||||
|
||||
Example:
|
||||
```JSX
|
||||
<CellActionsContextProvider
|
||||
// call uiActions.getTriggerCompatibleActions(triggerId, data)
|
||||
getCompatibleActions={getCompatibleActions}>
|
||||
...
|
||||
<CellActions mode={CellActionsMode.HOVER_POPOVER} triggerId={MY_TRIGGER_ID} config={{ field: 'fieldName', value: 'fieldValue', fieldType: 'text' }}>
|
||||
Hover me
|
||||
</CellActions>
|
||||
</CellActionsContextProvider>
|
||||
|
||||
```
|
||||
|
||||
`CellActions` component will display all compatible actions registered for the trigger id.
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import { ActionItem } from './cell_action_item';
|
||||
|
||||
describe('ActionItem', () => {
|
||||
it('renders', () => {
|
||||
const action = makeAction('test-action');
|
||||
const actionContext = {} as CellActionExecutionContext;
|
||||
const { queryByTestId } = render(
|
||||
<ActionItem action={action} actionContext={actionContext} showTooltip={false} />
|
||||
);
|
||||
expect(queryByTestId('actionItem-test-action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tooltip when showTooltip=true is received', () => {
|
||||
const action = makeAction('test-action');
|
||||
const actionContext = {} as CellActionExecutionContext;
|
||||
const { container } = render(
|
||||
<ActionItem action={action} actionContext={actionContext} showTooltip />
|
||||
);
|
||||
|
||||
expect(container.querySelector('.euiToolTipAnchor')).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { EuiButtonIcon, EuiToolTip, IconType } from '@elastic/eui';
|
||||
import type { Action } from '../../actions';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
|
||||
export const ActionItem = ({
|
||||
action,
|
||||
actionContext,
|
||||
showTooltip,
|
||||
}: {
|
||||
action: Action;
|
||||
actionContext: CellActionExecutionContext;
|
||||
showTooltip: boolean;
|
||||
}) => {
|
||||
const actionProps = useMemo(
|
||||
() => ({
|
||||
iconType: action.getIconType(actionContext) as IconType,
|
||||
onClick: () => action.execute(actionContext),
|
||||
'data-test-subj': `actionItem-${action.id}`,
|
||||
'aria-label': action.getDisplayName(actionContext),
|
||||
}),
|
||||
[action, actionContext]
|
||||
);
|
||||
|
||||
if (!actionProps.iconType) return null;
|
||||
|
||||
return showTooltip ? (
|
||||
<EuiToolTip
|
||||
content={action.getDisplayNameTooltip ? action.getDisplayNameTooltip(actionContext) : ''}
|
||||
>
|
||||
<EuiButtonIcon {...actionProps} iconSize="s" />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiButtonIcon {...actionProps} iconSize="s" />
|
||||
);
|
||||
};
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
import { CellActionsContextProvider } from './cell_actions_context';
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { CellActions, CellActionsMode, CellActionsProps } from './cell_actions';
|
||||
|
||||
const TRIGGER_ID = 'testTriggerId';
|
||||
|
||||
const FIELD = { name: 'name', value: '123', type: 'text' };
|
||||
|
||||
const getCompatibleActions = () =>
|
||||
Promise.resolve([
|
||||
makeAction('Filter in', 'plusInCircle', 2),
|
||||
makeAction('Filter out', 'minusInCircle', 3),
|
||||
makeAction('Minimize', 'minimize', 1),
|
||||
makeAction('Send email', 'email', 4),
|
||||
makeAction('Pin field', 'pin', 5),
|
||||
]);
|
||||
|
||||
export default {
|
||||
title: 'CellAction',
|
||||
decorators: [
|
||||
(storyFn: Function) => (
|
||||
<CellActionsContextProvider
|
||||
// call uiActions getTriggerCompatibleActions(triggerId, data)
|
||||
getTriggerCompatibleActions={getCompatibleActions}
|
||||
>
|
||||
<div style={{ paddingTop: '70px' }} />
|
||||
{storyFn()}
|
||||
</CellActionsContextProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const CellActionsTemplate: ComponentStory<React.FC<CellActionsProps>> = (args) => (
|
||||
<CellActions {...args}>Field value</CellActions>
|
||||
);
|
||||
|
||||
export const DefaultWithControls = CellActionsTemplate.bind({});
|
||||
|
||||
DefaultWithControls.argTypes = {
|
||||
mode: {
|
||||
options: [CellActionsMode.HOVER_POPOVER, CellActionsMode.ALWAYS_VISIBLE],
|
||||
defaultValue: CellActionsMode.HOVER_POPOVER,
|
||||
control: {
|
||||
type: 'radio',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
DefaultWithControls.args = {
|
||||
showActionTooltips: true,
|
||||
mode: CellActionsMode.ALWAYS_VISIBLE,
|
||||
triggerId: TRIGGER_ID,
|
||||
field: FIELD,
|
||||
visibleCellActions: 3,
|
||||
};
|
||||
|
||||
export const CellActionInline = ({}: {}) => (
|
||||
<CellActions mode={CellActionsMode.ALWAYS_VISIBLE} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Field value
|
||||
</CellActions>
|
||||
);
|
||||
|
||||
export const CellActionHoverPopup = ({}: {}) => (
|
||||
<CellActions mode={CellActionsMode.HOVER_POPOVER} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Hover me
|
||||
</CellActions>
|
||||
);
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CellActions, CellActionsMode } from './cell_actions';
|
||||
import { CellActionsContextProvider } from './cell_actions_context';
|
||||
|
||||
const TRIGGER_ID = 'test-trigger-id';
|
||||
const FIELD = { name: 'name', value: '123', type: 'text' };
|
||||
|
||||
describe('CellActions', () => {
|
||||
it('renders', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<CellActions mode={CellActionsMode.ALWAYS_VISIBLE} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Field value
|
||||
</CellActions>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(queryByTestId('cellActions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders InlineActions when mode is ALWAYS_VISIBLE', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<CellActions mode={CellActionsMode.ALWAYS_VISIBLE} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Field value
|
||||
</CellActions>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(queryByTestId('inlineActions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders HoverActionsPopover when mode is HOVER_POPOVER', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<CellActions mode={CellActionsMode.HOVER_POPOVER} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Field value
|
||||
</CellActions>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import type { ActionExecutionContext } from '../../actions';
|
||||
import { InlineActions } from './inline_actions';
|
||||
import { HoverActionsPopover } from './hover_actions_popover';
|
||||
|
||||
export interface CellActionField {
|
||||
/**
|
||||
* Field name.
|
||||
* Example: 'host.name'
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Field type.
|
||||
* Example: 'keyword'
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* Field value.
|
||||
* Example: 'My-Laptop'
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CellActionExecutionContext extends ActionExecutionContext {
|
||||
/**
|
||||
* Ref to a DOM node where the action can add custom HTML.
|
||||
*/
|
||||
extraContentNodeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
|
||||
/**
|
||||
* Ref to the node where the cell action are rendered.
|
||||
*/
|
||||
nodeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
|
||||
/**
|
||||
* Extra configurations for actions.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
field: CellActionField;
|
||||
}
|
||||
|
||||
export enum CellActionsMode {
|
||||
HOVER_POPOVER = 'hover-popover',
|
||||
ALWAYS_VISIBLE = 'always-visible',
|
||||
}
|
||||
|
||||
export interface CellActionsProps {
|
||||
/**
|
||||
* Common set of properties used by most actions.
|
||||
*/
|
||||
field: CellActionField;
|
||||
/**
|
||||
* The trigger in which the actions are registered.
|
||||
*/
|
||||
triggerId: string;
|
||||
/**
|
||||
* UI configuration. Possible options are `HOVER_POPOVER` and `ALWAYS_VISIBLE`.
|
||||
*
|
||||
* `HOVER_POPOVER` shows the actions when the children component is hovered.
|
||||
*
|
||||
* `ALWAYS_VISIBLE` always shows the actions.
|
||||
*/
|
||||
mode: CellActionsMode;
|
||||
|
||||
/**
|
||||
* It displays a tooltip for every action button when `true`.
|
||||
*/
|
||||
showActionTooltips?: boolean;
|
||||
/**
|
||||
* It shows 'more actions' button when the number of actions is bigger than this parameter.
|
||||
*/
|
||||
visibleCellActions?: number;
|
||||
/**
|
||||
* Custom set of properties used by some actions.
|
||||
* An action might require a specific set of metadata properties to render.
|
||||
* This data is sent directly to actions.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const CellActions: React.FC<CellActionsProps> = ({
|
||||
field,
|
||||
triggerId,
|
||||
children,
|
||||
mode,
|
||||
showActionTooltips = true,
|
||||
visibleCellActions = 3,
|
||||
metadata,
|
||||
}) => {
|
||||
const extraContentNodeRef = useRef<HTMLDivElement | null>(null);
|
||||
const nodeRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const actionContext: CellActionExecutionContext = useMemo(
|
||||
() => ({
|
||||
field,
|
||||
trigger: { id: triggerId },
|
||||
extraContentNodeRef,
|
||||
nodeRef,
|
||||
metadata,
|
||||
}),
|
||||
[field, triggerId, metadata]
|
||||
);
|
||||
|
||||
if (mode === CellActionsMode.HOVER_POPOVER) {
|
||||
return (
|
||||
<div ref={nodeRef} data-test-subj={'cellActions'}>
|
||||
<HoverActionsPopover
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={showActionTooltips}
|
||||
visibleCellActions={visibleCellActions}
|
||||
>
|
||||
{children}
|
||||
</HoverActionsPopover>
|
||||
|
||||
<div ref={extraContentNodeRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={nodeRef} data-test-subj={'cellActions'}>
|
||||
{children}
|
||||
<InlineActions
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={showActionTooltips}
|
||||
visibleCellActions={visibleCellActions}
|
||||
/>
|
||||
<div ref={extraContentNodeRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import {
|
||||
CellActionsContextProvider,
|
||||
useLoadActions,
|
||||
useLoadActionsFn,
|
||||
} from './cell_actions_context';
|
||||
|
||||
describe('CellActionsContextProvider', () => {
|
||||
const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext;
|
||||
|
||||
it('loads actions when useLoadActionsFn callback is called', async () => {
|
||||
const action = makeAction('action-1', 'icon', 1);
|
||||
const getActionsPromise = Promise.resolve([action]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useLoadActionsFn(),
|
||||
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
{children}
|
||||
</CellActionsContextProvider>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const [{ value: valueBeforeFnCalled }, loadActions] = result.current;
|
||||
|
||||
// value is undefined before loadActions is called
|
||||
expect(valueBeforeFnCalled).toBeUndefined();
|
||||
|
||||
await act(async () => {
|
||||
loadActions(actionContext);
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
const [{ value: valueAfterFnCalled }] = result.current;
|
||||
|
||||
expect(valueAfterFnCalled).toEqual([action]);
|
||||
});
|
||||
|
||||
it('loads actions when useLoadActions called', async () => {
|
||||
const action = makeAction('action-1', 'icon', 1);
|
||||
const getActionsPromise = Promise.resolve([action]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useLoadActions(actionContext),
|
||||
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
{children}
|
||||
</CellActionsContextProvider>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(result.current.value).toEqual([action]);
|
||||
});
|
||||
|
||||
it('sorts actions by order', async () => {
|
||||
const firstAction = makeAction('action-1', 'icon', 1);
|
||||
const secondAction = makeAction('action-2', 'icon', 2);
|
||||
const getActionsPromise = Promise.resolve([secondAction, firstAction]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useLoadActions(actionContext),
|
||||
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
{children}
|
||||
</CellActionsContextProvider>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(result.current.value).toEqual([firstAction, secondAction]);
|
||||
});
|
||||
|
||||
it('sorts actions by id when order is undefined', async () => {
|
||||
const firstAction = makeAction('action-1');
|
||||
const secondAction = makeAction('action-2');
|
||||
|
||||
const getActionsPromise = Promise.resolve([secondAction, firstAction]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useLoadActions(actionContext),
|
||||
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
{children}
|
||||
</CellActionsContextProvider>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(result.current.value).toEqual([firstAction, secondAction]);
|
||||
});
|
||||
|
||||
it('sorts actions by id and order', async () => {
|
||||
const actionWithoutOrder = makeAction('action-1-no-order');
|
||||
const secondAction = makeAction('action-2', 'icon', 2);
|
||||
const thirdAction = makeAction('action-3', 'icon', 3);
|
||||
|
||||
const getActionsPromise = Promise.resolve([secondAction, actionWithoutOrder, thirdAction]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useLoadActions(actionContext),
|
||||
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
{children}
|
||||
</CellActionsContextProvider>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(result.current.value).toEqual([secondAction, thirdAction, actionWithoutOrder]);
|
||||
});
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { orderBy } from 'lodash/fp';
|
||||
import React, { createContext, FC, useCallback, useContext } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||
import type { Action } from '../../actions';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
|
||||
// It must to match `UiActionsService.getTriggerCompatibleActions`
|
||||
type GetTriggerCompatibleActionsType = (triggerId: string, context: object) => Promise<Action[]>;
|
||||
|
||||
type GetActionsType = (context: CellActionExecutionContext) => Promise<Action[]>;
|
||||
|
||||
const CellActionsContext = createContext<{ getActions: GetActionsType } | null>(null);
|
||||
|
||||
interface CellActionsContextProviderProps {
|
||||
/**
|
||||
* Please assign `uiActions.getTriggerCompatibleActions` function.
|
||||
* This function should return a list of actions for a triggerId that are compatible with the provided context.
|
||||
*/
|
||||
getTriggerCompatibleActions: GetTriggerCompatibleActionsType;
|
||||
}
|
||||
|
||||
export const CellActionsContextProvider: FC<CellActionsContextProviderProps> = ({
|
||||
children,
|
||||
getTriggerCompatibleActions,
|
||||
}) => {
|
||||
const getSortedCompatibleActions = useCallback<GetActionsType>(
|
||||
(context) =>
|
||||
getTriggerCompatibleActions(context.trigger.id, context).then((actions) =>
|
||||
orderBy(['order', 'id'], ['asc', 'asc'], actions)
|
||||
),
|
||||
[getTriggerCompatibleActions]
|
||||
);
|
||||
|
||||
return (
|
||||
<CellActionsContext.Provider value={{ getActions: getSortedCompatibleActions }}>
|
||||
{children}
|
||||
</CellActionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useCellActions = () => {
|
||||
const context = useContext(CellActionsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'No CellActionsContext found. Please wrap the application with CellActionsContextProvider'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useLoadActions = (context: CellActionExecutionContext) => {
|
||||
const { getActions } = useCellActions();
|
||||
return useAsync(() => getActions(context), []);
|
||||
};
|
||||
|
||||
export const useLoadActionsFn = () => {
|
||||
const { getActions } = useCellActions();
|
||||
return useAsyncFn(getActions, []);
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ExtraActionsButton } from './extra_actions_button';
|
||||
|
||||
describe('ExtraActionsButton', () => {
|
||||
it('renders', () => {
|
||||
const { queryByTestId } = render(<ExtraActionsButton onClick={() => {}} showTooltip={false} />);
|
||||
|
||||
expect(queryByTestId('showExtraActionsButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tooltip when showTooltip=true is received', () => {
|
||||
const { container } = render(<ExtraActionsButton onClick={() => {}} showTooltip />);
|
||||
expect(container.querySelector('.euiToolTipAnchor')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('calls onClick when button is clicked', () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByTestId } = render(<ExtraActionsButton onClick={onClick} showTooltip />);
|
||||
|
||||
fireEvent.click(getByTestId('showExtraActionsButton'));
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { SHOW_MORE_ACTIONS } from './translations';
|
||||
|
||||
interface ExtraActionsButtonProps {
|
||||
onClick: () => void;
|
||||
showTooltip: boolean;
|
||||
}
|
||||
|
||||
export const ExtraActionsButton: React.FC<ExtraActionsButtonProps> = ({ onClick, showTooltip }) =>
|
||||
showTooltip ? (
|
||||
<EuiToolTip content={SHOW_MORE_ACTIONS}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="showExtraActionsButton"
|
||||
aria-label={SHOW_MORE_ACTIONS}
|
||||
iconType="boxesHorizontal"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="showExtraActionsButton"
|
||||
aria-label={SHOW_MORE_ACTIONS}
|
||||
iconType="boxesHorizontal"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { ExtraActionsPopOver, ExtraActionsPopOverWithAnchor } from './extra_actions_popover';
|
||||
|
||||
const actionContext = { field: { name: 'fieldName' } } as CellActionExecutionContext;
|
||||
describe('ExtraActionsPopOver', () => {
|
||||
it('renders', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ExtraActionsPopOver
|
||||
actionContext={actionContext}
|
||||
isOpen={false}
|
||||
closePopOver={() => {}}
|
||||
actions={[]}
|
||||
button={<span />}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('executes action and close popover when menu item is clicked', async () => {
|
||||
const executeAction = jest.fn();
|
||||
const closePopOver = jest.fn();
|
||||
const action = { ...makeAction('test-action'), execute: executeAction };
|
||||
const { getByLabelText } = render(
|
||||
<ExtraActionsPopOver
|
||||
actionContext={actionContext}
|
||||
isOpen={true}
|
||||
closePopOver={closePopOver}
|
||||
actions={[action]}
|
||||
button={<span />}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await fireEvent.click(getByLabelText('test-action'));
|
||||
});
|
||||
|
||||
expect(executeAction).toHaveBeenCalled();
|
||||
expect(closePopOver).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExtraActionsPopOverWithAnchor', () => {
|
||||
const anchorElement = document.createElement('span');
|
||||
document.body.appendChild(anchorElement);
|
||||
|
||||
it('renders', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ExtraActionsPopOverWithAnchor
|
||||
actionContext={actionContext}
|
||||
isOpen={false}
|
||||
closePopOver={() => {}}
|
||||
actions={[]}
|
||||
anchorRef={{ current: anchorElement }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('executes action and close popover when menu item is clicked', () => {
|
||||
const executeAction = jest.fn();
|
||||
const closePopOver = jest.fn();
|
||||
const action = { ...makeAction('test-action'), execute: executeAction };
|
||||
const { getByLabelText } = render(
|
||||
<ExtraActionsPopOverWithAnchor
|
||||
actionContext={actionContext}
|
||||
isOpen={true}
|
||||
closePopOver={closePopOver}
|
||||
actions={[action]}
|
||||
anchorRef={{ current: anchorElement }}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(getByLabelText('test-action'));
|
||||
|
||||
expect(executeAction).toHaveBeenCalled();
|
||||
expect(closePopOver).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
EuiScreenReaderOnly,
|
||||
EuiWrappingPopover,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
import type { Action } from '../../actions';
|
||||
import { EXTRA_ACTIONS_ARIA_LABEL, YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS } from './translations';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
|
||||
const euiContextMenuItemCSS = css`
|
||||
color: ${euiThemeVars.euiColorPrimaryText};
|
||||
`;
|
||||
|
||||
interface ActionsPopOverProps {
|
||||
actionContext: CellActionExecutionContext;
|
||||
isOpen: boolean;
|
||||
closePopOver: () => void;
|
||||
actions: Action[];
|
||||
button: JSX.Element;
|
||||
}
|
||||
|
||||
export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
||||
actions,
|
||||
actionContext,
|
||||
isOpen,
|
||||
closePopOver,
|
||||
button,
|
||||
}) => (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopOver}
|
||||
panelPaddingSize="xs"
|
||||
anchorPosition={'downCenter'}
|
||||
hasArrow
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
data-test-subj="extraActionsPopOver"
|
||||
aria-label={EXTRA_ACTIONS_ARIA_LABEL}
|
||||
>
|
||||
<ExtraActionsPopOverContent
|
||||
actions={actions}
|
||||
actionContext={actionContext}
|
||||
closePopOver={closePopOver}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
||||
interface ExtraActionsPopOverWithAnchorProps
|
||||
extends Pick<ActionsPopOverProps, 'actionContext' | 'closePopOver' | 'isOpen' | 'actions'> {
|
||||
anchorRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const ExtraActionsPopOverWithAnchor = ({
|
||||
anchorRef,
|
||||
actionContext,
|
||||
isOpen,
|
||||
closePopOver,
|
||||
actions,
|
||||
}: ExtraActionsPopOverWithAnchorProps) => {
|
||||
return anchorRef.current ? (
|
||||
<EuiWrappingPopover
|
||||
aria-label={EXTRA_ACTIONS_ARIA_LABEL}
|
||||
button={anchorRef.current}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopOver}
|
||||
panelPaddingSize="xs"
|
||||
anchorPosition={'downCenter'}
|
||||
hasArrow={false}
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
attachToAnchor={false}
|
||||
data-test-subj="extraActionsPopOverWithAnchor"
|
||||
>
|
||||
<ExtraActionsPopOverContent
|
||||
actions={actions}
|
||||
actionContext={actionContext}
|
||||
closePopOver={closePopOver}
|
||||
/>
|
||||
</EuiWrappingPopover>
|
||||
) : null;
|
||||
};
|
||||
|
||||
type ExtraActionsPopOverContentProps = Pick<
|
||||
ActionsPopOverProps,
|
||||
'actionContext' | 'closePopOver' | 'actions'
|
||||
>;
|
||||
|
||||
const ExtraActionsPopOverContent: React.FC<ExtraActionsPopOverContentProps> = ({
|
||||
actionContext,
|
||||
actions,
|
||||
closePopOver,
|
||||
}) => {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
actions.map((action) => (
|
||||
<EuiContextMenuItem
|
||||
css={euiContextMenuItemCSS}
|
||||
key={action.id}
|
||||
icon={action.getIconType(actionContext)}
|
||||
aria-label={action.getDisplayName(actionContext)}
|
||||
onClick={() => {
|
||||
closePopOver();
|
||||
action.execute(actionContext);
|
||||
}}
|
||||
>
|
||||
{action.getDisplayName(actionContext)}
|
||||
</EuiContextMenuItem>
|
||||
)),
|
||||
[actionContext, actions, closePopOver]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<EuiScreenReaderOnly>
|
||||
<p>{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { HoverActionsPopover } from './hover_actions_popover';
|
||||
import { CellActionsContextProvider } from './cell_actions_context';
|
||||
|
||||
describe('HoverActionsPopover', () => {
|
||||
const actionContext = {
|
||||
trigger: { id: 'triggerId' },
|
||||
field: { name: 'fieldName' },
|
||||
} as CellActionExecutionContext;
|
||||
const TestComponent = () => <span data-test-subj="test-component" />;
|
||||
jest.useFakeTimers();
|
||||
|
||||
it('renders', () => {
|
||||
const getActions = () => Promise.resolve([]);
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
children={null}
|
||||
visibleCellActions={4}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
/>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders actions when hovered', async () => {
|
||||
const action = makeAction('test-action');
|
||||
const getActionsPromise = Promise.resolve([action]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { queryByLabelText, getByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
visibleCellActions={4}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(queryByLabelText('test-action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hide actions when mouse stops hovering', async () => {
|
||||
const action = makeAction('test-action');
|
||||
const getActionsPromise = Promise.resolve([action]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { queryByLabelText, getByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
visibleCellActions={4}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
// Mouse leaves hover state
|
||||
await act(async () => {
|
||||
fireEvent.mouseLeave(getByTestId('test-component'));
|
||||
});
|
||||
|
||||
expect(queryByLabelText('test-action')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders extra actions button', async () => {
|
||||
const actions = [makeAction('test-action-1'), makeAction('test-action-2')];
|
||||
const getActionsPromise = Promise.resolve(actions);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { getByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
visibleCellActions={1}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(getByTestId('showExtraActionsButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows extra actions when extra actions button is clicked', async () => {
|
||||
const actions = [makeAction('test-action-1'), makeAction('test-action-2')];
|
||||
const getActionsPromise = Promise.resolve(actions);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { getByTestId, getByLabelText } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
visibleCellActions={1}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId('showExtraActionsButton'));
|
||||
});
|
||||
|
||||
expect(getByLabelText('test-action-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render visible actions if extra actions are already rendered', async () => {
|
||||
const actions = [
|
||||
makeAction('test-action-1'),
|
||||
// extra actions
|
||||
makeAction('test-action-2'),
|
||||
makeAction('test-action-3'),
|
||||
];
|
||||
const getActionsPromise = Promise.resolve(actions);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { getByTestId, queryByLabelText } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
visibleCellActions={2}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId('showExtraActionsButton'));
|
||||
});
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(queryByLabelText('test-action-1')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText('test-action-2')).toBeInTheDocument();
|
||||
expect(queryByLabelText('test-action-3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const hoverElement = async (element: Element, waitForChange: () => Promise<unknown>) => {
|
||||
await act(async () => {
|
||||
fireEvent.mouseEnter(element);
|
||||
await waitForChange();
|
||||
});
|
||||
};
|
|
@ -1,168 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiPopover, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
import { debounce } from 'lodash';
|
||||
import { ActionItem } from './cell_action_item';
|
||||
import { ExtraActionsButton } from './extra_actions_button';
|
||||
import { ACTIONS_AREA_LABEL, YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS } from './translations';
|
||||
import { partitionActions } from '../hooks/actions';
|
||||
import { ExtraActionsPopOverWithAnchor } from './extra_actions_popover';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import { useLoadActionsFn } from './cell_actions_context';
|
||||
|
||||
/** This class is added to the document body while dragging */
|
||||
export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
|
||||
|
||||
// Overwrite Popover default minWidth to avoid displaying empty space
|
||||
const PANEL_STYLE = { minWidth: `24px` };
|
||||
|
||||
const hoverContentWrapperCSS = css`
|
||||
padding: 0 ${euiThemeVars.euiSizeS};
|
||||
`;
|
||||
|
||||
/**
|
||||
* To avoid expensive changes to the DOM, delay showing the popover menu
|
||||
*/
|
||||
const HOVER_INTENT_DELAY = 100; // ms
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
visibleCellActions: number;
|
||||
actionContext: CellActionExecutionContext;
|
||||
showActionTooltips: boolean;
|
||||
}
|
||||
|
||||
export const HoverActionsPopover = React.memo<Props>(
|
||||
({ children, visibleCellActions, actionContext, showActionTooltips }) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isExtraActionsPopoverOpen, setIsExtraActionsPopoverOpen] = useState(false);
|
||||
const [showHoverContent, setShowHoverContent] = useState(false);
|
||||
const popoverRef = useRef<EuiPopover>(null);
|
||||
|
||||
const [{ value: actions }, loadActions] = useLoadActionsFn();
|
||||
|
||||
const { visibleActions, extraActions } = useMemo(
|
||||
() => partitionActions(actions ?? [], visibleCellActions),
|
||||
[actions, visibleCellActions]
|
||||
);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setShowHoverContent(false);
|
||||
}, []);
|
||||
|
||||
const closeExtraActions = useCallback(
|
||||
() => setIsExtraActionsPopoverOpen(false),
|
||||
[setIsExtraActionsPopoverOpen]
|
||||
);
|
||||
|
||||
const onShowExtraActionsClick = useCallback(() => {
|
||||
setIsExtraActionsPopoverOpen(true);
|
||||
closePopover();
|
||||
}, [closePopover, setIsExtraActionsPopoverOpen]);
|
||||
|
||||
const openPopOverDebounced = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) {
|
||||
setShowHoverContent(true);
|
||||
}
|
||||
}, HOVER_INTENT_DELAY),
|
||||
[]
|
||||
);
|
||||
|
||||
// prevent setState on an unMounted component
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
openPopOverDebounced.cancel();
|
||||
};
|
||||
}, [openPopOverDebounced]);
|
||||
|
||||
const onMouseEnter = useCallback(async () => {
|
||||
// Do not open actions with extra action popover is open
|
||||
if (isExtraActionsPopoverOpen) return;
|
||||
|
||||
// memoize actions after the first call
|
||||
if (actions === undefined) {
|
||||
loadActions(actionContext);
|
||||
}
|
||||
|
||||
openPopOverDebounced();
|
||||
}, [isExtraActionsPopoverOpen, actions, openPopOverDebounced, loadActions, actionContext]);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
closePopover();
|
||||
}, [closePopover]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
return (
|
||||
// Hack - Forces extra actions popover to close when hover content is clicked.
|
||||
// This hack is required because we anchor the popover to the hover content instead
|
||||
// of anchoring it to the button that triggers the popover.
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div ref={contentRef} onMouseEnter={onMouseEnter} onClick={closeExtraActions}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}, [onMouseEnter, closeExtraActions, children]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onMouseLeave={onMouseLeave}>
|
||||
<EuiPopover
|
||||
panelStyle={PANEL_STYLE}
|
||||
ref={popoverRef}
|
||||
anchorPosition={'downCenter'}
|
||||
button={content}
|
||||
closePopover={closePopover}
|
||||
hasArrow={false}
|
||||
isOpen={showHoverContent}
|
||||
panelPaddingSize="none"
|
||||
repositionOnScroll
|
||||
ownFocus={false}
|
||||
data-test-subj={'hoverActionsPopover'}
|
||||
aria-label={ACTIONS_AREA_LABEL}
|
||||
>
|
||||
{showHoverContent ? (
|
||||
<div css={hoverContentWrapperCSS}>
|
||||
<EuiScreenReaderOnly>
|
||||
<p>{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
{visibleActions.map((action) => (
|
||||
<ActionItem
|
||||
key={action.id}
|
||||
action={action}
|
||||
actionContext={actionContext}
|
||||
showTooltip={showActionTooltips}
|
||||
/>
|
||||
))}
|
||||
{extraActions.length > 0 ? (
|
||||
<ExtraActionsButton
|
||||
onClick={onShowExtraActionsClick}
|
||||
showTooltip={showActionTooltips}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</EuiPopover>
|
||||
</div>
|
||||
<ExtraActionsPopOverWithAnchor
|
||||
actions={extraActions}
|
||||
anchorRef={contentRef}
|
||||
actionContext={actionContext}
|
||||
closePopOver={closeExtraActions}
|
||||
isOpen={isExtraActionsPopoverOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { CellActions, CellActionsMode } from './cell_actions';
|
||||
export { CellActionsContextProvider } from './cell_actions_context';
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { InlineActions } from './inline_actions';
|
||||
import { CellActionsContextProvider } from '.';
|
||||
|
||||
describe('InlineActions', () => {
|
||||
const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext;
|
||||
it('renders', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<InlineActions
|
||||
visibleCellActions={5}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
/>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(queryByTestId('inlineActions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all actions', async () => {
|
||||
const getActionsPromise = Promise.resolve([
|
||||
makeAction('action-1'),
|
||||
makeAction('action-2'),
|
||||
makeAction('action-3'),
|
||||
makeAction('action-4'),
|
||||
makeAction('action-5'),
|
||||
]);
|
||||
const getActions = () => getActionsPromise;
|
||||
const { queryAllByRole } = render(
|
||||
<CellActionsContextProvider getTriggerCompatibleActions={getActions}>
|
||||
<InlineActions
|
||||
visibleCellActions={5}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
/>
|
||||
</CellActionsContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(queryAllByRole('button').length).toBe(5);
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { ActionItem } from './cell_action_item';
|
||||
import { usePartitionActions } from '../hooks/actions';
|
||||
import { ExtraActionsPopOver } from './extra_actions_popover';
|
||||
import { ExtraActionsButton } from './extra_actions_button';
|
||||
import { CellActionExecutionContext } from './cell_actions';
|
||||
import { useLoadActions } from './cell_actions_context';
|
||||
|
||||
interface InlineActionsProps {
|
||||
actionContext: CellActionExecutionContext;
|
||||
showActionTooltips: boolean;
|
||||
visibleCellActions: number;
|
||||
}
|
||||
|
||||
export const InlineActions: React.FC<InlineActionsProps> = ({
|
||||
actionContext,
|
||||
showActionTooltips,
|
||||
visibleCellActions,
|
||||
}) => {
|
||||
const { value: allActions } = useLoadActions(actionContext);
|
||||
const { extraActions, visibleActions } = usePartitionActions(
|
||||
allActions ?? [],
|
||||
visibleCellActions
|
||||
);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const togglePopOver = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []);
|
||||
const closePopOver = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const button = useMemo(
|
||||
() => <ExtraActionsButton onClick={togglePopOver} showTooltip={showActionTooltips} />,
|
||||
[togglePopOver, showActionTooltips]
|
||||
);
|
||||
|
||||
return (
|
||||
<span data-test-subj="inlineActions">
|
||||
{visibleActions.map((action, index) => (
|
||||
<ActionItem
|
||||
key={`action-item-${index}`}
|
||||
action={action}
|
||||
actionContext={actionContext}
|
||||
showTooltip={showActionTooltips}
|
||||
/>
|
||||
))}
|
||||
{extraActions.length > 0 ? (
|
||||
<ExtraActionsPopOver
|
||||
actions={extraActions}
|
||||
actionContext={actionContext}
|
||||
button={button}
|
||||
closePopOver={closePopOver}
|
||||
isOpen={isPopoverOpen}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS = (fieldName: string) =>
|
||||
i18n.translate('uiActions.cellActions.youAreInADialogContainingOptionsScreenReaderOnly', {
|
||||
values: { fieldName },
|
||||
defaultMessage: `You are in a dialog, containing options for field {fieldName}. Press tab to navigate options. Press escape to exit.`,
|
||||
});
|
||||
|
||||
export const EXTRA_ACTIONS_ARIA_LABEL = i18n.translate(
|
||||
'uiActions.cellActions.extraActionsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Extra actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_MORE_ACTIONS = i18n.translate('uiActions.showMoreActionsLabel', {
|
||||
defaultMessage: 'More actions',
|
||||
});
|
||||
|
||||
export const ACTIONS_AREA_LABEL = i18n.translate('uiActions.cellActions.actionsAriaLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { makeAction } from '../mocks/helpers';
|
||||
import { partitionActions } from './actions';
|
||||
|
||||
describe('InlineActions', () => {
|
||||
it('returns an empty array when actions is an empty array', async () => {
|
||||
const { extraActions, visibleActions } = partitionActions([], 5);
|
||||
|
||||
expect(visibleActions).toEqual([]);
|
||||
expect(extraActions).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns only visible actions when visibleCellActions > actions.length', async () => {
|
||||
const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')];
|
||||
const { extraActions, visibleActions } = partitionActions(actions, 4);
|
||||
|
||||
expect(visibleActions.length).toEqual(actions.length);
|
||||
expect(extraActions).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns only extra actions when visibleCellActions is 1', async () => {
|
||||
const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')];
|
||||
const { extraActions, visibleActions } = partitionActions(actions, 1);
|
||||
|
||||
expect(visibleActions).toEqual([]);
|
||||
expect(extraActions.length).toEqual(actions.length);
|
||||
});
|
||||
|
||||
it('returns only extra actions when visibleCellActions is 0', async () => {
|
||||
const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')];
|
||||
const { extraActions, visibleActions } = partitionActions(actions, 0);
|
||||
|
||||
expect(visibleActions).toEqual([]);
|
||||
expect(extraActions.length).toEqual(actions.length);
|
||||
});
|
||||
|
||||
it('returns only extra actions when visibleCellActions is negative', async () => {
|
||||
const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')];
|
||||
const { extraActions, visibleActions } = partitionActions(actions, -6);
|
||||
|
||||
expect(visibleActions).toEqual([]);
|
||||
expect(extraActions.length).toEqual(actions.length);
|
||||
});
|
||||
|
||||
it('returns only one visible action when visibleCellActionss 2 and action.length is 3', async () => {
|
||||
const { extraActions, visibleActions } = partitionActions(
|
||||
[makeAction('action-1'), makeAction('action-2'), makeAction('action-3')],
|
||||
2
|
||||
);
|
||||
|
||||
expect(visibleActions.length).toEqual(1);
|
||||
expect(extraActions.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('returns two visible actions when visibleCellActions is 3 and action.length is 5', async () => {
|
||||
const { extraActions, visibleActions } = partitionActions(
|
||||
[
|
||||
makeAction('action-1'),
|
||||
makeAction('action-2'),
|
||||
makeAction('action-3'),
|
||||
makeAction('action-4'),
|
||||
makeAction('action-5'),
|
||||
],
|
||||
3
|
||||
);
|
||||
expect(visibleActions.length).toEqual(2);
|
||||
expect(extraActions.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('returns three visible actions when visibleCellActions is 3 and action.length is 3', async () => {
|
||||
const { extraActions, visibleActions } = partitionActions(
|
||||
[makeAction('action-1'), makeAction('action-2'), makeAction('action-3')],
|
||||
3
|
||||
);
|
||||
expect(visibleActions.length).toEqual(3);
|
||||
expect(extraActions.length).toEqual(0);
|
||||
});
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Action } from '../../actions';
|
||||
|
||||
export const partitionActions = (actions: Action[], visibleCellActions: number) => {
|
||||
if (visibleCellActions <= 1) return { extraActions: actions, visibleActions: [] };
|
||||
if (actions.length <= visibleCellActions) return { extraActions: [], visibleActions: actions };
|
||||
|
||||
return {
|
||||
visibleActions: actions.slice(0, visibleCellActions - 1),
|
||||
extraActions: actions.slice(visibleCellActions - 1, actions.length),
|
||||
};
|
||||
};
|
||||
|
||||
export interface PartitionedActions {
|
||||
extraActions: Array<Action<object>>;
|
||||
visibleActions: Array<Action<object>>;
|
||||
}
|
||||
|
||||
export const usePartitionActions = (
|
||||
allActions: Action[],
|
||||
visibleCellActions: number
|
||||
): PartitionedActions => {
|
||||
return useMemo(() => {
|
||||
return partitionActions(allActions ?? [], visibleCellActions);
|
||||
}, [allActions, visibleCellActions]);
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const makeAction = (actionsName: string, icon: string = 'icon', order?: number) => ({
|
||||
id: actionsName,
|
||||
type: actionsName,
|
||||
order,
|
||||
getIconType: () => icon,
|
||||
getDisplayName: () => actionsName,
|
||||
getDisplayNameTooltip: () => actionsName,
|
||||
isCompatible: () => Promise.resolve(true),
|
||||
execute: () => {
|
||||
alert(actionsName);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
|
@ -39,10 +39,3 @@ export {
|
|||
ACTION_VISUALIZE_LENS_FIELD,
|
||||
} from './types';
|
||||
export type { ActionExecutionContext, ActionExecutionMeta, ActionMenuItemProps } from './actions';
|
||||
export {
|
||||
CellActions,
|
||||
CellActionsMode,
|
||||
CellActionsContextProvider,
|
||||
} from './cell_actions/components';
|
||||
|
||||
export type { CellActionExecutionContext } from './cell_actions/components/cell_actions';
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"@kbn/utility-types",
|
||||
"@kbn/i18n",
|
||||
"@kbn/es-query",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/ui-actions-browser",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue