mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Visualization] Get rid of saved object loader and use savedObjectClient resolve (#113121)
* First step: create saved_visualize_utils, starting use new get/save methods * Use new util methods in embeddable * move findListItem in utils * some clean up * clean up * Some fixes * Fix saved object tags * Some types fixes * Fix unit tests * Clean up code * Add unit tests for new utils * Fix lint * Fix tagging * Add unit tests * Some fixes * Clean up code * Fix lint * Fix types * put new methods in start contract * Fix imports * Fix lint * Fix comments * Fix lint * Fix CI * use local url instead of full path * Fix unit test * Some clean up * Fix nits * fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2dece3d446
commit
41c813bac4
31 changed files with 1291 additions and 124 deletions
|
@ -11,7 +11,7 @@
|
|||
"inspector",
|
||||
"savedObjects"
|
||||
],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"optionalPlugins": ["usageCollection", "spaces", "savedObjectsTaggingOss"],
|
||||
"requiredBundles": ["kibanaUtils", "discover"],
|
||||
"extraPublicDirs": ["common/constants", "common/prepare_log_table", "common/expression_functions"],
|
||||
"owner": {
|
||||
|
|
|
@ -20,16 +20,10 @@ import {
|
|||
AttributeService,
|
||||
} from '../../../../plugins/embeddable/public';
|
||||
import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
|
||||
import {
|
||||
getSavedVisualizationsLoader,
|
||||
getUISettings,
|
||||
getHttp,
|
||||
getTimeFilter,
|
||||
getCapabilities,
|
||||
} from '../services';
|
||||
import { getUISettings, getHttp, getTimeFilter, getCapabilities } from '../services';
|
||||
import { urlFor } from '../utils/saved_visualize_utils';
|
||||
import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory';
|
||||
import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants';
|
||||
import { SavedVisualizationsLoader } from '../saved_visualizations';
|
||||
import { IndexPattern } from '../../../data/public';
|
||||
import { createVisualizeEmbeddableAsync } from './visualize_embeddable_async';
|
||||
|
||||
|
@ -38,7 +32,6 @@ export const createVisEmbeddableFromObject =
|
|||
async (
|
||||
vis: Vis,
|
||||
input: Partial<VisualizeInput> & { id: string },
|
||||
savedVisualizationsLoader?: SavedVisualizationsLoader,
|
||||
attributeService?: AttributeService<
|
||||
VisualizeSavedObjectAttributes,
|
||||
VisualizeByValueInput,
|
||||
|
@ -46,16 +39,12 @@ export const createVisEmbeddableFromObject =
|
|||
>,
|
||||
parent?: IContainer
|
||||
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> => {
|
||||
const savedVisualizations = getSavedVisualizationsLoader();
|
||||
|
||||
try {
|
||||
const visId = vis.id as string;
|
||||
|
||||
const editPath = visId ? savedVisualizations.urlFor(visId) : '#/edit_by_value';
|
||||
const editPath = visId ? urlFor(visId) : '#/edit_by_value';
|
||||
|
||||
const editUrl = visId
|
||||
? getHttp().basePath.prepend(`/app/visualize${savedVisualizations.urlFor(visId)}`)
|
||||
: '';
|
||||
const editUrl = visId ? getHttp().basePath.prepend(`/app/visualize${urlFor(visId)}`) : '';
|
||||
const isLabsEnabled = getUISettings().get<boolean>(VISUALIZE_ENABLE_LABS_SETTING);
|
||||
|
||||
if (!isLabsEnabled && vis.type.stage === 'experimental') {
|
||||
|
@ -87,7 +76,6 @@ export const createVisEmbeddableFromObject =
|
|||
},
|
||||
input,
|
||||
attributeService,
|
||||
savedVisualizationsLoader,
|
||||
parent
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
|
@ -39,7 +39,7 @@ import { getExpressions, getUiActions } from '../services';
|
|||
import { VIS_EVENT_TO_TRIGGER } from './events';
|
||||
import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory';
|
||||
import { SavedObjectAttributes } from '../../../../core/types';
|
||||
import { SavedVisualizationsLoader } from '../saved_visualizations';
|
||||
import { getSavedVisualization } from '../utils/saved_visualize_utils';
|
||||
import { VisSavedObject } from '../types';
|
||||
import { toExpressionAst } from './to_ast';
|
||||
|
||||
|
@ -108,7 +108,6 @@ export class VisualizeEmbeddable
|
|||
VisualizeByValueInput,
|
||||
VisualizeByReferenceInput
|
||||
>;
|
||||
private savedVisualizationsLoader?: SavedVisualizationsLoader;
|
||||
|
||||
constructor(
|
||||
timefilter: TimefilterContract,
|
||||
|
@ -119,7 +118,6 @@ export class VisualizeEmbeddable
|
|||
VisualizeByValueInput,
|
||||
VisualizeByReferenceInput
|
||||
>,
|
||||
savedVisualizationsLoader?: SavedVisualizationsLoader,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(
|
||||
|
@ -144,7 +142,6 @@ export class VisualizeEmbeddable
|
|||
this.vis.uiState.on('change', this.uiStateChangeHandler);
|
||||
this.vis.uiState.on('reload', this.reload);
|
||||
this.attributeService = attributeService;
|
||||
this.savedVisualizationsLoader = savedVisualizationsLoader;
|
||||
|
||||
if (this.attributeService) {
|
||||
const isByValue = !this.inputIsRefType(initialInput);
|
||||
|
@ -455,7 +452,15 @@ export class VisualizeEmbeddable
|
|||
};
|
||||
|
||||
getInputAsRefType = async (): Promise<VisualizeByReferenceInput> => {
|
||||
const savedVis = await this.savedVisualizationsLoader?.get({});
|
||||
const { savedObjectsClient, data, spaces, savedObjectsTaggingOss } = await this.deps.start()
|
||||
.plugins;
|
||||
const savedVis = await getSavedVisualization({
|
||||
savedObjectsClient,
|
||||
search: data.search,
|
||||
dataViews: data.dataViews,
|
||||
spaces,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
});
|
||||
if (!savedVis) {
|
||||
throw new Error('Error creating a saved vis object');
|
||||
}
|
||||
|
|
|
@ -33,20 +33,20 @@ import type {
|
|||
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
|
||||
import type { SerializedVis, Vis } from '../vis';
|
||||
import { createVisAsync } from '../vis_async';
|
||||
import {
|
||||
getCapabilities,
|
||||
getTypes,
|
||||
getUISettings,
|
||||
getSavedVisualizationsLoader,
|
||||
} from '../services';
|
||||
import { getCapabilities, getTypes, getUISettings } from '../services';
|
||||
import { showNewVisModal } from '../wizard';
|
||||
import { convertToSerializedVis } from '../saved_visualizations/_saved_vis';
|
||||
import {
|
||||
convertToSerializedVis,
|
||||
getSavedVisualization,
|
||||
saveVisualization,
|
||||
getFullPath,
|
||||
} from '../utils/saved_visualize_utils';
|
||||
import {
|
||||
extractControlsReferences,
|
||||
extractTimeSeriesReferences,
|
||||
injectTimeSeriesReferences,
|
||||
injectControlsReferences,
|
||||
} from '../saved_visualizations/saved_visualization_references';
|
||||
} from '../utils/saved_visualization_references';
|
||||
import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object';
|
||||
import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants';
|
||||
import { checkForDuplicateTitle } from '../../../saved_objects/public';
|
||||
|
@ -59,7 +59,15 @@ interface VisualizationAttributes extends SavedObjectAttributes {
|
|||
|
||||
export interface VisualizeEmbeddableFactoryDeps {
|
||||
start: StartServicesGetter<
|
||||
Pick<VisualizationsStartDeps, 'inspector' | 'embeddable' | 'savedObjectsClient'>
|
||||
Pick<
|
||||
VisualizationsStartDeps,
|
||||
| 'inspector'
|
||||
| 'embeddable'
|
||||
| 'savedObjectsClient'
|
||||
| 'data'
|
||||
| 'savedObjectsTaggingOss'
|
||||
| 'spaces'
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
|
@ -147,17 +155,36 @@ export class VisualizeEmbeddableFactory
|
|||
input: Partial<VisualizeInput> & { id: string },
|
||||
parent?: IContainer
|
||||
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> {
|
||||
const savedVisualizations = getSavedVisualizationsLoader();
|
||||
const startDeps = await this.deps.start();
|
||||
|
||||
try {
|
||||
const savedObject = await savedVisualizations.get(savedObjectId);
|
||||
const savedObject = await getSavedVisualization(
|
||||
{
|
||||
savedObjectsClient: startDeps.core.savedObjects.client,
|
||||
search: startDeps.plugins.data.search,
|
||||
dataViews: startDeps.plugins.data.dataViews,
|
||||
spaces: startDeps.plugins.spaces,
|
||||
savedObjectsTagging: startDeps.plugins.savedObjectsTaggingOss?.getTaggingApi(),
|
||||
},
|
||||
savedObjectId
|
||||
);
|
||||
|
||||
if (savedObject.sharingSavedObjectProps?.outcome === 'conflict') {
|
||||
return new ErrorEmbeddable(
|
||||
i18n.translate('visualizations.embeddable.legacyURLConflict.errorMessage', {
|
||||
defaultMessage: `This visualization has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`,
|
||||
values: { json: savedObject.sharingSavedObjectProps?.errorJSON },
|
||||
}),
|
||||
input,
|
||||
parent
|
||||
);
|
||||
}
|
||||
const visState = convertToSerializedVis(savedObject);
|
||||
const vis = await createVisAsync(savedObject.visState.type, visState);
|
||||
|
||||
return createVisEmbeddableFromObject(this.deps)(
|
||||
vis,
|
||||
input,
|
||||
savedVisualizations,
|
||||
await this.getAttributeService(),
|
||||
parent
|
||||
);
|
||||
|
@ -173,11 +200,9 @@ export class VisualizeEmbeddableFactory
|
|||
if (input.savedVis) {
|
||||
const visState = input.savedVis;
|
||||
const vis = await createVisAsync(visState.type, visState);
|
||||
const savedVisualizations = getSavedVisualizationsLoader();
|
||||
return createVisEmbeddableFromObject(this.deps)(
|
||||
vis,
|
||||
input,
|
||||
savedVisualizations,
|
||||
await this.getAttributeService(),
|
||||
parent
|
||||
);
|
||||
|
@ -201,9 +226,9 @@ export class VisualizeEmbeddableFactory
|
|||
confirmOverwrite: false,
|
||||
returnToOrigin: true,
|
||||
isTitleDuplicateConfirmed: true,
|
||||
copyOnSave: false,
|
||||
};
|
||||
savedVis.title = title;
|
||||
savedVis.copyOnSave = false;
|
||||
savedVis.description = '';
|
||||
savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields();
|
||||
const serializedVis = (visObj as unknown as Vis).serialize();
|
||||
|
@ -217,7 +242,12 @@ export class VisualizeEmbeddableFactory
|
|||
if (visObj) {
|
||||
savedVis.uiStateJSON = visObj?.uiState.toString();
|
||||
}
|
||||
const id = await savedVis.save(saveOptions);
|
||||
const { core, plugins } = await this.deps.start();
|
||||
const id = await saveVisualization(savedVis, saveOptions, {
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
overlays: core.overlays,
|
||||
savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(),
|
||||
});
|
||||
if (!id || id === '') {
|
||||
throw new Error(
|
||||
i18n.translate('visualizations.savingVisualizationFailed.errorMsg', {
|
||||
|
@ -225,6 +255,7 @@ export class VisualizeEmbeddableFactory
|
|||
})
|
||||
);
|
||||
}
|
||||
core.chrome.recentlyAccessed.add(getFullPath(id), savedVis.title, String(id));
|
||||
return { id };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
|
|
@ -38,6 +38,7 @@ export {
|
|||
VisToExpressionAst,
|
||||
VisToExpressionAstParams,
|
||||
VisEditorOptionsProps,
|
||||
GetVisOptions,
|
||||
} from './types';
|
||||
export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry';
|
||||
export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
|
||||
|
@ -49,3 +50,4 @@ export {
|
|||
FakeParams,
|
||||
HistogramParams,
|
||||
} from '../common/expression_functions/xy_dimension';
|
||||
export { urlFor, getFullPath } from './utils/saved_visualize_utils';
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PluginInitializerContext } from '../../../core/public';
|
|||
import { Schema, VisualizationsSetup, VisualizationsStart } from './';
|
||||
import { Schemas } from './vis_types';
|
||||
import { VisualizationsPlugin } from './plugin';
|
||||
import { spacesPluginMock } from '../../../../x-pack/plugins/spaces/public/mocks';
|
||||
import { coreMock, applicationServiceMock } from '../../../core/public/mocks';
|
||||
import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks';
|
||||
import { expressionsPluginMock } from '../../../plugins/expressions/public/mocks';
|
||||
|
@ -18,6 +19,7 @@ import { usageCollectionPluginMock } from '../../../plugins/usage_collection/pub
|
|||
import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks';
|
||||
import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks';
|
||||
import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks';
|
||||
import { savedObjectTaggingOssPluginMock } from '../../saved_objects_tagging_oss/public/mocks';
|
||||
|
||||
const createSetupContract = (): VisualizationsSetup => ({
|
||||
createBaseVisualization: jest.fn(),
|
||||
|
@ -34,6 +36,9 @@ const createStartContract = (): VisualizationsStart => ({
|
|||
savedVisualizationsLoader: {
|
||||
get: jest.fn(),
|
||||
} as any,
|
||||
getSavedVisualization: jest.fn(),
|
||||
saveVisualization: jest.fn(),
|
||||
findListItems: jest.fn(),
|
||||
showNewVisModal: jest.fn(),
|
||||
createVis: jest.fn(),
|
||||
convertFromSerializedVis: jest.fn(),
|
||||
|
@ -61,9 +66,11 @@ const createInstance = async () => {
|
|||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
embeddable: embeddablePluginMock.createStartContract(),
|
||||
spaces: spacesPluginMock.createStartContract(),
|
||||
getAttributeService: jest.fn(),
|
||||
savedObjectsClient: coreMock.createStart().savedObjects.client,
|
||||
savedObjects: savedObjectsPluginMock.createStartContract(),
|
||||
savedObjectsTaggingOss: savedObjectTaggingOssPluginMock.createStart(),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsFindOptionsReference } from 'kibana/public';
|
||||
import {
|
||||
setUISettings,
|
||||
setTypes,
|
||||
|
@ -30,6 +32,7 @@ import {
|
|||
VisualizeEmbeddableFactory,
|
||||
createVisEmbeddableFromObject,
|
||||
} from './embeddable';
|
||||
import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public';
|
||||
import { TypesService } from './vis_types/types_service';
|
||||
import { range as rangeExpressionFunction } from '../common/expression_functions/range';
|
||||
import { visDimension as visDimensionExpressionFunction } from '../common/expression_functions/vis_dimension';
|
||||
|
@ -43,7 +46,10 @@ import { showNewVisModal } from './wizard';
|
|||
import {
|
||||
convertFromSerializedVis,
|
||||
convertToSerializedVis,
|
||||
} from './saved_visualizations/_saved_vis';
|
||||
getSavedVisualization,
|
||||
saveVisualization,
|
||||
findListItems,
|
||||
} from './utils/saved_visualize_utils';
|
||||
|
||||
import { createSavedSearchesLoader } from '../../discover/public';
|
||||
|
||||
|
@ -66,7 +72,9 @@ import type {
|
|||
import type { DataPublicPluginSetup, DataPublicPluginStart } from '../../../plugins/data/public';
|
||||
import type { ExpressionsSetup, ExpressionsStart } from '../../expressions/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public';
|
||||
import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public';
|
||||
import { createVisAsync } from './vis_async';
|
||||
import type { VisSavedObject, SaveVisOptions, GetVisOptions } from './types';
|
||||
|
||||
/**
|
||||
* Interface for this plugin's returned setup/start contracts.
|
||||
|
@ -82,6 +90,13 @@ export interface VisualizationsStart extends TypesStart {
|
|||
convertToSerializedVis: typeof convertToSerializedVis;
|
||||
convertFromSerializedVis: typeof convertFromSerializedVis;
|
||||
showNewVisModal: typeof showNewVisModal;
|
||||
getSavedVisualization: (opts?: GetVisOptions | string) => Promise<VisSavedObject>;
|
||||
saveVisualization: (savedVis: VisSavedObject, saveOptions: SaveVisOptions) => Promise<string>;
|
||||
findListItems: (
|
||||
searchTerm: string,
|
||||
listingLimit: number,
|
||||
references?: SavedObjectsFindOptionsReference[]
|
||||
) => Promise<{ hits: Array<Record<string, unknown>>; total: number }>;
|
||||
__LEGACY: { createVisEmbeddableFromObject: ReturnType<typeof createVisEmbeddableFromObject> };
|
||||
}
|
||||
|
||||
|
@ -103,6 +118,8 @@ export interface VisualizationsStartDeps {
|
|||
getAttributeService: EmbeddableStart['getAttributeService'];
|
||||
savedObjects: SavedObjectsStart;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
spaces?: SpacesPluginStart;
|
||||
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -149,7 +166,15 @@ export class VisualizationsPlugin
|
|||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{ data, expressions, uiActions, embeddable, savedObjects }: VisualizationsStartDeps
|
||||
{
|
||||
data,
|
||||
expressions,
|
||||
uiActions,
|
||||
embeddable,
|
||||
savedObjects,
|
||||
spaces,
|
||||
savedObjectsTaggingOss,
|
||||
}: VisualizationsStartDeps
|
||||
): VisualizationsStart {
|
||||
const types = this.types.start();
|
||||
setTypes(types);
|
||||
|
@ -181,6 +206,28 @@ export class VisualizationsPlugin
|
|||
return {
|
||||
...types,
|
||||
showNewVisModal,
|
||||
getSavedVisualization: async (opts) => {
|
||||
return getSavedVisualization(
|
||||
{
|
||||
search: data.search,
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
dataViews: data.dataViews,
|
||||
spaces,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
},
|
||||
opts
|
||||
);
|
||||
},
|
||||
saveVisualization: async (savedVis, saveOptions) => {
|
||||
return saveVisualization(savedVis, saveOptions, {
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
overlays: core.overlays,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
});
|
||||
},
|
||||
findListItems: async (searchTerm, listingLimit, references) => {
|
||||
return findListItems(core.savedObjects.client, types, searchTerm, listingLimit, references);
|
||||
},
|
||||
/**
|
||||
* creates new instance of Vis
|
||||
* @param {IndexPattern} indexPattern - index pattern to use
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
import type { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public';
|
||||
// @ts-ignore
|
||||
import { updateOldState } from '../legacy/vis_update_state';
|
||||
import { extractReferences, injectReferences } from './saved_visualization_references';
|
||||
import { extractReferences, injectReferences } from '../utils/saved_visualization_references';
|
||||
import { createSavedSearchesLoader } from '../../../discover/public';
|
||||
import type { SavedObjectsClientContract } from '../../../../core/public';
|
||||
import type { IndexPatternsContract } from '../../../../plugins/data/public';
|
||||
import type { ISavedVis, SerializedVis } from '../types';
|
||||
import type { ISavedVis } from '../types';
|
||||
|
||||
export interface SavedVisServices {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
|
@ -28,43 +28,7 @@ export interface SavedVisServices {
|
|||
indexPatterns: IndexPatternsContract;
|
||||
}
|
||||
|
||||
export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => {
|
||||
const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis;
|
||||
|
||||
const aggs = searchSourceFields && searchSourceFields.index ? visState.aggs || [] : visState.aggs;
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
type: visState.type,
|
||||
description,
|
||||
params: visState.params,
|
||||
uiState: JSON.parse(uiStateJSON || '{}'),
|
||||
data: {
|
||||
aggs,
|
||||
searchSource: searchSourceFields!,
|
||||
savedSearchId: savedVis.savedSearchId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => {
|
||||
return {
|
||||
id: vis.id,
|
||||
title: vis.title,
|
||||
description: vis.description,
|
||||
visState: {
|
||||
title: vis.title,
|
||||
type: vis.type,
|
||||
aggs: vis.data.aggs,
|
||||
params: vis.params,
|
||||
},
|
||||
uiStateJSON: JSON.stringify(vis.uiState),
|
||||
searchSourceFields: vis.data.searchSource,
|
||||
savedSearchId: vis.data.savedSearchId,
|
||||
};
|
||||
};
|
||||
|
||||
/** @deprecated **/
|
||||
export function createSavedVisClass(services: SavedVisServices) {
|
||||
const savedSearch = createSavedSearchesLoader(services);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface FindListItemsOptions {
|
|||
references?: SavedObjectsFindOptionsReference[];
|
||||
}
|
||||
|
||||
/** @deprecated **/
|
||||
export function createSavedVisLoader(services: SavedVisServicesWithVisualizations) {
|
||||
const { savedObjectsClient, visualizationTypes } = services;
|
||||
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SavedObject } from '../../../plugins/saved_objects/public';
|
||||
import type { SavedObjectsMigrationVersion } from 'kibana/public';
|
||||
import {
|
||||
IAggConfigs,
|
||||
SearchSourceFields,
|
||||
TimefilterContract,
|
||||
AggConfigSerialized,
|
||||
} from '../../../plugins/data/public';
|
||||
import type { ISearchSource } from '../../data/common';
|
||||
import { ExpressionAstExpression } from '../../expressions/public';
|
||||
|
||||
import type { SerializedVis, Vis } from './vis';
|
||||
|
@ -36,9 +37,39 @@ export interface ISavedVis {
|
|||
uiStateJSON?: string;
|
||||
savedSearchRefName?: string;
|
||||
savedSearchId?: string;
|
||||
sharingSavedObjectProps?: {
|
||||
outcome?: 'aliasMatch' | 'exactMatch' | 'conflict';
|
||||
aliasTargetId?: string;
|
||||
errorJSON?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VisSavedObject extends SavedObject, ISavedVis {}
|
||||
export interface VisSavedObject extends ISavedVis {
|
||||
lastSavedTitle: string;
|
||||
getEsType: () => string;
|
||||
getDisplayName?: () => string;
|
||||
displayName: string;
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
searchSource?: ISearchSource;
|
||||
version?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface SaveVisOptions {
|
||||
confirmOverwrite?: boolean;
|
||||
isTitleDuplicateConfirmed?: boolean;
|
||||
onTitleDuplicate?: () => void;
|
||||
copyOnSave?: boolean;
|
||||
}
|
||||
|
||||
export interface GetVisOptions {
|
||||
id?: string;
|
||||
searchSource?: boolean;
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
savedSearchId?: string;
|
||||
type?: string;
|
||||
indexPattern?: string;
|
||||
}
|
||||
|
||||
export interface VisToExpressionAstParams {
|
||||
timefilter: TimefilterContract;
|
||||
|
|
|
@ -0,0 +1,507 @@
|
|||
/*
|
||||
* 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 type { ISearchSource } from '../../../data/common';
|
||||
import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public';
|
||||
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { dataPluginMock } from '../../../data/public/mocks';
|
||||
import { SavedObjectsClientContract } from '../../../../core/public';
|
||||
import {
|
||||
findListItems,
|
||||
getSavedVisualization,
|
||||
saveVisualization,
|
||||
SAVED_VIS_TYPE,
|
||||
} from './saved_visualize_utils';
|
||||
import { VisTypeAlias, TypesStart } from '../vis_types';
|
||||
import type { VisSavedObject } from '../types';
|
||||
|
||||
let visTypes = [] as VisTypeAlias[];
|
||||
const mockGetAliases = jest.fn(() => visTypes);
|
||||
const mockGetTypes = jest.fn((type: string) => type) as unknown as TypesStart['get'];
|
||||
jest.mock('../services', () => ({
|
||||
getSpaces: jest.fn(() => ({
|
||||
getActiveSpace: () => ({
|
||||
id: 'test',
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockParseSearchSourceJSON = jest.fn();
|
||||
const mockInjectSearchSourceReferences = jest.fn();
|
||||
const mockExtractSearchSourceReferences = jest.fn((...args) => [{}, []]);
|
||||
|
||||
jest.mock('../../../../plugins/data/public', () => ({
|
||||
extractSearchSourceReferences: jest.fn((...args) => mockExtractSearchSourceReferences(...args)),
|
||||
injectSearchSourceReferences: jest.fn((...args) => mockInjectSearchSourceReferences(...args)),
|
||||
parseSearchSourceJSON: jest.fn((...args) => mockParseSearchSourceJSON(...args)),
|
||||
}));
|
||||
|
||||
const mockInjectReferences = jest.fn();
|
||||
const mockExtractReferences = jest.fn(() => ({ references: [], attributes: {} }));
|
||||
jest.mock('./saved_visualization_references', () => ({
|
||||
injectReferences: jest.fn((...args) => mockInjectReferences(...args)),
|
||||
extractReferences: jest.fn(() => mockExtractReferences()),
|
||||
}));
|
||||
|
||||
let isTitleDuplicateConfirmed = true;
|
||||
const mockCheckForDuplicateTitle = jest.fn(() => {
|
||||
if (!isTitleDuplicateConfirmed) {
|
||||
throw new Error();
|
||||
}
|
||||
});
|
||||
const mockSaveWithConfirmation = jest.fn(() => ({ id: 'test-after-confirm' }));
|
||||
jest.mock('../../../../plugins/saved_objects/public', () => ({
|
||||
checkForDuplicateTitle: jest.fn(() => mockCheckForDuplicateTitle()),
|
||||
saveWithConfirmation: jest.fn(() => mockSaveWithConfirmation()),
|
||||
isErrorNonFatal: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('saved_visualize_utils', () => {
|
||||
const { overlays, savedObjects } = coreMock.createStart();
|
||||
const savedObjectsClient = savedObjects.client as jest.Mocked<SavedObjectsClientContract>;
|
||||
(savedObjectsClient.resolve as jest.Mock).mockImplementation(() => ({
|
||||
saved_object: {
|
||||
references: [
|
||||
{
|
||||
id: 'test',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
attributes: {
|
||||
visState: JSON.stringify({ type: 'area' }),
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{filter: []}',
|
||||
},
|
||||
},
|
||||
_version: '1',
|
||||
},
|
||||
outcome: 'exact',
|
||||
alias_target_id: null,
|
||||
}));
|
||||
(savedObjectsClient.create as jest.Mock).mockImplementation(() => ({ id: 'test' }));
|
||||
const { dataViews, search } = dataPluginMock.createStartContract();
|
||||
|
||||
describe('getSavedVisualization', () => {
|
||||
beforeEach(() => {
|
||||
mockParseSearchSourceJSON.mockClear();
|
||||
mockInjectSearchSourceReferences.mockClear();
|
||||
mockInjectReferences.mockClear();
|
||||
});
|
||||
it('should return object with defaults if was not provided id', async () => {
|
||||
const savedVis = await getSavedVisualization({
|
||||
savedObjectsClient,
|
||||
search,
|
||||
dataViews,
|
||||
spaces: Promise.resolve({
|
||||
getActiveSpace: () => ({
|
||||
id: 'test',
|
||||
}),
|
||||
}) as unknown as SpacesPluginStart,
|
||||
});
|
||||
expect(savedVis).toBeDefined();
|
||||
expect(savedVis.title).toBe('');
|
||||
expect(savedVis.displayName).toBe(SAVED_VIS_TYPE);
|
||||
});
|
||||
|
||||
it('should create search source if saved object has searchSourceJSON', async () => {
|
||||
await getSavedVisualization(
|
||||
{
|
||||
savedObjectsClient,
|
||||
search,
|
||||
dataViews,
|
||||
spaces: Promise.resolve({
|
||||
getActiveSpace: () => ({
|
||||
id: 'test',
|
||||
}),
|
||||
}) as unknown as SpacesPluginStart,
|
||||
},
|
||||
{ id: 'test', searchSource: true }
|
||||
);
|
||||
expect(mockParseSearchSourceJSON).toHaveBeenCalledWith('{filter: []}');
|
||||
expect(mockInjectSearchSourceReferences).toHaveBeenCalled();
|
||||
expect(search.searchSource.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should inject references if saved object has references', async () => {
|
||||
await getSavedVisualization(
|
||||
{
|
||||
savedObjectsClient,
|
||||
search,
|
||||
dataViews,
|
||||
spaces: Promise.resolve({
|
||||
getActiveSpace: () => ({
|
||||
id: 'test',
|
||||
}),
|
||||
}) as unknown as SpacesPluginStart,
|
||||
},
|
||||
{ id: 'test', searchSource: true }
|
||||
);
|
||||
expect(mockInjectReferences.mock.calls[0][1]).toEqual([
|
||||
{
|
||||
id: 'test',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call getTagIdsFromReferences if we provide savedObjectsTagging service', async () => {
|
||||
const mockGetTagIdsFromReferences = jest.fn(() => ['test']);
|
||||
await getSavedVisualization(
|
||||
{
|
||||
savedObjectsClient,
|
||||
search,
|
||||
dataViews,
|
||||
spaces: Promise.resolve({
|
||||
getActiveSpace: () => ({
|
||||
id: 'test',
|
||||
}),
|
||||
}) as unknown as SpacesPluginStart,
|
||||
savedObjectsTagging: {
|
||||
ui: {
|
||||
getTagIdsFromReferences: mockGetTagIdsFromReferences,
|
||||
},
|
||||
} as unknown as SavedObjectsTaggingApi,
|
||||
},
|
||||
{ id: 'test', searchSource: true }
|
||||
);
|
||||
expect(mockGetTagIdsFromReferences).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveVisualization', () => {
|
||||
let vis: VisSavedObject;
|
||||
beforeEach(() => {
|
||||
mockExtractSearchSourceReferences.mockClear();
|
||||
mockExtractReferences.mockClear();
|
||||
mockSaveWithConfirmation.mockClear();
|
||||
savedObjectsClient.create.mockClear();
|
||||
vis = {
|
||||
visState: {
|
||||
type: 'area',
|
||||
},
|
||||
title: 'test',
|
||||
uiStateJSON: '{}',
|
||||
version: '1',
|
||||
__tags: [],
|
||||
lastSavedTitle: 'test',
|
||||
displayName: 'test',
|
||||
getEsType: () => 'vis',
|
||||
} as unknown as VisSavedObject;
|
||||
});
|
||||
|
||||
it('should return id after save', async () => {
|
||||
const savedVisId = await saveVisualization(vis, {}, { savedObjectsClient, overlays });
|
||||
expect(savedObjectsClient.create).toHaveBeenCalled();
|
||||
expect(mockExtractReferences).toHaveBeenCalled();
|
||||
expect(savedVisId).toBe('test');
|
||||
});
|
||||
|
||||
it('should call extractSearchSourceReferences if we new vis has searchSourceFields', async () => {
|
||||
vis.searchSourceFields = { fields: [] };
|
||||
await saveVisualization(vis, {}, { savedObjectsClient, overlays });
|
||||
expect(mockExtractSearchSourceReferences).toHaveBeenCalledWith(vis.searchSourceFields);
|
||||
});
|
||||
|
||||
it('should serialize searchSource', async () => {
|
||||
vis.searchSource = {
|
||||
serialize: jest.fn(() => ({ searchSourceJSON: '{}', references: [] })),
|
||||
} as unknown as ISearchSource;
|
||||
await saveVisualization(vis, {}, { savedObjectsClient, overlays });
|
||||
expect(vis.searchSource?.serialize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call updateTagsReferences if we provide savedObjectsTagging service', async () => {
|
||||
const mockUpdateTagsReferences = jest.fn(() => []);
|
||||
await saveVisualization(
|
||||
vis,
|
||||
{},
|
||||
{
|
||||
savedObjectsClient,
|
||||
overlays,
|
||||
savedObjectsTagging: {
|
||||
ui: {
|
||||
updateTagsReferences: mockUpdateTagsReferences,
|
||||
},
|
||||
} as unknown as SavedObjectsTaggingApi,
|
||||
}
|
||||
);
|
||||
expect(mockUpdateTagsReferences).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('confirmOverwrite', () => {
|
||||
it('as false we should not call saveWithConfirmation and just do create', async () => {
|
||||
const savedVisId = await saveVisualization(
|
||||
vis,
|
||||
{ confirmOverwrite: false },
|
||||
{ savedObjectsClient, overlays }
|
||||
);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalled();
|
||||
expect(mockExtractReferences).toHaveBeenCalled();
|
||||
expect(mockSaveWithConfirmation).not.toHaveBeenCalled();
|
||||
expect(savedVisId).toBe('test');
|
||||
});
|
||||
|
||||
it('as true we should call saveWithConfirmation', async () => {
|
||||
const savedVisId = await saveVisualization(
|
||||
vis,
|
||||
{ confirmOverwrite: true },
|
||||
{ savedObjectsClient, overlays }
|
||||
);
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(mockSaveWithConfirmation).toHaveBeenCalled();
|
||||
expect(savedVisId).toBe('test-after-confirm');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTitleDuplicateConfirmed', () => {
|
||||
it('as false we should not save vis with duplicated title', async () => {
|
||||
isTitleDuplicateConfirmed = false;
|
||||
const savedVisId = await saveVisualization(
|
||||
vis,
|
||||
{ isTitleDuplicateConfirmed },
|
||||
{ savedObjectsClient, overlays }
|
||||
);
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(mockSaveWithConfirmation).not.toHaveBeenCalled();
|
||||
expect(mockCheckForDuplicateTitle).toHaveBeenCalled();
|
||||
expect(savedVisId).toBe('');
|
||||
expect(vis.id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('as true we should save vis with duplicated title', async () => {
|
||||
isTitleDuplicateConfirmed = true;
|
||||
const savedVisId = await saveVisualization(
|
||||
vis,
|
||||
{ isTitleDuplicateConfirmed },
|
||||
{ savedObjectsClient, overlays }
|
||||
);
|
||||
expect(mockCheckForDuplicateTitle).toHaveBeenCalled();
|
||||
expect(savedObjectsClient.create).toHaveBeenCalled();
|
||||
expect(savedVisId).toBe('test');
|
||||
expect(vis.id).toBe('test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findListItems', () => {
|
||||
function testProps() {
|
||||
(savedObjectsClient.find as jest.Mock).mockImplementation(() => ({
|
||||
total: 0,
|
||||
savedObjects: [],
|
||||
}));
|
||||
return {
|
||||
savedObjectsClient,
|
||||
search: '',
|
||||
size: 10,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.find.mockClear();
|
||||
});
|
||||
|
||||
it('searches visualization title and description', async () => {
|
||||
const props = testProps();
|
||||
const { find } = props.savedObjectsClient;
|
||||
await findListItems(
|
||||
props.savedObjectsClient,
|
||||
{ get: mockGetTypes, getAliases: mockGetAliases },
|
||||
props.search,
|
||||
props.size
|
||||
);
|
||||
expect(find.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
type: ['visualization'],
|
||||
searchFields: ['title^3', 'description'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('searches searchFields and types specified by app extensions', async () => {
|
||||
const props = testProps();
|
||||
visTypes = [
|
||||
{
|
||||
appExtensions: {
|
||||
visualizations: {
|
||||
docTypes: ['bazdoc', 'etc'],
|
||||
searchFields: ['baz', 'bing'],
|
||||
},
|
||||
},
|
||||
} as VisTypeAlias,
|
||||
];
|
||||
const { find } = props.savedObjectsClient;
|
||||
await findListItems(
|
||||
props.savedObjectsClient,
|
||||
{ get: mockGetTypes, getAliases: mockGetAliases },
|
||||
props.search,
|
||||
props.size
|
||||
);
|
||||
expect(find.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
type: ['bazdoc', 'etc', 'visualization'],
|
||||
searchFields: ['baz', 'bing', 'title^3', 'description'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('deduplicates types and search fields', async () => {
|
||||
const props = testProps();
|
||||
visTypes = [
|
||||
{
|
||||
appExtensions: {
|
||||
visualizations: {
|
||||
docTypes: ['bazdoc', 'bar'],
|
||||
searchFields: ['baz', 'bing', 'barfield'],
|
||||
},
|
||||
},
|
||||
} as VisTypeAlias,
|
||||
{
|
||||
appExtensions: {
|
||||
visualizations: {
|
||||
docTypes: ['visualization', 'foo', 'bazdoc'],
|
||||
searchFields: ['baz', 'bing', 'foofield'],
|
||||
},
|
||||
},
|
||||
} as VisTypeAlias,
|
||||
];
|
||||
const { find } = props.savedObjectsClient;
|
||||
await findListItems(
|
||||
props.savedObjectsClient,
|
||||
{ get: mockGetTypes, getAliases: mockGetAliases },
|
||||
props.search,
|
||||
props.size
|
||||
);
|
||||
expect(find.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
type: ['bazdoc', 'bar', 'visualization', 'foo'],
|
||||
searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('searches the search term prefix', async () => {
|
||||
const props = {
|
||||
...testProps(),
|
||||
search: 'ahoythere',
|
||||
};
|
||||
const { find } = props.savedObjectsClient;
|
||||
await findListItems(
|
||||
props.savedObjectsClient,
|
||||
{ get: mockGetTypes, getAliases: mockGetAliases },
|
||||
props.search,
|
||||
props.size
|
||||
);
|
||||
expect(find.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
search: 'ahoythere*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('searches with references', async () => {
|
||||
const props = {
|
||||
...testProps(),
|
||||
references: [
|
||||
{ type: 'foo', id: 'hello' },
|
||||
{ type: 'bar', id: 'dolly' },
|
||||
],
|
||||
};
|
||||
const { find } = props.savedObjectsClient;
|
||||
await findListItems(
|
||||
props.savedObjectsClient,
|
||||
{ get: mockGetTypes, getAliases: mockGetAliases },
|
||||
props.search,
|
||||
props.size,
|
||||
props.references
|
||||
);
|
||||
expect(find.mock.calls).toMatchObject([
|
||||
[
|
||||
{
|
||||
hasReference: [
|
||||
{ type: 'foo', id: 'hello' },
|
||||
{ type: 'bar', id: 'dolly' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses type-specific toListItem function, if available', async () => {
|
||||
const props = testProps();
|
||||
|
||||
visTypes = [
|
||||
{
|
||||
appExtensions: {
|
||||
visualizations: {
|
||||
docTypes: ['wizard'],
|
||||
toListItem(savedObject) {
|
||||
return {
|
||||
id: savedObject.id,
|
||||
title: `${(savedObject.attributes as { label: string }).label} THE GRAY`,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
} as VisTypeAlias,
|
||||
];
|
||||
(props.savedObjectsClient.find as jest.Mock).mockImplementationOnce(async () => ({
|
||||
total: 2,
|
||||
savedObjects: [
|
||||
{
|
||||
id: 'lotr',
|
||||
type: 'wizard',
|
||||
attributes: { label: 'Gandalf' },
|
||||
},
|
||||
{
|
||||
id: 'wat',
|
||||
type: 'visualization',
|
||||
attributes: { title: 'WATEVER', typeName: 'test' },
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const items = await findListItems(
|
||||
props.savedObjectsClient,
|
||||
{ get: mockGetTypes, getAliases: mockGetAliases },
|
||||
props.search,
|
||||
props.size
|
||||
);
|
||||
expect(items).toEqual({
|
||||
total: 2,
|
||||
hits: [
|
||||
{
|
||||
id: 'lotr',
|
||||
references: undefined,
|
||||
title: 'Gandalf THE GRAY',
|
||||
},
|
||||
{
|
||||
id: 'wat',
|
||||
references: undefined,
|
||||
icon: undefined,
|
||||
savedObjectType: 'visualization',
|
||||
editUrl: '/edit/wat',
|
||||
type: 'test',
|
||||
typeName: 'test',
|
||||
typeTitle: undefined,
|
||||
title: 'WATEVER',
|
||||
url: '#/edit/wat',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
403
src/plugins/visualizations/public/utils/saved_visualize_utils.ts
Normal file
403
src/plugins/visualizations/public/utils/saved_visualize_utils.ts
Normal file
|
@ -0,0 +1,403 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import type {
|
||||
SavedObjectsFindOptionsReference,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectAttributes,
|
||||
SavedObjectReference,
|
||||
} from 'kibana/public';
|
||||
import type { OverlayStart } from '../../../../core/public';
|
||||
import { SavedObjectNotFound } from '../../../kibana_utils/public';
|
||||
import {
|
||||
extractSearchSourceReferences,
|
||||
injectSearchSourceReferences,
|
||||
parseSearchSourceJSON,
|
||||
DataPublicPluginStart,
|
||||
} from '../../../../plugins/data/public';
|
||||
import {
|
||||
checkForDuplicateTitle,
|
||||
saveWithConfirmation,
|
||||
isErrorNonFatal,
|
||||
} from '../../../../plugins/saved_objects/public';
|
||||
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
|
||||
import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public';
|
||||
import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry';
|
||||
import type {
|
||||
VisSavedObject,
|
||||
SerializedVis,
|
||||
ISavedVis,
|
||||
SaveVisOptions,
|
||||
GetVisOptions,
|
||||
} from '../types';
|
||||
import type { TypesStart, BaseVisType } from '../vis_types';
|
||||
// @ts-ignore
|
||||
import { updateOldState } from '../legacy/vis_update_state';
|
||||
import { injectReferences, extractReferences } from './saved_visualization_references';
|
||||
|
||||
export const SAVED_VIS_TYPE = 'visualization';
|
||||
|
||||
const getDefaults = (opts: GetVisOptions) => ({
|
||||
title: '',
|
||||
visState: !opts.type ? null : { type: opts.type },
|
||||
uiStateJSON: '{}',
|
||||
description: '',
|
||||
savedSearchId: opts.savedSearchId,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
export function getFullPath(id: string) {
|
||||
return `/app/visualize#/edit/${id}`;
|
||||
}
|
||||
|
||||
export function urlFor(id: string) {
|
||||
return `#/edit/${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
export function mapHitSource(
|
||||
visTypes: Pick<TypesStart, 'get'>,
|
||||
{
|
||||
attributes,
|
||||
id,
|
||||
references,
|
||||
}: {
|
||||
attributes: SavedObjectAttributes;
|
||||
id: string;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
) {
|
||||
const newAttributes: {
|
||||
id: string;
|
||||
references: SavedObjectReference[];
|
||||
url: string;
|
||||
savedObjectType?: string;
|
||||
editUrl?: string;
|
||||
type?: BaseVisType;
|
||||
icon?: BaseVisType['icon'];
|
||||
image?: BaseVisType['image'];
|
||||
typeTitle?: BaseVisType['title'];
|
||||
error?: string;
|
||||
} = {
|
||||
id,
|
||||
references,
|
||||
url: urlFor(id),
|
||||
...attributes,
|
||||
};
|
||||
|
||||
let typeName = attributes.typeName;
|
||||
if (attributes.visState) {
|
||||
try {
|
||||
typeName = JSON.parse(String(attributes.visState)).type;
|
||||
} catch (e) {
|
||||
/* missing typename handled below */
|
||||
}
|
||||
}
|
||||
|
||||
if (!typeName || !visTypes.get(typeName as string)) {
|
||||
newAttributes.error = 'Unknown visualization type';
|
||||
return newAttributes;
|
||||
}
|
||||
|
||||
newAttributes.type = visTypes.get(typeName as string);
|
||||
newAttributes.savedObjectType = 'visualization';
|
||||
newAttributes.icon = newAttributes.type?.icon;
|
||||
newAttributes.image = newAttributes.type?.image;
|
||||
newAttributes.typeTitle = newAttributes.type?.title;
|
||||
newAttributes.editUrl = `/edit/${id}`;
|
||||
|
||||
return newAttributes;
|
||||
}
|
||||
|
||||
export const convertToSerializedVis = (savedVis: VisSavedObject): SerializedVis => {
|
||||
const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis;
|
||||
|
||||
const aggs = searchSourceFields && searchSourceFields.index ? visState.aggs || [] : visState.aggs;
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
type: visState.type,
|
||||
description,
|
||||
params: visState.params,
|
||||
uiState: JSON.parse(uiStateJSON || '{}'),
|
||||
data: {
|
||||
aggs,
|
||||
searchSource: searchSourceFields!,
|
||||
savedSearchId: savedVis.savedSearchId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => {
|
||||
return {
|
||||
id: vis.id,
|
||||
title: vis.title,
|
||||
description: vis.description,
|
||||
visState: {
|
||||
title: vis.title,
|
||||
type: vis.type,
|
||||
aggs: vis.data.aggs,
|
||||
params: vis.params,
|
||||
},
|
||||
uiStateJSON: JSON.stringify(vis.uiState),
|
||||
searchSourceFields: vis.data.searchSource,
|
||||
savedSearchId: vis.data.savedSearchId,
|
||||
};
|
||||
};
|
||||
|
||||
export async function findListItems(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
visTypes: Pick<TypesStart, 'get' | 'getAliases'>,
|
||||
search: string,
|
||||
size: number,
|
||||
references?: SavedObjectsFindOptionsReference[]
|
||||
) {
|
||||
const visAliases = visTypes.getAliases();
|
||||
const extensions = visAliases
|
||||
.map((v) => v.appExtensions?.visualizations)
|
||||
.filter(Boolean) as VisualizationsAppExtension[];
|
||||
const extensionByType = extensions.reduce((acc, m) => {
|
||||
return m!.docTypes.reduce((_acc, type) => {
|
||||
acc[type] = m;
|
||||
return acc;
|
||||
}, acc);
|
||||
}, {} as { [visType: string]: VisualizationsAppExtension });
|
||||
const searchOption = (field: string, ...defaults: string[]) =>
|
||||
_(extensions).map(field).concat(defaults).compact().flatten().uniq().value() as string[];
|
||||
const searchOptions: SavedObjectsFindOptions = {
|
||||
type: searchOption('docTypes', 'visualization'),
|
||||
searchFields: searchOption('searchFields', 'title^3', 'description'),
|
||||
search: search ? `${search}*` : undefined,
|
||||
perPage: size,
|
||||
page: 1,
|
||||
defaultSearchOperator: 'AND' as 'AND',
|
||||
hasReference: references,
|
||||
};
|
||||
|
||||
const { total, savedObjects } = await savedObjectsClient.find<SavedObjectAttributes>(
|
||||
searchOptions
|
||||
);
|
||||
|
||||
return {
|
||||
total,
|
||||
hits: savedObjects.map((savedObject) => {
|
||||
const config = extensionByType[savedObject.type];
|
||||
|
||||
if (config) {
|
||||
return {
|
||||
...config.toListItem(savedObject),
|
||||
references: savedObject.references,
|
||||
};
|
||||
} else {
|
||||
return mapHitSource(visTypes, savedObject);
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSavedVisualization(
|
||||
services: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
search: DataPublicPluginStart['search'];
|
||||
dataViews: DataPublicPluginStart['dataViews'];
|
||||
spaces?: SpacesPluginStart;
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||
},
|
||||
opts?: GetVisOptions | string
|
||||
): Promise<VisSavedObject> {
|
||||
if (typeof opts !== 'object') {
|
||||
opts = { id: opts } as GetVisOptions;
|
||||
}
|
||||
|
||||
const id = (opts.id as string) || '';
|
||||
const savedObject = {
|
||||
id,
|
||||
migrationVersion: opts.migrationVersion,
|
||||
displayName: SAVED_VIS_TYPE,
|
||||
getEsType: () => SAVED_VIS_TYPE,
|
||||
getDisplayName: () => SAVED_VIS_TYPE,
|
||||
searchSource: opts.searchSource ? services.search.searchSource.createEmpty() : undefined,
|
||||
} as VisSavedObject;
|
||||
const defaultsProps = getDefaults(opts);
|
||||
|
||||
if (!id) {
|
||||
Object.assign(savedObject, defaultsProps);
|
||||
return savedObject;
|
||||
}
|
||||
|
||||
const {
|
||||
saved_object: resp,
|
||||
outcome,
|
||||
alias_target_id: aliasTargetId,
|
||||
} = await services.savedObjectsClient.resolve<SavedObjectAttributes>(SAVED_VIS_TYPE, id);
|
||||
|
||||
if (!resp._version) {
|
||||
throw new SavedObjectNotFound(SAVED_VIS_TYPE, id || '');
|
||||
}
|
||||
|
||||
const attributes = _.cloneDeep(resp.attributes);
|
||||
|
||||
if (attributes.visState && typeof attributes.visState === 'string') {
|
||||
attributes.visState = JSON.parse(attributes.visState);
|
||||
}
|
||||
|
||||
// assign the defaults to the response
|
||||
_.defaults(attributes, defaultsProps);
|
||||
|
||||
Object.assign(savedObject, attributes);
|
||||
savedObject.lastSavedTitle = savedObject.title;
|
||||
|
||||
savedObject.sharingSavedObjectProps = {
|
||||
aliasTargetId,
|
||||
outcome,
|
||||
errorJSON:
|
||||
outcome === 'conflict' && services.spaces
|
||||
? JSON.stringify({
|
||||
targetType: SAVED_VIS_TYPE,
|
||||
sourceId: id,
|
||||
targetSpace: (await services.spaces.getActiveSpace()).id,
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const meta = (attributes.kibanaSavedObjectMeta || {}) as SavedObjectAttributes;
|
||||
|
||||
if (meta.searchSourceJSON) {
|
||||
try {
|
||||
let searchSourceValues = parseSearchSourceJSON(meta.searchSourceJSON as string);
|
||||
|
||||
if (opts.searchSource) {
|
||||
searchSourceValues = injectSearchSourceReferences(
|
||||
searchSourceValues as any,
|
||||
resp.references
|
||||
);
|
||||
savedObject.searchSource = await services.search.searchSource.create(searchSourceValues);
|
||||
} else {
|
||||
savedObject.searchSourceFields = searchSourceValues;
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.references && resp.references.length > 0) {
|
||||
injectReferences(savedObject, resp.references);
|
||||
}
|
||||
|
||||
if (services.savedObjectsTagging) {
|
||||
savedObject.tags = services.savedObjectsTagging.ui.getTagIdsFromReferences(resp.references);
|
||||
}
|
||||
|
||||
savedObject.visState = await updateOldState(savedObject.visState);
|
||||
if (savedObject.searchSourceFields?.index) {
|
||||
await services.dataViews.get(savedObject.searchSourceFields.index as any);
|
||||
}
|
||||
|
||||
return savedObject;
|
||||
}
|
||||
|
||||
export async function saveVisualization(
|
||||
savedObject: VisSavedObject,
|
||||
{
|
||||
confirmOverwrite = false,
|
||||
isTitleDuplicateConfirmed = false,
|
||||
onTitleDuplicate,
|
||||
copyOnSave = false,
|
||||
}: SaveVisOptions,
|
||||
services: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
overlays: OverlayStart;
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||
}
|
||||
) {
|
||||
// Save the original id in case the save fails.
|
||||
const originalId = savedObject.id;
|
||||
// Read https://github.com/elastic/kibana/issues/9056 and
|
||||
// https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable
|
||||
// exists.
|
||||
// The goal is to move towards a better rename flow, but since our users have been conditioned
|
||||
// to expect a 'save as' flow during a rename, we are keeping the logic the same until a better
|
||||
// UI/UX can be worked out.
|
||||
if (copyOnSave) {
|
||||
delete savedObject.id;
|
||||
}
|
||||
|
||||
const attributes: SavedObjectAttributes = {
|
||||
visState: JSON.stringify(savedObject.visState),
|
||||
title: savedObject.title,
|
||||
uiStateJSON: savedObject.uiStateJSON,
|
||||
description: savedObject.description,
|
||||
savedSearchId: savedObject.savedSearchId,
|
||||
version: savedObject.version,
|
||||
};
|
||||
let references: SavedObjectReference[] = [];
|
||||
|
||||
if (savedObject.searchSource) {
|
||||
const { searchSourceJSON, references: searchSourceReferences } =
|
||||
savedObject.searchSource.serialize();
|
||||
attributes.kibanaSavedObjectMeta = { searchSourceJSON };
|
||||
references.push(...searchSourceReferences);
|
||||
}
|
||||
|
||||
if (savedObject.searchSourceFields) {
|
||||
const [searchSourceFields, searchSourceReferences] = extractSearchSourceReferences(
|
||||
savedObject.searchSourceFields
|
||||
);
|
||||
const searchSourceJSON = JSON.stringify(searchSourceFields);
|
||||
attributes.kibanaSavedObjectMeta = { searchSourceJSON };
|
||||
references.push(...searchSourceReferences);
|
||||
}
|
||||
|
||||
if (services.savedObjectsTagging) {
|
||||
references = services.savedObjectsTagging.ui.updateTagsReferences(
|
||||
references,
|
||||
savedObject.tags || []
|
||||
);
|
||||
}
|
||||
|
||||
const extractedRefs = extractReferences({ attributes, references });
|
||||
|
||||
if (!extractedRefs.references) {
|
||||
throw new Error('References not returned from extractReferences');
|
||||
}
|
||||
|
||||
try {
|
||||
await checkForDuplicateTitle(
|
||||
{
|
||||
...savedObject,
|
||||
copyOnSave,
|
||||
} as any,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
services as any
|
||||
);
|
||||
const createOpt = {
|
||||
id: savedObject.id,
|
||||
migrationVersion: savedObject.migrationVersion,
|
||||
references: extractedRefs.references,
|
||||
};
|
||||
const resp = confirmOverwrite
|
||||
? await saveWithConfirmation(attributes, savedObject, createOpt, services)
|
||||
: await services.savedObjectsClient.create(SAVED_VIS_TYPE, extractedRefs.attributes, {
|
||||
...createOpt,
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
savedObject.id = resp.id;
|
||||
savedObject.lastSavedTitle = savedObject.title;
|
||||
return savedObject.id;
|
||||
} catch (err: any) {
|
||||
savedObject.id = originalId;
|
||||
if (isErrorNonFatal(err)) {
|
||||
return '';
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
|
@ -23,5 +23,6 @@
|
|||
{ "path": "../usage_collection/tsconfig.json" },
|
||||
{ "path": "../kibana_utils/tsconfig.json" },
|
||||
{ "path": "../discover/tsconfig.json" },
|
||||
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"home",
|
||||
"share",
|
||||
"savedObjectsTaggingOss",
|
||||
"usageCollection"
|
||||
"usageCollection",
|
||||
"spaces"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaUtils",
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { shallow, mount } from 'enzyme';
|
||||
import { VisualizeEditorCommon } from './visualize_editor_common';
|
||||
import { VisualizeEditorVisInstance } from '../types';
|
||||
|
||||
const mockGetLegacyUrlConflict = jest.fn();
|
||||
const mockRedirectLegacyUrl = jest.fn(() => Promise.resolve());
|
||||
jest.mock('../../../../kibana_react/public', () => ({
|
||||
useKibana: jest.fn(() => ({
|
||||
services: {
|
||||
spaces: {
|
||||
ui: {
|
||||
redirectLegacyUrl: mockRedirectLegacyUrl,
|
||||
components: {
|
||||
getLegacyUrlConflict: mockGetLegacyUrlConflict,
|
||||
},
|
||||
},
|
||||
},
|
||||
history: {
|
||||
location: {
|
||||
search: '?_g=test',
|
||||
},
|
||||
},
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (url: string) => url,
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
withKibana: jest.fn((comp) => comp),
|
||||
}));
|
||||
|
||||
describe('VisualizeEditorCommon', () => {
|
||||
it('should display a conflict callout if saved object conflicts', async () => {
|
||||
shallow(
|
||||
<VisualizeEditorCommon
|
||||
appState={null}
|
||||
hasUnsavedChanges={false}
|
||||
setHasUnsavedChanges={() => {}}
|
||||
hasUnappliedChanges={false}
|
||||
isEmbeddableRendered={false}
|
||||
onAppLeave={() => {}}
|
||||
visEditorRef={React.createRef()}
|
||||
visInstance={
|
||||
{
|
||||
savedVis: {
|
||||
id: 'test',
|
||||
sharingSavedObjectProps: {
|
||||
outcome: 'conflict',
|
||||
aliasTargetId: 'alias_id',
|
||||
},
|
||||
},
|
||||
vis: {
|
||||
type: {
|
||||
title: 'TSVB',
|
||||
},
|
||||
},
|
||||
} as VisualizeEditorVisInstance
|
||||
}
|
||||
/>
|
||||
);
|
||||
expect(mockGetLegacyUrlConflict).toHaveBeenCalledWith({
|
||||
currentObjectId: 'test',
|
||||
objectNoun: 'TSVB visualization',
|
||||
otherObjectId: 'alias_id',
|
||||
otherObjectPath: '#/edit/alias_id?_g=test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to new id if saved object aliasMatch', async () => {
|
||||
mount(
|
||||
<VisualizeEditorCommon
|
||||
appState={null}
|
||||
hasUnsavedChanges={false}
|
||||
setHasUnsavedChanges={() => {}}
|
||||
hasUnappliedChanges={false}
|
||||
isEmbeddableRendered={false}
|
||||
onAppLeave={() => {}}
|
||||
visEditorRef={React.createRef()}
|
||||
visInstance={
|
||||
{
|
||||
savedVis: {
|
||||
id: 'test',
|
||||
sharingSavedObjectProps: {
|
||||
outcome: 'aliasMatch',
|
||||
aliasTargetId: 'alias_id',
|
||||
},
|
||||
},
|
||||
vis: {
|
||||
type: {
|
||||
title: 'TSVB',
|
||||
},
|
||||
},
|
||||
} as VisualizeEditorVisInstance
|
||||
}
|
||||
/>
|
||||
);
|
||||
expect(mockRedirectLegacyUrl).toHaveBeenCalledWith(
|
||||
'#/edit/alias_id?_g=test',
|
||||
'TSVB visualization'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -7,15 +7,19 @@
|
|||
*/
|
||||
|
||||
import './visualize_editor.scss';
|
||||
import React, { RefObject } from 'react';
|
||||
import React, { RefObject, useCallback, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { AppMountParameters } from 'kibana/public';
|
||||
import { VisualizeTopNav } from './visualize_top_nav';
|
||||
import { ExperimentalVisInfo } from './experimental_vis_info';
|
||||
import { useKibana } from '../../../../kibana_react/public';
|
||||
import { urlFor } from '../../../../visualizations/public';
|
||||
import {
|
||||
SavedVisInstance,
|
||||
VisualizeAppState,
|
||||
VisualizeServices,
|
||||
VisualizeAppStateContainer,
|
||||
VisualizeEditorVisInstance,
|
||||
} from '../types';
|
||||
|
@ -53,6 +57,55 @@ export const VisualizeEditorCommon = ({
|
|||
embeddableId,
|
||||
visEditorRef,
|
||||
}: VisualizeEditorCommonProps) => {
|
||||
const { services } = useKibana<VisualizeServices>();
|
||||
|
||||
useEffect(() => {
|
||||
async function aliasMatchRedirect() {
|
||||
const sharingSavedObjectProps = visInstance?.savedVis.sharingSavedObjectProps;
|
||||
if (services.spaces && sharingSavedObjectProps?.outcome === 'aliasMatch') {
|
||||
// We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash
|
||||
const newObjectId = sharingSavedObjectProps?.aliasTargetId; // This is always defined if outcome === 'aliasMatch'
|
||||
const newPath = `${urlFor(newObjectId!)}${services.history.location.search}`;
|
||||
await services.spaces.ui.redirectLegacyUrl(
|
||||
newPath,
|
||||
i18n.translate('visualize.legacyUrlConflict.objectNoun', {
|
||||
defaultMessage: '{visName} visualization',
|
||||
values: {
|
||||
visName: visInstance?.vis?.type.title,
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
aliasMatchRedirect();
|
||||
}, [visInstance?.savedVis.sharingSavedObjectProps, visInstance?.vis?.type.title, services]);
|
||||
|
||||
const getLegacyUrlConflictCallout = useCallback(() => {
|
||||
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
|
||||
const currentObjectId = visInstance?.savedVis.id;
|
||||
const sharingSavedObjectProps = visInstance?.savedVis.sharingSavedObjectProps;
|
||||
if (services.spaces && sharingSavedObjectProps?.outcome === 'conflict' && currentObjectId) {
|
||||
// We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a
|
||||
// callout with a warning for the user, and provide a way for them to navigate to the other object.
|
||||
const otherObjectId = sharingSavedObjectProps?.aliasTargetId!; // This is always defined if outcome === 'conflict'
|
||||
const otherObjectPath = `${urlFor(otherObjectId)}${services.history.location.search}`;
|
||||
return services.spaces.ui.components.getLegacyUrlConflict({
|
||||
objectNoun: i18n.translate('visualize.legacyUrlConflict.objectNoun', {
|
||||
defaultMessage: '{visName} visualization',
|
||||
values: {
|
||||
visName: visInstance?.vis?.type.title,
|
||||
},
|
||||
}),
|
||||
currentObjectId,
|
||||
otherObjectId,
|
||||
otherObjectPath,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [visInstance?.savedVis, services, visInstance?.vis?.type.title]);
|
||||
|
||||
return (
|
||||
<div className={`app-container visEditor visEditor--${visInstance?.vis.type.name}`}>
|
||||
{visInstance && appState && currentAppState && (
|
||||
|
@ -74,6 +127,7 @@ export const VisualizeEditorCommon = ({
|
|||
)}
|
||||
{visInstance?.vis?.type?.stage === 'experimental' && <ExperimentalVisInfo />}
|
||||
{visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)}
|
||||
{getLegacyUrlConflictCallout()}
|
||||
{visInstance && (
|
||||
<EuiScreenReaderOnly>
|
||||
<h1>
|
||||
|
|
|
@ -31,7 +31,6 @@ export const VisualizeListing = () => {
|
|||
chrome,
|
||||
dashboard,
|
||||
history,
|
||||
savedVisualizations,
|
||||
toastNotifications,
|
||||
visualizations,
|
||||
stateTransferService,
|
||||
|
@ -113,16 +112,16 @@ export const VisualizeListing = () => {
|
|||
}
|
||||
|
||||
const isLabsEnabled = uiSettings.get(VISUALIZE_ENABLE_LABS_SETTING);
|
||||
return savedVisualizations
|
||||
.findListItems(searchTerm, { size: listingLimit, references })
|
||||
.then(({ total, hits }: { total: number; hits: object[] }) => ({
|
||||
return visualizations
|
||||
.findListItems(searchTerm, listingLimit, references)
|
||||
.then(({ total, hits }: { total: number; hits: Array<Record<string, unknown>> }) => ({
|
||||
total,
|
||||
hits: hits.filter(
|
||||
(result: any) => isLabsEnabled || result.type?.stage !== 'experimental'
|
||||
),
|
||||
}));
|
||||
},
|
||||
[listingLimit, savedVisualizations, uiSettings, savedObjectsTagging]
|
||||
[listingLimit, uiSettings, savedObjectsTagging, visualizations]
|
||||
);
|
||||
|
||||
const deleteItems = useCallback(
|
||||
|
|
|
@ -42,6 +42,7 @@ import type { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/p
|
|||
import type { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
|
||||
import type { UrlForwardingStart } from 'src/plugins/url_forwarding/public';
|
||||
import type { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public';
|
||||
import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public';
|
||||
import type { DashboardStart } from '../../../dashboard/public';
|
||||
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
|
||||
import type { UsageCollectionStart } from '../../../usage_collection/public';
|
||||
|
@ -94,7 +95,6 @@ export interface VisualizeServices extends CoreStart {
|
|||
dashboardCapabilities: Record<string, boolean | Record<string, boolean>>;
|
||||
visualizations: VisualizationsStart;
|
||||
savedObjectsPublic: SavedObjectsStart;
|
||||
savedVisualizations: VisualizationsStart['savedVisualizationsLoader'];
|
||||
setActiveUrl: (newUrl: string) => void;
|
||||
createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject'];
|
||||
restorePreviousUrl: () => void;
|
||||
|
@ -105,6 +105,7 @@ export interface VisualizeServices extends CoreStart {
|
|||
presentationUtil: PresentationUtilPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
getKibanaVersion: () => string;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export interface SavedVisInstance {
|
||||
|
|
|
@ -14,7 +14,11 @@ import { parse } from 'query-string';
|
|||
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { TopNavMenuData } from 'src/plugins/navigation/public';
|
||||
import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public';
|
||||
import {
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
VisualizeInput,
|
||||
getFullPath,
|
||||
} from '../../../../visualizations/public';
|
||||
import {
|
||||
showSaveModal,
|
||||
SavedObjectSaveModalOrigin,
|
||||
|
@ -87,6 +91,7 @@ export const getTopNavConfig = (
|
|||
data,
|
||||
application,
|
||||
chrome,
|
||||
overlays,
|
||||
history,
|
||||
share,
|
||||
setActiveUrl,
|
||||
|
@ -99,6 +104,8 @@ export const getTopNavConfig = (
|
|||
presentationUtil,
|
||||
usageCollection,
|
||||
getKibanaVersion,
|
||||
savedObjects,
|
||||
visualizations,
|
||||
}: VisualizeServices
|
||||
) => {
|
||||
const { vis, embeddableHandler } = visInstance;
|
||||
|
@ -117,8 +124,10 @@ export const getTopNavConfig = (
|
|||
/**
|
||||
* Called when the user clicks "Save" button.
|
||||
*/
|
||||
async function doSave(saveOptions: SavedObjectSaveOpts & { dashboardId?: string }) {
|
||||
const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave;
|
||||
async function doSave(
|
||||
saveOptions: SavedObjectSaveOpts & { dashboardId?: string; copyOnSave?: boolean }
|
||||
) {
|
||||
const newlyCreated = !Boolean(savedVis.id) || saveOptions.copyOnSave;
|
||||
// vis.title was not bound and it's needed to reflect title into visState
|
||||
stateContainer.transitions.setVis({
|
||||
title: savedVis.title,
|
||||
|
@ -129,7 +138,7 @@ export const getTopNavConfig = (
|
|||
setHasUnsavedChanges(false);
|
||||
|
||||
try {
|
||||
const id = await savedVis.save(saveOptions);
|
||||
const id = await visualizations.saveVisualization(savedVis, saveOptions);
|
||||
|
||||
if (id) {
|
||||
toastNotifications.addSuccess({
|
||||
|
@ -142,6 +151,8 @@ export const getTopNavConfig = (
|
|||
'data-test-subj': 'saveVisualizationSuccess',
|
||||
});
|
||||
|
||||
chrome.recentlyAccessed.add(getFullPath(id), savedVis.title, String(id));
|
||||
|
||||
if ((originatingApp && saveOptions.returnToOrigin) || saveOptions.dashboardId) {
|
||||
if (!embeddableId) {
|
||||
const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`;
|
||||
|
@ -164,7 +175,7 @@ export const getTopNavConfig = (
|
|||
state: {
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
input: { savedObjectId: id },
|
||||
embeddableId: savedVis.copyOnSave ? undefined : embeddableId,
|
||||
embeddableId: saveOptions.copyOnSave ? undefined : embeddableId,
|
||||
searchSessionId: data.search.session.getSessionId(),
|
||||
},
|
||||
path,
|
||||
|
@ -392,11 +403,10 @@ export const getTopNavConfig = (
|
|||
const currentTitle = savedVis.title;
|
||||
savedVis.title = newTitle;
|
||||
embeddableHandler.updateInput({ title: newTitle });
|
||||
savedVis.copyOnSave = newCopyOnSave;
|
||||
savedVis.description = newDescription;
|
||||
|
||||
if (savedObjectsTagging && savedObjectsTagging.ui.hasTagDecoration(savedVis)) {
|
||||
savedVis.setTags(selectedTags);
|
||||
if (savedObjectsTagging) {
|
||||
savedVis.tags = selectedTags;
|
||||
}
|
||||
|
||||
const saveOptions = {
|
||||
|
@ -405,6 +415,7 @@ export const getTopNavConfig = (
|
|||
onTitleDuplicate,
|
||||
returnToOrigin,
|
||||
dashboardId: !!dashboardId ? dashboardId : undefined,
|
||||
copyOnSave: newCopyOnSave,
|
||||
};
|
||||
|
||||
// If we're adding to a dashboard and not saving to library,
|
||||
|
@ -457,9 +468,7 @@ export const getTopNavConfig = (
|
|||
let tagOptions: React.ReactNode | undefined;
|
||||
|
||||
if (savedObjectsTagging) {
|
||||
if (savedVis && savedObjectsTagging.ui.hasTagDecoration(savedVis)) {
|
||||
selectedTags = savedVis.getTags();
|
||||
}
|
||||
selectedTags = savedVis.tags || [];
|
||||
tagOptions = (
|
||||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={selectedTags}
|
||||
|
|
|
@ -30,11 +30,12 @@ jest.mock('../../../../discover/public', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
let savedVisMock: VisSavedObject;
|
||||
|
||||
describe('getVisualizationInstance', () => {
|
||||
const serializedVisMock = {
|
||||
type: 'area',
|
||||
};
|
||||
let savedVisMock: VisSavedObject;
|
||||
let visMock: Vis<VisParams>;
|
||||
let mockServices: jest.Mocked<VisualizeServices>;
|
||||
let subj: BehaviorSubject<any>;
|
||||
|
@ -47,13 +48,16 @@ describe('getVisualizationInstance', () => {
|
|||
data: {},
|
||||
} as Vis<VisParams>;
|
||||
savedVisMock = {} as VisSavedObject;
|
||||
|
||||
// @ts-expect-error
|
||||
mockServices.data.search.showError.mockImplementation(() => {});
|
||||
// @ts-expect-error
|
||||
mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock);
|
||||
// @ts-expect-error
|
||||
mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock);
|
||||
// @ts-expect-error
|
||||
mockServices.visualizations.getSavedVisualization.mockImplementation(
|
||||
(opts: unknown) => savedVisMock
|
||||
);
|
||||
// @ts-expect-error
|
||||
mockServices.visualizations.createVis.mockImplementation(() => visMock);
|
||||
// @ts-expect-error
|
||||
mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({
|
||||
|
@ -71,7 +75,9 @@ describe('getVisualizationInstance', () => {
|
|||
opts
|
||||
);
|
||||
|
||||
expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith(opts);
|
||||
expect((mockServices.visualizations.getSavedVisualization as jest.Mock).mock.calls[0][0]).toBe(
|
||||
opts
|
||||
);
|
||||
expect(savedVisMock.searchSourceFields).toEqual({
|
||||
index: opts.indexPattern,
|
||||
});
|
||||
|
@ -98,7 +104,9 @@ describe('getVisualizationInstance', () => {
|
|||
visMock.type.setup = jest.fn(() => newVisObj);
|
||||
const { vis } = await getVisualizationInstance(mockServices, 'saved_vis_id');
|
||||
|
||||
expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith('saved_vis_id');
|
||||
expect((mockServices.visualizations.getSavedVisualization as jest.Mock).mock.calls[0][0]).toBe(
|
||||
'saved_vis_id'
|
||||
);
|
||||
expect(savedVisMock.searchSourceFields).toBeUndefined();
|
||||
expect(visMock.type.setup).toHaveBeenCalledWith(visMock);
|
||||
expect(vis).toBe(newVisObj);
|
||||
|
@ -128,7 +136,6 @@ describe('getVisualizationInstanceInput', () => {
|
|||
const serializedVisMock = {
|
||||
type: 'pie',
|
||||
};
|
||||
let savedVisMock: VisSavedObject;
|
||||
let visMock: Vis<VisParams>;
|
||||
let mockServices: jest.Mocked<VisualizeServices>;
|
||||
let subj: BehaviorSubject<any>;
|
||||
|
@ -142,10 +149,12 @@ describe('getVisualizationInstanceInput', () => {
|
|||
} as Vis<VisParams>;
|
||||
savedVisMock = {} as VisSavedObject;
|
||||
// @ts-expect-error
|
||||
mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock);
|
||||
// @ts-expect-error
|
||||
mockServices.visualizations.createVis.mockImplementation(() => visMock);
|
||||
// @ts-expect-error
|
||||
mockServices.visualizations.getSavedVisualization.mockImplementation(
|
||||
(opts: unknown) => savedVisMock
|
||||
);
|
||||
// @ts-expect-error
|
||||
mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({
|
||||
getOutput$: jest.fn(() => subj.asObservable()),
|
||||
}));
|
||||
|
@ -183,7 +192,7 @@ describe('getVisualizationInstanceInput', () => {
|
|||
const { savedVis, savedSearch, vis, embeddableHandler } =
|
||||
await getVisualizationInstanceFromInput(mockServices, input);
|
||||
|
||||
expect(mockServices.savedVisualizations.get).toHaveBeenCalled();
|
||||
expect(mockServices.visualizations.getSavedVisualization).toHaveBeenCalled();
|
||||
expect(mockServices.visualizations.createVis).toHaveBeenCalledWith(
|
||||
serializedVisMock.type,
|
||||
input.savedVis
|
||||
|
|
|
@ -66,14 +66,15 @@ export const getVisualizationInstanceFromInput = async (
|
|||
visualizeServices: VisualizeServices,
|
||||
input: VisualizeInput
|
||||
) => {
|
||||
const { visualizations, savedVisualizations } = visualizeServices;
|
||||
const { visualizations } = visualizeServices;
|
||||
const visState = input.savedVis as SerializedVis;
|
||||
|
||||
/**
|
||||
* A saved vis is needed even in by value mode to support 'save to library' which converts the 'by value'
|
||||
* state of the visualization, into a new saved object.
|
||||
*/
|
||||
const savedVis: VisSavedObject = await savedVisualizations.get();
|
||||
const savedVis: VisSavedObject = await visualizations.getSavedVisualization();
|
||||
|
||||
if (visState.uiState && Object.keys(visState.uiState).length !== 0) {
|
||||
savedVis.uiStateJSON = JSON.stringify(visState.uiState);
|
||||
}
|
||||
|
@ -107,8 +108,8 @@ export const getVisualizationInstance = async (
|
|||
*/
|
||||
opts?: Record<string, unknown> | string
|
||||
) => {
|
||||
const { visualizations, savedVisualizations } = visualizeServices;
|
||||
const savedVis: VisSavedObject = await savedVisualizations.get(opts);
|
||||
const { visualizations } = visualizeServices;
|
||||
const savedVis: VisSavedObject = await visualizations.getSavedVisualization(opts);
|
||||
|
||||
if (typeof opts !== 'string') {
|
||||
savedVis.searchSourceFields = { index: opts?.indexPattern } as SearchSourceFields;
|
||||
|
|
|
@ -26,7 +26,6 @@ export const createVisualizeServicesMock = () => {
|
|||
location: { pathname: '' },
|
||||
},
|
||||
visualizations,
|
||||
savedVisualizations: visualizations.savedVisualizationsLoader,
|
||||
createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject,
|
||||
} as unknown as jest.Mocked<VisualizeServices>;
|
||||
};
|
||||
|
|
|
@ -22,7 +22,6 @@ import { createEmbeddableStateTransferMock } from '../../../../../embeddable/pub
|
|||
const mockDefaultEditorControllerDestroy = jest.fn();
|
||||
const mockEmbeddableHandlerDestroy = jest.fn();
|
||||
const mockEmbeddableHandlerRender = jest.fn();
|
||||
const mockSavedVisDestroy = jest.fn();
|
||||
const savedVisId = '9ca7aa90-b892-11e8-a6d9-e546fe2bba5f';
|
||||
const mockSavedVisInstance = {
|
||||
embeddableHandler: {
|
||||
|
@ -32,7 +31,6 @@ const mockSavedVisInstance = {
|
|||
savedVis: {
|
||||
id: savedVisId,
|
||||
title: 'Test Vis',
|
||||
destroy: mockSavedVisDestroy,
|
||||
},
|
||||
vis: {
|
||||
type: {},
|
||||
|
@ -103,7 +101,6 @@ describe('useSavedVisInstance', () => {
|
|||
mockDefaultEditorControllerDestroy.mockClear();
|
||||
mockEmbeddableHandlerDestroy.mockClear();
|
||||
mockEmbeddableHandlerRender.mockClear();
|
||||
mockSavedVisDestroy.mockClear();
|
||||
toastNotifications.addWarning.mockClear();
|
||||
mockGetVisualizationInstance.mockClear();
|
||||
});
|
||||
|
@ -153,7 +150,6 @@ describe('useSavedVisInstance', () => {
|
|||
|
||||
expect(mockDefaultEditorControllerDestroy.mock.calls.length).toBe(1);
|
||||
expect(mockEmbeddableHandlerDestroy).not.toHaveBeenCalled();
|
||||
expect(mockSavedVisDestroy.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -236,7 +232,6 @@ describe('useSavedVisInstance', () => {
|
|||
unmount();
|
||||
expect(mockDefaultEditorControllerDestroy).not.toHaveBeenCalled();
|
||||
expect(mockEmbeddableHandlerDestroy.mock.calls.length).toBe(1);
|
||||
expect(mockSavedVisDestroy.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -176,9 +176,6 @@ export const useSavedVisInstance = (
|
|||
} else if (state.savedVisInstance?.embeddableHandler) {
|
||||
state.savedVisInstance.embeddableHandler.destroy();
|
||||
}
|
||||
if (state.savedVisInstance?.savedVis) {
|
||||
state.savedVisInstance.savedVis.destroy();
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
createKbnUrlStateStorage,
|
||||
withNotifyOnErrors,
|
||||
} from '../../kibana_utils/public';
|
||||
import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public';
|
||||
|
||||
import { VisualizeConstants } from './application/visualize_constants';
|
||||
import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
|
||||
|
@ -61,6 +62,7 @@ export interface VisualizePluginStartDependencies {
|
|||
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
|
||||
presentationUtil: PresentationUtilPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
spaces: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export interface VisualizePluginSetupDependencies {
|
||||
|
@ -192,7 +194,6 @@ export class VisualizePlugin
|
|||
data: pluginsStart.data,
|
||||
localStorage: new Storage(localStorage),
|
||||
navigation: pluginsStart.navigation,
|
||||
savedVisualizations: pluginsStart.visualizations.savedVisualizationsLoader,
|
||||
share: pluginsStart.share,
|
||||
toastNotifications: coreStart.notifications.toasts,
|
||||
visualizeCapabilities: coreStart.application.capabilities.visualize,
|
||||
|
@ -212,6 +213,7 @@ export class VisualizePlugin
|
|||
presentationUtil: pluginsStart.presentationUtil,
|
||||
usageCollection: pluginsStart.usageCollection,
|
||||
getKibanaVersion: () => this.initializerContext.env.packageInfo.version,
|
||||
spaces: pluginsStart.spaces,
|
||||
};
|
||||
|
||||
params.element.classList.add('visAppWrapper');
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
{ "path": "../kibana_react/tsconfig.json" },
|
||||
{ "path": "../home/tsconfig.json" },
|
||||
{ "path": "../presentation_util/tsconfig.json" },
|
||||
{ "path": "../discover/tsconfig.json" }
|
||||
{ "path": "../discover/tsconfig.json" },
|
||||
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue