[SOM] display invalid references in the relationship flyout (#88814)

* return invalid relations and display them in SOM

* add FTR test
This commit is contained in:
Pierre Gayvallet 2021-02-01 11:03:44 +01:00 committed by GitHub
parent f0717a0a79
commit 84d49f1123
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 2093 additions and 770 deletions

View file

@ -6,4 +6,11 @@
* Public License, v 1.
*/
export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types';
export {
SavedObjectWithMetadata,
SavedObjectMetadata,
SavedObjectRelation,
SavedObjectRelationKind,
SavedObjectInvalidRelation,
SavedObjectGetRelationshipsResponse,
} from './types';

View file

@ -28,12 +28,26 @@ export type SavedObjectWithMetadata<T = unknown> = SavedObject<T> & {
meta: SavedObjectMetadata;
};
export type SavedObjectRelationKind = 'child' | 'parent';
/**
* Represents a relation between two {@link SavedObject | saved object}
*/
export interface SavedObjectRelation {
id: string;
type: string;
relationship: 'child' | 'parent';
relationship: SavedObjectRelationKind;
meta: SavedObjectMetadata;
}
export interface SavedObjectInvalidRelation {
id: string;
type: string;
relationship: SavedObjectRelationKind;
error: string;
}
export interface SavedObjectGetRelationshipsResponse {
relations: SavedObjectRelation[];
invalidRelations: SavedObjectInvalidRelation[];
}

View file

@ -6,6 +6,7 @@
* Public License, v 1.
*/
import { SavedObjectGetRelationshipsResponse } from '../types';
import { httpServiceMock } from '../../../../core/public/mocks';
import { getRelationships } from './get_relationships';
@ -22,13 +23,17 @@ describe('getRelationships', () => {
});
it('should handle successful responses', async () => {
httpMock.get.mockResolvedValue([1, 2]);
const serverResponse: SavedObjectGetRelationshipsResponse = {
relations: [],
invalidRelations: [],
};
httpMock.get.mockResolvedValue(serverResponse);
const response = await getRelationships(httpMock, 'dashboard', '1', [
'search',
'index-pattern',
]);
expect(response).toEqual([1, 2]);
expect(response).toEqual(serverResponse);
});
it('should handle errors', async () => {

View file

@ -8,19 +8,19 @@
import { HttpStart } from 'src/core/public';
import { get } from 'lodash';
import { SavedObjectRelation } from '../types';
import { SavedObjectGetRelationshipsResponse } from '../types';
export async function getRelationships(
http: HttpStart,
type: string,
id: string,
savedObjectTypes: string[]
): Promise<SavedObjectRelation[]> {
): Promise<SavedObjectGetRelationshipsResponse> {
const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent(
type
)}/${encodeURIComponent(id)}`;
try {
return await http.get<SavedObjectRelation[]>(url, {
return await http.get<SavedObjectGetRelationshipsResponse>(url, {
query: {
savedObjectTypes,
},

View file

@ -25,36 +25,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'search',
id: '1',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedSearches/1',
icon: 'search',
inAppUrl: {
path: '/app/discover#//1',
uiCapabilitiesPath: 'discover.show',
getRelationships: jest.fn().mockImplementation(() => ({
relations: [
{
type: 'search',
id: '1',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedSearches/1',
icon: 'search',
inAppUrl: {
path: '/app/discover#//1',
uiCapabilitiesPath: 'discover.show',
},
title: 'My Search Title',
},
title: 'My Search Title',
},
},
{
type: 'visualization',
id: '2',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/2',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
{
type: 'visualization',
id: '2',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/2',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
},
title: 'My Visualization Title',
},
title: 'My Visualization Title',
},
},
]),
],
invalidRelations: [],
})),
savedObject: {
id: '1',
type: 'index-pattern',
@ -92,36 +95,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'index-pattern',
id: '1',
relationship: 'child',
meta: {
editUrl: '/management/kibana/indexPatterns/patterns/1',
icon: 'indexPatternApp',
inAppUrl: {
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.indexPatterns',
getRelationships: jest.fn().mockImplementation(() => ({
relations: [
{
type: 'index-pattern',
id: '1',
relationship: 'child',
meta: {
editUrl: '/management/kibana/indexPatterns/patterns/1',
icon: 'indexPatternApp',
inAppUrl: {
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.indexPatterns',
},
title: 'My Index Pattern',
},
title: 'My Index Pattern',
},
},
{
type: 'visualization',
id: '2',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/2',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
{
type: 'visualization',
id: '2',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/2',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
},
title: 'My Visualization Title',
},
title: 'My Visualization Title',
},
},
]),
],
invalidRelations: [],
})),
savedObject: {
id: '1',
type: 'search',
@ -159,36 +165,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'dashboard',
id: '1',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedDashboards/1',
icon: 'dashboardApp',
inAppUrl: {
path: '/app/kibana#/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
getRelationships: jest.fn().mockImplementation(() => ({
relations: [
{
type: 'dashboard',
id: '1',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedDashboards/1',
icon: 'dashboardApp',
inAppUrl: {
path: '/app/kibana#/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
},
title: 'My Dashboard 1',
},
title: 'My Dashboard 1',
},
},
{
type: 'dashboard',
id: '2',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedDashboards/2',
icon: 'dashboardApp',
inAppUrl: {
path: '/app/kibana#/dashboard/2',
uiCapabilitiesPath: 'dashboard.show',
{
type: 'dashboard',
id: '2',
relationship: 'parent',
meta: {
editUrl: '/management/kibana/objects/savedDashboards/2',
icon: 'dashboardApp',
inAppUrl: {
path: '/app/kibana#/dashboard/2',
uiCapabilitiesPath: 'dashboard.show',
},
title: 'My Dashboard 2',
},
title: 'My Dashboard 2',
},
},
]),
],
invalidRelations: [],
})),
savedObject: {
id: '1',
type: 'visualization',
@ -226,36 +235,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'visualization',
id: '1',
relationship: 'child',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/1',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
getRelationships: jest.fn().mockImplementation(() => ({
relations: [
{
type: 'visualization',
id: '1',
relationship: 'child',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/1',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
},
title: 'My Visualization Title 1',
},
title: 'My Visualization Title 1',
},
},
{
type: 'visualization',
id: '2',
relationship: 'child',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/2',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
{
type: 'visualization',
id: '2',
relationship: 'child',
meta: {
editUrl: '/management/kibana/objects/savedVisualizations/2',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
},
title: 'My Visualization Title 2',
},
title: 'My Visualization Title 2',
},
},
]),
],
invalidRelations: [],
})),
savedObject: {
id: '1',
type: 'dashboard',
@ -324,4 +336,49 @@ describe('Relationships', () => {
expect(props.getRelationships).toHaveBeenCalled();
expect(component).toMatchSnapshot();
});
it('should render invalid relations', async () => {
const props: RelationshipsProps = {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => ({
relations: [],
invalidRelations: [
{
id: '1',
type: 'dashboard',
relationship: 'child',
error: 'Saved object [dashboard/1] not found',
},
],
})),
savedObject: {
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
meta: {
title: 'MyIndexPattern*',
icon: 'indexPatternApp',
editUrl: '#/management/kibana/indexPatterns/patterns/1',
inAppUrl: {
path: '/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.indexPatterns',
},
},
},
close: jest.fn(),
};
const component = shallowWithI18nProvider(<Relationships {...props} />);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(props.getRelationships).toHaveBeenCalled();
expect(component).toMatchSnapshot();
});
});

View file

@ -26,11 +26,17 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { IBasePath } from 'src/core/public';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types';
import {
SavedObjectWithMetadata,
SavedObjectRelationKind,
SavedObjectRelation,
SavedObjectInvalidRelation,
SavedObjectGetRelationshipsResponse,
} from '../../../types';
export interface RelationshipsProps {
basePath: IBasePath;
getRelationships: (type: string, id: string) => Promise<SavedObjectRelation[]>;
getRelationships: (type: string, id: string) => Promise<SavedObjectGetRelationshipsResponse>;
savedObject: SavedObjectWithMetadata;
close: () => void;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
@ -38,17 +44,47 @@ export interface RelationshipsProps {
}
export interface RelationshipsState {
relationships: SavedObjectRelation[];
relations: SavedObjectRelation[];
invalidRelations: SavedObjectInvalidRelation[];
isLoading: boolean;
error?: string;
}
const relationshipColumn = {
field: 'relationship',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnRelationshipName', {
defaultMessage: 'Direct relationship',
}),
dataType: 'string',
sortable: false,
width: '125px',
'data-test-subj': 'directRelationship',
render: (relationship: SavedObjectRelationKind) => {
return (
<EuiText size="s">
{relationship === 'parent' ? (
<FormattedMessage
id="savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue"
defaultMessage="Parent"
/>
) : (
<FormattedMessage
id="savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue"
defaultMessage="Child"
/>
)}
</EuiText>
);
},
};
export class Relationships extends Component<RelationshipsProps, RelationshipsState> {
constructor(props: RelationshipsProps) {
super(props);
this.state = {
relationships: [],
relations: [],
invalidRelations: [],
isLoading: false,
error: undefined,
};
@ -70,8 +106,11 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
this.setState({ isLoading: true });
try {
const relationships = await getRelationships(savedObject.type, savedObject.id);
this.setState({ relationships, isLoading: false, error: undefined });
const { relations, invalidRelations } = await getRelationships(
savedObject.type,
savedObject.id
);
this.setState({ relations, invalidRelations, isLoading: false, error: undefined });
} catch (err) {
this.setState({ error: err.message, isLoading: false });
}
@ -99,9 +138,83 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
);
}
renderRelationships() {
const { goInspectObject, savedObject, basePath } = this.props;
const { relationships, isLoading, error } = this.state;
renderInvalidRelationship() {
const { invalidRelations } = this.state;
if (!invalidRelations.length) {
return null;
}
const columns = [
{
field: 'type',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTypeName', {
defaultMessage: 'Type',
}),
width: '150px',
description: i18n.translate(
'savedObjectsManagement.objectsTable.relationships.columnTypeDescription',
{ defaultMessage: 'Type of the saved object' }
),
sortable: false,
'data-test-subj': 'relationshipsObjectType',
},
{
field: 'id',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnIdName', {
defaultMessage: 'Id',
}),
width: '150px',
description: i18n.translate(
'savedObjectsManagement.objectsTable.relationships.columnIdDescription',
{ defaultMessage: 'Id of the saved object' }
),
sortable: false,
'data-test-subj': 'relationshipsObjectId',
},
relationshipColumn,
{
field: 'error',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnErrorName', {
defaultMessage: 'Error',
}),
description: i18n.translate(
'savedObjectsManagement.objectsTable.relationships.columnErrorDescription',
{ defaultMessage: 'Error encountered with the relation' }
),
sortable: false,
'data-test-subj': 'relationshipsError',
},
];
return (
<>
<EuiCallOut
color="warning"
iconType="alert"
title={i18n.translate(
'savedObjectsManagement.objectsTable.relationships.invalidRelationShip',
{
defaultMessage: 'This saved object has some invalid relations.',
}
)}
/>
<EuiSpacer />
<EuiInMemoryTable
items={invalidRelations}
columns={columns as any}
pagination={true}
rowProps={() => ({
'data-test-subj': `invalidRelationshipsTableRow`,
})}
/>
<EuiSpacer />
</>
);
}
renderRelationshipsTable() {
const { goInspectObject, basePath, savedObject } = this.props;
const { relations, isLoading, error } = this.state;
if (error) {
return this.renderError();
@ -137,39 +250,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
);
},
},
{
field: 'relationship',
name: i18n.translate(
'savedObjectsManagement.objectsTable.relationships.columnRelationshipName',
{ defaultMessage: 'Direct relationship' }
),
dataType: 'string',
sortable: false,
width: '125px',
'data-test-subj': 'directRelationship',
render: (relationship: string) => {
if (relationship === 'parent') {
return (
<EuiText size="s">
<FormattedMessage
id="savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue"
defaultMessage="Parent"
/>
</EuiText>
);
}
if (relationship === 'child') {
return (
<EuiText size="s">
<FormattedMessage
id="savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue"
defaultMessage="Child"
/>
</EuiText>
);
}
},
},
relationshipColumn,
{
field: 'meta.title',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', {
@ -224,7 +305,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
];
const filterTypesMap = new Map(
relationships.map((relationship) => [
relations.map((relationship) => [
relationship.type,
{
value: relationship.type,
@ -277,7 +358,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
};
return (
<div>
<>
<EuiCallOut>
<p>
{i18n.translate(
@ -296,7 +377,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
</EuiCallOut>
<EuiSpacer />
<EuiInMemoryTable
items={relationships}
items={relations}
columns={columns as any}
pagination={true}
search={search}
@ -304,7 +385,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
'data-test-subj': `relationshipsTableRow`,
})}
/>
</div>
</>
);
}
@ -328,8 +409,10 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>{this.renderRelationships()}</EuiFlyoutBody>
<EuiFlyoutBody>
{this.renderInvalidRelationship()}
{this.renderRelationshipsTable()}
</EuiFlyoutBody>
</EuiFlyout>
);
}

View file

@ -6,4 +6,11 @@
* Public License, v 1.
*/
export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common';
export {
SavedObjectMetadata,
SavedObjectWithMetadata,
SavedObjectRelationKind,
SavedObjectRelation,
SavedObjectInvalidRelation,
SavedObjectGetRelationshipsResponse,
} from '../common';

View file

@ -6,10 +6,35 @@
* Public License, v 1.
*/
import type { SavedObject, SavedObjectError } from 'src/core/types';
import type { SavedObjectsFindResponse } from 'src/core/server';
import { findRelationships } from './find_relationships';
import { managementMock } from '../services/management.mock';
import { savedObjectsClientMock } from '../../../../core/server/mocks';
const createObj = (parts: Partial<SavedObject<any>>): SavedObject<any> => ({
id: 'id',
type: 'type',
attributes: {},
references: [],
...parts,
});
const createFindResponse = (objs: SavedObject[]): SavedObjectsFindResponse => ({
saved_objects: objs.map((obj) => ({ ...obj, score: 1 })),
total: objs.length,
per_page: 20,
page: 1,
});
const createError = (parts: Partial<SavedObjectError>): SavedObjectError => ({
error: 'error',
message: 'message',
metadata: {},
statusCode: 404,
...parts,
});
describe('findRelationships', () => {
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
let managementService: ReturnType<typeof managementMock.create>;
@ -19,7 +44,7 @@ describe('findRelationships', () => {
managementService = managementMock.create();
});
it('returns the child and parent references of the object', async () => {
it('calls the savedObjectClient APIs with the correct parameters', async () => {
const type = 'dashboard';
const id = 'some-id';
const references = [
@ -36,46 +61,35 @@ describe('findRelationships', () => {
];
const referenceTypes = ['some-type', 'another-type'];
savedObjectsClient.get.mockResolvedValue({
id,
type,
attributes: {},
references,
});
savedObjectsClient.get.mockResolvedValue(
createObj({
id,
type,
references,
})
);
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
createObj({
type: 'some-type',
id: 'ref-1',
attributes: {},
references: [],
},
{
}),
createObj({
type: 'another-type',
id: 'ref-2',
attributes: {},
references: [],
},
}),
],
});
savedObjectsClient.find.mockResolvedValue({
saved_objects: [
{
savedObjectsClient.find.mockResolvedValue(
createFindResponse([
createObj({
type: 'parent-type',
id: 'parent-id',
attributes: {},
score: 1,
references: [],
},
],
total: 1,
per_page: 20,
page: 1,
});
}),
])
);
const relationships = await findRelationships({
await findRelationships({
type,
id,
size: 20,
@ -101,8 +115,63 @@ describe('findRelationships', () => {
perPage: 20,
type: referenceTypes,
});
});
expect(relationships).toEqual([
it('returns the child and parent references of the object', async () => {
const type = 'dashboard';
const id = 'some-id';
const references = [
{
type: 'some-type',
id: 'ref-1',
name: 'ref 1',
},
{
type: 'another-type',
id: 'ref-2',
name: 'ref 2',
},
];
const referenceTypes = ['some-type', 'another-type'];
savedObjectsClient.get.mockResolvedValue(
createObj({
id,
type,
references,
})
);
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
createObj({
type: 'some-type',
id: 'ref-1',
}),
createObj({
type: 'another-type',
id: 'ref-2',
}),
],
});
savedObjectsClient.find.mockResolvedValue(
createFindResponse([
createObj({
type: 'parent-type',
id: 'parent-id',
}),
])
);
const { relations, invalidRelations } = await findRelationships({
type,
id,
size: 20,
client: savedObjectsClient,
referenceTypes,
savedObjectsManagement: managementService,
});
expect(relations).toEqual([
{
id: 'ref-1',
relationship: 'child',
@ -122,6 +191,70 @@ describe('findRelationships', () => {
meta: expect.any(Object),
},
]);
expect(invalidRelations).toHaveLength(0);
});
it('returns the invalid relations', async () => {
const type = 'dashboard';
const id = 'some-id';
const references = [
{
type: 'some-type',
id: 'ref-1',
name: 'ref 1',
},
{
type: 'another-type',
id: 'ref-2',
name: 'ref 2',
},
];
const referenceTypes = ['some-type', 'another-type'];
savedObjectsClient.get.mockResolvedValue(
createObj({
id,
type,
references,
})
);
const ref1Error = createError({ message: 'Not found' });
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
createObj({
type: 'some-type',
id: 'ref-1',
error: ref1Error,
}),
createObj({
type: 'another-type',
id: 'ref-2',
}),
],
});
savedObjectsClient.find.mockResolvedValue(createFindResponse([]));
const { relations, invalidRelations } = await findRelationships({
type,
id,
size: 20,
client: savedObjectsClient,
referenceTypes,
savedObjectsManagement: managementService,
});
expect(relations).toEqual([
{
id: 'ref-2',
relationship: 'child',
type: 'another-type',
meta: expect.any(Object),
},
]);
expect(invalidRelations).toEqual([
{ type: 'some-type', id: 'ref-1', relationship: 'child', error: ref1Error.message },
]);
});
it('uses the management service to consolidate the relationship objects', async () => {
@ -144,32 +277,24 @@ describe('findRelationships', () => {
uiCapabilitiesPath: 'uiCapabilitiesPath',
});
savedObjectsClient.get.mockResolvedValue({
id,
type,
attributes: {},
references,
});
savedObjectsClient.get.mockResolvedValue(
createObj({
id,
type,
references,
})
);
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
createObj({
type: 'some-type',
id: 'ref-1',
attributes: {},
references: [],
},
}),
],
});
savedObjectsClient.find.mockResolvedValue(createFindResponse([]));
savedObjectsClient.find.mockResolvedValue({
saved_objects: [],
total: 0,
per_page: 20,
page: 1,
});
const relationships = await findRelationships({
const { relations } = await findRelationships({
type,
id,
size: 20,
@ -183,7 +308,7 @@ describe('findRelationships', () => {
expect(managementService.getEditUrl).toHaveBeenCalledTimes(1);
expect(managementService.getInAppUrl).toHaveBeenCalledTimes(1);
expect(relationships).toEqual([
expect(relations).toEqual([
{
id: 'ref-1',
relationship: 'child',

View file

@ -9,7 +9,11 @@
import { SavedObjectsClientContract } from 'src/core/server';
import { injectMetaAttributes } from './inject_meta_attributes';
import { ISavedObjectsManagement } from '../services';
import { SavedObjectRelation, SavedObjectWithMetadata } from '../types';
import {
SavedObjectInvalidRelation,
SavedObjectWithMetadata,
SavedObjectGetRelationshipsResponse,
} from '../types';
export async function findRelationships({
type,
@ -25,17 +29,19 @@ export async function findRelationships({
client: SavedObjectsClientContract;
referenceTypes: string[];
savedObjectsManagement: ISavedObjectsManagement;
}): Promise<SavedObjectRelation[]> {
}): Promise<SavedObjectGetRelationshipsResponse> {
const { references = [] } = await client.get(type, id);
// Use a map to avoid duplicates, it does happen but have a different "name" in the reference
const referencedToBulkGetOpts = new Map(
references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }])
);
const childrenReferences = [
...new Map(
references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }])
).values(),
];
const [childReferencesResponse, parentReferencesResponse] = await Promise.all([
referencedToBulkGetOpts.size > 0
? client.bulkGet([...referencedToBulkGetOpts.values()])
childrenReferences.length > 0
? client.bulkGet(childrenReferences)
: Promise.resolve({ saved_objects: [] }),
client.find({
hasReference: { type, id },
@ -44,28 +50,37 @@ export async function findRelationships({
}),
]);
return childReferencesResponse.saved_objects
.map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
.map(extractCommonProperties)
.map(
(obj) =>
({
...obj,
relationship: 'child',
} as SavedObjectRelation)
)
.concat(
parentReferencesResponse.saved_objects
.map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
.map(extractCommonProperties)
.map(
(obj) =>
({
...obj,
relationship: 'parent',
} as SavedObjectRelation)
)
);
const invalidRelations: SavedObjectInvalidRelation[] = childReferencesResponse.saved_objects
.filter((obj) => Boolean(obj.error))
.map((obj) => ({
id: obj.id,
type: obj.type,
relationship: 'child',
error: obj.error!.message,
}));
const relations = [
...childReferencesResponse.saved_objects
.filter((obj) => !obj.error)
.map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
.map(extractCommonProperties)
.map((obj) => ({
...obj,
relationship: 'child' as const,
})),
...parentReferencesResponse.saved_objects
.map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
.map(extractCommonProperties)
.map((obj) => ({
...obj,
relationship: 'parent' as const,
})),
];
return {
relations,
invalidRelations,
};
}
function extractCommonProperties(savedObject: SavedObjectWithMetadata) {

View file

@ -38,7 +38,7 @@ export const registerRelationshipsRoute = (
? req.query.savedObjectTypes
: [req.query.savedObjectTypes];
const relations = await findRelationships({
const findRelationsResponse = await findRelationships({
type,
id,
client,
@ -48,7 +48,7 @@ export const registerRelationshipsRoute = (
});
return res.ok({
body: relations,
body: findRelationsResponse,
});
})
);

View file

@ -12,4 +12,11 @@ export interface SavedObjectsManagementPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SavedObjectsManagementPluginStart {}
export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common';
export {
SavedObjectMetadata,
SavedObjectWithMetadata,
SavedObjectRelationKind,
SavedObjectRelation,
SavedObjectInvalidRelation,
SavedObjectGetRelationshipsResponse,
} from '../common';

View file

@ -14,23 +14,32 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const responseSchema = schema.arrayOf(
schema.object({
id: schema.string(),
type: schema.string(),
relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
meta: schema.object({
title: schema.string(),
icon: schema.string(),
editUrl: schema.string(),
inAppUrl: schema.object({
path: schema.string(),
uiCapabilitiesPath: schema.string(),
}),
namespaceType: schema.string(),
const relationSchema = schema.object({
id: schema.string(),
type: schema.string(),
relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
meta: schema.object({
title: schema.string(),
icon: schema.string(),
editUrl: schema.string(),
inAppUrl: schema.object({
path: schema.string(),
uiCapabilitiesPath: schema.string(),
}),
})
);
namespaceType: schema.string(),
}),
});
const invalidRelationSchema = schema.object({
id: schema.string(),
type: schema.string(),
relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
error: schema.string(),
});
const responseSchema = schema.object({
relations: schema.arrayOf(relationSchema),
invalidRelations: schema.arrayOf(invalidRelationSchema),
});
describe('relationships', () => {
before(async () => {
@ -64,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357'))
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
type: 'index-pattern',
@ -108,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
type: 'index-pattern',
@ -145,8 +154,7 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
// TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail.
it.skip('should return 404 if search finds no results', async () => {
it('should return 404 if search finds no results', async () => {
await supertest
.get(relationshipsUrl('search', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'))
.expect(404);
@ -169,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357'))
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
type: 'visualization',
@ -210,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357', ['search']))
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
type: 'visualization',
@ -246,8 +254,7 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
// TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail.
it.skip('should return 404 if dashboard finds no results', async () => {
it('should return 404 if dashboard finds no results', async () => {
await supertest
.get(relationshipsUrl('dashboard', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'))
.expect(404);
@ -270,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357'))
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@ -313,7 +320,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@ -356,7 +363,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357'))
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@ -399,7 +406,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
expect(resp.body).to.eql([
expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@ -425,5 +432,48 @@ export default function ({ getService }: FtrProviderContext) {
.expect(404);
});
});
describe('invalid references', () => {
it('should validate the response schema', async () => {
const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200);
expect(() => {
responseSchema.validate(resp.body);
}).not.to.throwError();
});
it('should return the invalid relations', async () => {
const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200);
expect(resp.body).to.eql({
invalidRelations: [
{
error: 'Saved object [visualization/invalid-vis] not found',
id: 'invalid-vis',
relationship: 'child',
type: 'visualization',
},
],
relations: [
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
meta: {
editUrl:
'/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
title: 'Visualization',
},
relationship: 'child',
type: 'visualization',
},
],
});
});
});
});
}

View file

@ -0,0 +1,190 @@
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "timelion-sheet:190f3e90-2ec3-11e8-ba48-69fc4e41e1f6",
"source": {
"type": "timelion-sheet",
"updated_at": "2018-03-23T17:53:30.872Z",
"timelion-sheet": {
"title": "New TimeLion Sheet",
"hits": 0,
"description": "",
"timelion_sheet": [
".es(*)"
],
"timelion_interval": "auto",
"timelion_chart_height": 275,
"timelion_columns": 2,
"timelion_rows": 2,
"version": 1
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357",
"source": {
"type": "index-pattern",
"updated_at": "2018-03-28T01:08:34.290Z",
"index-pattern": {
"title": "saved_objects*",
"fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]"
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "config:7.0.0-alpha1",
"source": {
"type": "config",
"updated_at": "2018-03-28T01:08:39.248Z",
"config": {
"buildNum": 8467,
"telemetry:optIn": false,
"defaultIndex": "8963ca30-3224-11e8-a572-ffca06da1357"
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "search:960372e0-3224-11e8-a572-ffca06da1357",
"source": {
"type": "search",
"updated_at": "2018-03-28T01:08:55.182Z",
"search": {
"title": "OneRecord",
"description": "",
"hits": 0,
"columns": [
"_source"
],
"sort": [
"_score",
"desc"
],
"version": 1,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[]}"
}
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357",
"source": {
"type": "visualization",
"updated_at": "2018-03-28T01:09:18.936Z",
"visualization": {
"title": "VisualizationFromSavedSearch",
"visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}",
"uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
"description": "",
"savedSearchId": "960372e0-3224-11e8-a572-ffca06da1357",
"version": 1,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
}
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "visualization:add810b0-3224-11e8-a572-ffca06da1357",
"source": {
"type": "visualization",
"updated_at": "2018-03-28T01:09:35.163Z",
"visualization": {
"title": "Visualization",
"visState": "{\"title\":\"Visualization\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}",
"uiStateJSON": "{}",
"description": "",
"version": 1,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
}
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357",
"source": {
"type": "dashboard",
"updated_at": "2018-03-28T01:09:50.606Z",
"dashboard": {
"title": "Dashboard",
"hits": 0,
"description": "",
"panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"add810b0-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"2\",\"type\":\"visualization\",\"id\":\"a42c0580-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}}]",
"optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}",
"version": 1,
"timeRestore": false,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
}
}
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "dashboard:invalid-refs",
"source": {
"type": "dashboard",
"updated_at": "2018-03-28T01:09:50.606Z",
"dashboard": {
"title": "Dashboard",
"hits": 0,
"description": "",
"panelsJSON": "[]",
"optionsJSON": "{}",
"version": 1,
"timeRestore": false,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{}"
}
},
"references": [
{
"type":"visualization",
"id": "add810b0-3224-11e8-a572-ffca06da1357",
"name": "valid-ref"
},
{
"type":"visualization",
"id": "invalid-vis",
"name": "missing-ref"
}
]
}
}
}

View file

@ -12,6 +12,20 @@
"mappings": {
"dynamic": "strict",
"properties": {
"references": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"type": {
"type": "keyword"
}
},
"type": "nested"
},
"config": {
"dynamic": "true",
"properties": {
@ -280,4 +294,4 @@
}
}
}
}
}

View file

@ -12,5 +12,6 @@ export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderC
describe('saved objects management', function savedObjectsManagementAppTestSuite() {
this.tags('ciGroup7');
loadTestFile(require.resolve('./edit_saved_object'));
loadTestFile(require.resolve('./show_relationships'));
});
}

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']);
describe('saved objects relationships flyout', () => {
beforeEach(async () => {
await esArchiver.load('saved_objects_management/show_relationships');
});
afterEach(async () => {
await esArchiver.unload('saved_objects_management/show_relationships');
});
it('displays the invalid references', async () => {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
const objects = await PageObjects.savedObjects.getRowTitles();
expect(objects.includes('Dashboard with missing refs')).to.be(true);
await PageObjects.savedObjects.clickRelationshipsByTitle('Dashboard with missing refs');
const invalidRelations = await PageObjects.savedObjects.getInvalidRelations();
expect(invalidRelations).to.eql([
{
error: 'Saved object [visualization/missing-vis-ref] not found',
id: 'missing-vis-ref',
relationship: 'Child',
type: 'visualization',
},
{
error: 'Saved object [dashboard/missing-dashboard-ref] not found',
id: 'missing-dashboard-ref',
relationship: 'Child',
type: 'dashboard',
},
]);
});
});
}

View file

@ -0,0 +1,36 @@
{
"type": "doc",
"value": {
"index": ".kibana",
"type": "doc",
"id": "dashboard:dash-with-missing-refs",
"source": {
"dashboard": {
"title": "Dashboard with missing refs",
"hits": 0,
"description": "",
"panelsJSON": "[]",
"optionsJSON": "{}",
"version": 1,
"timeRestore": false,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{}"
}
},
"type": "dashboard",
"references": [
{
"type": "visualization",
"id": "missing-vis-ref",
"name": "some missing ref"
},
{
"type": "dashboard",
"id": "missing-dashboard-ref",
"name": "some other missing ref"
}
],
"updated_at": "2019-01-22T19:32:47.232Z"
}
}
}

View file

@ -0,0 +1,473 @@
{
"type": "index",
"value": {
"index": ".kibana",
"settings": {
"index": {
"number_of_shards": "1",
"auto_expand_replicas": "0-1",
"number_of_replicas": "0"
}
},
"mappings": {
"dynamic": "strict",
"properties": {
"references": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"type": {
"type": "keyword"
}
},
"type": "nested"
},
"apm-telemetry": {
"properties": {
"has_any_services": {
"type": "boolean"
},
"services_per_agent": {
"properties": {
"go": {
"type": "long",
"null_value": 0
},
"java": {
"type": "long",
"null_value": 0
},
"js-base": {
"type": "long",
"null_value": 0
},
"nodejs": {
"type": "long",
"null_value": 0
},
"python": {
"type": "long",
"null_value": 0
},
"ruby": {
"type": "long",
"null_value": 0
}
}
}
}
},
"canvas-workpad": {
"dynamic": "false",
"properties": {
"@created": {
"type": "date"
},
"@timestamp": {
"type": "date"
},
"id": {
"type": "text",
"index": false
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
},
"config": {
"dynamic": "true",
"properties": {
"accessibility:disableAnimations": {
"type": "boolean"
},
"buildNum": {
"type": "keyword"
},
"dateFormat:tz": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"defaultIndex": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"telemetry:optIn": {
"type": "boolean"
}
}
},
"dashboard": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"optionsJSON": {
"type": "text"
},
"panelsJSON": {
"type": "text"
},
"refreshInterval": {
"properties": {
"display": {
"type": "keyword"
},
"pause": {
"type": "boolean"
},
"section": {
"type": "integer"
},
"value": {
"type": "integer"
}
}
},
"timeFrom": {
"type": "keyword"
},
"timeRestore": {
"type": "boolean"
},
"timeTo": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"map": {
"properties": {
"bounds": {
"type": "geo_shape",
"tree": "quadtree"
},
"description": {
"type": "text"
},
"layerListJSON": {
"type": "text"
},
"mapStateJSON": {
"type": "text"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"graph-workspace": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"numLinks": {
"type": "integer"
},
"numVertices": {
"type": "integer"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
},
"wsState": {
"type": "text"
}
}
},
"index-pattern": {
"properties": {
"fieldFormatMap": {
"type": "text"
},
"fields": {
"type": "text"
},
"intervalName": {
"type": "keyword"
},
"notExpandable": {
"type": "boolean"
},
"sourceFilters": {
"type": "text"
},
"timeFieldName": {
"type": "keyword"
},
"title": {
"type": "text"
},
"type": {
"type": "keyword"
},
"typeMeta": {
"type": "keyword"
}
}
},
"kql-telemetry": {
"properties": {
"optInCount": {
"type": "long"
},
"optOutCount": {
"type": "long"
}
}
},
"migrationVersion": {
"dynamic": "true",
"properties": {
"index-pattern": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"space": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
"namespace": {
"type": "keyword"
},
"search": {
"properties": {
"columns": {
"type": "keyword"
},
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"sort": {
"type": "keyword"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"server": {
"properties": {
"uuid": {
"type": "keyword"
}
}
},
"space": {
"properties": {
"_reserved": {
"type": "boolean"
},
"color": {
"type": "keyword"
},
"description": {
"type": "text"
},
"disabledFeatures": {
"type": "keyword"
},
"initials": {
"type": "keyword"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
},
"spaceId": {
"type": "keyword"
},
"telemetry": {
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"timelion-sheet": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"timelion_chart_height": {
"type": "integer"
},
"timelion_columns": {
"type": "integer"
},
"timelion_interval": {
"type": "keyword"
},
"timelion_other_interval": {
"type": "keyword"
},
"timelion_rows": {
"type": "integer"
},
"timelion_sheet": {
"type": "text"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"type": {
"type": "keyword"
},
"updated_at": {
"type": "date"
},
"url": {
"properties": {
"accessCount": {
"type": "long"
},
"accessDate": {
"type": "date"
},
"createDate": {
"type": "date"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
},
"visualization": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"savedSearchId": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
},
"visState": {
"type": "text"
}
}
}
}
}
}
}

View file

@ -257,6 +257,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
});
}
async getInvalidRelations() {
const rows = await testSubjects.findAll('invalidRelationshipsTableRow');
return mapAsync(rows, async (row) => {
const objectType = await row.findByTestSubject('relationshipsObjectType');
const objectId = await row.findByTestSubject('relationshipsObjectId');
const relationship = await row.findByTestSubject('directRelationship');
const error = await row.findByTestSubject('relationshipsError');
return {
type: await objectType.getVisibleText(),
id: await objectId.getVisibleText(),
relationship: await relationship.getVisibleText(),
error: await error.getVisibleText(),
};
});
}
async getTableSummary() {
const table = await testSubjects.find('savedObjectsTable');
const $ = await table.parseDomContent();