Skip to content

Commit 338a679

Browse files
authored
[Dashboard First] Genericize Attribute Service (#76057) (#76806)
Genericized attribute service, with custom save and unwrap methods and added unit tests.
1 parent 0ef4b56 commit 338a679

File tree

8 files changed

+349
-77
lines changed

8 files changed

+349
-77
lines changed

examples/embeddable_examples/public/book/book_embeddable.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,7 @@ export class BookEmbeddable
7171

7272
constructor(
7373
initialInput: BookEmbeddableInput,
74-
private attributeService: AttributeService<
75-
BookSavedObjectAttributes,
76-
BookByValueInput,
77-
BookByReferenceInput
78-
>,
74+
private attributeService: AttributeService<BookSavedObjectAttributes>,
7975
{
8076
parent,
8177
}: {
@@ -99,18 +95,21 @@ export class BookEmbeddable
9995
});
10096
}
10197

102-
inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => {
98+
readonly inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => {
10399
return this.attributeService.inputIsRefType(input);
104100
};
105101

106-
getInputAsValueType = async (): Promise<BookByValueInput> => {
102+
readonly getInputAsValueType = async (): Promise<BookByValueInput> => {
107103
const input = this.attributeService.getExplicitInputFromEmbeddable(this);
108104
return this.attributeService.getInputAsValueType(input);
109105
};
110106

111-
getInputAsRefType = async (): Promise<BookByReferenceInput> => {
107+
readonly getInputAsRefType = async (): Promise<BookByReferenceInput> => {
112108
const input = this.attributeService.getExplicitInputFromEmbeddable(this);
113-
return this.attributeService.getInputAsRefType(input, { showSaveModal: true });
109+
return this.attributeService.getInputAsRefType(input, {
110+
showSaveModal: true,
111+
saveModalTitle: this.getTitle(),
112+
});
114113
};
115114

116115
public render(node: HTMLElement) {

examples/embeddable_examples/public/book/book_embeddable_factory.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import {
3131
BOOK_EMBEDDABLE,
3232
BookEmbeddableInput,
3333
BookEmbeddableOutput,
34-
BookByValueInput,
35-
BookByReferenceInput,
3634
} from './book_embeddable';
3735
import { CreateEditBookComponent } from './create_edit_book_component';
3836
import { OverlayStart } from '../../../../src/core/public';
@@ -66,11 +64,7 @@ export class BookEmbeddableFactoryDefinition
6664
getIconForSavedObject: () => 'pencil',
6765
};
6866

69-
private attributeService?: AttributeService<
70-
BookSavedObjectAttributes,
71-
BookByValueInput,
72-
BookByReferenceInput
73-
>;
67+
private attributeService?: AttributeService<BookSavedObjectAttributes>;
7468

7569
constructor(private getStartServices: () => Promise<StartServices>) {}
7670

@@ -126,9 +120,7 @@ export class BookEmbeddableFactoryDefinition
126120
private async getAttributeService() {
127121
if (!this.attributeService) {
128122
this.attributeService = await (await this.getStartServices()).getAttributeService<
129-
BookSavedObjectAttributes,
130-
BookByValueInput,
131-
BookByReferenceInput
123+
BookSavedObjectAttributes
132124
>(this.type);
133125
}
134126
return this.attributeService!;

examples/embeddable_examples/public/book/edit_book_action.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ export const createEditBookAction = (getStartServices: () => Promise<StartServic
5757
},
5858
execute: async ({ embeddable }: ActionContext) => {
5959
const { openModal, getAttributeService } = await getStartServices();
60-
const attributeService = getAttributeService<
61-
BookSavedObjectAttributes,
62-
BookByValueInput,
63-
BookByReferenceInput
64-
>(BOOK_SAVED_OBJECT);
60+
const attributeService = getAttributeService<BookSavedObjectAttributes>(BOOK_SAVED_OBJECT);
6561
const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
66-
const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable);
62+
const newInput = await attributeService.wrapAttributes(
63+
attributes,
64+
useRefType,
65+
attributeService.getExplicitInputFromEmbeddable(embeddable)
66+
);
6767
if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) {
6868
// Set the saved object ID to null so that update input will remove the existing savedObjectId...
6969
(newInput as BookByValueInput & { savedObjectId: unknown }).savedObjectId = null;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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 { ATTRIBUTE_SERVICE_KEY } from './attribute_service';
21+
import { mockAttributeService } from './attribute_service_mock';
22+
import { coreMock } from '../../../../core/public/mocks';
23+
24+
interface TestAttributes {
25+
title: string;
26+
testAttr1?: string;
27+
testAttr2?: { array: unknown[]; testAttr3: string };
28+
}
29+
30+
interface TestByValueInput {
31+
id: string;
32+
[ATTRIBUTE_SERVICE_KEY]: TestAttributes;
33+
}
34+
35+
describe('attributeService', () => {
36+
const defaultTestType = 'defaultTestType';
37+
let attributes: TestAttributes;
38+
let byValueInput: TestByValueInput;
39+
let byReferenceInput: { id: string; savedObjectId: string };
40+
41+
beforeEach(() => {
42+
attributes = {
43+
title: 'ultra title',
44+
testAttr1: 'neat first attribute',
45+
testAttr2: { array: [1, 2, 3], testAttr3: 'super attribute' },
46+
};
47+
byValueInput = {
48+
id: '456',
49+
attributes,
50+
};
51+
byReferenceInput = {
52+
id: '456',
53+
savedObjectId: '123',
54+
};
55+
});
56+
57+
describe('determining input type', () => {
58+
const defaultAttributeService = mockAttributeService<TestAttributes>(defaultTestType);
59+
const customAttributeService = mockAttributeService<TestAttributes, TestByValueInput>(
60+
defaultTestType
61+
);
62+
63+
it('can determine input type given default types', () => {
64+
expect(
65+
defaultAttributeService.inputIsRefType({ id: '456', savedObjectId: '123' })
66+
).toBeTruthy();
67+
expect(
68+
defaultAttributeService.inputIsRefType({
69+
id: '456',
70+
attributes: { title: 'wow I am by value' },
71+
})
72+
).toBeFalsy();
73+
});
74+
it('can determine input type given custom types', () => {
75+
expect(
76+
customAttributeService.inputIsRefType({ id: '456', savedObjectId: '123' })
77+
).toBeTruthy();
78+
expect(
79+
customAttributeService.inputIsRefType({
80+
id: '456',
81+
[ATTRIBUTE_SERVICE_KEY]: { title: 'wow I am by value' },
82+
})
83+
).toBeFalsy();
84+
});
85+
});
86+
87+
describe('unwrapping attributes', () => {
88+
it('can unwrap all default attributes when given reference type input', async () => {
89+
const core = coreMock.createStart();
90+
core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({
91+
attributes,
92+
});
93+
const attributeService = mockAttributeService<TestAttributes>(
94+
defaultTestType,
95+
undefined,
96+
core
97+
);
98+
expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual(attributes);
99+
});
100+
101+
it('returns attributes when when given value type input', async () => {
102+
const attributeService = mockAttributeService<TestAttributes>(defaultTestType);
103+
expect(await attributeService.unwrapAttributes(byValueInput)).toEqual(attributes);
104+
});
105+
106+
it('runs attributes through a custom unwrap method', async () => {
107+
const core = coreMock.createStart();
108+
core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({
109+
attributes,
110+
});
111+
const attributeService = mockAttributeService<TestAttributes>(
112+
defaultTestType,
113+
{
114+
customUnwrapMethod: (savedObject) => ({
115+
...savedObject.attributes,
116+
testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' },
117+
}),
118+
},
119+
core
120+
);
121+
expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual({
122+
...attributes,
123+
testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' },
124+
});
125+
});
126+
});
127+
128+
describe('wrapping attributes', () => {
129+
it('returns given attributes when use ref type is false', async () => {
130+
const attributeService = mockAttributeService<TestAttributes>(defaultTestType);
131+
expect(await attributeService.wrapAttributes(attributes, false)).toEqual({ attributes });
132+
});
133+
134+
it('updates existing saved object with new attributes when given id', async () => {
135+
const core = coreMock.createStart();
136+
const attributeService = mockAttributeService<TestAttributes>(
137+
defaultTestType,
138+
undefined,
139+
core
140+
);
141+
expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual(
142+
byReferenceInput
143+
);
144+
expect(core.savedObjects.client.update).toHaveBeenCalledWith(
145+
defaultTestType,
146+
'123',
147+
attributes
148+
);
149+
});
150+
151+
it('creates new saved object with attributes when given no id', async () => {
152+
const core = coreMock.createStart();
153+
core.savedObjects.client.create = jest.fn().mockResolvedValueOnce({
154+
id: '678',
155+
});
156+
const attributeService = mockAttributeService<TestAttributes>(
157+
defaultTestType,
158+
undefined,
159+
core
160+
);
161+
expect(await attributeService.wrapAttributes(attributes, true)).toEqual({
162+
savedObjectId: '678',
163+
});
164+
expect(core.savedObjects.client.create).toHaveBeenCalledWith(defaultTestType, attributes);
165+
});
166+
167+
it('uses custom save method when given an id', async () => {
168+
const customSaveMethod = jest.fn().mockReturnValue({ id: '123' });
169+
const attributeService = mockAttributeService<TestAttributes>(defaultTestType, {
170+
customSaveMethod,
171+
});
172+
expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual(
173+
byReferenceInput
174+
);
175+
expect(customSaveMethod).toHaveBeenCalledWith(
176+
defaultTestType,
177+
attributes,
178+
byReferenceInput.savedObjectId
179+
);
180+
});
181+
182+
it('uses custom save method given no id', async () => {
183+
const customSaveMethod = jest.fn().mockReturnValue({ id: '678' });
184+
const attributeService = mockAttributeService<TestAttributes>(defaultTestType, {
185+
customSaveMethod,
186+
});
187+
expect(await attributeService.wrapAttributes(attributes, true)).toEqual({
188+
savedObjectId: '678',
189+
});
190+
expect(customSaveMethod).toHaveBeenCalledWith(defaultTestType, attributes, undefined);
191+
});
192+
});
193+
});

0 commit comments

Comments
 (0)