Skip to content

Commit c1b55d5

Browse files
authored
[Lens] Clear out all attribute properties before updating (#74483)
1 parent 244ed04 commit c1b55d5

File tree

2 files changed

+58
-33
lines changed

2 files changed

+58
-33
lines changed

x-pack/plugins/lens/public/persistence/saved_object_store.test.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { SavedObjectsClientContract, SavedObjectsBulkUpdateObject } from 'kibana/public';
78
import { SavedObjectIndexStore } from './saved_object_store';
89

910
describe('LensStore', () => {
1011
function testStore(testId?: string) {
1112
const client = {
1213
create: jest.fn(() => Promise.resolve({ id: testId || 'testid' })),
13-
update: jest.fn((_type: string, id: string) => Promise.resolve({ id })),
14+
bulkUpdate: jest.fn(([{ id }]: SavedObjectsBulkUpdateObject[]) =>
15+
Promise.resolve({ savedObjects: [{ id }, { id }] })
16+
),
1417
get: jest.fn(),
1518
};
1619

1720
return {
1821
client,
19-
store: new SavedObjectIndexStore(client),
22+
store: new SavedObjectIndexStore((client as unknown) as SavedObjectsClientContract),
2023
};
2124
}
2225

@@ -108,19 +111,35 @@ describe('LensStore', () => {
108111
},
109112
});
110113

111-
expect(client.update).toHaveBeenCalledTimes(1);
112-
expect(client.update).toHaveBeenCalledWith('lens', 'Gandalf', {
113-
title: 'Even the very wise cannot see all ends.',
114-
visualizationType: 'line',
115-
expression: '',
116-
state: {
117-
datasourceMetaData: { filterableIndexPatterns: [] },
118-
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
119-
visualization: { gear: ['staff', 'pointy hat'] },
120-
query: { query: '', language: 'lucene' },
121-
filters: [],
114+
expect(client.bulkUpdate).toHaveBeenCalledTimes(1);
115+
expect(client.bulkUpdate).toHaveBeenCalledWith([
116+
{
117+
type: 'lens',
118+
id: 'Gandalf',
119+
attributes: {
120+
title: null,
121+
visualizationType: null,
122+
expression: null,
123+
state: null,
124+
},
122125
},
123-
});
126+
{
127+
type: 'lens',
128+
id: 'Gandalf',
129+
attributes: {
130+
title: 'Even the very wise cannot see all ends.',
131+
visualizationType: 'line',
132+
expression: '',
133+
state: {
134+
datasourceMetaData: { filterableIndexPatterns: [] },
135+
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
136+
visualization: { gear: ['staff', 'pointy hat'] },
137+
query: { query: '', language: 'lucene' },
138+
filters: [],
139+
},
140+
},
141+
},
142+
]);
124143
});
125144
});
126145

x-pack/plugins/lens/public/persistence/saved_object_store.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { SavedObjectAttributes } from 'kibana/server';
7+
import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public';
88
import { Query, Filter } from '../../../../../src/plugins/data/public';
99

1010
export interface Document {
@@ -27,20 +27,6 @@ export interface Document {
2727

2828
export const DOC_TYPE = 'lens';
2929

30-
interface SavedObjectClient {
31-
create: (type: string, object: SavedObjectAttributes) => Promise<{ id: string }>;
32-
update: (type: string, id: string, object: SavedObjectAttributes) => Promise<{ id: string }>;
33-
get: (
34-
type: string,
35-
id: string
36-
) => Promise<{
37-
id: string;
38-
type: string;
39-
attributes: SavedObjectAttributes;
40-
error?: { statusCode: number; message: string };
41-
}>;
42-
}
43-
4430
export interface DocumentSaver {
4531
save: (vis: Document) => Promise<{ id: string }>;
4632
}
@@ -52,9 +38,9 @@ export interface DocumentLoader {
5238
export type SavedObjectStore = DocumentLoader & DocumentSaver;
5339

5440
export class SavedObjectIndexStore implements SavedObjectStore {
55-
private client: SavedObjectClient;
41+
private client: SavedObjectsClientContract;
5642

57-
constructor(client: SavedObjectClient) {
43+
constructor(client: SavedObjectsClientContract) {
5844
this.client = client;
5945
}
6046

@@ -63,13 +49,33 @@ export class SavedObjectIndexStore implements SavedObjectStore {
6349
// TODO: SavedObjectAttributes should support this kind of object,
6450
// remove this workaround when SavedObjectAttributes is updated.
6551
const attributes = (rest as unknown) as SavedObjectAttributes;
52+
6653
const result = await (id
67-
? this.client.update(DOC_TYPE, id, attributes)
54+
? this.safeUpdate(id, attributes)
6855
: this.client.create(DOC_TYPE, attributes));
6956

7057
return { ...vis, id: result.id };
7158
}
7259

60+
// As Lens is using an object to store its attributes, using the update API
61+
// will merge the new attribute object with the old one, not overwriting deleted
62+
// keys. As Lens is using objects as maps in various places, this is a problem because
63+
// deleted subtrees make it back into the object after a load.
64+
// This function fixes this by doing two updates - one to empty out the document setting
65+
// every key to null, and a second one to load the new content.
66+
private async safeUpdate(id: string, attributes: SavedObjectAttributes) {
67+
const resetAttributes: SavedObjectAttributes = {};
68+
Object.keys(attributes).forEach((key) => {
69+
resetAttributes[key] = null;
70+
});
71+
return (
72+
await this.client.bulkUpdate([
73+
{ type: DOC_TYPE, id, attributes: resetAttributes },
74+
{ type: DOC_TYPE, id, attributes },
75+
])
76+
).savedObjects[1];
77+
}
78+
7379
async load(id: string): Promise<Document> {
7480
const { type, attributes, error } = await this.client.get(DOC_TYPE, id);
7581

@@ -78,7 +84,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
7884
}
7985

8086
return {
81-
...attributes,
87+
...(attributes as SavedObjectAttributes),
8288
id,
8389
type,
8490
} as Document;

0 commit comments

Comments
 (0)