Skip to content

Commit

Permalink
[Lens] Enable actions on Lens Embeddable (#102038) (#103398)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
  • Loading branch information
kibanamachine and dej611 committed Jun 25, 2021
1 parent 8bc10ab commit 4ee2429
Show file tree
Hide file tree
Showing 17 changed files with 333 additions and 100 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export {
EmbeddablePackageState,
EmbeddableRenderer,
EmbeddableRendererProps,
useEmbeddableFactory,
} from './lib';

export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
});
Loading

0 comments on commit 4ee2429

Please sign in to comment.