Fix sample data for share-capable objects (#116378)

This commit is contained in:
Joe Portner 2021-11-02 07:57:21 -04:00 committed by GitHub
parent f2b9acf67b
commit 61ab4a3c59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 743 additions and 278 deletions

View file

@ -67,7 +67,6 @@ export class SampleDataSetCards extends React.Component {
sampleDataSets: sampleDataSets.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}),
processingStatus: {},
});
};
@ -82,6 +81,7 @@ export class SampleDataSetCards extends React.Component {
try {
await installSampleDataSet(id, targetSampleDataSet.defaultIndex);
await this.loadSampleDataSets(); // reload the list of sample data sets
} catch (fetchError) {
if (this._isMounted) {
this.setState((prevState) => ({

View file

@ -19,7 +19,9 @@ export type {
export { FeatureCatalogueCategory } from './services';
export type {
AddDataTab,
FeatureCatalogueEntry,
FeatureCatalogueRegistry,
FeatureCatalogueSolution,
Environment,
TutorialVariables,

View file

@ -7,8 +7,21 @@
*/
export type { HomeServerPluginSetup, HomeServerPluginStart } from './plugin';
export type { TutorialProvider } from './services';
export type { SampleDatasetProvider, SampleDataRegistrySetup } from './services';
export { EmbeddableTypes, TutorialsCategory } from './services';
export type {
AppLinkData,
ArtifactsSchema,
TutorialProvider,
TutorialSchema,
InstructionSetSchema,
InstructionsSchema,
TutorialContext,
SampleDatasetProvider,
SampleDataRegistrySetup,
SampleDatasetDashboardPanel,
SampleObject,
ScopedTutorialContextFactory,
} from './services';
import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server';
import { HomeServerPlugin } from './plugin';
import { configSchema, ConfigSchema } from '../config';
@ -23,10 +36,3 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext);
export { INSTRUCTION_VARIANT } from '../common/instruction_variant';
export { TutorialsCategory } from './services/tutorials';
export type {
ArtifactsSchema,
TutorialSchema,
InstructionSetSchema,
InstructionsSchema,
} from './services/tutorials';

View file

@ -9,10 +9,9 @@
// provided to other plugins as APIs
// should model the plugin lifecycle
export { TutorialsRegistry } from './tutorials';
export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials';
export { TutorialsRegistry, TutorialsCategory } from './tutorials';
export { TutorialsCategory } from './tutorials';
export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials';
export type {
InstructionSetSchema,
@ -22,12 +21,19 @@ export type {
ArtifactsSchema,
TutorialSchema,
TutorialProvider,
TutorialContext,
TutorialContextFactory,
ScopedTutorialContextFactory,
} from './tutorials';
export { SampleDataRegistry } from './sample_data';
export { EmbeddableTypes, SampleDataRegistry } from './sample_data';
export type { SampleDataRegistrySetup, SampleDataRegistryStart } from './sample_data';
export type { SampleDatasetSchema, SampleDatasetProvider } from './sample_data';
export type {
AppLinkData,
SampleDataRegistrySetup,
SampleDataRegistryStart,
SampleDatasetDashboardPanel,
SampleDatasetProvider,
SampleDatasetSchema,
SampleObject,
} from './sample_data';

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema, AppLinkSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
const ecommerceName = i18n.translate('home.sampleData.ecommerceSpecTitle', {
defaultMessage: 'Sample eCommerce orders',
@ -18,7 +18,6 @@ const ecommerceName = i18n.translate('home.sampleData.ecommerceSpecTitle', {
const ecommerceDescription = i18n.translate('home.sampleData.ecommerceSpecDescription', {
defaultMessage: 'Sample data, visualizations, and dashboards for tracking eCommerce orders.',
});
const initialAppLinks = [] as AppLinkSchema[];
export const ecommerceSpecProvider = function (): SampleDatasetSchema {
return {
@ -28,7 +27,6 @@ export const ecommerceSpecProvider = function (): SampleDatasetSchema {
previewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard.png',
darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard_dark.png',
overviewDashboard: '722b74f0-b882-11e8-a6d9-e546fe2bba5f',
appLinks: initialAppLinks,
defaultIndex: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
savedObjects: getSavedObjects(),
dataIndices: [

View file

@ -140,9 +140,10 @@ export const getSavedObjects = (): SavedObject[] => [
{
id: '9c6f83f0-bb4d-11e8-9c84-77068524bcab',
type: 'visualization',
updated_at: '2018-10-01T15:13:03.270Z',
updated_at: '2021-10-28T15:07:24.077Z',
version: '1',
migrationVersion: {},
coreMigrationVersion: '8.0.0',
migrationVersion: { visualization: '8.0.0' },
attributes: {
title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', {
defaultMessage: '[eCommerce] Sales Count Map',
@ -154,10 +155,16 @@ export const getSavedObjects = (): SavedObject[] => [
version: 1,
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"index":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","query":{"query":"","language":"kuery"},"filter":[]}',
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
},
},
references: [],
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
},
],
},
{
attributes: {

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema, AppLinkSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
const flightsName = i18n.translate('home.sampleData.flightsSpecTitle', {
defaultMessage: 'Sample flight data',
@ -18,7 +18,6 @@ const flightsName = i18n.translate('home.sampleData.flightsSpecTitle', {
const flightsDescription = i18n.translate('home.sampleData.flightsSpecDescription', {
defaultMessage: 'Sample data, visualizations, and dashboards for monitoring flight routes.',
});
const initialAppLinks = [] as AppLinkSchema[];
export const flightsSpecProvider = function (): SampleDatasetSchema {
return {
@ -28,7 +27,6 @@ export const flightsSpecProvider = function (): SampleDatasetSchema {
previewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard.png',
darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard_dark.png',
overviewDashboard: '7adfa750-4c81-11e8-b3d7-01146121b73d',
appLinks: initialAppLinks,
defaultIndex: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
savedObjects: getSavedObjects(),
dataIndices: [

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema, AppLinkSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
const logsName = i18n.translate('home.sampleData.logsSpecTitle', {
defaultMessage: 'Sample web logs',
@ -18,7 +18,6 @@ const logsName = i18n.translate('home.sampleData.logsSpecTitle', {
const logsDescription = i18n.translate('home.sampleData.logsSpecDescription', {
defaultMessage: 'Sample data, visualizations, and dashboards for monitoring web logs.',
});
const initialAppLinks = [] as AppLinkSchema[];
export const GLOBE_ICON_PATH = '/plugins/home/assets/sample_data_resources/logs/icon.svg';
export const logsSpecProvider = function (): SampleDatasetSchema {
@ -29,7 +28,6 @@ export const logsSpecProvider = function (): SampleDatasetSchema {
previewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard.png',
darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.png',
overviewDashboard: 'edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b',
appLinks: initialAppLinks,
defaultIndex: '90943e30-9a47-11e8-b64d-95841ca0b247',
savedObjects: getSavedObjects(),
dataIndices: [

View file

@ -14,9 +14,10 @@ export const getSavedObjects = (): SavedObject[] => [
{
id: '06cf9c40-9ee8-11e7-8711-e7a007dcef99',
type: 'visualization',
updated_at: '2018-08-29T13:22:17.617Z',
updated_at: '2021-10-28T15:07:36.622Z',
version: '1',
migrationVersion: {},
coreMigrationVersion: '8.0.0',
migrationVersion: { visualization: '8.0.0' },
attributes: {
title: i18n.translate('home.sampleData.logsSpec.visitorsMapTitle', {
defaultMessage: '[Logs] Visitors Map',
@ -28,10 +29,16 @@ export const getSavedObjects = (): SavedObject[] => [
version: 1,
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","filter":[],"query":{"query":"","language":"kuery"}}',
'{"filter":[],"query":{"query":"","language":"kuery"},"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
},
},
references: [],
references: [
{
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
},
],
},
{
id: 'cb099a20-ea66-11eb-9425-113343a037e3',
@ -88,25 +95,32 @@ export const getSavedObjects = (): SavedObject[] => [
{
id: '69a34b00-9ee8-11e7-8711-e7a007dcef99',
type: 'visualization',
updated_at: '2018-08-29T13:24:46.136Z',
updated_at: '2021-10-28T14:38:21.435Z',
version: '2',
migrationVersion: {},
coreMigrationVersion: '8.0.0',
migrationVersion: { visualization: '8.0.0' },
attributes: {
title: i18n.translate('home.sampleData.logsSpec.goalsTitle', {
defaultMessage: '[Logs] Goals',
}),
visState:
'{"title":"[Logs] Goals","type":"gauge","params":{"type":"gauge","addTooltip":true,"addLegend":false,"gauge":{"verticalSplit":false,"extendRange":true,"percentageMode":false,"gaugeType":"Arc","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"Labels","colorsRange":[{"from":0,"to":500},{"from":500,"to":1000},{"from":1000,"to":1500}],"invertColors":true,"labels":{"show":false,"color":"black"},"scale":{"show":true,"labels":false,"color":"#333"},"type":"meter","style":{"bgWidth":0.9,"width":0.9,"mask":false,"bgMask":false,"maskBars":50,"bgFill":"#eee","bgColor":false,"subText":"visitors","fontSize":60,"labelColor":true}},"isDisplayWarning":false},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}}]}',
'{"title":"[Logs] Goals","type":"gauge","params":{"type":"gauge","addTooltip":true,"addLegend":false,"gauge":{"extendRange":true,"percentageMode":false,"gaugeType":"Arc","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"Labels","colorsRange":[{"from":0,"to":500},{"from":500,"to":1000},{"from":1000,"to":1500}],"invertColors":true,"labels":{"show":false,"color":"black"},"scale":{"show":true,"labels":false,"color":"#333"},"type":"meter","style":{"bgWidth":0.9,"width":0.9,"mask":false,"bgMask":false,"maskBars":50,"bgFill":"#eee","bgColor":false,"subText":"visitors","fontSize":60,"labelColor":true},"alignment":"horizontal"},"isDisplayWarning":false},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}}]}',
uiStateJSON:
'{"vis":{"defaultColors":{"0 - 500":"rgb(165,0,38)","500 - 1000":"rgb(255,255,190)","1000 - 1500":"rgb(0,104,55)"},"colors":{"75 - 100":"#629E51","50 - 75":"#EAB839","0 - 50":"#E24D42","0 - 100":"#E24D42","200 - 300":"#7EB26D","500 - 1000":"#E5AC0E","0 - 500":"#E24D42","1000 - 1500":"#7EB26D"},"legendOpen":true}}',
description: '',
version: 1,
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","filter":[],"query":{"query":"","language":"kuery"}}',
'{"filter":[],"query":{"query":"","language":"kuery"},"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
},
},
references: [],
references: [
{
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
},
],
},
{
id: '7cbd2350-2223-11e8-b802-5bcf64c2cfb4',
@ -366,13 +380,13 @@ export const getSavedObjects = (): SavedObject[] => [
{
id: 'edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b',
type: 'dashboard',
updated_at: '2021-07-21T21:43:43.870Z',
updated_at: '2021-10-28T15:07:36.622Z',
version: '3',
references: [
{
id: '06cf9c40-9ee8-11e7-8711-e7a007dcef99',
name: '4:panel_4',
type: 'map',
type: 'visualization',
},
{
id: '4eb6e500-e1c7-11e7-b6d5-4dc382ef7f5b',

View file

@ -10,7 +10,12 @@ export { SampleDataRegistry } from './sample_data_registry';
export type { SampleDataRegistrySetup, SampleDataRegistryStart } from './sample_data_registry';
export { EmbeddableTypes } from './lib/sample_dataset_registry_types';
export type {
SampleDatasetSchema,
AppLinkData,
SampleDatasetDashboardPanel,
SampleDatasetProvider,
SampleDatasetSchema,
SampleObject,
} from './lib/sample_dataset_registry_types';

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
export const mockBuildNode = jest.fn();
jest.mock('@kbn/es-query', () => {
return {
nodeTypes: {
function: {
buildNode: mockBuildNode,
},
},
};
});

View file

@ -0,0 +1,125 @@
/*
* 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 { mockBuildNode } from './find_sample_objects.test.mock';
import type { SavedObject, SavedObjectsFindResponse } from 'src/core/server';
import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks';
import { findSampleObjects } from './find_sample_objects';
describe('findSampleObjects', () => {
function setup() {
const mockClient = savedObjectsClientMock.create();
const mockLogger = loggingSystemMock.createLogger();
return {
client: mockClient,
logger: mockLogger,
};
}
beforeEach(() => {
mockBuildNode.mockReset();
});
it('searches for objects and returns expected results', async () => {
const { client, logger } = setup();
const obj1 = { type: 'obj-type-1', id: 'obj-id-1' };
const obj2 = { type: 'obj-type-2', id: 'obj-id-2' };
const obj3 = { type: 'obj-type-3', id: 'obj-id-3' };
const obj4 = { type: 'obj-type-3', id: 'obj-id-4' };
const objects = [obj1, obj2, obj3, obj4];
const params = { client, logger, objects };
client.bulkGet.mockResolvedValue({
saved_objects: [
obj1, // bulkGet success for obj1
{ ...obj2, error: { statusCode: 403 } }, // bulkGet failure - will not attempt to find by originId since the error is not 404
{ ...obj3, error: { statusCode: 404 } }, // bulkGet failure - will attempt to find by originId since the error is 404
{ ...obj4, error: { statusCode: 404 } }, // bulkGet failure - will attempt to find by originId since the error is 404
] as SavedObject[],
});
client.find.mockResolvedValue({
saved_objects: [{ type: obj4.type, id: 'obj-id-x', originId: obj4.id }], // find success for obj4
total: 1,
} as SavedObjectsFindResponse);
const result = await findSampleObjects(params);
expect(result).toEqual([
{ ...obj1, foundObjectId: obj1.id },
{ ...obj2, foundObjectId: undefined },
{ ...obj3, foundObjectId: undefined },
{ ...obj4, foundObjectId: 'obj-id-x' },
]);
expect(client.bulkGet).toHaveBeenCalledWith(objects);
expect(mockBuildNode).toHaveBeenCalledTimes(3);
expect(mockBuildNode).toHaveBeenNthCalledWith(1, 'is', `${obj3.type}.originId`, obj3.id);
expect(mockBuildNode).toHaveBeenNthCalledWith(2, 'is', `${obj4.type}.originId`, obj4.id);
expect(mockBuildNode).toHaveBeenNthCalledWith(3, 'or', expect.any(Array));
expect(client.find).toHaveBeenCalledWith(expect.objectContaining({ type: ['obj-type-3'] })); // obj3 and obj4 have the same type; the type param is deduplicated
expect(logger.warn).not.toHaveBeenCalled();
});
it('skips find if there are no objects left to search for', async () => {
const { client, logger } = setup();
const obj1 = { type: 'obj-type-1', id: 'obj-id-1' };
const obj2 = { type: 'obj-type-2', id: 'obj-id-2' };
const objects = [obj1, obj2];
const params = { client, logger, objects };
client.bulkGet.mockResolvedValue({
saved_objects: [
obj1, // bulkGet success for obj1
{ ...obj2, error: { statusCode: 403 } }, // bulkGet failure - will not attempt to find by originId since the error is not 404
] as SavedObject[],
});
const result = await findSampleObjects(params);
expect(result).toEqual([
{ ...obj1, foundObjectId: obj1.id },
{ ...obj2, foundObjectId: undefined },
]);
expect(client.bulkGet).toHaveBeenCalledWith(objects);
expect(mockBuildNode).not.toHaveBeenCalled();
expect(client.find).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
it('logs expected warnings', async () => {
const { client, logger } = setup();
const obj1 = { type: 'obj-type-1', id: 'obj-id-1' };
const objects = [obj1];
const params = { client, logger, objects };
client.bulkGet.mockResolvedValue({
saved_objects: [
{ ...obj1, error: { statusCode: 404 } }, // bulkGet failure - will attempt to find by originId since the error is 404
] as SavedObject[],
});
client.find.mockResolvedValue({
saved_objects: [
{ type: obj1.type, id: 'obj-id-x', originId: obj1.id }, // find success for obj4
{ type: obj1.type, id: 'obj-id-y', originId: obj1.id }, // find success for obj4
],
total: 10001,
} as SavedObjectsFindResponse);
const result = await findSampleObjects(params);
expect(result).toEqual([{ ...obj1, foundObjectId: 'obj-id-x' }]); // obj-id-y is ignored
expect(client.bulkGet).toHaveBeenCalledWith(objects);
expect(mockBuildNode).toHaveBeenCalledTimes(2);
expect(mockBuildNode).toHaveBeenNthCalledWith(1, 'is', `${obj1.type}.originId`, obj1.id);
expect(mockBuildNode).toHaveBeenNthCalledWith(2, 'or', expect.any(Array));
expect(client.find).toHaveBeenCalledWith(expect.objectContaining({ type: ['obj-type-1'] }));
expect(logger.warn).toHaveBeenCalledTimes(2);
expect(logger.warn).toHaveBeenCalledWith(
'findSampleObjects got 10001 results, only using the first 10000'
);
expect(logger.warn).toHaveBeenCalledWith(
'Found two sample objects with the same origin "obj-id-1" (previously found "obj-id-x", ignoring "obj-id-y")'
);
});
});

View file

@ -0,0 +1,104 @@
/*
* 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 * as esKuery from '@kbn/es-query';
import type { Logger, SavedObjectsClientContract } from 'src/core/server';
const MAX_OBJECTS_TO_FIND = 10000; // we only expect up to a few dozen, search for 10k to be safe; anything over this is ignored
export interface FindSampleObjectsParams {
client: SavedObjectsClientContract;
logger: Logger;
objects: SampleObject[];
}
export interface SampleObject {
type: string;
id: string;
}
export interface FindSampleObjectsResponseObject {
type: string;
id: string;
/** Contains a string if this sample data object was found, or undefined if it was not. */
foundObjectId: string | undefined;
}
/**
* Given an array of objects in a sample dataset, this function attempts to find if those objects exist in the current space.
* It attempts to find objects with an origin of the sample data (e.g., matching `id` or `originId`).
*/
export async function findSampleObjects({ client, logger, objects }: FindSampleObjectsParams) {
const bulkGetResponse = await client.bulkGet(objects);
let resultsMap = new Map<string, string>();
const objectsToFind: SampleObject[] = [];
objects.forEach((object, i) => {
const bulkGetResult = bulkGetResponse.saved_objects[i];
if (!bulkGetResult.error) {
const { type, id } = object;
const key = getObjKey(type, id);
resultsMap.set(key, id);
} else if (bulkGetResult.error.statusCode === 404) {
objectsToFind.push(object);
}
});
if (objectsToFind.length > 0) {
const options = {
type: getUniqueTypes(objectsToFind),
filter: createKueryFilter(objectsToFind),
fields: ['title'], // we don't want to return all source fields, so we have to specify at least one source field
perPage: MAX_OBJECTS_TO_FIND,
};
const findResponse = await client.find(options);
if (findResponse.total > MAX_OBJECTS_TO_FIND) {
// As of this writing, it is not possible to encounter this scenario when using Kibana import or copy-to-space, because at most one
// object can exist in a given space. However, as of today, when objects are shareable you _could_ get Kibana into a state where
// multiple objects of the same origin exist in the same space.
// #116677 describes solutions to fully mitigate this edge case in the future.
logger.warn(
`findSampleObjects got ${findResponse.total} results, only using the first ${MAX_OBJECTS_TO_FIND}`
);
}
resultsMap = findResponse.saved_objects.reduce((acc, { type, id, originId }) => {
const key = getObjKey(type, originId!);
const existing = acc.get(key);
if (existing) {
// As of this writing, it is not possible to encounter this scenario when using Kibana import or copy-to-space, because at most one
// object can exist in a given space. However, as of today, when objects are shareable you _could_ get Kibana into a state where
// multiple objects of the same origin exist in the same space.
// #116677 describes solutions to fully mitigate this edge case in the future.
logger.warn(
`Found two sample objects with the same origin "${originId}" (previously found "${existing}", ignoring "${id}")`
);
return acc;
}
return acc.set(key, id);
}, resultsMap);
}
return objects.map<FindSampleObjectsResponseObject>(({ type, id }) => {
const key = getObjKey(type, id);
return { type, id, foundObjectId: resultsMap.get(key) };
});
}
function getUniqueTypes(objects: SampleObject[]) {
return [...new Set(objects.map(({ type }) => type))];
}
function createKueryFilter(objects: SampleObject[]) {
const { buildNode } = esKuery.nodeTypes.function;
const kueryNodes = objects.map(({ type, id }) => buildNode('is', `${type}.originId`, id)); // the repository converts this node into "and (type is ..., originId is ...)"
return buildNode('or', kueryNodes);
}
function getObjKey(type: string, id: string) {
return `${type}:${id}`;
}

View file

@ -7,7 +7,7 @@
*/
import type { SampleDatasetSchema } from './sample_dataset_schema';
export type { SampleDatasetSchema, AppLinkSchema, DataIndexSchema } from './sample_dataset_schema';
export type { SampleDatasetSchema, DataIndexSchema } from './sample_dataset_schema';
export enum DatasetStatusTypes {
NOT_INSTALLED = 'not_installed',
@ -28,3 +28,34 @@ export enum EmbeddableTypes {
VISUALIZE_EMBEDDABLE_TYPE = 'visualization',
}
export type SampleDatasetProvider = () => SampleDatasetSchema;
/** This type is used to identify an object in a sample dataset. */
export interface SampleObject {
/** The type of the sample object. */
type: string;
/** The ID of the sample object. */
id: string;
}
/**
* This type is used by consumers to register a new app link for a sample dataset.
*/
export interface AppLinkData {
/**
* The sample object that is used for this app link's path; if the path does not use an object ID, set this to null.
*/
sampleObject: SampleObject | null;
/**
* Function that returns the path for this app link. Note that the `objectId` can be different than the given `sampleObject.id`, depending
* on how the sample data was installed. If the `sampleObject` is null, the `objectId` argument will be an empty string.
*/
getPath: (objectId: string) => string;
/**
* The label for this app link.
*/
label: string;
/**
* The icon for this app link.
*/
icon: string;
}

View file

@ -48,13 +48,6 @@ const dataIndexSchema = schema.object({
export type DataIndexSchema = TypeOf<typeof dataIndexSchema>;
const appLinkSchema = schema.object({
path: schema.string(),
label: schema.string(),
icon: schema.string(),
});
export type AppLinkSchema = TypeOf<typeof appLinkSchema>;
export const sampleDataSchema = schema.object({
id: schema.string({
validate(value: string) {
@ -71,7 +64,6 @@ export const sampleDataSchema = schema.object({
// saved object id of main dashboard for sample data set
overviewDashboard: schema.string(),
appLinks: schema.arrayOf(appLinkSchema, { defaultValue: [] }),
// saved object id of default index-pattern for sample data set
defaultIndex: schema.string(),

View file

@ -0,0 +1,13 @@
/*
* 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 { SampleObject } from './sample_dataset_registry_types';
export function getUniqueObjectTypes(objects: SampleObject[]) {
return [...new Set(objects.map(({ type }) => type))];
}

View file

@ -6,13 +6,9 @@
* Side Public License, v 1.
*/
import { Readable } from 'stream';
import { schema } from '@kbn/config-schema';
import type {
IRouter,
Logger,
IScopedClusterClient,
SavedObjectsBulkCreateObject,
} from 'src/core/server';
import { IRouter, Logger, IScopedClusterClient } from 'src/core/server';
import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
import { createIndexName } from '../lib/create_index_name';
import {
@ -22,6 +18,8 @@ import {
} from '../lib/translate_timestamp';
import { loadData } from '../lib/load_data';
import { SampleDataUsageTracker } from '../usage/usage';
import { getSavedObjectsClient } from './utils';
import { getUniqueObjectTypes } from '../lib/utils';
const insertDataIntoIndex = (
dataIndexConfig: any,
@ -143,35 +141,31 @@ export function createInstallRoute(
}
}
let createResults;
const { getImporter } = context.core.savedObjects;
const objectTypes = getUniqueObjectTypes(sampleDataset.savedObjects);
const savedObjectsClient = getSavedObjectsClient(context, objectTypes);
const importer = getImporter(savedObjectsClient);
const savedObjects = sampleDataset.savedObjects.map(({ version, ...obj }) => obj);
const readStream = Readable.from(savedObjects);
try {
const { getClient, typeRegistry } = context.core.savedObjects;
const includedHiddenTypes = sampleDataset.savedObjects
.map((object) => object.type)
.filter((supportedType) => typeRegistry.isHidden(supportedType));
const client = getClient({ includedHiddenTypes });
const savedObjects = sampleDataset.savedObjects as SavedObjectsBulkCreateObject[];
createResults = await client.bulkCreate(
savedObjects.map(({ version, ...savedObject }) => savedObject),
{ overwrite: true }
);
const { errors = [] } = await importer.import({
readStream,
overwrite: true,
createNewCopies: false,
});
if (errors.length > 0) {
const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify(
errors.map(({ type, id, error }) => ({ type, id, error })) // discard other fields
)}`;
logger.warn(errMsg);
return res.customError({ body: errMsg, statusCode: 500 });
}
} catch (err) {
const errMsg = `bulkCreate failed, error: ${err.message}`;
const errMsg = `import failed, error: ${err.message}`;
throw new Error(errMsg);
}
const errors = createResults.saved_objects.filter((savedObjectCreateResult) => {
return Boolean(savedObjectCreateResult.error);
});
if (errors.length > 0) {
const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify(
errors
)}`;
logger.warn(errMsg);
return res.customError({ body: errMsg, statusCode: 403 });
}
usageTracker.addInstall(params.id);
// FINALLY

View file

@ -6,75 +6,122 @@
* Side Public License, v 1.
*/
import { IRouter } from 'src/core/server';
import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
import type { IRouter, Logger, RequestHandlerContext } from 'src/core/server';
import type { AppLinkData, SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
import { createIndexName } from '../lib/create_index_name';
import type { FindSampleObjectsResponseObject } from '../lib/find_sample_objects';
import { findSampleObjects } from '../lib/find_sample_objects';
import { getUniqueObjectTypes } from '../lib/utils';
import { getSavedObjectsClient } from './utils';
const NOT_INSTALLED = 'not_installed';
const INSTALLED = 'installed';
const UNKNOWN = 'unknown';
export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSchema[]) => {
router.get({ path: '/api/sample_data', validate: false }, async (context, req, res) => {
const registeredSampleDatasets = sampleDatasets.map((sampleDataset) => {
return {
id: sampleDataset.id,
name: sampleDataset.name,
description: sampleDataset.description,
previewImagePath: sampleDataset.previewImagePath,
darkPreviewImagePath: sampleDataset.darkPreviewImagePath,
overviewDashboard: sampleDataset.overviewDashboard,
appLinks: sampleDataset.appLinks,
defaultIndex: sampleDataset.defaultIndex,
dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })),
status: sampleDataset.status,
statusMsg: sampleDataset.statusMsg,
};
});
const isInstalledPromises = registeredSampleDatasets.map(async (sampleDataset) => {
for (let i = 0; i < sampleDataset.dataIndices.length; i++) {
const dataIndexConfig = sampleDataset.dataIndices[i];
const index = createIndexName(sampleDataset.id, dataIndexConfig.id);
try {
const { body: indexExists } =
await context.core.elasticsearch.client.asCurrentUser.indices.exists({
index,
});
if (!indexExists) {
sampleDataset.status = NOT_INSTALLED;
return;
export const createListRoute = (
router: IRouter,
sampleDatasets: SampleDatasetSchema[],
appLinksMap: Map<string, AppLinkData[]>,
logger: Logger
) => {
router.get({ path: '/api/sample_data', validate: false }, async (context, _req, res) => {
const allExistingObjects = await findExistingSampleObjects(context, logger, sampleDatasets);
const registeredSampleDatasets = await Promise.all(
sampleDatasets.map(async (sampleDataset) => {
const existingObjects = allExistingObjects.get(sampleDataset.id)!;
const findObjectId = (type: string, id: string) =>
existingObjects.find((object) => object.type === type && object.id === id)
?.foundObjectId ?? id;
const appLinks = (appLinksMap.get(sampleDataset.id) ?? []).map((data) => {
const { sampleObject, getPath, label, icon } = data;
if (sampleObject === null) {
return { path: getPath(''), label, icon };
}
const objectId = findObjectId(sampleObject.type, sampleObject.id);
return { path: getPath(objectId), label, icon };
});
const sampleDataStatus = await getSampleDatasetStatus(
context,
allExistingObjects,
sampleDataset
);
const { body: count } = await context.core.elasticsearch.client.asCurrentUser.count({
index,
});
if (count.count === 0) {
sampleDataset.status = NOT_INSTALLED;
return;
}
} catch (err) {
sampleDataset.status = UNKNOWN;
sampleDataset.statusMsg = err.message;
return;
}
}
try {
await context.core.savedObjects.client.get('dashboard', sampleDataset.overviewDashboard);
} catch (err) {
if (context.core.savedObjects.client.errors.isNotFoundError(err)) {
sampleDataset.status = NOT_INSTALLED;
return;
}
return {
id: sampleDataset.id,
name: sampleDataset.name,
description: sampleDataset.description,
previewImagePath: sampleDataset.previewImagePath,
darkPreviewImagePath: sampleDataset.darkPreviewImagePath,
overviewDashboard: findObjectId('dashboard', sampleDataset.overviewDashboard),
appLinks,
defaultIndex: findObjectId('index-pattern', sampleDataset.defaultIndex),
dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })),
...sampleDataStatus,
};
})
);
sampleDataset.status = UNKNOWN;
sampleDataset.statusMsg = err.message;
return;
}
sampleDataset.status = INSTALLED;
});
await Promise.all(isInstalledPromises);
return res.ok({ body: registeredSampleDatasets });
});
};
type ExistingSampleObjects = Map<string, FindSampleObjectsResponseObject[]>;
async function findExistingSampleObjects(
context: RequestHandlerContext,
logger: Logger,
sampleDatasets: SampleDatasetSchema[]
) {
const objects = sampleDatasets
.map(({ savedObjects }) => savedObjects.map(({ type, id }) => ({ type, id })))
.flat();
const objectTypes = getUniqueObjectTypes(objects);
const client = getSavedObjectsClient(context, objectTypes);
const findSampleObjectsResult = await findSampleObjects({ client, logger, objects });
let objectCounter = 0;
return sampleDatasets.reduce<ExistingSampleObjects>((acc, { id, savedObjects }) => {
const datasetResults = savedObjects.map(() => findSampleObjectsResult[objectCounter++]);
return acc.set(id, datasetResults);
}, new Map());
}
// TODO: introduce PARTIALLY_INSTALLED status (#116677)
async function getSampleDatasetStatus(
context: RequestHandlerContext,
existingSampleObjects: ExistingSampleObjects,
sampleDataset: SampleDatasetSchema
): Promise<{ status: string; statusMsg?: string }> {
const dashboard = existingSampleObjects
.get(sampleDataset.id)!
.find(({ type, id }) => type === 'dashboard' && id === sampleDataset.overviewDashboard);
if (!dashboard?.foundObjectId) {
return { status: NOT_INSTALLED };
}
for (let i = 0; i < sampleDataset.dataIndices.length; i++) {
const dataIndexConfig = sampleDataset.dataIndices[i];
const index = createIndexName(sampleDataset.id, dataIndexConfig.id);
try {
const { body: indexExists } =
await context.core.elasticsearch.client.asCurrentUser.indices.exists({
index,
});
if (!indexExists) {
return { status: NOT_INSTALLED };
}
const { body: count } = await context.core.elasticsearch.client.asCurrentUser.count({
index,
});
if (count.count === 0) {
return { status: NOT_INSTALLED };
}
} catch (err) {
return { status: UNKNOWN, statusMsg: err.message };
}
}
return { status: INSTALLED };
}

View file

@ -6,16 +6,20 @@
* Side Public License, v 1.
*/
import { isBoom } from '@hapi/boom';
import { schema } from '@kbn/config-schema';
import _ from 'lodash';
import { IRouter } from 'src/core/server';
import type { IRouter, Logger } from 'src/core/server';
import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
import { createIndexName } from '../lib/create_index_name';
import { SampleDataUsageTracker } from '../usage/usage';
import { findSampleObjects } from '../lib/find_sample_objects';
import { getUniqueObjectTypes } from '../lib/utils';
import { getSavedObjectsClient } from './utils';
export function createUninstallRoute(
router: IRouter,
sampleDatasets: SampleDatasetSchema[],
logger: Logger,
usageTracker: SampleDataUsageTracker
): void {
router.delete(
@ -25,16 +29,7 @@ export function createUninstallRoute(
params: schema.object({ id: schema.string() }),
},
},
async (
{
core: {
elasticsearch: { client: esClient },
savedObjects: { getClient: getSavedObjectsClient, typeRegistry },
},
},
request,
response
) => {
async (context, request, response) => {
const sampleDataset = sampleDatasets.find(({ id }) => id === request.params.id);
if (!sampleDataset) {
@ -46,41 +41,46 @@ export function createUninstallRoute(
const index = createIndexName(sampleDataset.id, dataIndexConfig.id);
try {
await esClient.asCurrentUser.indices.delete({
index,
});
// TODO: don't delete the index if sample data exists in other spaces (#116677)
await context.core.elasticsearch.client.asCurrentUser.indices.delete({ index });
} catch (err) {
return response.customError({
statusCode: err.status,
body: {
message: `Unable to delete sample data index "${index}", error: ${err.message}`,
},
});
// if the index doesn't exist, ignore the error and proceed
if (err.body.status !== 404) {
return response.customError({
statusCode: err.body.status,
body: {
message: `Unable to delete sample data index "${index}", error: ${err.body.error.type}`,
},
});
}
}
}
const includedHiddenTypes = sampleDataset.savedObjects
.map((object) => object.type)
.filter((supportedType) => typeRegistry.isHidden(supportedType));
const objects = sampleDataset.savedObjects.map(({ type, id }) => ({ type, id }));
const objectTypes = getUniqueObjectTypes(objects);
const client = getSavedObjectsClient(context, objectTypes);
const findSampleObjectsResult = await findSampleObjects({ client, logger, objects });
const savedObjectsClient = getSavedObjectsClient({ includedHiddenTypes });
const deletePromises = sampleDataset.savedObjects.map(({ type, id }) =>
savedObjectsClient.delete(type, id)
const objectsToDelete = findSampleObjectsResult.filter(({ foundObjectId }) => foundObjectId);
const deletePromises = objectsToDelete.map(({ type, foundObjectId }) =>
client.delete(type, foundObjectId!).catch((err) => {
// if the object doesn't exist, ignore the error and proceed
if (isBoom(err) && err.output.statusCode === 404) {
return;
}
throw err;
})
);
try {
await Promise.all(deletePromises);
} catch (err) {
// ignore 404s since users could have deleted some of the saved objects via the UI
if (_.get(err, 'output.statusCode') !== 404) {
return response.customError({
statusCode: err.status,
body: {
message: `Unable to delete sample dataset saved objects, error: ${err.message}`,
},
});
}
return response.customError({
statusCode: err.body.status,
body: {
message: `Unable to delete sample dataset saved objects, error: ${err.body.error.type}`,
},
});
}
// track the usage operation in a non-blocking way

View file

@ -0,0 +1,17 @@
/*
* 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 { RequestHandlerContext } from 'src/core/server';
export function getSavedObjectsClient(context: RequestHandlerContext, objectTypes: string[]) {
const { getClient, typeRegistry } = context.core.savedObjects;
const includedHiddenTypes = objectTypes.filter((supportedType) =>
typeRegistry.isHidden(supportedType)
);
return getClient({ includedHiddenTypes });
}

View file

@ -11,8 +11,8 @@ import { SavedObject } from 'src/core/public';
import {
SampleDatasetProvider,
SampleDatasetSchema,
AppLinkSchema,
SampleDatasetDashboardPanel,
AppLinkData,
} from './lib/sample_dataset_registry_types';
import { sampleDataSchema } from './lib/sample_dataset_schema';
@ -27,6 +27,7 @@ import { registerSampleDatasetWithIntegration } from './lib/register_with_integr
export class SampleDataRegistry {
constructor(private readonly initContext: PluginInitializerContext) {}
private readonly sampleDatasets: SampleDatasetSchema[] = [];
private readonly appLinksMap = new Map<string, AppLinkData[]>();
private registerSampleDataSet(specProvider: SampleDatasetProvider) {
let value: SampleDatasetSchema;
@ -69,14 +70,10 @@ export class SampleDataRegistry {
this.initContext.logger.get('sample_data', 'usage')
);
const router = core.http.createRouter();
createListRoute(router, this.sampleDatasets);
createInstallRoute(
router,
this.sampleDatasets,
this.initContext.logger.get('sampleData'),
usageTracker
);
createUninstallRoute(router, this.sampleDatasets, usageTracker);
const logger = this.initContext.logger.get('sampleData');
createListRoute(router, this.sampleDatasets, this.appLinksMap, logger);
createInstallRoute(router, this.sampleDatasets, logger, usageTracker);
createUninstallRoute(router, this.sampleDatasets, logger, usageTracker);
this.registerSampleDataSet(flightsSpecProvider);
this.registerSampleDataSet(logsSpecProvider);
@ -100,7 +97,7 @@ export class SampleDataRegistry {
sampleDataset.savedObjects = sampleDataset.savedObjects.concat(savedObjects);
},
addAppLinksToSampleDataset: (id: string, appLinks: AppLinkSchema[]) => {
addAppLinksToSampleDataset: (id: string, appLinks: AppLinkData[]) => {
const sampleDataset = this.sampleDatasets.find((dataset) => {
return dataset.id === id;
});
@ -109,9 +106,8 @@ export class SampleDataRegistry {
throw new Error(`Unable to find sample dataset with id: ${id}`);
}
sampleDataset.appLinks = sampleDataset.appLinks
? sampleDataset.appLinks.concat(appLinks)
: [];
const existingAppLinks = this.appLinksMap.get(id) ?? [];
this.appLinksMap.set(id, [...existingAppLinks, ...appLinks]);
},
replacePanelInSampleDatasetDashboard: ({

View file

@ -19,6 +19,7 @@ export type {
ArtifactsSchema,
TutorialSchema,
TutorialProvider,
TutorialContext,
TutorialContextFactory,
ScopedTutorialContextFactory,
} from './lib/tutorials_registry_types';

View file

@ -7,6 +7,7 @@
*/
import expect from '@kbn/expect';
import type { Response } from 'superagent';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
@ -15,81 +16,142 @@ export default function ({ getService }: FtrProviderContext) {
const es = getService('es');
const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7;
const SPACES = ['default', 'other'];
const FLIGHTS_OVERVIEW_DASHBOARD_ID = '7adfa750-4c81-11e8-b3d7-01146121b73d'; // default ID of the flights overview dashboard
const FLIGHTS_CANVAS_APPLINK_PATH =
'/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0'; // includes default ID of the flights canvas applink path
describe('sample data apis', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
});
describe('list', () => {
it('should return list of sample data sets with installed status', async () => {
const resp = await supertest.get(`/api/sample_data`).set('kbn-xsrf', 'kibana').expect(200);
expect(resp.body).to.be.an('array');
expect(resp.body.length).to.be.above(0);
expect(resp.body[0].status).to.be('not_installed');
});
after(async () => {
await esArchiver.emptyKibanaIndex();
});
describe('install', () => {
it('should return 404 if id does not match any sample data sets', async () => {
await supertest.post(`/api/sample_data/xxxx`).set('kbn-xsrf', 'kibana').expect(404);
});
for (const space of SPACES) {
const apiPath = `/s/${space}/api/sample_data`;
it('should return 200 if success', async () => {
const resp = await supertest
.post(`/api/sample_data/flights`)
.set('kbn-xsrf', 'kibana')
.expect(200);
describe(`list in the ${space} space (before install)`, () => {
it('should return list of sample data sets with installed status', async () => {
const resp = await supertest.get(apiPath).set('kbn-xsrf', 'kibana').expect(200);
expect(resp.body).to.eql({
elasticsearchIndicesCreated: { kibana_sample_data_flights: 13059 },
kibanaSavedObjectsLoaded: 11,
const flightsData = findFlightsData(resp);
expect(flightsData.status).to.be('not_installed');
// Check and make sure the sample dataset reflects the default object IDs, because no sample data objects exist.
// Instead of checking each object ID, we check the dashboard and canvas app link as representatives.
expect(flightsData.overviewDashboard).to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).to.be(FLIGHTS_CANVAS_APPLINK_PATH);
});
});
it('should load elasticsearch index containing sample data with dates relative to current time', async () => {
const resp = await es.search<{ timestamp: string }>({
index: 'kibana_sample_data_flights',
describe(`install in the ${space} space`, () => {
it('should return 404 if id does not match any sample data sets', async () => {
await supertest.post(`${apiPath}/xxxx`).set('kbn-xsrf', 'kibana').expect(404);
});
const doc = resp.hits.hits[0];
const docMilliseconds = Date.parse(doc._source!.timestamp);
const nowMilliseconds = Date.now();
const delta = Math.abs(nowMilliseconds - docMilliseconds);
expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 4);
});
it('should return 200 if success', async () => {
const resp = await supertest
.post(`${apiPath}/flights`)
.set('kbn-xsrf', 'kibana')
.expect(200);
describe('parameters', () => {
it('should load elasticsearch index containing sample data with dates relative to now parameter', async () => {
const nowString = `2000-01-01T00:00:00`;
await supertest
.post(`/api/sample_data/flights?now=${nowString}`)
.set('kbn-xsrf', 'kibana');
expect(resp.body).to.eql({
elasticsearchIndicesCreated: { kibana_sample_data_flights: 13059 },
kibanaSavedObjectsLoaded: 11,
});
});
it('should load elasticsearch index containing sample data with dates relative to current time', async () => {
const resp = await es.search<{ timestamp: string }>({
index: 'kibana_sample_data_flights',
});
const doc = resp.hits.hits[0];
const docMilliseconds = Date.parse(doc._source!.timestamp);
const nowMilliseconds = Date.parse(nowString);
const nowMilliseconds = Date.now();
const delta = Math.abs(nowMilliseconds - docMilliseconds);
expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 4);
});
});
});
describe('uninstall', () => {
it('should uninstall sample data', async () => {
await supertest.delete(`/api/sample_data/flights`).set('kbn-xsrf', 'kibana').expect(204);
});
describe('parameters', () => {
it('should load elasticsearch index containing sample data with dates relative to now parameter', async () => {
const nowString = `2000-01-01T00:00:00`;
await supertest.post(`${apiPath}/flights?now=${nowString}`).set('kbn-xsrf', 'kibana');
it('should remove elasticsearch index containing sample data', async () => {
const resp = await es.indices.exists({
index: 'kibana_sample_data_flights',
const resp = await es.search<{ timestamp: string }>({
index: 'kibana_sample_data_flights',
});
const doc = resp.hits.hits[0];
const docMilliseconds = Date.parse(doc._source!.timestamp);
const nowMilliseconds = Date.parse(nowString);
const delta = Math.abs(nowMilliseconds - docMilliseconds);
expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 4);
});
});
expect(resp).to.be(false);
});
});
describe(`list in the ${space} space (after install)`, () => {
it('should return list of sample data sets with installed status', async () => {
const resp = await supertest.get(apiPath).set('kbn-xsrf', 'kibana').expect(200);
const flightsData = findFlightsData(resp);
expect(flightsData.status).to.be('installed');
// Check and make sure the sample dataset reflects the existing object IDs in each space.
// Instead of checking each object ID, we check the dashboard and canvas app link as representatives.
if (space === 'default') {
expect(flightsData.overviewDashboard).to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).to.be(FLIGHTS_CANVAS_APPLINK_PATH);
} else {
// the sample data objects installed in the 'other' space had their IDs regenerated upon import
expect(flightsData.overviewDashboard).not.to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).not.to.be(FLIGHTS_CANVAS_APPLINK_PATH);
}
});
});
}
for (const space of SPACES) {
const apiPath = `/s/${space}/api/sample_data`;
describe(`uninstall in the ${space} space`, () => {
it('should uninstall sample data', async () => {
// Note: the second time this happens, the index has already been removed, but the uninstall works anyway
await supertest.delete(`${apiPath}/flights`).set('kbn-xsrf', 'kibana').expect(204);
});
it('should remove elasticsearch index containing sample data', async () => {
const resp = await es.indices.exists({
index: 'kibana_sample_data_flights',
});
expect(resp).to.be(false);
});
});
describe(`list in the ${space} space (after uninstall)`, () => {
it('should return list of sample data sets with installed status', async () => {
const resp = await supertest.get(apiPath).set('kbn-xsrf', 'kibana').expect(200);
const flightsData = findFlightsData(resp);
expect(flightsData.status).to.be('not_installed');
// Check and make sure the sample dataset reflects the default object IDs, because no sample data objects exist.
// Instead of checking each object ID, we check the dashboard and canvas app link as representatives.
expect(flightsData.overviewDashboard).to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).to.be(FLIGHTS_CANVAS_APPLINK_PATH);
});
});
}
});
}
function findFlightsData(response: Response) {
expect(response.body).to.be.an('array');
expect(response.body.length).to.be.above(0);
// @ts-expect-error Binding element 'id' implicitly has an 'any' type.
const flightsData = response.body.find(({ id }) => id === 'flights');
if (!flightsData) {
throw new Error('Could not find flights data');
}
return flightsData;
}

View file

@ -28,11 +28,16 @@ export function loadSampleData(
return savedObject;
});
}
const getPath = (objectId: string) => `/app/canvas#/workpad/${objectId}`;
addSavedObjectsToSampleDataset('ecommerce', updateCanvasWorkpadTimestamps(ecommerceSavedObjects));
addAppLinksToSampleDataset('ecommerce', [
{
path: '/app/canvas#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e',
sampleObject: {
type: 'canvas-workpad',
id: 'workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e',
},
getPath,
icon: 'canvasApp',
label,
},
@ -41,7 +46,11 @@ export function loadSampleData(
addSavedObjectsToSampleDataset('flights', updateCanvasWorkpadTimestamps(flightsSavedObjects));
addAppLinksToSampleDataset('flights', [
{
path: '/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0',
sampleObject: {
type: 'canvas-workpad',
id: 'workpad-a474e74b-aedc-47c3-894a-db77e62c41e0',
},
getPath,
icon: 'canvasApp',
label,
},
@ -50,7 +59,11 @@ export function loadSampleData(
addSavedObjectsToSampleDataset('logs', updateCanvasWorkpadTimestamps(webLogsSavedObjects));
addAppLinksToSampleDataset('logs', [
{
path: '/app/canvas#/workpad/workpad-ad72a4e9-b422-480c-be6d-a64a0b79541d',
sampleObject: {
type: 'canvas-workpad',
id: 'workpad-ad72a4e9-b422-480c-be6d-a64a0b79541d',
},
getPath,
icon: 'canvasApp',
label,
},

View file

@ -348,7 +348,6 @@ const wsState: any = {
maxValuesPerDoc: 1,
minDocCount: 3,
},
indexPatternRefName: 'indexPattern_0',
};
export function registerEcommerceSampleData(sampleDataRegistry: SampleDataRegistrySetup) {
@ -365,16 +364,11 @@ export function registerEcommerceSampleData(sampleDataRegistry: SampleDataRegist
numVertices: 12,
version: 1,
wsState: JSON.stringify(JSON.stringify(wsState)),
legacyIndexPatternRef: 'kibana_sample_data_ecommerce',
},
references: [
{
name: 'indexPattern_0',
type: 'index-pattern',
id: 'kibana_sample_data_ecommerce',
},
],
references: [],
migrationVersion: {
'graph-workspace': '7.0.0',
'graph-workspace': '7.11.0',
},
updated_at: '2020-01-09T16:40:36.122Z',
},
@ -383,7 +377,11 @@ export function registerEcommerceSampleData(sampleDataRegistry: SampleDataRegist
export function registerEcommerceSampleDataLink(sampleDataRegistry: SampleDataRegistrySetup) {
sampleDataRegistry.addAppLinksToSampleDataset(datasetId, [
{
path: createWorkspacePath('46fa9d30-319c-11ea-bbe4-818d9c786051'),
sampleObject: {
type: 'graph-workspace',
id: '46fa9d30-319c-11ea-bbe4-818d9c786051',
},
getPath: createWorkspacePath,
label: i18n.translate('xpack.graph.sampleData.label', { defaultMessage: 'Graph' }),
icon: APP_ICON,
},

View file

@ -1602,7 +1602,6 @@ const wsState: any = {
maxValuesPerDoc: 1,
minDocCount: 3,
},
indexPatternRefName: 'indexPattern_0',
};
export function registerFlightsSampleData(sampleDataRegistry: SampleDataRegistrySetup) {
@ -1619,16 +1618,11 @@ export function registerFlightsSampleData(sampleDataRegistry: SampleDataRegistry
numVertices: 91,
version: 1,
wsState: JSON.stringify(JSON.stringify(wsState)),
legacyIndexPatternRef: 'kibana_sample_data_flights',
},
references: [
{
name: 'indexPattern_0',
type: 'index-pattern',
id: 'kibana_sample_data_flights',
},
],
references: [],
migrationVersion: {
'graph-workspace': '7.0.0',
'graph-workspace': '7.11.0',
},
updated_at: '2020-01-09T15:55:24.013Z',
},
@ -1637,7 +1631,11 @@ export function registerFlightsSampleData(sampleDataRegistry: SampleDataRegistry
export function registerFlightsSampleDataLink(sampleDataRegistry: SampleDataRegistrySetup) {
sampleDataRegistry.addAppLinksToSampleDataset(datasetId, [
{
path: createWorkspacePath('5dc018d0-32f8-11ea-bbe4-818d9c786051'),
sampleObject: {
type: 'graph-workspace',
id: '5dc018d0-32f8-11ea-bbe4-818d9c786051',
},
getPath: createWorkspacePath,
label: i18n.translate('xpack.graph.sampleData.label', { defaultMessage: 'Graph' }),
icon: APP_ICON,
},

View file

@ -419,7 +419,6 @@ const wsState: any = {
maxValuesPerDoc: 1,
minDocCount: 3,
},
indexPatternRefName: 'indexPattern_0',
};
export function registerLogsSampleData(sampleDataRegistry: SampleDataRegistrySetup) {
@ -436,16 +435,11 @@ export function registerLogsSampleData(sampleDataRegistry: SampleDataRegistrySet
numVertices: 27,
version: 1,
wsState: JSON.stringify(JSON.stringify(wsState)),
legacyIndexPatternRef: 'kibana_sample_data_logs',
},
references: [
{
name: 'indexPattern_0',
type: 'index-pattern',
id: 'kibana_sample_data_logs',
},
],
references: [],
migrationVersion: {
'graph-workspace': '7.0.0',
'graph-workspace': '7.11.0',
},
updated_at: '2020-01-09T16:40:36.122Z',
},
@ -454,7 +448,11 @@ export function registerLogsSampleData(sampleDataRegistry: SampleDataRegistrySet
export function registerLogsSampleDataLink(sampleDataRegistry: SampleDataRegistrySetup) {
sampleDataRegistry.addAppLinksToSampleDataset(datasetId, [
{
path: createWorkspacePath('e2141080-32fa-11ea-bbe4-818d9c786051'),
sampleObject: {
type: 'graph-workspace',
id: 'e2141080-32fa-11ea-bbe4-818d9c786051',
},
getPath: createWorkspacePath,
label: i18n.translate('xpack.graph.sampleData.label', { defaultMessage: 'Graph' }),
icon: APP_ICON,
},

View file

@ -158,7 +158,8 @@ export class InfraServerPlugin implements Plugin<InfraPluginSetup> {
plugins.home.sampleData.addAppLinksToSampleDataset('logs', [
{
path: `/app/logs`,
sampleObject: null, // indicates that there is no sample object associated with this app link's path
getPath: () => `/app/logs`,
label: logsSampleDataLinkLabel,
icon: 'logsApp',
},

View file

@ -77,7 +77,11 @@ export class MapsPlugin implements Plugin {
home.sampleData.addAppLinksToSampleDataset('ecommerce', [
{
path: getFullPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'),
sampleObject: {
type: MAP_SAVED_OBJECT_TYPE,
id: '2c9c1f60-1909-11e9-919b-ffe5949a18d2',
},
getPath: getFullPath,
label: sampleDataLinkLabel,
icon: APP_ICON,
},
@ -99,7 +103,11 @@ export class MapsPlugin implements Plugin {
home.sampleData.addAppLinksToSampleDataset('flights', [
{
path: getFullPath('5dd88580-1906-11e9-919b-ffe5949a18d2'),
sampleObject: {
type: MAP_SAVED_OBJECT_TYPE,
id: '5dd88580-1906-11e9-919b-ffe5949a18d2',
},
getPath: getFullPath,
label: sampleDataLinkLabel,
icon: APP_ICON,
},
@ -120,7 +128,11 @@ export class MapsPlugin implements Plugin {
home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects());
home.sampleData.addAppLinksToSampleDataset('logs', [
{
path: getFullPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'),
sampleObject: {
type: MAP_SAVED_OBJECT_TYPE,
id: 'de71f4f0-1902-11e9-919b-ffe5949a18d2',
},
getPath: getFullPath,
label: sampleDataLinkLabel,
icon: APP_ICON,
},

View file

@ -15,10 +15,16 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup)
defaultMessage: 'ML jobs',
});
const { addAppLinksToSampleDataset } = plugins.home.sampleData;
const getCreateJobPath = (jobId: string, dataViewId: string) =>
`/app/ml/modules/check_view_or_create?id=${jobId}&index=${dataViewId}`;
addAppLinksToSampleDataset('ecommerce', [
{
path: '/app/ml/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f',
sampleObject: {
type: 'index-pattern',
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
},
getPath: (objectId) => getCreateJobPath('sample_data_ecommerce', objectId),
label: sampleDataLinkLabel,
icon: 'machineLearningApp',
},
@ -26,7 +32,11 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup)
addAppLinksToSampleDataset('logs', [
{
path: '/app/ml/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247',
sampleObject: {
type: 'index-pattern',
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
},
getPath: (objectId) => getCreateJobPath('sample_data_weblogs', objectId),
label: sampleDataLinkLabel,
icon: 'machineLearningApp',
},