Clean up saved object based embeddable examples (#162987)

## Summary

These examples are outdated and don't show recent embeddable best
practices. They also use client-side saved object client and block
making `SavedObjectFinder` backward compatible
https://github.com/elastic/kibana/pull/162904 as the `foobar` saved
objects need to be added to content management. We decided that it is
better to clean them up, as fixing them is not a small effort and it is
not worth it on this point as a large embeddable refactor is coming.
This commit is contained in:
Anton Dosov 2023-08-03 12:14:36 +02:00 committed by GitHub
parent bb68c20d99
commit 65fd7ad260
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 24 additions and 2538 deletions

View file

@ -1,14 +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 BOOK_SAVED_OBJECT = 'book';
export interface BookSavedObjectAttributes {
title: string;
author?: string;
readIt?: boolean;
}

View file

@ -1,11 +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 type { TodoSavedObjectAttributes } from './todo_saved_object_attributes';
export type { BookSavedObjectAttributes } from './book_saved_object_attributes';
export { BOOK_SAVED_OBJECT } from './book_saved_object_attributes';

View file

@ -1,13 +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 interface TodoSavedObjectAttributes {
task: string;
icon?: string;
title?: string;
}

View file

@ -10,17 +10,10 @@
"requiredPlugins": [
"embeddable",
"uiActions",
"savedObjects",
"dashboard",
"kibanaUtils"
],
"requiredBundles": [
"kibanaReact"
],
"extraPublicDirs": [
"public/todo",
"public/hello_world",
"public/todo/todo_ref_embeddable"
"public/hello_world"
]
}
}

View file

@ -1,47 +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';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { ViewMode, isReferenceOrValueEmbeddable } from '@kbn/embeddable-plugin/public';
import { DASHBOARD_CONTAINER_TYPE } from '@kbn/dashboard-plugin/public';
import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable';
interface ActionContext {
embeddable: BookEmbeddable;
}
export const ACTION_ADD_BOOK_TO_LIBRARY = 'ACTION_ADD_BOOK_TO_LIBRARY';
export const createAddBookToLibraryActionDefinition = () => ({
getDisplayName: () =>
i18n.translate('embeddableExamples.book.addToLibrary', {
defaultMessage: 'Add Book To Library',
}),
id: ACTION_ADD_BOOK_TO_LIBRARY,
type: ACTION_ADD_BOOK_TO_LIBRARY,
order: 100,
getIconType: () => 'folderCheck',
isCompatible: async ({ embeddable }: ActionContext) => {
return (
embeddable.type === BOOK_EMBEDDABLE &&
embeddable.getInput().viewMode === ViewMode.EDIT &&
embeddable.getRoot().isContainer &&
embeddable.getRoot().type !== DASHBOARD_CONTAINER_TYPE &&
isReferenceOrValueEmbeddable(embeddable) &&
!embeddable.inputIsRefType(embeddable.getInput())
);
},
execute: async ({ embeddable }: ActionContext) => {
if (!isReferenceOrValueEmbeddable(embeddable)) {
throw new IncompatibleActionError();
}
const newInput = await embeddable.getInputAsRefType();
embeddable.updateInput(newInput);
},
});

View file

@ -1,100 +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 { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { withEmbeddableSubscription } from '@kbn/embeddable-plugin/public';
import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable';
interface Props {
input: BookEmbeddableInput;
output: BookEmbeddableOutput;
embeddable: BookEmbeddable;
}
function wrapSearchTerms(task?: string, search?: string) {
if (!search || !task) return task;
const parts = task.split(new RegExp(`(${search})`, 'g'));
return parts.map((part, i) =>
part === search ? (
<span key={i} style={{ backgroundColor: 'yellow' }}>
{part}
</span>
) : (
part
)
);
}
export function BookEmbeddableComponentInner({
input: { search },
output: { attributes },
embeddable,
}: Props) {
const title = attributes?.title;
const author = attributes?.author;
const readIt = attributes?.readIt;
const byReference = embeddable.inputIsRefType(embeddable.getInput());
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s">
{title ? (
<EuiFlexItem>
<EuiText data-test-subj="bookEmbeddableTitle">
<h3>{wrapSearchTerms(title, search)}</h3>
</EuiText>
</EuiFlexItem>
) : null}
{author ? (
<EuiFlexItem>
<EuiText data-test-subj="bookEmbeddableAuthor">
-{wrapSearchTerms(author, search)}
</EuiText>
</EuiFlexItem>
) : null}
{readIt ? (
<EuiFlexItem>
<EuiIcon type="check" />
</EuiFlexItem>
) : (
<EuiFlexItem>
<EuiIcon type="cross" />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiText data-test-subj="bookEmbeddableAuthor">
<EuiIcon type={byReference ? 'folderCheck' : 'folderExclamation'} />{' '}
<span>
{byReference
? i18n.translate('embeddableExamples.book.byReferenceLabel', {
defaultMessage: 'Book is By Reference',
})
: i18n.translate('embeddableExamples.book.byValueLabel', {
defaultMessage: 'Book is By Value',
})}
</span>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export const BookEmbeddableComponent = withEmbeddableSubscription<
BookEmbeddableInput,
BookEmbeddableOutput,
BookEmbeddable,
{}
>(BookEmbeddableComponentInner);

View file

@ -1,134 +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 ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
import {
Embeddable,
EmbeddableInput,
IContainer,
EmbeddableOutput,
SavedObjectEmbeddableInput,
ReferenceOrValueEmbeddable,
AttributeService,
} from '@kbn/embeddable-plugin/public';
import { BookSavedObjectAttributes } from '../../common';
import { BookEmbeddableComponent } from './book_component';
export const BOOK_EMBEDDABLE = 'book';
export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput;
export interface BookEmbeddableOutput extends EmbeddableOutput {
hasMatch: boolean;
attributes: BookSavedObjectAttributes;
}
interface BookInheritedInput extends EmbeddableInput {
search?: string;
}
export type BookByValueInput = { attributes: BookSavedObjectAttributes } & BookInheritedInput;
export type BookByReferenceInput = SavedObjectEmbeddableInput & BookInheritedInput;
/**
* Returns whether any attributes contain the search string. If search is empty, true is returned. If
* there are no savedAttributes, false is returned.
* @param search - the search string
* @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId`
*/
function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttributes): boolean {
if (!search) return true;
if (!savedAttributes) return false;
return Boolean(
(savedAttributes.author && savedAttributes.author.match(search)) ||
(savedAttributes.title && savedAttributes.title.match(search))
);
}
export class BookEmbeddable
extends Embeddable<BookEmbeddableInput, BookEmbeddableOutput>
implements ReferenceOrValueEmbeddable<BookByValueInput, BookByReferenceInput>
{
public readonly type = BOOK_EMBEDDABLE;
private subscription: Subscription;
private node?: HTMLElement;
private savedObjectId?: string;
private attributes?: BookSavedObjectAttributes;
constructor(
initialInput: BookEmbeddableInput,
private attributeService: AttributeService<BookSavedObjectAttributes>,
{
parent,
}: {
parent?: IContainer;
}
) {
super(initialInput, {} as BookEmbeddableOutput, parent);
this.subscription = this.getInput$().subscribe(async () => {
const savedObjectId = (this.getInput() as BookByReferenceInput).savedObjectId;
const attributes = (this.getInput() as BookByValueInput).attributes;
if (this.attributes !== attributes || this.savedObjectId !== savedObjectId) {
this.savedObjectId = savedObjectId;
this.reload();
} else {
this.updateOutput({
attributes: this.attributes,
defaultTitle: this.attributes.title,
hasMatch: getHasMatch(this.input.search, this.attributes),
});
}
});
}
readonly inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => {
return this.attributeService.inputIsRefType(input);
};
readonly getInputAsValueType = async (): Promise<BookByValueInput> => {
return this.attributeService.getInputAsValueType(this.getExplicitInput());
};
readonly getInputAsRefType = async (): Promise<BookByReferenceInput> => {
return this.attributeService.getInputAsRefType(this.getExplicitInput(), {
showSaveModal: true,
saveModalTitle: this.getTitle(),
});
};
public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(<BookEmbeddableComponent embeddable={this} />, node);
}
public async reload() {
this.attributes = (await this.attributeService.unwrapAttributes(this.input)).attributes;
this.updateOutput({
attributes: this.attributes,
defaultTitle: this.attributes.title,
hasMatch: getHasMatch(this.input.search, this.attributes),
});
}
public getTitle() {
return this.getOutput()?.title || this.getOutput().attributes?.title;
}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -1,163 +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 { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import {
EmbeddableFactoryDefinition,
IContainer,
EmbeddableFactory,
EmbeddableStart,
AttributeService,
} from '@kbn/embeddable-plugin/public';
import { OverlayStart, SavedObjectsClientContract, SimpleSavedObject } from '@kbn/core/public';
import { checkForDuplicateTitle, OnSaveProps } from '@kbn/saved-objects-plugin/public';
import {
BookEmbeddable,
BOOK_EMBEDDABLE,
BookEmbeddableInput,
BookEmbeddableOutput,
} from './book_embeddable';
import { CreateEditBookComponent } from './create_edit_book_component';
import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
interface StartServices {
getAttributeService: EmbeddableStart['getAttributeService'];
openModal: OverlayStart['openModal'];
savedObjectsClient: SavedObjectsClientContract;
overlays: OverlayStart;
}
export type BookEmbeddableFactory = EmbeddableFactory<
BookEmbeddableInput,
BookEmbeddableOutput,
BookEmbeddable,
BookSavedObjectAttributes
>;
export class BookEmbeddableFactoryDefinition
implements
EmbeddableFactoryDefinition<
BookEmbeddableInput,
BookEmbeddableOutput,
BookEmbeddable,
BookSavedObjectAttributes
>
{
public readonly type = BOOK_EMBEDDABLE;
public savedObjectMetaData = {
name: 'Book',
includeFields: ['title', 'author', 'readIt'],
type: BOOK_SAVED_OBJECT,
getIconForSavedObject: () => 'pencil',
};
private attributeService?: AttributeService<BookSavedObjectAttributes>;
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public async create(input: BookEmbeddableInput, parent?: IContainer) {
return new BookEmbeddable(input, await this.getAttributeService(), {
parent,
});
}
// This is currently required due to the distinction in container.ts and the
// default error implementation in default_embeddable_factory_provider.ts
public async createFromSavedObject(
savedObjectId: string,
input: BookEmbeddableInput,
parent?: IContainer
) {
return this.create(input, parent);
}
public getDisplayName() {
return i18n.translate('embeddableExamples.book.displayName', {
defaultMessage: 'Book',
});
}
public async getExplicitInput(): Promise<Omit<BookEmbeddableInput, 'id'>> {
const { openModal } = await this.getStartServices();
return new Promise<Omit<BookEmbeddableInput, 'id'>>((resolve) => {
const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
const wrappedAttributes = (await this.getAttributeService()).wrapAttributes(
attributes,
useRefType
);
resolve(wrappedAttributes);
};
const overlay = openModal(
toMountPoint(
<CreateEditBookComponent
onSave={(attributes: BookSavedObjectAttributes, useRefType: boolean) => {
onSave(attributes, useRefType);
overlay.close();
}}
/>
)
);
});
}
private async unwrapMethod(
savedObjectId: string
): Promise<{ attributes: BookSavedObjectAttributes }> {
const { savedObjectsClient } = await this.getStartServices();
const savedObject: SimpleSavedObject<BookSavedObjectAttributes> =
await savedObjectsClient.get<BookSavedObjectAttributes>(this.type, savedObjectId);
return { attributes: { ...savedObject.attributes } };
}
private async saveMethod(attributes: BookSavedObjectAttributes, savedObjectId?: string) {
const { savedObjectsClient } = await this.getStartServices();
if (savedObjectId) {
return savedObjectsClient.update(this.type, savedObjectId, attributes);
}
return savedObjectsClient.create(this.type, attributes);
}
private async checkForDuplicateTitleMethod(props: OnSaveProps): Promise<true> {
const start = await this.getStartServices();
const { savedObjectsClient, overlays } = start;
return checkForDuplicateTitle(
{
title: props.newTitle,
copyOnSave: false,
lastSavedTitle: '',
getEsType: () => this.type,
getDisplayName: this.getDisplayName || (() => this.type),
},
props.isTitleDuplicateConfirmed,
props.onTitleDuplicate,
{
savedObjectsClient,
overlays,
}
);
}
private async getAttributeService() {
if (!this.attributeService) {
this.attributeService = (
await this.getStartServices()
).getAttributeService<BookSavedObjectAttributes>(this.type, {
saveMethod: this.saveMethod.bind(this),
unwrapMethod: this.unwrapMethod.bind(this),
checkForDuplicateTitle: this.checkForDuplicateTitleMethod.bind(this),
});
}
return this.attributeService!;
}
}

View file

@ -1,78 +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, { useState } from 'react';
import { EuiModalBody, EuiCheckbox } from '@elastic/eui';
import { EuiFieldText } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { EuiModalFooter } from '@elastic/eui';
import { EuiModalHeader } from '@elastic/eui';
import { EuiFormRow } from '@elastic/eui';
import { BookSavedObjectAttributes } from '../../common';
export function CreateEditBookComponent({
savedObjectId,
attributes,
onSave,
}: {
savedObjectId?: string;
attributes?: BookSavedObjectAttributes;
onSave: (attributes: BookSavedObjectAttributes, useRefType: boolean) => void;
}) {
const [title, setTitle] = useState(attributes?.title ?? '');
const [author, setAuthor] = useState(attributes?.author ?? '');
const [readIt, setReadIt] = useState(attributes?.readIt ?? false);
return (
<EuiModalBody>
<EuiModalHeader>
<h1>{`${savedObjectId ? 'Create new ' : 'Edit '}`}</h1>
</EuiModalHeader>
<EuiModalBody>
<EuiFormRow label="Title">
<EuiFieldText
data-test-subj="titleInputField"
value={title}
placeholder="Title"
onChange={(e) => setTitle(e.target.value)}
/>
</EuiFormRow>
<EuiFormRow label="Author">
<EuiFieldText
data-test-subj="authorInputField"
value={author}
placeholder="Author"
onChange={(e) => setAuthor(e.target.value)}
/>
</EuiFormRow>
<EuiFormRow label="Read It">
<EuiCheckbox
id="ReadIt"
checked={readIt}
onChange={(event) => setReadIt(event.target.checked)}
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
data-test-subj="saveBookEmbeddableByValue"
disabled={title === ''}
onClick={() => onSave({ title, author, readIt }, false)}
>
{savedObjectId ? 'Unlink from library item' : 'Save and Return'}
</EuiButton>
<EuiButton
data-test-subj="saveBookEmbeddableByRef"
disabled={title === ''}
onClick={() => onSave({ title, author, readIt }, true)}
>
{savedObjectId ? 'Update library item' : 'Save to library'}
</EuiButton>
</EuiModalFooter>
</EuiModalBody>
);
}

View file

@ -1,95 +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 { OverlayStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import {
ViewMode,
SavedObjectEmbeddableInput,
EmbeddableStart,
} from '@kbn/embeddable-plugin/public';
import { OnSaveProps } from '@kbn/saved-objects-plugin/public';
import { SavedObjectsClientContract } from '@kbn/core/public';
import {
BookEmbeddable,
BOOK_EMBEDDABLE,
BookByReferenceInput,
BookByValueInput,
} from './book_embeddable';
import { CreateEditBookComponent } from './create_edit_book_component';
import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
interface StartServices {
openModal: OverlayStart['openModal'];
getAttributeService: EmbeddableStart['getAttributeService'];
savedObjectsClient: SavedObjectsClientContract;
}
interface ActionContext {
embeddable: BookEmbeddable;
}
export const ACTION_EDIT_BOOK = 'ACTION_EDIT_BOOK';
export const createEditBookActionDefinition = (getStartServices: () => Promise<StartServices>) => ({
getDisplayName: () =>
i18n.translate('embeddableExamples.book.edit', { defaultMessage: 'Edit Book' }),
id: ACTION_EDIT_BOOK,
type: ACTION_EDIT_BOOK,
order: 100,
getIconType: () => 'documents',
isCompatible: async ({ embeddable }: ActionContext) => {
return embeddable.type === BOOK_EMBEDDABLE && embeddable.getInput().viewMode === ViewMode.EDIT;
},
execute: async ({ embeddable }: ActionContext) => {
const { openModal, getAttributeService, savedObjectsClient } = await getStartServices();
const attributeService = getAttributeService<BookSavedObjectAttributes>(BOOK_SAVED_OBJECT, {
saveMethod: async (attributes: BookSavedObjectAttributes, savedObjectId?: string) => {
if (savedObjectId) {
return savedObjectsClient.update(BOOK_EMBEDDABLE, savedObjectId, attributes);
}
return savedObjectsClient.create(BOOK_EMBEDDABLE, attributes);
},
checkForDuplicateTitle: (props: OnSaveProps) => {
return new Promise(() => {
return true;
});
},
});
const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
const newInput = await attributeService.wrapAttributes(
attributes,
useRefType,
embeddable.getExplicitInput()
);
if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) {
// Set the saved object ID to null so that update input will remove the existing savedObjectId...
(newInput as BookByValueInput & { savedObjectId: unknown }).savedObjectId = null;
}
embeddable.updateInput(newInput);
if (useRefType) {
// Ensures that any duplicate embeddables also register the changes. This mirrors the behavior of going back and forth between apps
embeddable.getRoot().reload();
}
};
const overlay = openModal(
toMountPoint(
<CreateEditBookComponent
savedObjectId={(embeddable.getInput() as BookByReferenceInput).savedObjectId}
attributes={embeddable.getOutput().attributes}
onSave={(attributes: BookSavedObjectAttributes, useRefType: boolean) => {
overlay.close();
onSave(attributes, useRefType);
}}
/>
)
);
},
});

View file

@ -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 * from './book_embeddable';
export * from './book_embeddable_factory';

View file

@ -1,47 +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';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { ViewMode, isReferenceOrValueEmbeddable } from '@kbn/embeddable-plugin/public';
import { DASHBOARD_CONTAINER_TYPE } from '@kbn/dashboard-plugin/public';
import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable';
interface ActionContext {
embeddable: BookEmbeddable;
}
export const ACTION_UNLINK_BOOK_FROM_LIBRARY = 'ACTION_UNLINK_BOOK_FROM_LIBRARY';
export const createUnlinkBookFromLibraryActionDefinition = () => ({
getDisplayName: () =>
i18n.translate('embeddableExamples.book.unlinkFromLibrary', {
defaultMessage: 'Unlink Book from Library Item',
}),
id: ACTION_UNLINK_BOOK_FROM_LIBRARY,
type: ACTION_UNLINK_BOOK_FROM_LIBRARY,
order: 100,
getIconType: () => 'folderExclamation',
isCompatible: async ({ embeddable }: ActionContext) => {
return (
embeddable.type === BOOK_EMBEDDABLE &&
embeddable.getInput().viewMode === ViewMode.EDIT &&
embeddable.getRoot().isContainer &&
embeddable.getRoot().type !== DASHBOARD_CONTAINER_TYPE &&
isReferenceOrValueEmbeddable(embeddable) &&
embeddable.inputIsRefType(embeddable.getInput())
);
},
execute: async ({ embeddable }: ActionContext) => {
if (!isReferenceOrValueEmbeddable(embeddable)) {
throw new IncompatibleActionError();
}
const newInput = await embeddable.getInputAsValueType();
embeddable.updateInput(newInput);
},
});

View file

@ -1,38 +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 { SavedObjectsClientContract } from '@kbn/core/public';
import { TodoSavedObjectAttributes, BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../common';
export async function createSampleData(client: SavedObjectsClientContract, overwrite = true) {
await client.create<TodoSavedObjectAttributes>(
'todo',
{
task: 'Take the garbage out',
title: 'Garbage',
icon: 'trash',
},
{
id: 'sample-todo-saved-object',
overwrite,
}
);
await client.create<BookSavedObjectAttributes>(
BOOK_SAVED_OBJECT,
{
title: 'Pillars of the Earth',
author: 'Ken Follett',
readIt: true,
},
{
id: 'sample-book-saved-object',
overwrite,
}
);
}

View file

@ -14,10 +14,6 @@ export {
} from './hello_world';
export type { ListContainerFactory } from './list_container';
export { ListContainer, LIST_CONTAINER } from './list_container';
export type { TodoEmbeddableFactory } from './todo';
export { TODO_EMBEDDABLE } from './todo';
export { BOOK_EMBEDDABLE } from './book';
export { SIMPLE_EMBEDDABLE } from './migrations';
export {
@ -27,9 +23,4 @@ export {
import { EmbeddableExamplesPlugin } from './plugin';
export type { SearchableListContainerFactory } from './searchable_list_container';
export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container';
export type { MultiTaskTodoEmbeddableFactory } from './multi_task_todo';
export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo';
export const plugin = () => new EmbeddableExamplesPlugin();

View file

@ -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 * from './multi_task_todo_embeddable';
export * from './multi_task_todo_embeddable_factory';

View file

@ -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 React from 'react';
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import {
EuiText,
EuiAvatar,
EuiIcon,
EuiFlexGrid,
EuiListGroup,
EuiListGroupItem,
} from '@elastic/eui';
import { withEmbeddableSubscription } from '@kbn/embeddable-plugin/public';
import {
MultiTaskTodoEmbeddable,
MultiTaskTodoOutput,
MultiTaskTodoInput,
} from './multi_task_todo_embeddable';
interface Props {
embeddable: MultiTaskTodoEmbeddable;
input: MultiTaskTodoInput;
output: MultiTaskTodoOutput;
}
function wrapSearchTerms(task: string, search?: string) {
if (!search) return task;
const parts = task.split(new RegExp(`(${search})`, 'g'));
return parts.map((part, i) =>
part === search ? (
<span key={i} style={{ backgroundColor: 'yellow' }}>
{part}
</span>
) : (
part
)
);
}
function renderTasks(tasks: MultiTaskTodoInput['tasks'], search?: string) {
return tasks.map((task) => (
<EuiListGroupItem
key={task}
data-test-subj="multiTaskTodoTask"
label={wrapSearchTerms(task, search)}
/>
));
}
export function MultiTaskTodoEmbeddableComponentInner({
input: { title, icon, search, tasks },
}: Props) {
return (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false}>
{icon ? <EuiIcon type={icon} size="l" /> : <EuiAvatar name={title} size="l" />}
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={1} gutterSize="none">
<EuiFlexItem>
<EuiText data-test-subj="multiTaskTodoTitle">
<h3>{wrapSearchTerms(title, search)}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiListGroup bordered={true}>{renderTasks(tasks, search)}</EuiListGroup>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription<
MultiTaskTodoInput,
MultiTaskTodoOutput,
MultiTaskTodoEmbeddable
>(MultiTaskTodoEmbeddableComponentInner);

View file

@ -1,86 +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 ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
import {
Embeddable,
EmbeddableInput,
IContainer,
EmbeddableOutput,
} from '@kbn/embeddable-plugin/public';
import { MultiTaskTodoEmbeddableComponent } from './multi_task_todo_component';
export const MULTI_TASK_TODO_EMBEDDABLE = 'MULTI_TASK_TODO_EMBEDDABLE';
export interface MultiTaskTodoInput extends EmbeddableInput {
tasks: string[];
icon?: string;
search?: string;
title: string;
}
// This embeddable has output! Output state is something only the embeddable itself
// can update. It can be something completely internal, or it can be state that is
// derived from input state and updates when input does.
export interface MultiTaskTodoOutput extends EmbeddableOutput {
hasMatch: boolean;
}
function getHasMatch(tasks: string[], title?: string, search?: string) {
if (search === undefined || search === '') return false;
if (title && title.match(search)) return true;
const match = tasks.find((task) => task.match(search));
if (match) return true;
return false;
}
function getOutput(input: MultiTaskTodoInput) {
const hasMatch = getHasMatch(input.tasks, input.title, input.search);
return { hasMatch };
}
export class MultiTaskTodoEmbeddable extends Embeddable<MultiTaskTodoInput, MultiTaskTodoOutput> {
public readonly type = MULTI_TASK_TODO_EMBEDDABLE;
private subscription: Subscription;
private node?: HTMLElement;
constructor(initialInput: MultiTaskTodoInput, parent?: IContainer) {
super(initialInput, getOutput(initialInput), parent);
// If you have any output state that changes as a result of input state changes, you
// should use an subcription. Here, any time input tasks list, or the input filter
// changes, we want to update the output tasks list as well as whether a match has
// been found.
this.subscription = this.getInput$().subscribe(() => {
this.updateOutput(getOutput(this.input));
});
}
public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(<MultiTaskTodoEmbeddableComponent embeddable={this} />, node);
}
public reload() {}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -1,56 +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';
import {
IContainer,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import {
MultiTaskTodoEmbeddable,
MULTI_TASK_TODO_EMBEDDABLE,
MultiTaskTodoInput,
MultiTaskTodoOutput,
} from './multi_task_todo_embeddable';
export type MultiTaskTodoEmbeddableFactory = EmbeddableFactory<
MultiTaskTodoInput,
MultiTaskTodoOutput,
MultiTaskTodoEmbeddable
>;
export class MultiTaskTodoEmbeddableFactoryDefinition
implements
EmbeddableFactoryDefinition<MultiTaskTodoInput, MultiTaskTodoOutput, MultiTaskTodoEmbeddable>
{
public readonly type = MULTI_TASK_TODO_EMBEDDABLE;
public async isEditable() {
return true;
}
public async create(initialInput: MultiTaskTodoInput, parent?: IContainer) {
return new MultiTaskTodoEmbeddable(initialInput, parent);
}
/**
* Check out todo_embeddable_factory for a better example that asks for data from
* the user. This just returns default data. That's okay too though, if you want to
* start with default data and expose an "edit" action to modify it.
*/
public async getExplicitInput() {
return { title: 'default title', tasks: ['Im default data'] };
}
public getDisplayName() {
return i18n.translate('embeddableExamples.multiTaskTodo.displayName', {
defaultMessage: 'Multi-task todo item',
});
}
}

View file

@ -6,49 +6,21 @@
* Side Public License, v 1.
*/
import {
EmbeddableSetup,
EmbeddableStart,
CONTEXT_MENU_TRIGGER,
} from '@kbn/embeddable-plugin/public';
import { Plugin, CoreSetup, CoreStart, SavedObjectsClientContract } from '@kbn/core/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import {
HelloWorldEmbeddableFactory,
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactoryDefinition,
} from './hello_world';
import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoEmbeddableFactoryDefinition } from './todo';
import {
MULTI_TASK_TODO_EMBEDDABLE,
MultiTaskTodoEmbeddableFactory,
MultiTaskTodoEmbeddableFactoryDefinition,
} from './multi_task_todo';
import {
SEARCHABLE_LIST_CONTAINER,
SearchableListContainerFactoryDefinition,
SearchableListContainerFactory,
} from './searchable_list_container';
import {
LIST_CONTAINER,
ListContainerFactoryDefinition,
ListContainerFactory,
} from './list_container';
import { createSampleData } from './create_sample_data';
import { TODO_REF_EMBEDDABLE } from './todo/todo_ref_embeddable';
import {
TodoRefEmbeddableFactory,
TodoRefEmbeddableFactoryDefinition,
} from './todo/todo_ref_embeddable_factory';
import { createEditBookActionDefinition } from './book/edit_book_action';
import { BOOK_EMBEDDABLE } from './book/book_embeddable';
import {
BookEmbeddableFactory,
BookEmbeddableFactoryDefinition,
} from './book/book_embeddable_factory';
import { createAddBookToLibraryActionDefinition } from './book/add_book_to_library_action';
import { createUnlinkBookFromLibraryActionDefinition } from './book/unlink_book_from_library_action';
import {
SIMPLE_EMBEDDABLE,
SimpleEmbeddableFactory,
@ -67,17 +39,11 @@ export interface EmbeddableExamplesSetupDependencies {
export interface EmbeddableExamplesStartDependencies {
embeddable: EmbeddableStart;
savedObjectsClient: SavedObjectsClientContract;
}
interface ExampleEmbeddableFactories {
getHelloWorldEmbeddableFactory: () => HelloWorldEmbeddableFactory;
getMultiTaskTodoEmbeddableFactory: () => MultiTaskTodoEmbeddableFactory;
getSearchableListContainerEmbeddableFactory: () => SearchableListContainerFactory;
getListContainerEmbeddableFactory: () => ListContainerFactory;
getTodoEmbeddableFactory: () => TodoEmbeddableFactory;
getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory;
getBookEmbeddableFactory: () => BookEmbeddableFactory;
getMigrationsEmbeddableFactory: () => SimpleEmbeddableFactory;
getFilterDebuggerEmbeddableFactory: () => FilterDebuggerEmbeddableFactory;
}
@ -114,20 +80,6 @@ export class EmbeddableExamplesPlugin
new SimpleEmbeddableFactoryDefinition()
);
this.exampleEmbeddableFactories.getMultiTaskTodoEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
MULTI_TASK_TODO_EMBEDDABLE,
new MultiTaskTodoEmbeddableFactoryDefinition()
);
this.exampleEmbeddableFactories.getSearchableListContainerEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
SEARCHABLE_LIST_CONTAINER,
new SearchableListContainerFactoryDefinition(async () => ({
embeddableServices: (await core.getStartServices())[1].embeddable,
}))
);
this.exampleEmbeddableFactories.getListContainerEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
LIST_CONTAINER,
@ -136,54 +88,11 @@ export class EmbeddableExamplesPlugin
}))
);
this.exampleEmbeddableFactories.getTodoEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
TODO_EMBEDDABLE,
new TodoEmbeddableFactoryDefinition(async () => ({
openModal: (await core.getStartServices())[0].overlays.openModal,
}))
);
this.exampleEmbeddableFactories.getTodoRefEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
TODO_REF_EMBEDDABLE,
new TodoRefEmbeddableFactoryDefinition(async () => ({
savedObjectsClient: (await core.getStartServices())[0].savedObjects.client,
getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory,
}))
);
this.exampleEmbeddableFactories.getBookEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
BOOK_EMBEDDABLE,
new BookEmbeddableFactoryDefinition(async () => ({
getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
openModal: (await core.getStartServices())[0].overlays.openModal,
savedObjectsClient: (await core.getStartServices())[0].savedObjects.client,
overlays: (await core.getStartServices())[0].overlays,
}))
);
this.exampleEmbeddableFactories.getFilterDebuggerEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
FILTER_DEBUGGER_EMBEDDABLE,
new FilterDebuggerEmbeddableFactoryDefinition()
);
const editBookAction = createEditBookActionDefinition(async () => ({
getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
openModal: (await core.getStartServices())[0].overlays.openModal,
savedObjectsClient: (await core.getStartServices())[0].savedObjects.client,
}));
deps.uiActions.registerAction(editBookAction);
deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id);
const addBookToLibraryAction = createAddBookToLibraryActionDefinition();
deps.uiActions.registerAction(addBookToLibraryAction);
deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, addBookToLibraryAction.id);
const unlinkBookFromLibraryAction = createUnlinkBookFromLibraryActionDefinition();
deps.uiActions.registerAction(unlinkBookFromLibraryAction);
deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkBookFromLibraryAction.id);
}
public start(
@ -191,7 +100,7 @@ export class EmbeddableExamplesPlugin
deps: EmbeddableExamplesStartDependencies
): EmbeddableExamplesStart {
return {
createSampleData: () => createSampleData(core.savedObjects.client),
createSampleData: async () => {},
factories: this.exampleEmbeddableFactories as ExampleEmbeddableFactories,
};
}

View file

@ -1,11 +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 { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container';
export type { SearchableListContainerFactory } from './searchable_list_container_factory';
export { SearchableListContainerFactoryDefinition } from './searchable_list_container_factory';

View file

@ -1,67 +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 ReactDOM from 'react-dom';
import {
Container,
ContainerInput,
EmbeddableStart,
EmbeddableInput,
} from '@kbn/embeddable-plugin/public';
import { SearchableListContainerComponent } from './searchable_list_container_component';
export const SEARCHABLE_LIST_CONTAINER = 'SEARCHABLE_LIST_CONTAINER';
export interface SearchableContainerInput extends ContainerInput {
search?: string;
}
interface ChildInput extends EmbeddableInput {
search?: string;
}
export class SearchableListContainer extends Container<ChildInput, SearchableContainerInput> {
public readonly type = SEARCHABLE_LIST_CONTAINER;
private node?: HTMLElement;
constructor(input: SearchableContainerInput, private embeddableServices: EmbeddableStart) {
super(input, { embeddableLoaded: {} }, embeddableServices.getEmbeddableFactory);
}
// TODO: add a more advanced example here where inherited child input is derived from container
// input and not just an exact pass through.
getInheritedInput(id: string) {
return {
id,
search: this.getInput().search,
viewMode: this.input.viewMode,
};
}
public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(
<SearchableListContainerComponent
embeddable={this}
embeddableServices={this.embeddableServices}
/>,
node
);
}
public destroy() {
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -1,222 +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, { Component } from 'react';
import {
EuiLoadingSpinner,
EuiButton,
EuiFormRow,
EuiFlexGroup,
EuiSpacer,
EuiFlexItem,
EuiFieldText,
EuiPanel,
EuiCheckbox,
} from '@elastic/eui';
import * as Rx from 'rxjs';
import {
withEmbeddableSubscription,
ContainerOutput,
EmbeddableOutput,
EmbeddableStart,
EmbeddablePanel,
openAddPanelFlyout,
} from '@kbn/embeddable-plugin/public';
import { SearchableListContainer, SearchableContainerInput } from './searchable_list_container';
interface Props {
embeddable: SearchableListContainer;
input: SearchableContainerInput;
output: ContainerOutput;
embeddableServices: EmbeddableStart;
}
interface State {
checked: { [key: string]: boolean };
hasMatch: { [key: string]: boolean };
}
interface HasMatchOutput {
hasMatch: boolean;
}
function hasHasMatchOutput(output: EmbeddableOutput | HasMatchOutput): output is HasMatchOutput {
return (output as HasMatchOutput).hasMatch !== undefined;
}
export class SearchableListContainerComponentInner extends Component<Props, State> {
private subscriptions: { [id: string]: Rx.Subscription } = {};
constructor(props: Props) {
super(props);
const checked: { [id: string]: boolean } = {};
const hasMatch: { [id: string]: boolean } = {};
props.embeddable.getChildIds().forEach((id) => {
checked[id] = false;
const output = props.embeddable.getChild(id).getOutput();
hasMatch[id] = hasHasMatchOutput(output) && output.hasMatch;
});
props.embeddable.getChildIds().forEach((id) => (checked[id] = false));
this.state = {
checked,
hasMatch,
};
}
componentDidMount() {
this.props.embeddable.getChildIds().forEach((id) => {
this.subscriptions[id] = this.props.embeddable
.getChild(id)
.getOutput$()
.subscribe((output) => {
if (hasHasMatchOutput(output)) {
this.setState((prevState) => ({
hasMatch: {
...prevState.hasMatch,
[id]: output.hasMatch,
},
}));
}
});
});
}
componentWillUnmount() {
Object.values(this.subscriptions).forEach((sub) => sub.unsubscribe());
}
private updateSearch = (search: string) => {
this.props.embeddable.updateInput({ search });
};
private deleteChecked = () => {
Object.values(this.props.input.panels).map((panel) => {
if (this.state.checked[panel.explicitInput.id]) {
this.props.embeddable.removeEmbeddable(panel.explicitInput.id);
this.subscriptions[panel.explicitInput.id].unsubscribe();
}
});
};
private checkMatching = () => {
const { input, embeddable } = this.props;
const checked: { [key: string]: boolean } = {};
Object.values(input.panels).map((panel) => {
const child = embeddable.getChild(panel.explicitInput.id);
const output = child.getOutput();
if (hasHasMatchOutput(output) && output.hasMatch) {
checked[panel.explicitInput.id] = true;
}
});
this.setState({ checked });
};
private toggleCheck = (isChecked: boolean, id: string) => {
this.setState((prevState) => ({ checked: { ...prevState.checked, [id]: isChecked } }));
};
public renderControls() {
const { input, embeddable } = this.props;
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
<EuiButton data-test-subj="deleteCheckedTodos" onClick={() => this.deleteChecked()}>
Delete checked
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
<EuiButton
data-test-subj="checkMatchingTodos"
disabled={input.search === ''}
onClick={() => this.checkMatching()}
>
Check matching
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="Filter">
<EuiFieldText
data-test-subj="filterTodos"
value={this.props.input.search || ''}
onChange={(ev) => this.updateSearch(ev.target.value)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
<EuiButton
data-test-subj="addPanelToListContainer"
disabled={input.search === ''}
onClick={() => openAddPanelFlyout({ container: embeddable })}
>
Add panel
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>
);
}
public render() {
const { embeddable } = this.props;
return (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<h2 data-test-subj="searchableListContainerTitle">{embeddable.getTitle()}</h2>
<EuiSpacer size="l" />
{this.renderControls()}
<EuiSpacer size="l" />
{this.renderList()}
</EuiFlexItem>
</EuiFlexGroup>
);
}
private renderList() {
const { input, embeddable } = this.props;
let id = 0;
const list = Object.values(input.panels).map((panel) => {
const childEmbeddable = embeddable.getChild(panel.explicitInput.id);
id++;
return childEmbeddable ? (
<EuiPanel key={childEmbeddable.id}>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false}>
<EuiCheckbox
data-test-subj={`todoCheckBox-${childEmbeddable.id}`}
disabled={!childEmbeddable}
id={childEmbeddable ? childEmbeddable.id : ''}
checked={this.state.checked[childEmbeddable.id]}
onChange={(e) => this.toggleCheck(e.target.checked, childEmbeddable.id)}
/>
</EuiFlexItem>
<EuiFlexItem>
<EmbeddablePanel embeddable={childEmbeddable} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
) : (
<EuiLoadingSpinner size="l" key={id} />
);
});
return list;
}
}
export const SearchableListContainerComponent = withEmbeddableSubscription<
SearchableContainerInput,
ContainerOutput,
SearchableListContainer,
{ embeddableServices: EmbeddableStart }
>(SearchableListContainerComponentInner);

View file

@ -1,52 +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';
import {
ContainerOutput,
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableStart,
} from '@kbn/embeddable-plugin/public';
import {
SEARCHABLE_LIST_CONTAINER,
SearchableListContainer,
SearchableContainerInput,
} from './searchable_list_container';
interface StartServices {
embeddableServices: EmbeddableStart;
}
export type SearchableListContainerFactory = EmbeddableFactory<
SearchableContainerInput,
ContainerOutput
>;
export class SearchableListContainerFactoryDefinition
implements EmbeddableFactoryDefinition<SearchableContainerInput, ContainerOutput>
{
public readonly type = SEARCHABLE_LIST_CONTAINER;
public readonly isContainerType = true;
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public create = async (initialInput: SearchableContainerInput) => {
const { embeddableServices } = await this.getStartServices();
return new SearchableListContainer(initialInput, embeddableServices);
};
public getDisplayName() {
return i18n.translate('embeddableExamples.searchableListContainer.displayName', {
defaultMessage: 'Searchable list container',
});
}
}

View file

@ -1,43 +0,0 @@
There are two examples in here:
- TodoEmbeddable
- TodoRefEmbeddable
# TodoEmbeddable
The first example you should review is the HelloWorldEmbeddable. That is as basic an embeddable as you can get.
This embeddable is the next step up - an embeddable that renders dynamic input data. The data is simple:
- a required task string
- an optional title
- an optional icon string
- an optional search string
It also has output data, which is `hasMatch` - whether or not the search string has matched any input data.
`hasMatch` is a better fit for output data than input data, because it's state that is _derived_ from input data.
For example, if it was input data, you could create a TodoEmbeddable with input like this:
```ts
todoEmbeddableFactory.create({ task: 'take out the garabage', search: 'garbage', hasMatch: false });
```
That's wrong because there is actually a match from the search string inside the task.
The TodoEmbeddable component itself doesn't do anything with the `hasMatch` variable other than set it, but
if you check out `SearchableListContainer`, you can see an example where this output data is being used.
## TodoRefEmbeddable
This is an example of an embeddable based off of a saved object. The input is just the `savedObjectId` and
the `search` string. It has even more output parameters, and this time, it does read it's own output parameters in
order to calculate `hasMatch`.
Output:
```ts
{
hasMatch: boolean,
savedAttributes?: TodoSavedAttributes
}
```
`savedAttributes` is optional because it's possible a TodoSavedObject could not be found with the given savedObjectId.

View file

@ -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 * from './todo_embeddable';
export * from './todo_embeddable_factory';

View file

@ -1,65 +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 { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { EuiAvatar } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { EuiFlexGrid } from '@elastic/eui';
import { withEmbeddableSubscription, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
import { TodoEmbeddable, TodoInput } from './todo_embeddable';
interface Props {
embeddable: TodoEmbeddable;
input: TodoInput;
output: EmbeddableOutput;
}
function wrapSearchTerms(task: string, search?: string) {
if (!search) return task;
const parts = task.split(new RegExp(`(${search})`, 'g'));
return parts.map((part, i) =>
part === search ? (
<span key={i} style={{ backgroundColor: 'yellow' }}>
{part}
</span>
) : (
part
)
);
}
export function TodoEmbeddableComponentInner({ input: { icon, title, task, search } }: Props) {
return (
<EuiFlexGroup gutterSize="none" data-render-complete="true">
<EuiFlexItem grow={false}>
{icon ? <EuiIcon type={icon} size="l" /> : <EuiAvatar name={title || task} size="l" />}
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={1} gutterSize="none">
<EuiFlexItem>
<EuiText data-test-subj="todoEmbeddableTitle">
<h3>{wrapSearchTerms(title || '', search)}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText data-test-subj="todoEmbeddableTask">{wrapSearchTerms(task, search)}</EuiText>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export const TodoEmbeddableComponent = withEmbeddableSubscription<
TodoInput,
EmbeddableOutput,
TodoEmbeddable
>(TodoEmbeddableComponentInner);

View file

@ -1,78 +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 ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
import {
Embeddable,
EmbeddableInput,
IContainer,
EmbeddableOutput,
} from '@kbn/embeddable-plugin/public';
import { TodoEmbeddableComponent } from './todo_component';
export const TODO_EMBEDDABLE = 'TODO_EMBEDDABLE';
export interface TodoInput extends EmbeddableInput {
task: string;
icon?: string;
search?: string;
}
export interface TodoOutput extends EmbeddableOutput {
hasMatch: boolean;
}
function getOutput(input: TodoInput): TodoOutput {
return {
hasMatch: input.search
? Boolean(input.task.match(input.search) || (input.title && input.title.match(input.search)))
: true,
};
}
export class TodoEmbeddable extends Embeddable<TodoInput, TodoOutput> {
// The type of this embeddable. This will be used to find the appropriate factory
// to instantiate this kind of embeddable.
public readonly type = TODO_EMBEDDABLE;
private subscription: Subscription;
private node?: HTMLElement;
constructor(initialInput: TodoInput, parent?: IContainer) {
super(initialInput, getOutput(initialInput), parent);
// If you have any output state that changes as a result of input state changes, you
// should use an subcription. Here, we use output to indicate whether this task
// matches the search string.
this.subscription = this.getInput$().subscribe(() => {
this.updateOutput(getOutput(this.input));
});
}
public render(node: HTMLElement) {
this.node = node;
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
ReactDOM.render(<TodoEmbeddableComponent embeddable={this} />, node);
}
/**
* Not relevant.
*/
public reload() {}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -1,89 +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, { useState } from 'react';
import { EuiModalBody } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { OverlayStart } from '@kbn/core/public';
import { EuiFieldText } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import {
IContainer,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { TodoEmbeddable, TODO_EMBEDDABLE, TodoInput, TodoOutput } from './todo_embeddable';
function TaskInput({ onSave }: { onSave: (task: string) => void }) {
const [task, setTask] = useState('');
return (
<EuiModalBody>
<EuiFieldText
data-test-subj="taskInputField"
value={task}
placeholder="Enter task here"
onChange={(e) => setTask(e.target.value)}
/>
<EuiButton data-test-subj="createTodoEmbeddable" onClick={() => onSave(task)}>
Save
</EuiButton>
</EuiModalBody>
);
}
interface StartServices {
openModal: OverlayStart['openModal'];
}
export type TodoEmbeddableFactory = EmbeddableFactory<TodoInput, TodoOutput, TodoEmbeddable>;
export class TodoEmbeddableFactoryDefinition
implements EmbeddableFactoryDefinition<TodoInput, TodoOutput, TodoEmbeddable>
{
public readonly type = TODO_EMBEDDABLE;
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public async create(initialInput: TodoInput, parent?: IContainer) {
return new TodoEmbeddable(initialInput, parent);
}
/**
* This function is used when dynamically creating a new embeddable to add to a
* container. Some input may be inherited from the container, but not all. This can be
* used to collect specific embeddable input that the container will not provide, like
* in this case, the task string.
*/
public getExplicitInput = async () => {
const { openModal } = await this.getStartServices();
return new Promise<{ task: string }>((resolve) => {
const onSave = (task: string) => resolve({ task });
const overlay = openModal(
toMountPoint(
<TaskInput
onSave={(task: string) => {
onSave(task);
overlay.close();
}}
/>
)
);
});
};
public getDisplayName() {
return i18n.translate('embeddableExamples.todo.displayName', {
defaultMessage: 'Todo item',
});
}
}

View file

@ -1,76 +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 { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { EuiAvatar } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { EuiFlexGrid } from '@elastic/eui';
import { withEmbeddableSubscription } from '@kbn/embeddable-plugin/public';
import { TodoRefInput, TodoRefOutput, TodoRefEmbeddable } from './todo_ref_embeddable';
interface Props {
embeddable: TodoRefEmbeddable;
input: TodoRefInput;
output: TodoRefOutput;
}
function wrapSearchTerms(task?: string, search?: string) {
if (!search) return task;
if (!task) return task;
const parts = task.split(new RegExp(`(${search})`, 'g'));
return parts.map((part, i) =>
part === search ? (
<span key={i} style={{ backgroundColor: 'yellow' }}>
{part}
</span>
) : (
part
)
);
}
export function TodoRefEmbeddableComponentInner({
input: { search },
output: { savedAttributes },
}: Props) {
const icon = savedAttributes?.icon;
const title = savedAttributes?.title;
const task = savedAttributes?.task;
return (
<EuiFlexGroup data-render-complete="true">
<EuiFlexItem grow={false}>
{icon ? (
<EuiIcon type={icon} size="l" />
) : (
<EuiAvatar name={title || task || ''} size="l" />
)}
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={1}>
<EuiFlexItem>
<EuiText data-test-subj="todoEmbeddableTitle">
<h3>{wrapSearchTerms(title || '', search)}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText data-test-subj="todoEmbeddableTask">{wrapSearchTerms(task, search)}</EuiText>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export const TodoRefEmbeddableComponent = withEmbeddableSubscription<
TodoRefInput,
TodoRefOutput,
TodoRefEmbeddable
>(TodoRefEmbeddableComponentInner);

View file

@ -1,143 +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 ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
import { SavedObjectsClientContract } from '@kbn/core/public';
import {
Embeddable,
IContainer,
EmbeddableOutput,
SavedObjectEmbeddableInput,
} from '@kbn/embeddable-plugin/public';
import { TodoSavedObjectAttributes } from '../../common';
import { TodoRefEmbeddableComponent } from './todo_ref_component';
// Notice this is not the same value as the 'todo' saved object type. Many of our
// cases in prod today use the same value, but this is unnecessary.
export const TODO_REF_EMBEDDABLE = 'TODO_REF_EMBEDDABLE';
export interface TodoRefInput extends SavedObjectEmbeddableInput {
/**
* Optional search string which will be used to highlight search terms as
* well as calculate `output.hasMatch`.
*/
search?: string;
}
export interface TodoRefOutput extends EmbeddableOutput {
/**
* Should be true if input.search is defined and the task or title contain
* search as a substring.
*/
hasMatch: boolean;
/**
* Will contain the saved object attributes of the Todo Saved Object that matches
* `input.savedObjectId`. If the id is invalid, this may be undefined.
*/
savedAttributes?: TodoSavedObjectAttributes;
}
/**
* Returns whether any attributes contain the search string. If search is empty, true is returned. If
* there are no savedAttributes, false is returned.
* @param search - the search string
* @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId`
*/
function getHasMatch(search?: string, savedAttributes?: TodoSavedObjectAttributes): boolean {
if (!search) return true;
if (!savedAttributes) return false;
return Boolean(
(savedAttributes.task && savedAttributes.task.match(search)) ||
(savedAttributes.title && savedAttributes.title.match(search))
);
}
/**
* This is an example of an embeddable that is backed by a saved object. It's essentially the
* same as `TodoEmbeddable` but that is "by value", while this is "by reference".
*/
export class TodoRefEmbeddable extends Embeddable<TodoRefInput, TodoRefOutput> {
public readonly type = TODO_REF_EMBEDDABLE;
private subscription: Subscription;
private node?: HTMLElement;
private savedObjectsClient: SavedObjectsClientContract;
private savedObjectId?: string;
constructor(
initialInput: TodoRefInput,
{
parent,
savedObjectsClient,
}: {
parent?: IContainer;
savedObjectsClient: SavedObjectsClientContract;
}
) {
super(initialInput, { hasMatch: false }, parent);
this.savedObjectsClient = savedObjectsClient;
this.subscription = this.getInput$().subscribe(async () => {
// There is a little more work today for this embeddable because it has
// more output it needs to update in response to input state changes.
let savedAttributes: TodoSavedObjectAttributes | undefined;
// Since this is an expensive task, we save a local copy of the previous
// savedObjectId locally and only retrieve the new saved object if the id
// actually changed.
if (this.savedObjectId !== this.input.savedObjectId) {
this.savedObjectId = this.input.savedObjectId;
const todoSavedObject = await this.savedObjectsClient.get<TodoSavedObjectAttributes>(
'todo',
this.input.savedObjectId
);
savedAttributes = todoSavedObject?.attributes;
}
// The search string might have changed as well so we need to make sure we recalculate
// hasMatch.
this.updateOutput({
hasMatch: getHasMatch(this.input.search, savedAttributes),
savedAttributes,
});
});
}
public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(<TodoRefEmbeddableComponent embeddable={this} />, node);
}
/**
* Lets re-sync our saved object to make sure it's up to date!
*/
public async reload() {
this.savedObjectId = this.input.savedObjectId;
const todoSavedObject = await this.savedObjectsClient.get<TodoSavedObjectAttributes>(
'todo',
this.input.savedObjectId
);
const savedAttributes = todoSavedObject?.attributes;
this.updateOutput({
hasMatch: getHasMatch(this.input.search, savedAttributes),
savedAttributes,
});
}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -1,82 +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';
import { SavedObjectsClientContract } from '@kbn/core/public';
import {
IContainer,
EmbeddableStart,
ErrorEmbeddable,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { TodoSavedObjectAttributes } from '../../common';
import {
TodoRefEmbeddable,
TODO_REF_EMBEDDABLE,
TodoRefInput,
TodoRefOutput,
} from './todo_ref_embeddable';
interface StartServices {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
savedObjectsClient: SavedObjectsClientContract;
}
export type TodoRefEmbeddableFactory = EmbeddableFactory<
TodoRefInput,
TodoRefOutput,
TodoRefEmbeddable,
TodoSavedObjectAttributes
>;
export class TodoRefEmbeddableFactoryDefinition
implements
EmbeddableFactoryDefinition<
TodoRefInput,
TodoRefOutput,
TodoRefEmbeddable,
TodoSavedObjectAttributes
>
{
public readonly type = TODO_REF_EMBEDDABLE;
public readonly savedObjectMetaData = {
name: 'Todo',
includeFields: ['task', 'icon', 'title'],
type: 'todo',
getIconForSavedObject: () => 'pencil',
};
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public createFromSavedObject = (
savedObjectId: string,
input: Partial<TodoRefInput> & { id: string },
parent?: IContainer
): Promise<TodoRefEmbeddable | ErrorEmbeddable> => {
return this.create({ ...input, savedObjectId }, parent);
};
public async create(input: TodoRefInput, parent?: IContainer) {
const { savedObjectsClient } = await this.getStartServices();
return new TodoRefEmbeddable(input, {
parent,
savedObjectsClient,
});
}
public getDisplayName() {
return i18n.translate('embeddableExamples.todo.displayName', {
defaultMessage: 'Todo (by reference)',
});
}
}

View file

@ -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 { SavedObjectsType } from '@kbn/core/server';
export const bookSavedObject: SavedObjectsType = {
name: 'book',
hidden: false,
namespaceType: 'agnostic',
mappings: {
properties: {
title: {
type: 'keyword',
},
author: {
type: 'keyword',
},
readIt: {
type: 'boolean',
},
},
},
migrations: {},
};

View file

@ -1,25 +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 { mergeWith } from 'lodash';
import type { SerializableRecord } from '@kbn/utility-types';
import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common';
export const mergeMigrationFunctionMaps = (
obj1: MigrateFunctionsObject,
obj2: MigrateFunctionsObject
) => {
const customizer = (objValue: MigrateFunction, srcValue: MigrateFunction) => {
if (!srcValue || !objValue) {
return srcValue || objValue;
}
return (state: SerializableRecord) => objValue(srcValue(state));
};
return mergeWith({ ...obj1 }, obj2, customizer);
};

View file

@ -8,9 +8,6 @@
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
import { todoSavedObject } from './todo_saved_object';
import { bookSavedObject } from './book_saved_object';
import { searchableListSavedObject } from './searchable_list_saved_object';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
@ -19,11 +16,7 @@ export interface EmbeddableExamplesSetupDependencies {
export class EmbeddableExamplesPlugin
implements Plugin<void, void, EmbeddableExamplesSetupDependencies>
{
public setup(core: CoreSetup, { embeddable }: EmbeddableExamplesSetupDependencies) {
core.savedObjects.registerType(todoSavedObject);
core.savedObjects.registerType(bookSavedObject);
core.savedObjects.registerType(searchableListSavedObject(embeddable));
}
public setup(core: CoreSetup, { embeddable }: EmbeddableExamplesSetupDependencies) {}
public start(core: CoreStart) {}

View file

@ -1,54 +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 { mapValues } from 'lodash';
import { SavedObjectsType, SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
// NOTE: this should rather be imported from 'plugins/kibana_utils/server' but examples at the moment don't
// allow static imports from plugins so this code was duplicated
import { mergeMigrationFunctionMaps } from './merge_migration_function_maps';
export const searchableListSavedObject = (embeddable: EmbeddableSetup) => {
const searchableListSO: SavedObjectsType = {
name: 'searchableList',
hidden: false,
namespaceType: 'single',
management: {
icon: 'visualizeApp',
defaultSearchField: 'title',
importableAndExportable: true,
getTitle(obj: any) {
return obj.attributes.title;
},
},
mappings: {
properties: {
title: { type: 'text' },
version: { type: 'integer' },
},
},
migrations: () => {
// there are no migrations defined for the saved object at the moment, possibly they would be added in the future
const searchableListSavedObjectMigrations = {};
// we don't know if embeddables have any migrations defined so we need to fetch them and map the received functions so we pass
// them the correct input and that we correctly map the response
const embeddableMigrations = mapValues(embeddable.getAllMigrations(), (migrate) => {
return (state: SavedObjectUnsanitizedDoc) => ({
...state,
attributes: migrate(state.attributes),
});
});
// we merge our and embeddable migrations and return
return mergeMigrationFunctionMaps(searchableListSavedObjectMigrations, embeddableMigrations);
},
};
return searchableListSO;
};

View file

@ -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 { SavedObjectsType } from '@kbn/core/server';
export const todoSavedObject: SavedObjectsType = {
name: 'todo',
hidden: false,
namespaceType: 'agnostic',
mappings: {
properties: {
title: {
type: 'keyword',
},
task: {
type: 'text',
},
icon: {
type: 'keyword',
},
},
},
migrations: {},
};

View file

@ -17,13 +17,9 @@
"kbn_references": [
"@kbn/core",
"@kbn/kibana-utils-plugin",
"@kbn/kibana-react-plugin",
"@kbn/ui-actions-plugin",
"@kbn/embeddable-plugin",
"@kbn/dashboard-plugin",
"@kbn/saved-objects-plugin",
"@kbn/i18n",
"@kbn/utility-types",
"@kbn/es-query",
]
}

View file

@ -14,8 +14,7 @@
"embeddableExamples",
"developerExamples",
"dashboard",
"kibanaReact",
"savedObjects"
"kibanaReact"
]
}
}

View file

@ -11,20 +11,12 @@ import ReactDOM from 'react-dom';
import { BrowserRouter as Router, withRouter, RouteComponentProps } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import { EuiPageTemplate, EuiSideNav } from '@elastic/eui';
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
import {
AppMountParameters,
CoreStart,
SavedObjectsStart,
IUiSettingsClient,
OverlayStart,
} from '@kbn/core/public';
import { AppMountParameters, CoreStart, IUiSettingsClient, OverlayStart } from '@kbn/core/public';
import { EmbeddableExamplesStart } from '@kbn/embeddable-examples-plugin/public/plugin';
import { HelloWorldEmbeddableExample } from './hello_world_embeddable_example';
import { TodoEmbeddableExample } from './todo_embeddable_example';
import { ListContainerExample } from './list_container_example';
import { EmbeddablePanelExample } from './embeddable_panel_example';
@ -68,7 +60,6 @@ interface Props {
overlays: OverlayStart;
notifications: CoreStart['notifications'];
inspector: InspectorStartContract;
savedObject: SavedObjectsStart;
uiSettingsClient: IUiSettingsClient;
embeddableExamples: EmbeddableExamplesStart;
}
@ -89,15 +80,6 @@ const EmbeddableExplorerApp = ({
/>
),
},
{
title: 'Update embeddable state',
id: 'todoEmbeddableSection',
component: (
<TodoEmbeddableExample
todoEmbeddableFactory={embeddableExamples.factories.getTodoEmbeddableFactory()}
/>
),
},
{
title: 'Groups of embeddables',
id: 'listContainerSection',
@ -112,7 +94,7 @@ const EmbeddableExplorerApp = ({
id: 'embeddablePanelExample',
component: (
<EmbeddablePanelExample
searchListContainerFactory={embeddableExamples.factories.getSearchableListContainerEmbeddableFactory()}
helloWorldFactory={embeddableExamples.factories.getHelloWorldEmbeddableFactory()}
/>
),
},

View file

@ -10,80 +10,13 @@ import React, { useState, useEffect, useRef } from 'react';
import { EuiPanel, EuiText, EuiPageTemplate } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import { IEmbeddable, EmbeddablePanel } from '@kbn/embeddable-plugin/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
BOOK_EMBEDDABLE,
MULTI_TASK_TODO_EMBEDDABLE,
SearchableListContainerFactory,
} from '@kbn/embeddable-examples-plugin/public';
import { HelloWorldEmbeddableFactory } from '@kbn/embeddable-examples-plugin/public';
interface Props {
searchListContainerFactory: SearchableListContainerFactory;
helloWorldFactory: HelloWorldEmbeddableFactory;
}
export function EmbeddablePanelExample({ searchListContainerFactory }: Props) {
const searchableInput = {
id: '1',
title: 'My searchable todo list',
panels: {
'1': {
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '1',
title: 'Hello',
},
},
'2': {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '2',
task: 'Goes out on Wednesdays!',
icon: 'broom',
title: 'Take out the trash',
},
},
'3': {
type: MULTI_TASK_TODO_EMBEDDABLE,
explicitInput: {
id: '3',
icon: 'searchProfilerApp',
title: 'Learn more',
tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'],
},
},
'4': {
type: BOOK_EMBEDDABLE,
explicitInput: {
id: '4',
savedObjectId: 'sample-book-saved-object',
},
},
'5': {
type: BOOK_EMBEDDABLE,
explicitInput: {
id: '5',
attributes: {
title: 'The Sympathizer',
author: 'Viet Thanh Nguyen',
readIt: true,
},
},
},
'6': {
type: BOOK_EMBEDDABLE,
explicitInput: {
id: '6',
attributes: {
title: 'The Hobbit',
author: 'J.R.R. Tolkien',
readIt: false,
},
},
},
},
};
export function EmbeddablePanelExample({ helloWorldFactory }: Props) {
const [embeddable, setEmbeddable] = useState<IEmbeddable | undefined>(undefined);
const ref = useRef(false);
@ -91,7 +24,7 @@ export function EmbeddablePanelExample({ searchListContainerFactory }: Props) {
useEffect(() => {
ref.current = true;
if (!embeddable) {
const promise = searchListContainerFactory.create(searchableInput);
const promise = helloWorldFactory.create({ id: '1', title: 'Hello World!' });
if (promise) {
promise.then((e) => {
if (ref.current) {

View file

@ -11,10 +11,8 @@ import { EuiPanel, EuiSpacer, EuiText, EuiPageTemplate, EuiCodeBlock } from '@el
import { EmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
ListContainerFactory,
} from '@kbn/embeddable-examples-plugin/public';
import { TodoInput } from '@kbn/embeddable-examples-plugin/public/todo';
interface Props {
listContainerEmbeddableFactory: ListContainerFactory;
@ -33,7 +31,7 @@ export function ListContainerExample({ listContainerEmbeddableFactory }: Props)
factory={listContainerEmbeddableFactory}
input={{
id: 'hello',
title: 'Todo list',
title: 'Hello world list',
viewMode: ViewMode.VIEW,
panels: {
'1': {
@ -43,21 +41,10 @@ export function ListContainerExample({ listContainerEmbeddableFactory }: Props)
},
},
'2': {
type: TODO_EMBEDDABLE,
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '2',
task: 'Goes out on Wednesdays!',
icon: 'broom',
title: 'Take out the trash',
} as TodoInput,
},
'3': {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '3',
icon: 'broom',
title: 'Vaccum the floor',
} as TodoInput,
},
},
},
}}
@ -77,24 +64,13 @@ export function ListContainerExample({ listContainerEmbeddableFactory }: Props)
explicitInput: {
id: '1',
},
},
'2': {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '2',
task: 'Goes out on Wednesdays!',
icon: 'broom',
title: 'Take out the trash',
} as TodoInput,
},
'3': {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '3',
icon: 'broom',
title: 'Vaccum the floor',
} as TodoInput,
},
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
}
},
}}
/>`}

View file

@ -48,7 +48,6 @@ export class EmbeddableExplorerPlugin implements Plugin<void, void, {}, StartDep
uiActionsApi: depsStart.uiActions,
basename: params.appBasePath,
uiSettingsClient: coreStart.uiSettings,
savedObject: coreStart.savedObjects,
overlays: coreStart.overlays,
navigateToApp: coreStart.application.navigateToApp,
embeddableExamples: depsStart.embeddableExamples,

View file

@ -1,120 +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 {
EuiCodeBlock,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiPanel,
EuiText,
EuiTextArea,
EuiPageTemplate,
EuiSpacer,
EuiSelect,
} from '@elastic/eui';
import { TodoEmbeddableFactory } from '@kbn/embeddable-examples-plugin/public';
import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public';
interface Props {
todoEmbeddableFactory: TodoEmbeddableFactory;
}
interface State {
task: string;
title: string;
icon: string;
}
const ICON_OPTIONS = [
{ value: 'beaker', text: 'beaker' },
{ value: 'bell', text: 'bell' },
{ value: 'bolt', text: 'bolt' },
{ value: 'broom', text: 'broom' },
{ value: 'bug', text: 'bug' },
{ value: 'bullseye', text: 'bullseye' },
];
export class TodoEmbeddableExample extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
icon: 'broom',
task: 'Take out the trash',
title: 'Trash',
};
}
public render() {
return (
<>
<EuiPageTemplate.Header pageTitle="Update embeddable state" />
<EuiPageTemplate.Section grow={false}>
<>
<EuiText>
Use <strong>input</strong> prop to update embeddable state.
</EuiText>
<EuiSpacer />
<EuiForm>
<EuiFormRow label="Title">
<EuiFieldText
data-test-subj="titleTodo"
value={this.state.title}
onChange={(ev) => this.setState({ title: ev.target.value })}
/>
</EuiFormRow>
<EuiFormRow label="Icon">
<EuiSelect
data-test-subj="iconTodo"
value={this.state.icon}
options={ICON_OPTIONS}
onChange={(ev) => this.setState({ icon: ev.target.value })}
/>
</EuiFormRow>
<EuiFormRow label="Task">
<EuiTextArea
fullWidth
resize="horizontal"
data-test-subj="taskTodo"
value={this.state.task}
onChange={(ev) => this.setState({ task: ev.target.value })}
/>
</EuiFormRow>
</EuiForm>
<EuiSpacer />
<EuiPanel data-test-subj="todoEmbeddable" role="figure">
<EmbeddableRenderer
factory={this.props.todoEmbeddableFactory}
input={{
id: '1',
task: this.state.task,
title: this.state.title,
icon: this.state.icon,
}}
/>
</EuiPanel>
<EuiSpacer />
<EuiCodeBlock language="jsx" fontSize="m" paddingSize="m">
{`<EmbeddableRenderer
factory={this.props.todoEmbeddableFactory}
input={{
id: '1',
task: this.state.task,
title: this.state.title,
icon: this.state.icon,
}}
/>`}
</EuiCodeBlock>
</>
</EuiPageTemplate.Section>
</>
);
}
}

View file

@ -1,55 +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 expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from '../../plugin_functional/services';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: PluginFunctionalProviderContext) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const flyout = getService('flyout');
const toggleFilterPopover = async () => {
const filtersHolder = await find.byClassName('euiSearchBar__filtersHolder');
const filtersButton = await filtersHolder.findByCssSelector('button');
await filtersButton.click();
};
const clickFilter = async (type: string) => {
const list = await testSubjects.find('euiSelectableList');
const listItems = await list.findAllByCssSelector('li');
for (let i = 0; i < listItems.length; i++) {
const listItem = await listItems[i].findByClassName('euiSelectableListItem__text');
const text = await listItem.getVisibleText();
if (text.includes(type)) {
await listItem.click();
await toggleFilterPopover();
break;
}
}
};
describe('adding children', () => {
before(async () => {
await testSubjects.click('embeddablePanelExample');
});
it('Can add a child backed off a saved object', async () => {
await testSubjects.click('addPanelToListContainer');
await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator');
await toggleFilterPopover();
await clickFilter('Todo');
await testSubjects.click('savedObjectTitleGarbage');
await testSubjects.moveMouseTo('euiFlyoutCloseButton');
await flyout.ensureClosed('dashboardAddPanel');
const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask');
expect(tasks).to.eql(['Goes out on Wednesdays!', 'Take the garbage out']);
});
});
}

View file

@ -24,8 +24,6 @@ export default function ({
});
loadTestFile(require.resolve('./hello_world_embeddable'));
loadTestFile(require.resolve('./todo_embeddable'));
loadTestFile(require.resolve('./list_container'));
loadTestFile(require.resolve('./adding_children'));
});
}

View file

@ -22,13 +22,10 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
it('list containers render', async () => {
await retry.try(async () => {
const title = await testSubjects.getVisibleText('listContainerTitle');
expect(title).to.be('Todo list');
const titles = await testSubjects.getVisibleTextAll('todoEmbeddableTitle');
expect(titles).to.eql(['Take out the trash', 'Vaccum the floor']);
expect(title).to.be('Hello world list');
const text = await testSubjects.getVisibleTextAll('helloWorldEmbeddable');
expect(text).to.eql(['HELLO WORLD!']);
expect(text).to.eql(['HELLO WORLD!', 'HELLO WORLD!']);
});
});
});

View file

@ -1,43 +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 expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from '../../plugin_functional/services';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: PluginFunctionalProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
describe('todo embeddable', () => {
before(async () => {
await testSubjects.click('todoEmbeddableSection');
});
it('todo embeddable renders', async () => {
await retry.try(async () => {
const title = await testSubjects.getVisibleText('todoEmbeddableTitle');
expect(title).to.be('Trash');
const task = await testSubjects.getVisibleText('todoEmbeddableTask');
expect(task).to.be('Take out the trash');
});
});
it('todo embeddable updates', async () => {
await testSubjects.setValue('taskTodo', 'read a book');
await testSubjects.setValue('titleTodo', 'Learn');
await retry.try(async () => {
const title = await testSubjects.getVisibleText('todoEmbeddableTitle');
expect(title).to.be('Learn');
const task = await testSubjects.getVisibleText('todoEmbeddableTask');
expect(task).to.be('read a book');
});
});
});
}