Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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