mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ContentManagement] Fix Visualize List search and CRUD operations via CM (#165485)
## Summary Fix #163246 This PR fixes the CM problems within the Visualize List page leveraging the services already in place. The approach here is lighter than #165292 as it passes each client via the TypesService already used to register extensions in the Visualization scope. Also the `search` method now transparently uses the `mSearch` if more content types are detected without leaking any implementation detail outside the `VisualizationClient` interface. More fixes/features: * fixed Maps update operation definition which was missing the `overwrite` flag * Allow `mSearch` to accept an options object argument * Added new helper functions to interact with the metadata flyout in Listing page ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
691311ce7c
commit
01ad4c2b44
33 changed files with 491 additions and 417 deletions
|
@ -66,6 +66,13 @@ const deleteVisualization = async (id: string) => {
|
|||
};
|
||||
|
||||
const search = async (query: SearchQuery = {}, options?: VisualizationSearchQuery) => {
|
||||
if (options && options.types && options.types.length > 1) {
|
||||
const { types } = options;
|
||||
return getContentManagement().client.mSearch<VisualizationSearchOut['hits'][number]>({
|
||||
contentTypes: types.map((type) => ({ contentTypeId: type })),
|
||||
query,
|
||||
});
|
||||
}
|
||||
return getContentManagement().client.search<VisualizationSearchIn, VisualizationSearchOut>({
|
||||
contentTypeId: 'visualization',
|
||||
query,
|
||||
|
|
|
@ -24,7 +24,15 @@ export { getVisSchemas } from './vis_schemas';
|
|||
/** @public types */
|
||||
export type { VisualizationsSetup, VisualizationsStart };
|
||||
export { VisGroups } from './vis_types/vis_groups_enum';
|
||||
export type { BaseVisType, VisTypeAlias, VisTypeDefinition, Schema, ISchemas } from './vis_types';
|
||||
export type {
|
||||
BaseVisType,
|
||||
VisTypeAlias,
|
||||
VisTypeDefinition,
|
||||
Schema,
|
||||
ISchemas,
|
||||
VisualizationClient,
|
||||
SerializableAttributes,
|
||||
} from './vis_types';
|
||||
export type { Vis, SerializedVis, SerializedVisData, VisData } from './vis';
|
||||
export type VisualizeEmbeddableFactoryContract = PublicContract<VisualizeEmbeddableFactory>;
|
||||
export type VisualizeEmbeddableContract = PublicContract<VisualizeEmbeddable>;
|
||||
|
|
|
@ -333,6 +333,7 @@ export class VisualizationsPlugin
|
|||
unifiedSearch: pluginsStart.unifiedSearch,
|
||||
serverless: pluginsStart.serverless,
|
||||
noDataPage: pluginsStart.noDataPage,
|
||||
contentManagement: pluginsStart.contentManagement,
|
||||
};
|
||||
|
||||
params.element.classList.add('visAppWrapper');
|
||||
|
|
|
@ -9,12 +9,42 @@
|
|||
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import type { OverlayStart } from '@kbn/core-overlays-browser';
|
||||
|
||||
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import { extractReferences } from '../saved_visualization_references';
|
||||
import { visualizationsClient } from '../../content_management';
|
||||
import { TypesStart } from '../../vis_types';
|
||||
|
||||
interface UpdateBasicSoAttributesDependencies {
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||
overlays: OverlayStart;
|
||||
typesService: TypesStart;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
}
|
||||
|
||||
function getClientForType(
|
||||
type: string,
|
||||
typesService: TypesStart,
|
||||
contentManagement: ContentManagementPublicStart
|
||||
) {
|
||||
const visAliases = typesService.getAliases();
|
||||
return (
|
||||
visAliases
|
||||
.find((v) => v.appExtensions?.visualizations.docTypes.includes(type))
|
||||
?.appExtensions?.visualizations.client(contentManagement) || visualizationsClient
|
||||
);
|
||||
}
|
||||
|
||||
function getAdditionalOptionsForUpdate(
|
||||
type: string,
|
||||
typesService: TypesStart,
|
||||
method: 'update' | 'create'
|
||||
) {
|
||||
const visAliases = typesService.getAliases();
|
||||
const aliasType = visAliases.find((v) => v.appExtensions?.visualizations.docTypes.includes(type));
|
||||
if (!aliasType) {
|
||||
return { overwrite: true };
|
||||
}
|
||||
return aliasType?.appExtensions?.visualizations?.clientOptions?.[method];
|
||||
}
|
||||
|
||||
export const updateBasicSoAttributes = async (
|
||||
|
@ -27,7 +57,9 @@ export const updateBasicSoAttributes = async (
|
|||
},
|
||||
dependencies: UpdateBasicSoAttributesDependencies
|
||||
) => {
|
||||
const so = await visualizationsClient.get(soId);
|
||||
const client = getClientForType(type, dependencies.typesService, dependencies.contentManagement);
|
||||
|
||||
const so = await client.get(soId);
|
||||
const extractedReferences = extractReferences({
|
||||
attributes: so.item.attributes,
|
||||
references: so.item.references,
|
||||
|
@ -48,14 +80,14 @@ export const updateBasicSoAttributes = async (
|
|||
);
|
||||
}
|
||||
|
||||
return await visualizationsClient.update({
|
||||
return await client.update({
|
||||
id: soId,
|
||||
data: {
|
||||
...attributes,
|
||||
},
|
||||
options: {
|
||||
overwrite: true,
|
||||
references,
|
||||
...getAdditionalOptionsForUpdate(type, dependencies.typesService, 'update'),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,17 +12,17 @@ import {
|
|||
injectSearchSourceReferences,
|
||||
SerializedSearchSourceFields,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { SavedVisState, VisSavedObject } from '../../types';
|
||||
|
||||
import { extractTimeSeriesReferences, injectTimeSeriesReferences } from './timeseries_references';
|
||||
import { extractControlsReferences, injectControlsReferences } from './controls_references';
|
||||
import type { SerializableAttributes } from '../../vis_types/vis_type_alias_registry';
|
||||
|
||||
export function extractReferences({
|
||||
attributes,
|
||||
references = [],
|
||||
}: {
|
||||
attributes: SerializableRecord;
|
||||
attributes: SerializableAttributes;
|
||||
references: SavedObjectReference[];
|
||||
}) {
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
|
|
@ -73,6 +73,7 @@ jest.mock('../services', () => ({
|
|||
update: mockUpdateContent,
|
||||
get: mockGetContent,
|
||||
search: mockFindContent,
|
||||
mSearch: mockFindContent,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
@ -358,10 +359,11 @@ describe('saved_visualize_utils', () => {
|
|||
expect(mockFindContent.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
options: {
|
||||
types: ['bazdoc', 'etc', 'visualization'],
|
||||
searchFields: ['baz', 'bing', 'title^3', 'description'],
|
||||
},
|
||||
contentTypes: [
|
||||
{ contentTypeId: 'bazdoc' },
|
||||
{ contentTypeId: 'etc' },
|
||||
{ contentTypeId: 'visualization' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
@ -395,10 +397,12 @@ describe('saved_visualize_utils', () => {
|
|||
expect(mockFindContent.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
options: {
|
||||
types: ['bazdoc', 'bar', 'visualization', 'foo'],
|
||||
searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'],
|
||||
},
|
||||
contentTypes: [
|
||||
{ contentTypeId: 'bazdoc' },
|
||||
{ contentTypeId: 'bar' },
|
||||
{ contentTypeId: 'visualization' },
|
||||
{ contentTypeId: 'foo' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
|
|
@ -11,3 +11,4 @@ export { Schemas } from './schemas';
|
|||
export { VisGroups } from './vis_groups_enum';
|
||||
export { BaseVisType } from './base_vis_type';
|
||||
export type { VisTypeDefinition, ISchemas, Schema } from './types';
|
||||
export type { VisualizationClient, SerializableAttributes } from './vis_type_alias_registry';
|
||||
|
|
|
@ -6,6 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import {
|
||||
ContentManagementCrudTypes,
|
||||
SavedObjectCreateOptions,
|
||||
SavedObjectUpdateOptions,
|
||||
} from '@kbn/content-management-utils';
|
||||
import type { SimpleSavedObject } from '@kbn/core/public';
|
||||
import { BaseVisType } from './base_vis_type';
|
||||
|
||||
|
@ -27,9 +34,54 @@ export interface VisualizationListItem {
|
|||
type?: BaseVisType | string;
|
||||
}
|
||||
|
||||
export interface SerializableAttributes {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type GenericVisualizationCrudTypes<
|
||||
ContentType extends string,
|
||||
Attr extends SerializableAttributes
|
||||
> = ContentManagementCrudTypes<
|
||||
ContentType,
|
||||
Attr,
|
||||
Pick<SavedObjectCreateOptions, 'overwrite' | 'references'>,
|
||||
Pick<SavedObjectUpdateOptions, 'references'>,
|
||||
object
|
||||
>;
|
||||
|
||||
export interface VisualizationClient<
|
||||
ContentType extends string = string,
|
||||
Attr extends SerializableAttributes = SerializableAttributes
|
||||
> {
|
||||
get: (id: string) => Promise<GenericVisualizationCrudTypes<ContentType, Attr>['GetOut']>;
|
||||
create: (
|
||||
visualization: Omit<
|
||||
GenericVisualizationCrudTypes<ContentType, Attr>['CreateIn'],
|
||||
'contentTypeId'
|
||||
>
|
||||
) => Promise<GenericVisualizationCrudTypes<ContentType, Attr>['CreateOut']>;
|
||||
update: (
|
||||
visualization: Omit<
|
||||
GenericVisualizationCrudTypes<ContentType, Attr>['UpdateIn'],
|
||||
'contentTypeId'
|
||||
>
|
||||
) => Promise<GenericVisualizationCrudTypes<ContentType, Attr>['UpdateOut']>;
|
||||
delete: (id: string) => Promise<GenericVisualizationCrudTypes<ContentType, Attr>['DeleteOut']>;
|
||||
search: (
|
||||
query: SearchQuery,
|
||||
options?: object
|
||||
) => Promise<GenericVisualizationCrudTypes<ContentType, Attr>['SearchOut']>;
|
||||
}
|
||||
|
||||
export interface VisualizationsAppExtension {
|
||||
docTypes: string[];
|
||||
searchFields?: string[];
|
||||
/** let each visualization client pass its own custom options if required */
|
||||
clientOptions?: {
|
||||
update?: { overwrite?: boolean; [otherOption: string]: unknown };
|
||||
create?: { [otherOption: string]: unknown };
|
||||
};
|
||||
client: (contentManagement: ContentManagementPublicStart) => VisualizationClient;
|
||||
toListItem: (savedObject: SimpleSavedObject<any>) => VisualizationListItem;
|
||||
}
|
||||
|
||||
|
|
|
@ -109,6 +109,7 @@ const useTableListViewProps = (
|
|||
overlays,
|
||||
toastNotifications,
|
||||
visualizeCapabilities,
|
||||
contentManagement,
|
||||
},
|
||||
} = useKibana<VisualizeServices>();
|
||||
|
||||
|
@ -176,11 +177,16 @@ const useTableListViewProps = (
|
|||
description: args.description ?? '',
|
||||
tags: args.tags,
|
||||
},
|
||||
{ overlays, savedObjectsTagging }
|
||||
{
|
||||
overlays,
|
||||
savedObjectsTagging,
|
||||
typesService: getTypes(),
|
||||
contentManagement,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[overlays, savedObjectsTagging]
|
||||
[overlays, savedObjectsTagging, contentManagement]
|
||||
);
|
||||
|
||||
const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo(
|
||||
|
|
|
@ -42,6 +42,7 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug
|
|||
import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
|
||||
import type { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
|
||||
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type {
|
||||
Vis,
|
||||
VisualizeEmbeddableContract,
|
||||
|
@ -119,6 +120,7 @@ export interface VisualizeServices extends CoreStart {
|
|||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
serverless?: ServerlessPluginStart;
|
||||
noDataPage?: NoDataPagePluginStart;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
}
|
||||
|
||||
export interface VisInstance {
|
||||
|
|
|
@ -14,8 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const PageObjects = getPageObjects(['dashboard', 'header', 'common']);
|
||||
const browser = getService('browser');
|
||||
const listingTable = getService('listingTable');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
|
||||
describe('dashboard listing page', function describeIndexTests() {
|
||||
|
@ -217,12 +215,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await listingTable.searchForItemWithName(`${dashboardName}-editMetaData`);
|
||||
await testSubjects.click('inspect-action');
|
||||
await testSubjects.setValue('nameInput', 'new title');
|
||||
await testSubjects.setValue('descriptionInput', 'new description');
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('saveButton');
|
||||
await testSubjects.missingOrFail('flyoutTitle');
|
||||
await listingTable.inspectVisualization();
|
||||
await listingTable.editVisualizationDetails({
|
||||
title: 'new title',
|
||||
description: 'new description',
|
||||
});
|
||||
|
||||
await listingTable.searchAndExpectItemsCount('dashboard', 'new title', 1);
|
||||
|
|
|
@ -84,5 +84,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await listingTable.expectItemsCount('visualize', 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit', () => {
|
||||
before(async () => {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
});
|
||||
|
||||
it('should edit the title and description of a visualization', async () => {
|
||||
await listingTable.searchForItemWithName('Hello');
|
||||
await listingTable.inspectVisualization();
|
||||
await listingTable.editVisualizationDetails({
|
||||
title: 'new title',
|
||||
description: 'new description',
|
||||
});
|
||||
await listingTable.searchForItemWithName('new title');
|
||||
await listingTable.expectItemsCount('visualize', 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -154,6 +154,35 @@ export class ListingTableService extends FtrService {
|
|||
return visualizationNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the inspect flyout
|
||||
*/
|
||||
public async inspectVisualization(index: number = 0) {
|
||||
const inspectButtons = await this.testSubjects.findAll('inspect-action');
|
||||
await inspectButtons[index].click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit Visualization title and description in the flyout
|
||||
*/
|
||||
public async editVisualizationDetails(
|
||||
{ title, description }: { title?: string; description?: string } = {},
|
||||
shouldSave: boolean = true
|
||||
) {
|
||||
if (title) {
|
||||
await this.testSubjects.setValue('nameInput', title);
|
||||
}
|
||||
if (description) {
|
||||
await this.testSubjects.setValue('descriptionInput', description);
|
||||
}
|
||||
if (shouldSave) {
|
||||
await this.retry.try(async () => {
|
||||
await this.testSubjects.click('saveButton');
|
||||
await this.testSubjects.missingOrFail('flyoutTitle');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns items count on landing page
|
||||
*/
|
||||
|
|
|
@ -26,6 +26,7 @@ export type {
|
|||
LensSearchIn,
|
||||
LensSearchOut,
|
||||
LensSearchQuery,
|
||||
LensCrudTypes,
|
||||
} from './latest';
|
||||
|
||||
export * as LensV1 from './v1';
|
||||
|
|
|
@ -22,5 +22,5 @@ export type {
|
|||
LensSearchIn,
|
||||
LensSearchOut,
|
||||
LensSearchQuery,
|
||||
Reference,
|
||||
LensCrudTypes,
|
||||
} from './types';
|
||||
|
|
|
@ -5,20 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
GetIn,
|
||||
CreateIn,
|
||||
SearchIn,
|
||||
UpdateIn,
|
||||
DeleteIn,
|
||||
DeleteResult,
|
||||
SearchResult,
|
||||
GetResult,
|
||||
CreateResult,
|
||||
UpdateResult,
|
||||
} from '@kbn/content-management-plugin/common';
|
||||
import type { UpdateIn } from '@kbn/content-management-plugin/common';
|
||||
import type { ContentManagementCrudTypes } from '@kbn/content-management-utils';
|
||||
|
||||
import { LensContentType } from '../types';
|
||||
import type { LensContentType } from '../types';
|
||||
|
||||
export interface Reference {
|
||||
type: string;
|
||||
|
@ -26,6 +16,22 @@ export interface Reference {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export interface CreateOptions {
|
||||
/** If a document with the given `id` already exists, overwrite it's contents (default=false). */
|
||||
overwrite?: boolean;
|
||||
/** Array of referenced saved objects. */
|
||||
references?: Reference[];
|
||||
}
|
||||
|
||||
export interface UpdateOptions {
|
||||
/** Array of referenced saved objects. */
|
||||
references?: Reference[];
|
||||
}
|
||||
|
||||
export interface LensSearchQuery {
|
||||
searchFields?: string[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type LensSavedObjectAttributes = {
|
||||
title: string;
|
||||
|
@ -34,78 +40,41 @@ export type LensSavedObjectAttributes = {
|
|||
state?: unknown;
|
||||
};
|
||||
|
||||
export interface LensSavedObject {
|
||||
id: string;
|
||||
type: string;
|
||||
version?: string;
|
||||
updatedAt?: string;
|
||||
createdAt?: string;
|
||||
attributes: LensSavedObjectAttributes;
|
||||
references: Reference[];
|
||||
namespaces?: string[];
|
||||
originId?: string;
|
||||
error?: {
|
||||
error: string;
|
||||
message: string;
|
||||
statusCode: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
// Need to handle update in Lens in a bit different way
|
||||
export type LensCrudTypes = Omit<
|
||||
ContentManagementCrudTypes<
|
||||
LensContentType,
|
||||
LensSavedObjectAttributes,
|
||||
CreateOptions,
|
||||
UpdateOptions,
|
||||
LensSearchQuery
|
||||
>,
|
||||
'UpdateIn'
|
||||
> & { UpdateIn: UpdateIn<LensContentType, LensSavedObjectAttributes, UpdateOptions> };
|
||||
|
||||
export type LensSavedObject = LensCrudTypes['Item'];
|
||||
export type PartialLensSavedObject = LensCrudTypes['PartialItem'];
|
||||
|
||||
export type PartialLensSavedObject = Omit<LensSavedObject, 'attributes' | 'references'> & {
|
||||
attributes: Partial<LensSavedObjectAttributes>;
|
||||
references: Reference[] | undefined;
|
||||
};
|
||||
// ----------- GET --------------
|
||||
|
||||
export type LensGetIn = GetIn<LensContentType>;
|
||||
export type LensGetIn = LensCrudTypes['GetIn'];
|
||||
|
||||
export type LensGetOut = GetResult<
|
||||
LensSavedObject,
|
||||
{
|
||||
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
|
||||
aliasTargetId?: string;
|
||||
aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport';
|
||||
}
|
||||
>;
|
||||
export type LensGetOut = LensCrudTypes['GetOut'];
|
||||
|
||||
// ----------- CREATE --------------
|
||||
|
||||
export interface CreateOptions {
|
||||
/** If a document with the given `id` already exists, overwrite it's contents (default=false). */
|
||||
overwrite?: boolean;
|
||||
/** Array of referenced saved objects. */
|
||||
references?: Reference[];
|
||||
}
|
||||
|
||||
export type LensCreateIn = CreateIn<LensContentType, LensSavedObjectAttributes, CreateOptions>;
|
||||
|
||||
export type LensCreateOut = CreateResult<LensSavedObject>;
|
||||
export type LensCreateIn = LensCrudTypes['CreateIn'];
|
||||
|
||||
export type LensCreateOut = LensCrudTypes['CreateOut'];
|
||||
// ----------- UPDATE --------------
|
||||
|
||||
export interface UpdateOptions {
|
||||
/** Array of referenced saved objects. */
|
||||
references?: Reference[];
|
||||
}
|
||||
|
||||
export type LensUpdateIn = UpdateIn<LensContentType, LensSavedObjectAttributes, UpdateOptions>;
|
||||
|
||||
export type LensUpdateOut = UpdateResult<PartialLensSavedObject>;
|
||||
|
||||
export type LensUpdateIn = LensCrudTypes['UpdateIn'];
|
||||
export type LensUpdateOut = LensCrudTypes['UpdateOut'];
|
||||
// ----------- DELETE --------------
|
||||
|
||||
export type LensDeleteIn = DeleteIn<LensContentType>;
|
||||
|
||||
export type LensDeleteOut = DeleteResult;
|
||||
|
||||
export type LensDeleteIn = LensCrudTypes['DeleteIn'];
|
||||
export type LensDeleteOut = LensCrudTypes['DeleteOut'];
|
||||
// ----------- SEARCH --------------
|
||||
|
||||
export interface LensSearchQuery {
|
||||
types?: string[];
|
||||
searchFields?: string[];
|
||||
}
|
||||
|
||||
export type LensSearchIn = SearchIn<LensContentType, {}>;
|
||||
|
||||
export type LensSearchOut = SearchResult<LensSavedObject>;
|
||||
export type LensSearchIn = LensCrudTypes['SearchIn'];
|
||||
export type LensSearchOut = LensCrudTypes['SearchOut'];
|
||||
|
|
|
@ -129,7 +129,7 @@ export async function getLensServices(
|
|||
settings: coreStart.settings,
|
||||
application: coreStart.application,
|
||||
notifications: coreStart.notifications,
|
||||
savedObjectStore: new SavedObjectIndexStore(startDependencies.contentManagement.client),
|
||||
savedObjectStore: new SavedObjectIndexStore(startDependencies.contentManagement),
|
||||
presentationUtil: startDependencies.presentationUtil,
|
||||
dataViewEditor: startDependencies.dataViewEditor,
|
||||
dataViewFieldEditor: startDependencies.dataViewFieldEditor,
|
||||
|
|
|
@ -30,7 +30,7 @@ export function getLensAttributeService(
|
|||
core: CoreStart,
|
||||
startDependencies: LensPluginStartDependencies
|
||||
): LensAttributeService {
|
||||
const savedObjectStore = new SavedObjectIndexStore(startDependencies.contentManagement.client);
|
||||
const savedObjectStore = new SavedObjectIndexStore(startDependencies.contentManagement);
|
||||
|
||||
return startDependencies.embeddable.getAttributeService<
|
||||
LensSavedObjectAttributes,
|
||||
|
|
81
x-pack/plugins/lens/public/persistence/lens_client.ts
Normal file
81
x-pack/plugins/lens/public/persistence/lens_client.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type {
|
||||
SerializableAttributes,
|
||||
VisualizationClient,
|
||||
} from '@kbn/visualizations-plugin/public';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
import {
|
||||
LensCreateIn,
|
||||
LensCreateOut,
|
||||
LensDeleteIn,
|
||||
LensDeleteOut,
|
||||
LensGetIn,
|
||||
LensGetOut,
|
||||
LensSearchIn,
|
||||
LensSearchOut,
|
||||
LensSearchQuery,
|
||||
LensUpdateIn,
|
||||
LensUpdateOut,
|
||||
} from '../../common/content_management';
|
||||
|
||||
export function getLensClient<Attr extends SerializableAttributes = SerializableAttributes>(
|
||||
cm: ContentManagementPublicStart
|
||||
): VisualizationClient<'lens', Attr> {
|
||||
const get = async (id: string) => {
|
||||
return cm.client.get<LensGetIn, LensGetOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const create = async ({ data, options }: Omit<LensCreateIn, 'contentTypeId'>) => {
|
||||
const res = await cm.client.create<LensCreateIn, LensCreateOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
data,
|
||||
options,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
const update = async ({ id, data, options }: Omit<LensUpdateIn, 'contentTypeId'>) => {
|
||||
const res = await cm.client.update<LensUpdateIn, LensUpdateOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
id,
|
||||
data,
|
||||
options,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
const deleteLens = async (id: string) => {
|
||||
const res = await cm.client.delete<LensDeleteIn, LensDeleteOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
id,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
const search = async (query: SearchQuery = {}, options?: LensSearchQuery) => {
|
||||
return cm.client.search<LensSearchIn, LensSearchOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
query,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
get,
|
||||
create,
|
||||
update,
|
||||
delete: deleteLens,
|
||||
search,
|
||||
} as unknown as VisualizationClient<'lens', Attr>;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ContentClient } from '@kbn/content-management-plugin/public';
|
||||
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import { SavedObjectIndexStore } from './saved_object_store';
|
||||
|
||||
describe('LensStore', () => {
|
||||
|
@ -18,7 +18,10 @@ describe('LensStore', () => {
|
|||
|
||||
return {
|
||||
client,
|
||||
store: new SavedObjectIndexStore(client as unknown as ContentClient),
|
||||
store: new SavedObjectIndexStore({
|
||||
client,
|
||||
registry: jest.fn(),
|
||||
} as unknown as ContentManagementPublicStart),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,21 +7,12 @@
|
|||
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { SavedObjectReference } from '@kbn/core/public';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { ContentClient } from '@kbn/content-management-plugin/public';
|
||||
import { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
import {
|
||||
LensCreateIn,
|
||||
LensCreateOut,
|
||||
LensGetIn,
|
||||
LensGetOut,
|
||||
LensSearchIn,
|
||||
LensSearchOut,
|
||||
LensSearchQuery,
|
||||
LensUpdateIn,
|
||||
LensUpdateOut,
|
||||
} from '../../common/content_management';
|
||||
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import type { VisualizationClient } from '@kbn/visualizations-plugin/public';
|
||||
import type { LensSavedObjectAttributes, LensSearchQuery } from '../../common/content_management';
|
||||
import { getLensClient } from './lens_client';
|
||||
|
||||
export interface Document {
|
||||
savedObjectId?: string;
|
||||
|
@ -55,10 +46,10 @@ export interface DocumentLoader {
|
|||
export type SavedObjectStore = DocumentLoader & DocumentSaver;
|
||||
|
||||
export class SavedObjectIndexStore implements SavedObjectStore {
|
||||
private client: ContentClient;
|
||||
private client: VisualizationClient<'lens', LensSavedObjectAttributes>;
|
||||
|
||||
constructor(client: ContentClient) {
|
||||
this.client = client;
|
||||
constructor(cm: ContentManagementPublicStart) {
|
||||
this.client = getLensClient(cm);
|
||||
}
|
||||
|
||||
save = async (vis: Document) => {
|
||||
|
@ -66,8 +57,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
const attributes = rest;
|
||||
|
||||
if (savedObjectId) {
|
||||
const result = await this.client.update<LensUpdateIn, LensUpdateOut>({
|
||||
contentTypeId: 'lens',
|
||||
const result = await this.client.update({
|
||||
id: savedObjectId,
|
||||
data: attributes,
|
||||
options: {
|
||||
|
@ -76,8 +66,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
});
|
||||
return { ...vis, savedObjectId: result.item.id };
|
||||
} else {
|
||||
const result = await this.client.create<LensCreateIn, LensCreateOut>({
|
||||
contentTypeId: 'lens',
|
||||
const result = await this.client.create({
|
||||
data: attributes,
|
||||
options: {
|
||||
references,
|
||||
|
@ -88,10 +77,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
};
|
||||
|
||||
async load(savedObjectId: string) {
|
||||
const resolveResult = await this.client.get<LensGetIn, LensGetOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
id: savedObjectId,
|
||||
});
|
||||
const resolveResult = await this.client.get(savedObjectId);
|
||||
|
||||
if (resolveResult.item.error) {
|
||||
throw resolveResult.item.error;
|
||||
|
@ -101,11 +87,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
}
|
||||
|
||||
async search(query: SearchQuery, options: LensSearchQuery) {
|
||||
const result = await this.client.search<LensSearchIn, LensSearchOut>({
|
||||
contentTypeId: DOC_TYPE,
|
||||
query,
|
||||
options,
|
||||
});
|
||||
const result = await this.client.search(query, options);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
import { getBasePath, getEditPath } from '../common/constants';
|
||||
import { getLensClient } from './persistence/lens_client';
|
||||
|
||||
export const getLensAliasConfig = (): VisTypeAlias => ({
|
||||
aliasPath: getBasePath(),
|
||||
|
@ -30,6 +31,8 @@ export const getLensAliasConfig = (): VisTypeAlias => ({
|
|||
visualizations: {
|
||||
docTypes: ['lens'],
|
||||
searchFields: ['title^3'],
|
||||
clientOptions: { update: { overwrite: true } },
|
||||
client: getLensClient,
|
||||
toListItem(savedObject) {
|
||||
const { id, type, updatedAt, attributes } = savedObject;
|
||||
const { title, description } = attributes as { title: string; description?: string };
|
||||
|
|
|
@ -5,34 +5,35 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import type { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import type { ContentStorage, StorageContext } from '@kbn/content-management-plugin/server';
|
||||
import type {
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsFindOptions,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import type { StorageContext } from '@kbn/content-management-plugin/server';
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||
|
||||
import { getMSearch, type GetMSearchType } from '@kbn/content-management-utils';
|
||||
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
import type {
|
||||
LensSavedObjectAttributes,
|
||||
LensSavedObject,
|
||||
PartialLensSavedObject,
|
||||
LensContentType,
|
||||
LensGetOut,
|
||||
LensCreateIn,
|
||||
LensCreateOut,
|
||||
CreateOptions,
|
||||
LensUpdateIn,
|
||||
LensUpdateOut,
|
||||
UpdateOptions,
|
||||
LensDeleteOut,
|
||||
LensSearchQuery,
|
||||
LensSearchOut,
|
||||
import {
|
||||
CONTENT_ID,
|
||||
type LensCrudTypes,
|
||||
type LensSavedObject,
|
||||
type LensSavedObjectAttributes,
|
||||
type PartialLensSavedObject,
|
||||
} from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
|
||||
const searchArgsToSOFindOptions = (args: LensCrudTypes['SearchIn']): SavedObjectsFindOptions => {
|
||||
const { query, contentTypeId, options } = args;
|
||||
|
||||
return {
|
||||
type: contentTypeId,
|
||||
searchFields: ['title^3', 'description'],
|
||||
fields: ['description', 'title'],
|
||||
search: query.text,
|
||||
perPage: query.limit,
|
||||
page: query.cursor ? +query.cursor : undefined,
|
||||
defaultSearchOperator: 'AND',
|
||||
...options,
|
||||
...tagsToFindOptions(query.tags),
|
||||
};
|
||||
};
|
||||
|
||||
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
|
||||
if (!ctx.requestHandlerContext) {
|
||||
|
@ -47,16 +48,6 @@ type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
|
|||
references: SavedObjectReference[] | undefined;
|
||||
};
|
||||
|
||||
function savedObjectToLensSavedObject(
|
||||
savedObject: SavedObject<LensSavedObjectAttributes>,
|
||||
partial: false
|
||||
): LensSavedObject;
|
||||
|
||||
function savedObjectToLensSavedObject(
|
||||
savedObject: PartialSavedObject<LensSavedObjectAttributes>,
|
||||
partial: true
|
||||
): PartialLensSavedObject;
|
||||
|
||||
function savedObjectToLensSavedObject(
|
||||
savedObject:
|
||||
| SavedObject<LensSavedObjectAttributes>
|
||||
|
@ -90,117 +81,27 @@ function savedObjectToLensSavedObject(
|
|||
};
|
||||
}
|
||||
|
||||
const SO_TYPE: LensContentType = 'lens';
|
||||
|
||||
export class LensStorage implements ContentStorage<LensSavedObject, PartialLensSavedObject> {
|
||||
mSearch: GetMSearchType<LensSavedObject>;
|
||||
export class LensStorage extends SOContentStorage<LensCrudTypes> {
|
||||
constructor() {
|
||||
this.mSearch = getMSearch<LensSavedObject, LensSearchOut>({
|
||||
savedObjectType: SO_TYPE,
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
searchArgsToSOFindOptions,
|
||||
enableMSearch: true,
|
||||
allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'],
|
||||
});
|
||||
}
|
||||
|
||||
async get(ctx: StorageContext, id: string): Promise<LensGetOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
version: { request: requestVersion },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition, requestVersion);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Save data in DB
|
||||
const {
|
||||
saved_object: savedObject,
|
||||
alias_purpose: aliasPurpose,
|
||||
alias_target_id: aliasTargetId,
|
||||
outcome,
|
||||
} = await soClient.resolve<LensSavedObjectAttributes>(SO_TYPE, id);
|
||||
|
||||
const response: LensGetOut = {
|
||||
item: savedObjectToLensSavedObject(savedObject, false),
|
||||
meta: {
|
||||
aliasPurpose,
|
||||
aliasTargetId,
|
||||
outcome,
|
||||
},
|
||||
};
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.get.out.result.down<LensGetOut, LensGetOut>(
|
||||
response
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async bulkGet(): Promise<never> {
|
||||
// Not implemented. Lens does not use bulkGet
|
||||
throw new Error(`[bulkGet] has not been implemented. See LensStorage class.`);
|
||||
}
|
||||
|
||||
async create(
|
||||
ctx: StorageContext,
|
||||
data: LensCreateIn['data'],
|
||||
options: CreateOptions
|
||||
): Promise<LensCreateOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
version: { request: requestVersion },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition, requestVersion);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
|
||||
LensSavedObjectAttributes,
|
||||
LensSavedObjectAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
|
||||
CreateOptions,
|
||||
CreateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
// Save data in DB
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
const savedObject = await soClient.create<LensSavedObjectAttributes>(
|
||||
SO_TYPE,
|
||||
dataToLatest,
|
||||
optionsToLatest
|
||||
);
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.create.out.result.down<
|
||||
LensCreateOut,
|
||||
LensCreateOut
|
||||
>({
|
||||
item: savedObjectToLensSavedObject(savedObject, false),
|
||||
});
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lens requires a custom update function because of https://github.com/elastic/kibana/issues/160116
|
||||
* where a forced create with overwrite flag is used instead of regular update
|
||||
*/
|
||||
async update(
|
||||
ctx: StorageContext,
|
||||
id: string,
|
||||
data: LensUpdateIn['data'],
|
||||
options: UpdateOptions
|
||||
): Promise<LensUpdateOut> {
|
||||
data: LensCrudTypes['UpdateIn']['data'],
|
||||
options: LensCrudTypes['UpdateOptions']
|
||||
): Promise<LensCrudTypes['UpdateOut']> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
version: { request: requestVersion },
|
||||
|
@ -217,8 +118,8 @@ export class LensStorage implements ContentStorage<LensSavedObject, PartialLensS
|
|||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
|
||||
CreateOptions,
|
||||
CreateOptions
|
||||
LensCrudTypes['CreateOptions'],
|
||||
LensCrudTypes['CreateOptions']
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
|
@ -227,7 +128,7 @@ export class LensStorage implements ContentStorage<LensSavedObject, PartialLensS
|
|||
// Save data in DB
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
const savedObject = await soClient.create<LensSavedObjectAttributes>(SO_TYPE, dataToLatest, {
|
||||
const savedObject = await soClient.create<LensSavedObjectAttributes>(CONTENT_ID, dataToLatest, {
|
||||
id,
|
||||
overwrite: true,
|
||||
...optionsToLatest,
|
||||
|
@ -235,85 +136,10 @@ export class LensStorage implements ContentStorage<LensSavedObject, PartialLensS
|
|||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.update.out.result.down<
|
||||
LensUpdateOut,
|
||||
LensUpdateOut
|
||||
LensCrudTypes['UpdateOut'],
|
||||
LensCrudTypes['UpdateOut']
|
||||
>({
|
||||
item: savedObjectToLensSavedObject(savedObject, true),
|
||||
});
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async delete(ctx: StorageContext, id: string): Promise<LensDeleteOut> {
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
await soClient.delete(SO_TYPE, id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async search(
|
||||
ctx: StorageContext,
|
||||
query: SearchQuery,
|
||||
options: LensSearchQuery = {}
|
||||
): Promise<LensSearchOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
version: { request: requestVersion },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition, requestVersion);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate and UP transform the options
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
|
||||
LensSearchQuery,
|
||||
LensSearchQuery
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
|
||||
}
|
||||
const { searchFields = ['title^3', 'description'], types = [CONTENT_ID] } = optionsToLatest;
|
||||
|
||||
const { included, excluded } = query.tags ?? {};
|
||||
const hasReference: SavedObjectsFindOptions['hasReference'] = included
|
||||
? included.map((id) => ({
|
||||
id,
|
||||
type: 'tag',
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
const hasNoReference: SavedObjectsFindOptions['hasNoReference'] = excluded
|
||||
? excluded.map((id) => ({
|
||||
id,
|
||||
type: 'tag',
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
const soQuery: SavedObjectsFindOptions = {
|
||||
type: types,
|
||||
search: query.text,
|
||||
perPage: query.limit,
|
||||
page: query.cursor ? Number(query.cursor) : undefined,
|
||||
defaultSearchOperator: 'AND',
|
||||
searchFields,
|
||||
hasReference,
|
||||
hasNoReference,
|
||||
};
|
||||
|
||||
// Execute the query in the DB
|
||||
const response = await soClient.find<LensSavedObjectAttributes>(soQuery);
|
||||
|
||||
// Validate the response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.search.out.result.down<
|
||||
LensSearchOut,
|
||||
LensSearchOut
|
||||
>({
|
||||
hits: response.saved_objects.map((so) => savedObjectToLensSavedObject(so, false)),
|
||||
pagination: {
|
||||
total: response.total,
|
||||
},
|
||||
item: savedObjectToLensSavedObject(savedObject),
|
||||
});
|
||||
|
||||
if (resultError) {
|
||||
|
|
|
@ -67,7 +67,7 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
update: {
|
||||
in: {
|
||||
options: {
|
||||
schema: createOptionsSchema, // same schema as "create"
|
||||
schema: createOptionsSchema, // same as create
|
||||
},
|
||||
data: {
|
||||
schema: mapAttributesSchema,
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { OverlayStart } from '@kbn/core/public';
|
||||
|
||||
import { mapsClient } from './maps_client';
|
||||
import type { MapAttributes } from '../../common/content_management';
|
||||
import { getMapClient } from './maps_client';
|
||||
|
||||
const rejectErrorMessage = i18n.translate('xpack.maps.saveDuplicateRejectedDescription', {
|
||||
defaultMessage: 'Save with duplicate title confirmation was rejected',
|
||||
|
@ -48,7 +49,7 @@ export const checkForDuplicateTitle = async (
|
|||
return true;
|
||||
}
|
||||
|
||||
const { hits } = await mapsClient.search(
|
||||
const { hits } = await getMapClient<MapAttributes>().search(
|
||||
{
|
||||
text: `"${title}"`,
|
||||
limit: 10,
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { mapsClient } from './maps_client';
|
||||
export { getMapClient } from './maps_client';
|
||||
|
||||
export { checkForDuplicateTitle } from './duplicate_title_check';
|
||||
|
|
|
@ -5,62 +5,65 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type {
|
||||
SerializableAttributes,
|
||||
VisualizationClient,
|
||||
} from '@kbn/visualizations-plugin/public/vis_types/vis_type_alias_registry';
|
||||
|
||||
import type { MapCrudTypes } from '../../common/content_management';
|
||||
import { CONTENT_ID as contentTypeId } from '../../common/content_management';
|
||||
import { getContentManagement } from '../kibana_services';
|
||||
|
||||
const get = async (id: string) => {
|
||||
return getContentManagement().client.get<MapCrudTypes['GetIn'], MapCrudTypes['GetOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
});
|
||||
};
|
||||
export function getMapClient<Attr extends SerializableAttributes = SerializableAttributes>(
|
||||
cm: ContentManagementPublicStart = getContentManagement()
|
||||
): VisualizationClient<'map', Attr> {
|
||||
const get = async (id: string) => {
|
||||
return cm.client.get<MapCrudTypes['GetIn'], MapCrudTypes['GetOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const create = async ({ data, options }: Omit<MapCrudTypes['CreateIn'], 'contentTypeId'>) => {
|
||||
const res = await getContentManagement().client.create<
|
||||
MapCrudTypes['CreateIn'],
|
||||
MapCrudTypes['CreateOut']
|
||||
>({
|
||||
contentTypeId,
|
||||
data,
|
||||
options,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
const create = async ({ data, options }: Omit<MapCrudTypes['CreateIn'], 'contentTypeId'>) => {
|
||||
const res = await cm.client.create<MapCrudTypes['CreateIn'], MapCrudTypes['CreateOut']>({
|
||||
contentTypeId,
|
||||
data,
|
||||
options,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
const update = async ({ id, data, options }: Omit<MapCrudTypes['UpdateIn'], 'contentTypeId'>) => {
|
||||
const res = await getContentManagement().client.update<
|
||||
MapCrudTypes['UpdateIn'],
|
||||
MapCrudTypes['UpdateOut']
|
||||
>({
|
||||
contentTypeId,
|
||||
id,
|
||||
data,
|
||||
options,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
const update = async ({ id, data, options }: Omit<MapCrudTypes['UpdateIn'], 'contentTypeId'>) => {
|
||||
const res = await cm.client.update<MapCrudTypes['UpdateIn'], MapCrudTypes['UpdateOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
data,
|
||||
options,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
const deleteMap = async (id: string) => {
|
||||
await getContentManagement().client.delete<MapCrudTypes['DeleteIn'], MapCrudTypes['DeleteOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
});
|
||||
};
|
||||
const deleteMap = async (id: string) => {
|
||||
return await cm.client.delete<MapCrudTypes['DeleteIn'], MapCrudTypes['DeleteOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const search = async (query: SearchQuery = {}, options?: MapCrudTypes['SearchOptions']) => {
|
||||
return getContentManagement().client.search<MapCrudTypes['SearchIn'], MapCrudTypes['SearchOut']>({
|
||||
contentTypeId,
|
||||
query,
|
||||
options,
|
||||
});
|
||||
};
|
||||
const search = async (query: SearchQuery = {}, options?: MapCrudTypes['SearchOptions']) => {
|
||||
return cm.client.search<MapCrudTypes['SearchIn'], MapCrudTypes['SearchOut']>({
|
||||
contentTypeId,
|
||||
query,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
export const mapsClient = {
|
||||
get,
|
||||
create,
|
||||
update,
|
||||
delete: deleteMap,
|
||||
search,
|
||||
};
|
||||
return {
|
||||
get,
|
||||
create,
|
||||
update,
|
||||
delete: deleteMap,
|
||||
search,
|
||||
} as unknown as VisualizationClient<'map', Attr>;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { MapAttributes } from '../common/content_management';
|
|||
import { MAP_EMBEDDABLE_NAME, MAP_SAVED_OBJECT_TYPE } from '../common/constants';
|
||||
import { getCoreOverlays, getEmbeddableService } from './kibana_services';
|
||||
import { extractReferences, injectReferences } from '../common/migrations/references';
|
||||
import { mapsClient, checkForDuplicateTitle } from './content_management';
|
||||
import { getMapClient, checkForDuplicateTitle } from './content_management';
|
||||
import { MapByValueInput, MapByReferenceInput } from './embeddable/types';
|
||||
|
||||
export interface SharingSavedObjectProps {
|
||||
|
@ -65,8 +65,12 @@ export function getMapAttributeService(): MapAttributeService {
|
|||
const {
|
||||
item: { id },
|
||||
} = await (savedObjectId
|
||||
? mapsClient.update({ id: savedObjectId, data: updatedAttributes, options: { references } })
|
||||
: mapsClient.create({ data: updatedAttributes, options: { references } }));
|
||||
? getMapClient().update({
|
||||
id: savedObjectId,
|
||||
data: updatedAttributes,
|
||||
options: { references },
|
||||
})
|
||||
: getMapClient().create({ data: updatedAttributes, options: { references } }));
|
||||
return { id };
|
||||
},
|
||||
unwrapMethod: async (
|
||||
|
@ -78,7 +82,7 @@ export function getMapAttributeService(): MapAttributeService {
|
|||
const {
|
||||
item: savedObject,
|
||||
meta: { outcome, aliasPurpose, aliasTargetId },
|
||||
} = await mapsClient.get(savedObjectId);
|
||||
} = await getMapClient<MapAttributes>().get(savedObjectId);
|
||||
|
||||
if (savedObject.error) {
|
||||
throw savedObject.error;
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
MAP_PATH,
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
} from '../common/constants';
|
||||
import { getMapClient } from './content_management';
|
||||
|
||||
export function getMapsVisTypeAlias() {
|
||||
const appDescription = i18n.translate('xpack.maps.visTypeAlias.description', {
|
||||
|
@ -34,6 +35,7 @@ export function getMapsVisTypeAlias() {
|
|||
visualizations: {
|
||||
docTypes: [MAP_SAVED_OBJECT_TYPE],
|
||||
searchFields: ['title^3'],
|
||||
client: getMapClient,
|
||||
toListItem(mapItem: MapItem) {
|
||||
const { id, type, updatedAt, attributes } = mapItem;
|
||||
const { title, description } = attributes;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
|
|||
import { ScopedHistory } from '@kbn/core/public';
|
||||
import { MapsListView } from './maps_list_view';
|
||||
import { APP_ID } from '../../../common/constants';
|
||||
import { mapsClient } from '../../content_management';
|
||||
import { getMapClient } from '../../content_management';
|
||||
|
||||
interface Props {
|
||||
history: ScopedHistory;
|
||||
|
@ -26,7 +26,7 @@ export function LoadListAndRender(props: Props) {
|
|||
props.stateTransfer.clearEditorState(APP_ID);
|
||||
|
||||
let ignore = false;
|
||||
mapsClient
|
||||
getMapClient()
|
||||
.search({ limit: 1 })
|
||||
.then((results) => {
|
||||
if (!ignore) {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { TableListView } from '@kbn/content-management-table-list-view';
|
||||
import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view';
|
||||
|
||||
import type { MapItem } from '../../../common/content_management';
|
||||
import type { MapAttributes, MapItem } from '../../../common/content_management';
|
||||
import { APP_ID, APP_NAME, getEditPath, MAP_PATH } from '../../../common/constants';
|
||||
import {
|
||||
getMapsCapabilities,
|
||||
|
@ -23,7 +23,7 @@ import {
|
|||
getUsageCollection,
|
||||
getServerless,
|
||||
} from '../../kibana_services';
|
||||
import { mapsClient } from '../../content_management';
|
||||
import { getMapClient } from '../../content_management';
|
||||
|
||||
const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit';
|
||||
const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage';
|
||||
|
@ -55,7 +55,7 @@ const toTableListViewSavedObject = (mapItem: MapItem): MapUserContent => {
|
|||
};
|
||||
|
||||
async function deleteMaps(items: Array<{ id: string }>) {
|
||||
await Promise.all(items.map(({ id }) => mapsClient.delete(id)));
|
||||
await Promise.all(items.map(({ id }) => getMapClient().delete(id)));
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -97,7 +97,7 @@ function MapsListViewComp({ history }: Props) {
|
|||
referencesToExclude?: SavedObjectsFindOptionsReference[];
|
||||
} = {}
|
||||
) => {
|
||||
return mapsClient
|
||||
return getMapClient<MapAttributes>()
|
||||
.search({
|
||||
text: searchTerm ? `${searchTerm}*` : undefined,
|
||||
limit: getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING),
|
||||
|
|
|
@ -762,6 +762,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(hasVisualOptionsButton).to.be(false);
|
||||
});
|
||||
|
||||
it('should allow edit meta-data for Lens chart on listing page', async () => {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await listingTable.searchForItemWithName('Afancilenstest');
|
||||
await listingTable.inspectVisualization();
|
||||
await listingTable.editVisualizationDetails({
|
||||
title: 'Anewfancilenstest',
|
||||
description: 'new description',
|
||||
});
|
||||
await listingTable.searchForItemWithName('Anewfancilenstest');
|
||||
await listingTable.expectItemsCount('visualize', 1);
|
||||
});
|
||||
|
||||
it('should correctly optimize multiple percentile metrics', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
|
|
|
@ -9,7 +9,7 @@ import expect from '@kbn/expect';
|
|||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const PageObjects = getPageObjects(['visualize', 'header', 'maps']);
|
||||
|
||||
const listingTable = getService('listingTable');
|
||||
const security = getService('security');
|
||||
|
||||
describe('visualize create menu', () => {
|
||||
|
@ -85,5 +85,37 @@ export default function ({ getService, getPageObjects }) {
|
|||
expect(hasLegecyViz).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('edit meta-data', () => {
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(
|
||||
['global_maps_all', 'global_visualize_all', 'test_logstash_reader'],
|
||||
{
|
||||
skipBrowserRefresh: true,
|
||||
}
|
||||
);
|
||||
|
||||
await PageObjects.visualize.navigateToNewAggBasedVisualization();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.testUser.restoreDefaults();
|
||||
});
|
||||
|
||||
it('should allow to change meta-data on a map visualization', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickMapsApp();
|
||||
await PageObjects.maps.waitForLayersToLoad();
|
||||
await PageObjects.maps.saveMap('myTestMap');
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await listingTable.searchForItemWithName('myTestMap');
|
||||
await listingTable.inspectVisualization();
|
||||
await listingTable.editVisualizationDetails({
|
||||
title: 'AnotherTestMap',
|
||||
description: 'new description',
|
||||
});
|
||||
await listingTable.searchForItemWithName('AnotherTestMap');
|
||||
await listingTable.expectItemsCount('visualize', 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue