Skip to content

Commit 33fd5cf

Browse files
authored
[Example] Embeddable by Reference and Value (#68719)
Added an attribute service to embeddable start contract which provides a higher level abstraction for embeddables that can be by reference OR by value. Added an example that uses this service.
1 parent b25b690 commit 33fd5cf

File tree

21 files changed

+781
-8
lines changed

21 files changed

+781
-8
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { SavedObjectAttributes } from '../../../src/core/types';
21+
22+
export const BOOK_SAVED_OBJECT = 'book';
23+
24+
export interface BookSavedObjectAttributes extends SavedObjectAttributes {
25+
title: string;
26+
author?: string;
27+
readIt?: boolean;
28+
}

examples/embeddable_examples/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
*/
1919

2020
export { TodoSavedObjectAttributes } from './todo_saved_object_attributes';
21+
export { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from './book_saved_object_attributes';

examples/embeddable_examples/kibana.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"kibanaVersion": "kibana",
55
"server": true,
66
"ui": true,
7-
"requiredPlugins": ["embeddable"],
7+
"requiredPlugins": ["embeddable", "uiActions"],
88
"optionalPlugins": [],
99
"extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"]
1010
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import React from 'react';
20+
import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui';
21+
22+
import { EuiText } from '@elastic/eui';
23+
import { EuiFlexGrid } from '@elastic/eui';
24+
import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public';
25+
import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable';
26+
27+
interface Props {
28+
input: BookEmbeddableInput;
29+
output: BookEmbeddableOutput;
30+
embeddable: BookEmbeddable;
31+
}
32+
33+
function wrapSearchTerms(task?: string, search?: string) {
34+
if (!search || !task) return task;
35+
const parts = task.split(new RegExp(`(${search})`, 'g'));
36+
return parts.map((part, i) =>
37+
part === search ? (
38+
<span key={i} style={{ backgroundColor: 'yellow' }}>
39+
{part}
40+
</span>
41+
) : (
42+
part
43+
)
44+
);
45+
}
46+
47+
export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) {
48+
const title = attributes?.title;
49+
const author = attributes?.author;
50+
const readIt = attributes?.readIt;
51+
52+
return (
53+
<EuiFlexGroup gutterSize="s">
54+
<EuiFlexItem>
55+
<EuiFlexGrid columns={1} gutterSize="none">
56+
{title ? (
57+
<EuiFlexItem>
58+
<EuiText data-test-subj="bookEmbeddableTitle">
59+
<h3>{wrapSearchTerms(title, search)},</h3>
60+
</EuiText>
61+
</EuiFlexItem>
62+
) : null}
63+
{author ? (
64+
<EuiFlexItem>
65+
<EuiText data-test-subj="bookEmbeddableAuthor">
66+
<h5>-{wrapSearchTerms(author, search)}</h5>
67+
</EuiText>
68+
</EuiFlexItem>
69+
) : null}
70+
{readIt ? (
71+
<EuiFlexItem>
72+
<EuiIcon type="check" />
73+
</EuiFlexItem>
74+
) : (
75+
<EuiFlexItem>
76+
<EuiIcon type="cross" />
77+
</EuiFlexItem>
78+
)}
79+
</EuiFlexGrid>
80+
</EuiFlexItem>
81+
</EuiFlexGroup>
82+
);
83+
}
84+
85+
export const BookEmbeddableComponent = withEmbeddableSubscription<
86+
BookEmbeddableInput,
87+
BookEmbeddableOutput,
88+
BookEmbeddable,
89+
{}
90+
>(BookEmbeddableComponentInner);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import React from 'react';
20+
import ReactDOM from 'react-dom';
21+
import { Subscription } from 'rxjs';
22+
import {
23+
Embeddable,
24+
EmbeddableInput,
25+
IContainer,
26+
EmbeddableOutput,
27+
SavedObjectEmbeddableInput,
28+
AttributeService,
29+
} from '../../../../src/plugins/embeddable/public';
30+
import { BookSavedObjectAttributes } from '../../common';
31+
import { BookEmbeddableComponent } from './book_component';
32+
33+
export const BOOK_EMBEDDABLE = 'book';
34+
export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput;
35+
export interface BookEmbeddableOutput extends EmbeddableOutput {
36+
hasMatch: boolean;
37+
attributes: BookSavedObjectAttributes;
38+
}
39+
40+
interface BookInheritedInput extends EmbeddableInput {
41+
search?: string;
42+
}
43+
44+
export type BookByValueInput = { attributes: BookSavedObjectAttributes } & BookInheritedInput;
45+
export type BookByReferenceInput = SavedObjectEmbeddableInput & BookInheritedInput;
46+
47+
/**
48+
* Returns whether any attributes contain the search string. If search is empty, true is returned. If
49+
* there are no savedAttributes, false is returned.
50+
* @param search - the search string
51+
* @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId`
52+
*/
53+
function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttributes): boolean {
54+
if (!search) return true;
55+
if (!savedAttributes) return false;
56+
return Boolean(
57+
(savedAttributes.author && savedAttributes.author.match(search)) ||
58+
(savedAttributes.title && savedAttributes.title.match(search))
59+
);
60+
}
61+
62+
export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddableOutput> {
63+
public readonly type = BOOK_EMBEDDABLE;
64+
private subscription: Subscription;
65+
private node?: HTMLElement;
66+
private savedObjectId?: string;
67+
private attributes?: BookSavedObjectAttributes;
68+
69+
constructor(
70+
initialInput: BookEmbeddableInput,
71+
private attributeService: AttributeService<
72+
BookSavedObjectAttributes,
73+
BookByValueInput,
74+
BookByReferenceInput
75+
>,
76+
{
77+
parent,
78+
}: {
79+
parent?: IContainer;
80+
}
81+
) {
82+
super(initialInput, {} as BookEmbeddableOutput, parent);
83+
84+
this.subscription = this.getInput$().subscribe(async () => {
85+
const savedObjectId = (this.getInput() as BookByReferenceInput).savedObjectId;
86+
const attributes = (this.getInput() as BookByValueInput).attributes;
87+
if (this.attributes !== attributes || this.savedObjectId !== savedObjectId) {
88+
this.savedObjectId = savedObjectId;
89+
this.reload();
90+
} else {
91+
this.updateOutput({
92+
attributes: this.attributes,
93+
hasMatch: getHasMatch(this.input.search, this.attributes),
94+
});
95+
}
96+
});
97+
}
98+
99+
public render(node: HTMLElement) {
100+
if (this.node) {
101+
ReactDOM.unmountComponentAtNode(this.node);
102+
}
103+
this.node = node;
104+
ReactDOM.render(<BookEmbeddableComponent embeddable={this} />, node);
105+
}
106+
107+
public async reload() {
108+
this.attributes = await this.attributeService.unwrapAttributes(this.input);
109+
110+
this.updateOutput({
111+
attributes: this.attributes,
112+
hasMatch: getHasMatch(this.input.search, this.attributes),
113+
});
114+
}
115+
116+
public destroy() {
117+
super.destroy();
118+
this.subscription.unsubscribe();
119+
if (this.node) {
120+
ReactDOM.unmountComponentAtNode(this.node);
121+
}
122+
}
123+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import React from 'react';
21+
import { i18n } from '@kbn/i18n';
22+
import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
23+
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
24+
import {
25+
EmbeddableFactoryDefinition,
26+
EmbeddableStart,
27+
IContainer,
28+
AttributeService,
29+
EmbeddableFactory,
30+
} from '../../../../src/plugins/embeddable/public';
31+
import {
32+
BookEmbeddable,
33+
BOOK_EMBEDDABLE,
34+
BookEmbeddableInput,
35+
BookEmbeddableOutput,
36+
BookByValueInput,
37+
BookByReferenceInput,
38+
} from './book_embeddable';
39+
import { CreateEditBookComponent } from './create_edit_book_component';
40+
import { OverlayStart } from '../../../../src/core/public';
41+
42+
interface StartServices {
43+
getAttributeService: EmbeddableStart['getAttributeService'];
44+
openModal: OverlayStart['openModal'];
45+
}
46+
47+
export type BookEmbeddableFactory = EmbeddableFactory<
48+
BookEmbeddableInput,
49+
BookEmbeddableOutput,
50+
BookEmbeddable,
51+
BookSavedObjectAttributes
52+
>;
53+
54+
export class BookEmbeddableFactoryDefinition
55+
implements
56+
EmbeddableFactoryDefinition<
57+
BookEmbeddableInput,
58+
BookEmbeddableOutput,
59+
BookEmbeddable,
60+
BookSavedObjectAttributes
61+
> {
62+
public readonly type = BOOK_EMBEDDABLE;
63+
public savedObjectMetaData = {
64+
name: 'Book',
65+
includeFields: ['title', 'author', 'readIt'],
66+
type: BOOK_SAVED_OBJECT,
67+
getIconForSavedObject: () => 'pencil',
68+
};
69+
70+
private attributeService?: AttributeService<
71+
BookSavedObjectAttributes,
72+
BookByValueInput,
73+
BookByReferenceInput
74+
>;
75+
76+
constructor(private getStartServices: () => Promise<StartServices>) {}
77+
78+
public async isEditable() {
79+
return true;
80+
}
81+
82+
public async create(input: BookEmbeddableInput, parent?: IContainer) {
83+
return new BookEmbeddable(input, await this.getAttributeService(), {
84+
parent,
85+
});
86+
}
87+
88+
public getDisplayName() {
89+
return i18n.translate('embeddableExamples.book.displayName', {
90+
defaultMessage: 'Book',
91+
});
92+
}
93+
94+
public async getExplicitInput(): Promise<Omit<BookEmbeddableInput, 'id'>> {
95+
const { openModal } = await this.getStartServices();
96+
return new Promise<Omit<BookEmbeddableInput, 'id'>>((resolve) => {
97+
const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
98+
const wrappedAttributes = (await this.getAttributeService()).wrapAttributes(
99+
attributes,
100+
useRefType
101+
);
102+
resolve(wrappedAttributes);
103+
};
104+
const overlay = openModal(
105+
toMountPoint(
106+
<CreateEditBookComponent
107+
onSave={(attributes: BookSavedObjectAttributes, useRefType: boolean) => {
108+
onSave(attributes, useRefType);
109+
overlay.close();
110+
}}
111+
/>
112+
)
113+
);
114+
});
115+
}
116+
117+
private async getAttributeService() {
118+
if (!this.attributeService) {
119+
this.attributeService = await (await this.getStartServices()).getAttributeService<
120+
BookSavedObjectAttributes,
121+
BookByValueInput,
122+
BookByReferenceInput
123+
>(this.type);
124+
}
125+
return this.attributeService;
126+
}
127+
}

0 commit comments

Comments
 (0)