Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic actions #58216

Merged
merged 50 commits into from
Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
2dada48
feat: 🎸 add DynamicAction and FactoryAction types
streamich Feb 20, 2020
0cae149
feat: 🎸 add Mutable<T> type to @kbn/utility-types
streamich Feb 20, 2020
171a481
feat: 🎸 add ActionInternal and ActionContract
streamich Feb 20, 2020
6cfc9e7
chore: 🤖 remove unused file
streamich Feb 20, 2020
8e6c65a
feat: 🎸 improve action interfaces
streamich Feb 20, 2020
d14d876
docs: ✏️ add JSDocs
streamich Feb 20, 2020
ac04c80
feat: 🎸 simplify ui_actions interfaces
streamich Feb 20, 2020
179ff8a
fix: 🐛 fix TypeScript types
streamich Feb 21, 2020
0796903
feat: 🎸 add AbstractPresentable interface
streamich Feb 21, 2020
cfce078
feat: 🎸 add AbstractConfigurable interface
streamich Feb 21, 2020
bd0eaf3
feat: 🎸 use AbstractPresentable in ActionInternal
streamich Feb 21, 2020
3700583
test: 💍 fix ui_actions Jest tests
streamich Feb 21, 2020
6e2e30e
feat: 🎸 add state container to action
streamich Feb 24, 2020
4403691
perf: ⚡️ convert MenuItem to React component on Action instance
streamich Feb 24, 2020
3796961
refactor: 💡 rename AbsractPresentable -> Presentable
streamich Feb 24, 2020
9ca0267
refactor: 💡 rename AbstractConfigurable -> Configurable
streamich Feb 24, 2020
6e3d10f
feat: 🎸 add Storybook to ui_actions
streamich Feb 24, 2020
dad08ba
feat: 🎸 add <ErrorConfigureAction> component
streamich Feb 24, 2020
12f09dc
feat: 🎸 improve <ConfigureAction> component
streamich Feb 24, 2020
60d11b8
chore: 🤖 use .story file extension prefix for Storybook
streamich Feb 24, 2020
70061bb
feat: 🎸 improve <ErrorConfigureAction> component
streamich Feb 24, 2020
34c0843
feat: 🎸 show error if dynamic action has CollectConfig missing
streamich Feb 24, 2020
7cbd860
feat: 🎸 render sample action configuration component
streamich Feb 24, 2020
93e17cb
feat: 🎸 connect action config to <ConfigureAction>
streamich Feb 24, 2020
8209a59
feat: 🎸 improve <ConfigureAction> stories
streamich Feb 24, 2020
8982ca0
test: 💍 add ActionInternal serialize/deserialize tests
streamich Feb 25, 2020
358d72c
feat: 🎸 add ActionContract
streamich Feb 25, 2020
59ef699
feat: 🎸 split action Context into Execution and Presentation
streamich Feb 25, 2020
399fa80
fix: 🐛 fix TypeScript error
streamich Feb 25, 2020
95d1424
refactor: 💡 extract state container hooks to module scope
streamich Feb 25, 2020
3c55bd4
docs: ✏️ fix typos
streamich Feb 25, 2020
199d8d3
Merge remote-tracking branch 'upstream/master' into dynamic-action
streamich Feb 26, 2020
cf3669a
Merge remote-tracking branch 'upstream/master' into dynamic-action
streamich Feb 26, 2020
b8bacce
chore: 🤖 remove Mutable<t> type
streamich Feb 26, 2020
e63e92d
test: 💍 don't cast to any getActions() function
streamich Feb 26, 2020
22f3188
style: 💄 avoid using unnecessary types
streamich Feb 26, 2020
079a7c8
chore: 🤖 address PR review comments
streamich Feb 26, 2020
0279111
chore: 🤖 rename ActionContext generic
streamich Feb 27, 2020
a05490d
chore: 🤖 remove order from state container
streamich Feb 27, 2020
bed623c
chore: 🤖 remove deprecation notice on getHref
streamich Feb 27, 2020
3e81792
Merge remote-tracking branch 'upstream/master' into dynamic-action
streamich Feb 27, 2020
7732c84
test: 💍 fix tests after order field change
streamich Feb 27, 2020
55b8721
Merge remote-tracking branch 'upstream/master' into dynamic-action
streamich Feb 27, 2020
a25cf30
Merge branch 'master' into dynamic-action
mattkime Mar 5, 2020
a045b9c
Merge branch 'master' into dynamic-action
mattkime Mar 5, 2020
d0f67ec
Merge branch 'master' into dynamic-action
mattkime Mar 5, 2020
7fbd1fb
Merge branch 'master' into dynamic-action
elasticmachine Mar 5, 2020
5883734
remove comments
mattkime Mar 5, 2020
d056277
Merge branch 'dynamic-action' of github.com:streamich/kibana into dyn…
mattkime Mar 5, 2020
2700d00
chore: 🤖 catch up with master
streamich Mar 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dev/storybook/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export const storybookAliases = {
embeddable: 'src/plugins/embeddable/scripts/storybook.js',
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js',
ui_actions: 'src/plugins/ui_actions/scripts/storybook.js',
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { I18nProvider } from '@kbn/i18n/react';
import { CONTEXT_MENU_TRIGGER } from '../triggers';
import { Action, UiActionsStart } from 'src/plugins/ui_actions/public';
import { Action, UiActionsStart, UiActionsActionContract } from 'src/plugins/ui_actions/public';
import { Trigger, GetEmbeddableFactory, ViewMode } from '../types';
import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables';
import { EmbeddablePanel } from './embeddable_panel';
Expand Down Expand Up @@ -213,14 +213,14 @@ const renderInEditModeAndOpenContextMenu = async (
};

test('HelloWorldContainer in edit mode hides disabledActions', async () => {
const action = {
const action = ({
id: 'FOO',
type: 'FOO',
getIconType: () => undefined,
getDisplayName: () => 'foo',
isCompatible: async () => true,
execute: async () => {},
};
} as any) as UiActionsActionContract<any>;
const getActions = () => Promise.resolve([action]);

const { component: component1 } = await renderInEditModeAndOpenContextMenu(
Expand All @@ -245,14 +245,14 @@ test('HelloWorldContainer in edit mode hides disabledActions', async () => {
});

test('HelloWorldContainer hides disabled badges', async () => {
const action = {
const action = ({
id: 'BAR',
type: 'BAR',
getIconType: () => undefined,
getDisplayName: () => 'bar',
isCompatible: async () => true,
execute: async () => {},
};
} as any) as UiActionsActionContract<any>;
const getActions = () => Promise.resolve([action]);

const { component: component1 } = await renderInEditModeAndOpenContextMenu(
Expand Down
12 changes: 7 additions & 5 deletions src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,13 @@ export class EmbeddablePanel extends React.Component<Props, State> {
new EditPanelAction(this.props.getEmbeddableFactory),
];

const sorted = actions.concat(extraActions).sort((a: Action, b: Action) => {
const bOrder = b.order || 0;
const aOrder = a.order || 0;
return bOrder - aOrder;
});
const sorted = (actions as Array<Action<{ embeddable: IEmbeddable }>>)
.concat(extraActions)
.sort((a, b) => {
const bOrder = b.order || 0;
const aOrder = a.order || 0;
return bOrder - aOrder;
});

return await buildContextMenuForActions({
actions: sorted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,58 @@ import { Comparator, Connect, StateContainer, UnboxState } from './types';

const { useContext, useLayoutEffect, useRef, createElement: h } = React;

/**
* Returns the latest state of a state container.
*
* @param container State container which state to track.
*/
export const useContainerState = <Container extends StateContainer<any, any>>(
container: Container
): UnboxState<Container> => useObservable(container.state$, container.get());

/**
* Apply selector to state container to extract only needed information. Will
* re-render your component only when the section changes.
*
* @param container State container which state to track.
* @param selector Function used to pick parts of state.
* @param comparator Comparator function used to memoize previous result, to not
* re-render React component if state did not change. By default uses
* `fast-deep-equal` package.
*/
export const useContainerSelector = <Container extends StateContainer<any, any>, Result>(
container: Container,
selector: (state: UnboxState<Container>) => Result,
comparator: Comparator<Result> = defaultComparator
): Result => {
const { state$, get } = container;
const lastValueRef = useRef<Result>(get());
const [value, setValue] = React.useState<Result>(() => {
const newValue = selector(get());
lastValueRef.current = newValue;
return newValue;
});
useLayoutEffect(() => {
const subscription = state$.subscribe((currentState: UnboxState<Container>) => {
const newValue = selector(currentState);
if (!comparator(lastValueRef.current, newValue)) {
lastValueRef.current = newValue;
setValue(newValue);
}
});
return () => subscription.unsubscribe();
}, [state$, comparator]);
return value;
};

export const createStateContainerReactHelpers = <Container extends StateContainer<any, any>>() => {
const context = React.createContext<Container>(null as any);

const useContainer = (): Container => useContext(context);

const useState = (): UnboxState<Container> => {
const { state$, get } = useContainer();
const value = useObservable(state$, get());
return value;
const container = useContainer();
return useContainerState(container);
streamich marked this conversation as resolved.
Show resolved Hide resolved
};

const useTransitions: () => Container['transitions'] = () => useContainer().transitions;
Expand All @@ -41,24 +84,8 @@ export const createStateContainerReactHelpers = <Container extends StateContaine
selector: (state: UnboxState<Container>) => Result,
comparator: Comparator<Result> = defaultComparator
): Result => {
const { state$, get } = useContainer();
const lastValueRef = useRef<Result>(get());
const [value, setValue] = React.useState<Result>(() => {
const newValue = selector(get());
lastValueRef.current = newValue;
return newValue;
});
useLayoutEffect(() => {
const subscription = state$.subscribe((currentState: UnboxState<Container>) => {
const newValue = selector(currentState);
if (!comparator(lastValueRef.current, newValue)) {
lastValueRef.current = newValue;
setValue(newValue);
}
});
return () => subscription.unsubscribe();
}, [state$, comparator]);
return value;
const container = useContainer();
return useContainerSelector<Container, Result>(container, selector, comparator);
};

const connect: Connect<UnboxState<Container>> = mapStateToProp => component => props =>
Expand Down
78 changes: 59 additions & 19 deletions src/plugins/ui_actions/public/actions/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,89 @@
* under the License.
*/

import { UiComponent } from 'src/plugins/kibana_utils/common';

export interface Action<ActionContext extends {} = {}> {
/**
* Determined the order when there is more than one action matched to a trigger.
* Higher numbers are displayed first.
*/
order?: number;
import { Presentable } from '../util/presentable';
import { Configurable } from '../util/configurable';

/**
* Legacy action interface, do not use. Use @type {ActionDefinition} and
* @type {ActionInternal} instead.
*
* @deprecated
*/
export interface Action<Context extends {} = {}> extends Partial<Presentable<Context>> {
id: string;

readonly type: string;

/**
* Optional EUI icon type that can be displayed along with the title.
*/
getIconType(context: ActionContext): string | undefined;
getIconType(context: Context): string | undefined;

/**
* Returns a title to be displayed to the user.
* @param context
*/
getDisplayName(context: ActionContext): string;

/**
* `UiComponent` to render when displaying this action as a context menu item.
* If not provided, `getDisplayName` will be used instead.
*/
MenuItem?: UiComponent<{ context: ActionContext }>;
getDisplayName(context: Context): string;

/**
* Returns a promise that resolves to true if this action is compatible given the context,
* otherwise resolves to false.
*/
isCompatible(context: ActionContext): Promise<boolean>;
isCompatible(context: Context): Promise<boolean>;

/**
* If this returns something truthy, this is used in addition to the `execute` method when clicked.
*/
getHref?(context: ActionContext): string | undefined;
getHref?(context: Context): string | undefined;

/**
* Executes the action.
*/
execute(context: Context): Promise<void>;
}

/**
* A convenience interface used to register an action.
*/
export interface ActionDefinition<
streamich marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this has been merged to master after my typescript PR, this is a duplicate type, there is a file action_definition with a ActionDefinition type as well. I think we should use that one, and like @Dosant suggested, tie Config shape with the factory which I think should go in x-pack.

Context extends object = object,
Config extends object | undefined = undefined
> extends Partial<Presentable<Context>>, Partial<Configurable<Config, Context>> {
/**
* ID of the action that uniquely identifies this action in the actions registry.
*/
readonly id: string;

/**
* ID of the factory for this action. Used to construct dynamic actions.
*/
readonly type?: string;

getHref?(context: Context): string | undefined;

/**
* Executes the action.
*/
execute(context: ActionContext): Promise<void>;
execute(context: Context): Promise<void>;
}

export type AnyActionDefinition = ActionDefinition<any, any>;
export type ActionContext<A> = A extends ActionDefinition<infer Context, any> ? Context : never;
export type ActionConfig<A> = A extends ActionDefinition<any, infer Config> ? Config : never;

/**
* A convenience interface used to register a dynamic action.
*
* A dynamic action is one that can be create by user and registered into the
* actions registry at runtime. User can also provide custom config for this
* action. And dynamic actions can be serialized for storage and deserialized
* back.
*/
export type DynamicActionDefinition<
Context extends object = object,
Config extends object | undefined = undefined
> = ActionDefinition<Context, Config> &
Required<Pick<ActionDefinition<Context, Config>, 'CollectConfig' | 'defaultConfig' | 'type'>>;

export type AnyDynamicActionDefinition = DynamicActionDefinition<any, any>;
38 changes: 38 additions & 0 deletions src/plugins/ui_actions/public/actions/action_contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ActionInternal } from './action_internal';
import { AnyActionDefinition } from './action';

/**
* Action representation that is exposed out to other plugins.
*/
export type ActionContract<A extends AnyActionDefinition> = Pick<
ActionInternal<A>,
| 'id'
| 'type'
| 'order'
| 'getIconType'
| 'getDisplayName'
| 'isCompatible'
| 'getHref'
| 'execute'
>;

export type AnyActionContract = ActionContract<any>;
Loading