Skip to content

Commit

Permalink
[Lens] Enable actions on Lens Embeddable (elastic#102038)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
dej611 and kibanamachine committed Jun 25, 2021
1 parent 922d7cc commit dfc70bd
Show file tree
Hide file tree
Showing 17 changed files with 333 additions and 100 deletions.
Expand Up @@ -36,6 +36,7 @@
| [isSavedObjectEmbeddableInput(input)](./kibana-plugin-plugins-embeddable-public.issavedobjectembeddableinput.md) | |
| [openAddPanelFlyout(options)](./kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-embeddable-public.plugin.md) | |
| [useEmbeddableFactory({ input, factory, onInputUpdated, })](./kibana-plugin-plugins-embeddable-public.useembeddablefactory.md) | |

## Interfaces

Expand Down
@@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [useEmbeddableFactory](./kibana-plugin-plugins-embeddable-public.useembeddablefactory.md)

## useEmbeddableFactory() function

<b>Signature:</b>

```typescript
export declare function useEmbeddableFactory<I extends EmbeddableInput>({ input, factory, onInputUpdated, }: EmbeddableRendererWithFactory<I>): readonly [ErrorEmbeddable | IEmbeddable<I, import("./i_embeddable").EmbeddableOutput> | undefined, boolean, string | undefined];
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| { input, factory, onInputUpdated, } | <code>EmbeddableRendererWithFactory&lt;I&gt;</code> | |

<b>Returns:</b>

`readonly [ErrorEmbeddable | IEmbeddable<I, import("./i_embeddable").EmbeddableOutput> | undefined, boolean, string | undefined]`

1 change: 1 addition & 0 deletions src/plugins/embeddable/public/index.ts
Expand Up @@ -69,6 +69,7 @@ export {
EmbeddablePackageState,
EmbeddableRenderer,
EmbeddableRendererProps,
useEmbeddableFactory,
} from './lib';

export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
Expand Down
Expand Up @@ -9,14 +9,39 @@
import React from 'react';
import { waitFor } from '@testing-library/dom';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import {
HelloWorldEmbeddable,
HelloWorldEmbeddableFactoryDefinition,
HELLO_WORLD_EMBEDDABLE,
} from '../../tests/fixtures';
import { EmbeddableRenderer } from './embeddable_renderer';
import { EmbeddableRenderer, useEmbeddableFactory } from './embeddable_renderer';
import { embeddablePluginMock } from '../../mocks';

describe('useEmbeddableFactory', () => {
it('should update upstream value changes', async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
const getFactory = setup.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
new HelloWorldEmbeddableFactoryDefinition()
);
doStart();

const { result, waitForNextUpdate } = renderHook(() =>
useEmbeddableFactory({ factory: getFactory(), input: { id: 'hello' } })
);

const [, loading] = result.current;

expect(loading).toBe(true);

await waitForNextUpdate();

const [embeddable] = result.current;
expect(embeddable).toBeDefined();
});
});

describe('<EmbeddableRenderer/>', () => {
test('Render embeddable', () => {
const embeddable = new HelloWorldEmbeddable({ id: 'hello' });
Expand Down
154 changes: 82 additions & 72 deletions src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx
Expand Up @@ -28,12 +28,6 @@ interface EmbeddableRendererPropsWithEmbeddable<I extends EmbeddableInput> {
embeddable: IEmbeddable<I>;
}

function isWithEmbeddable<I extends EmbeddableInput>(
props: EmbeddableRendererProps<I>
): props is EmbeddableRendererPropsWithEmbeddable<I> {
return 'embeddable' in props;
}

interface EmbeddableRendererWithFactory<I extends EmbeddableInput> {
input: I;
onInputUpdated?: (newInput: I) => void;
Expand All @@ -46,6 +40,72 @@ function isWithFactory<I extends EmbeddableInput>(
return 'factory' in props;
}

export function useEmbeddableFactory<I extends EmbeddableInput>({
input,
factory,
onInputUpdated,
}: EmbeddableRendererWithFactory<I>) {
const [embeddable, setEmbeddable] = useState<IEmbeddable<I> | ErrorEmbeddable | undefined>(
undefined
);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | undefined>();
const latestInput = React.useRef(input);
useEffect(() => {
latestInput.current = input;
}, [input]);

useEffect(() => {
let canceled = false;

// keeping track of embeddables created by this component to be able to destroy them
let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined;
setEmbeddable(undefined);
setLoading(true);
factory
.create(latestInput.current!)
.then((createdEmbeddable) => {
if (canceled) {
if (createdEmbeddable) {
createdEmbeddable.destroy();
}
} else {
createdEmbeddableRef = createdEmbeddable;
setEmbeddable(createdEmbeddable);
}
})
.catch((err) => {
if (canceled) return;
setError(err?.message);
})
.finally(() => {
if (canceled) return;
setLoading(false);
});

return () => {
canceled = true;
if (createdEmbeddableRef) {
createdEmbeddableRef.destroy();
}
};
}, [factory]);

useEffect(() => {
if (!embeddable) return;
if (isErrorEmbeddable(embeddable)) return;
if (!onInputUpdated) return;
const sub = embeddable.getInput$().subscribe((newInput) => {
onInputUpdated(newInput);
});
return () => {
sub.unsubscribe();
};
}, [embeddable, onInputUpdated]);

return [embeddable, loading, error] as const;
}

/**
* Helper react component to render an embeddable
* Can be used if you have an embeddable object or an embeddable factory
Expand Down Expand Up @@ -82,72 +142,22 @@ function isWithFactory<I extends EmbeddableInput>(
export const EmbeddableRenderer = <I extends EmbeddableInput>(
props: EmbeddableRendererProps<I>
) => {
const { input, onInputUpdated } = props;
const [embeddable, setEmbeddable] = useState<IEmbeddable<I> | ErrorEmbeddable | undefined>(
isWithEmbeddable(props) ? props.embeddable : undefined
);
const [loading, setLoading] = useState<boolean>(!isWithEmbeddable(props));
const [error, setError] = useState<string | undefined>();
const latestInput = React.useRef(props.input);
useEffect(() => {
latestInput.current = input;
}, [input]);

const factoryFromProps = isWithFactory(props) ? props.factory : undefined;
const embeddableFromProps = isWithEmbeddable(props) ? props.embeddable : undefined;
useEffect(() => {
let canceled = false;
if (embeddableFromProps) {
setEmbeddable(embeddableFromProps);
return;
}

// keeping track of embeddables created by this component to be able to destroy them
let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined;
if (factoryFromProps) {
setEmbeddable(undefined);
setLoading(true);
factoryFromProps
.create(latestInput.current!)
.then((createdEmbeddable) => {
if (canceled) {
if (createdEmbeddable) {
createdEmbeddable.destroy();
}
} else {
createdEmbeddableRef = createdEmbeddable;
setEmbeddable(createdEmbeddable);
}
})
.catch((err) => {
if (canceled) return;
setError(err?.message);
})
.finally(() => {
if (canceled) return;
setLoading(false);
});
}

return () => {
canceled = true;
if (createdEmbeddableRef) {
createdEmbeddableRef.destroy();
}
};
}, [factoryFromProps, embeddableFromProps]);

useEffect(() => {
if (!embeddable) return;
if (isErrorEmbeddable(embeddable)) return;
if (!onInputUpdated) return;
const sub = embeddable.getInput$().subscribe((newInput) => {
onInputUpdated(newInput);
});
return () => {
sub.unsubscribe();
};
}, [embeddable, onInputUpdated]);
if (isWithFactory(props)) {
return <EmbeddableByFactory {...props} />;
}
return <EmbeddableRoot embeddable={props.embeddable} input={props.input} />;
};

//
const EmbeddableByFactory = <I extends EmbeddableInput>({
factory,
input,
onInputUpdated,
}: EmbeddableRendererWithFactory<I>) => {
const [embeddable, loading, error] = useEmbeddableFactory({
factory,
input,
onInputUpdated,
});
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
};
6 changes: 5 additions & 1 deletion src/plugins/embeddable/public/lib/embeddables/index.ts
Expand Up @@ -16,4 +16,8 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
export { withEmbeddableSubscription } from './with_subscription';
export { EmbeddableRoot } from './embeddable_root';
export * from '../../../common/lib/saved_object_embeddable';
export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer';
export {
EmbeddableRenderer,
EmbeddableRendererProps,
useEmbeddableFactory,
} from './embeddable_renderer';
37 changes: 37 additions & 0 deletions src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
Expand Up @@ -542,3 +542,40 @@ test('Check when hide header option is true', async () => {
const title = findTestSubject(component, `embeddablePanelHeading-HelloAryaStark`);
expect(title.length).toBe(0);
});

test('Should work in minimal way rendering only the inspector action', async () => {
const inspector = inspectorPluginMock.createStartContract();
inspector.isAvailable = jest.fn(() => true);

const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
getEmbeddableFactory,
} as any);

const embeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'Arya',
lastName: 'Stark',
});

const component = mount(
<I18nProvider>
<EmbeddablePanel
embeddable={embeddable}
getActions={() => Promise.resolve([])}
inspector={inspector}
hideHeader={false}
/>
</I18nProvider>
);

findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
await nextTick();
component.update();
expect(findTestSubject(component, `embeddablePanelAction-openInspector`).length).toBe(1);
const action = findTestSubject(component, `embeddablePanelAction-ACTION_CUSTOMIZE_PANEL`);
expect(action.length).toBe(0);
});

0 comments on commit dfc70bd

Please sign in to comment.