diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 72fa6c5553f77..ec4ffab4cce85 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -241,6 +241,7 @@ export class DocLinksService { kibanaSearchSettings: `${KIBANA_DOCS}advanced-options.html#kibana-search-settings`, visualizationSettings: `${KIBANA_DOCS}advanced-options.html#kibana-visualization-settings`, timelionSettings: `${KIBANA_DOCS}advanced-options.html#kibana-timelion-settings`, + savedObjectsApiList: `${KIBANA_DOCS}saved-objects-api.html#saved-objects-api`, }, ml: { guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index dfda251e28779..068883c429e61 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -27,9 +27,6 @@ export const createDashboardSavedObjectType = ({ getTitle(obj) { return obj.attributes.title; }, - getEditUrl(obj) { - return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; - }, getInAppUrl(obj) { return { path: `/app/dashboards#/view/${encodeURIComponent(obj.id)}`, diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index 070f0253f17e0..46284f3cf33b6 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -20,9 +20,6 @@ export const searchSavedObjectType: SavedObjectsType = { getTitle(obj) { return obj.attributes.title; }, - getEditUrl(obj) { - return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`; - }, getInAppUrl(obj) { return { path: `/app/discover#/view/${encodeURIComponent(obj.id)}`, diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 43cd5c93aa807..aa7797fbc95b5 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -62,12 +62,11 @@ export const mountManagementSection = async ({ - + }> diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/__snapshots__/saved_object_view.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/__snapshots__/saved_object_view.test.tsx.snap new file mode 100644 index 0000000000000..8e3a954677b87 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/__snapshots__/saved_object_view.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SavedObjectEdition should render normally 1`] = ` + + + +
+ + + + + + +`; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap index c72e000e95e75..26ff23b50d87b 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap @@ -11,17 +11,7 @@ exports[`Intro component renders correctly 1`] = ` > - } + pageTitle="Inspect saved object" rightSideItems={ Array [ @@ -48,13 +38,9 @@ exports[`Intro component renders correctly 1`] = ` size="s" > , ] @@ -64,17 +50,7 @@ exports[`Intro component renders correctly 1`] = ` className="euiPageHeader euiPageHeader--bottomBorder euiPageHeader--responsive euiPageHeader--center" > - } + pageTitle="Inspect saved object" responsive={true} rightSideItems={ Array [ @@ -89,7 +65,7 @@ exports[`Intro component renders correctly 1`] = ` id="savedObjectsManagement.view.viewItemButtonLabel" values={ Object { - "title": "search", + "title": "saved object", } } /> @@ -102,13 +78,9 @@ exports[`Intro component renders correctly 1`] = ` size="s" > , ] @@ -136,17 +108,7 @@ exports[`Intro component renders correctly 1`] = `

- - Edit search - + Inspect saved object

@@ -234,11 +196,11 @@ exports[`Intro component renders correctly 1`] = ` id="savedObjectsManagement.view.viewItemButtonLabel" values={ Object { - "title": "search", + "title": "saved object", } } > - View search + View saved object
@@ -316,15 +278,11 @@ exports[`Intro component renders correctly 1`] = ` className="euiButton__text" > - Delete search + Delete diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/inspect.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/inspect.test.tsx.snap new file mode 100644 index 0000000000000..f35030ad736cc --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/inspect.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Inspect component renders correctly 1`] = ` + +`; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap deleted file mode 100644 index 9c9349b0524c0..0000000000000 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Intro component renders correctly 1`] = ` - - - } - > -
-
- - - - Proceed with caution! - - -
- -
- -
-
- - Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. - -
-
-
-
-
-
-
-
-`; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap index 4227351f8e94d..c55583679f264 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap @@ -1,353 +1,541 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = ` - + } > - - } +
- - + +
+ +
-
- -
-
- - The index pattern associated with this object no longer exists. - -
-
- + + The index pattern associated with this object no longer exists. + +
+
+ + Saved objects APIs + , + } + } + > + If you know what this error means, you can use the + - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
+ + Saved objects APIs + + + + + + + (opens in a new tab or window) + + + + + + to fix it — otherwise click the delete button above. +
-
-
- -
- - +
+ + + + +
`; exports[`NotFoundErrors component renders correctly for index-pattern-field type 1`] = ` - + } > - - } +
- - + +
+ +
-
- -
-
- - A field associated with this object no longer exists in the index pattern. - -
-
- + + A field associated with this object no longer exists in the index pattern. + +
+
+ + Saved objects APIs + , + } + } + > + If you know what this error means, you can use the + - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
+ + Saved objects APIs + + + + + + + (opens in a new tab or window) + + + + + + to fix it — otherwise click the delete button above. +
-
-
- -
- - +
+ + + + +
`; exports[`NotFoundErrors component renders correctly for search type 1`] = ` - + } > - - } +
- - + +
+ +
-
- -
-
- - The saved search associated with this object no longer exists. - -
-
- + + The saved search associated with this object no longer exists. + +
+
+ + Saved objects APIs + , + } + } + > + If you know what this error means, you can use the + - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
+ + Saved objects APIs + + + + + + + (opens in a new tab or window) + + + + + + to fix it — otherwise click the delete button above. +
-
-
- -
- - +
+ + + + +
`; exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` - + } > - - } +
- - + +
+ +
-
- -
-
-
- +
+ + Saved objects APIs + , + } + } + > + If you know what this error means, you can use the + - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
+ + Saved objects APIs + + + + + + + (opens in a new tab or window) + + + + + + to fix it — otherwise click the delete button above. +
- -
- -
- - +
+ +
+
+
+
`; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx deleted file mode 100644 index 1fd70c65fd9c4..0000000000000 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@kbn/i18n/react'; -import { Field } from './field'; -import { FieldState, FieldType } from '../../types'; - -describe('Field component', () => { - const mountField = (props: { - type: FieldType; - name: string; - value: any; - disabled: boolean; - state?: FieldState; - onChange: (name: string, state: FieldState) => void; - }) => - mount( - - - - ).find('Field'); - - const defaultProps = { - type: 'text' as FieldType, - name: 'field', - value: '', - disabled: false, - state: undefined, - onChange: (name: string, state: FieldState) => undefined, - }; - - it('uses the field name as the label', () => { - let mounted = mountField({ ...defaultProps, name: 'some.name' }); - expect(mounted.find('EuiFormLabel').text()).toMatchInlineSnapshot(`"some.name"`); - - mounted = mountField({ ...defaultProps, name: 'someother.name' }); - expect(mounted.find('EuiFormLabel').text()).toMatchInlineSnapshot(`"someother.name"`); - }); - - it('renders a EuiCodeEditor for json type', () => { - const mounted = mountField({ ...defaultProps, type: 'json' }); - expect(mounted.exists('EuiCodeEditor')).toEqual(true); - }); - - it('renders a EuiCodeEditor for array type', () => { - const mounted = mountField({ ...defaultProps, type: 'array' }); - expect(mounted.exists('EuiCodeEditor')).toEqual(true); - }); - - it('renders a EuiSwitch for boolean type', () => { - const mounted = mountField({ ...defaultProps, type: 'boolean' }); - expect(mounted.exists('EuiSwitch')).toEqual(true); - }); - - it('display correct label for boolean type depending on value', () => { - let mounted = mountField({ ...defaultProps, type: 'boolean', value: true }); - expect(mounted.find('EuiSwitch').text()).toMatchInlineSnapshot(`"On"`); - - mounted = mountField({ ...defaultProps, type: 'boolean', value: false }); - expect(mounted.find('EuiSwitch').text()).toMatchInlineSnapshot(`"Off"`); - }); - - it('renders a EuiFieldNumber for number type', () => { - const mounted = mountField({ ...defaultProps, type: 'number' }); - expect(mounted.exists('EuiFieldNumber')).toEqual(true); - }); - - it('renders a EuiFieldText for text type', () => { - const mounted = mountField({ ...defaultProps, type: 'text' }); - expect(mounted.exists('EuiFieldText')).toEqual(true); - }); - - it('renders a EuiFieldText as fallback', () => { - const mounted = mountField({ ...defaultProps, type: 'unknown-type' as any }); - expect(mounted.exists('EuiFieldText')).toEqual(true); - }); -}); diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx deleted file mode 100644 index 2273527dd63f1..0000000000000 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { PureComponent } from 'react'; -import { EuiFieldNumber, EuiFieldText, EuiFormRow, EuiSwitch, EuiCodeEditor } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldState, FieldType } from '../../types'; - -interface FieldProps { - type: FieldType; - name: string; - value: any; - disabled: boolean; - state?: FieldState; - onChange: (name: string, state: FieldState) => void; -} - -export class Field extends PureComponent { - render() { - const { name } = this.props; - - return ( - - {this.renderField()} - - ); - } - - onCodeEditorChange(targetValue: any) { - const { name, onChange } = this.props; - let invalid = false; - try { - JSON.parse(targetValue); - } catch (e) { - invalid = true; - } - onChange(name, { - value: targetValue, - invalid, - }); - } - - onFieldChange(targetValue: any) { - const { name, type, onChange } = this.props; - - let newParsedValue = targetValue; - let invalid = false; - if (type === 'number') { - try { - newParsedValue = Number(newParsedValue); - } catch (e) { - invalid = true; - } - } - onChange(name, { - value: newParsedValue, - invalid, - }); - } - - renderField() { - const { type, name, state, disabled } = this.props; - const currentValue = state?.value ?? this.props.value; - - switch (type) { - case 'number': - return ( - this.onFieldChange(e.target.value)} - disabled={disabled} - data-test-subj={`savedObjects-editField-${name}`} - /> - ); - case 'boolean': - return ( - - ) : ( - - ) - } - checked={!!currentValue} - onChange={(e) => this.onFieldChange(e.target.checked)} - disabled={disabled} - data-test-subj={`savedObjects-editField-${name}`} - /> - ); - case 'json': - case 'array': - return ( -
- this.onCodeEditorChange(value)} - width="100%" - height="auto" - minLines={6} - maxLines={30} - isReadOnly={disabled} - setOptions={{ - showLineNumbers: true, - tabSize: 2, - useSoftTabs: true, - }} - editorProps={{ - $blockScrolling: Infinity, - }} - showGutter={true} - /> -
- ); - default: - return ( - this.onFieldChange(e.target.value)} - disabled={disabled} - data-test-subj={`savedObjects-editField-${name}`} - /> - ); - } - } - - private get fieldId() { - const { name } = this.props; - return `savedObjects-editField-${name}`; - } -} diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx deleted file mode 100644 index 8e33e0fbdc5e2..0000000000000 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { Component } from 'react'; -import { - EuiForm, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiSpacer, -} from '@elastic/eui'; -import { set } from '@elastic/safer-lodash-set'; -import { cloneDeep } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectsClientContract } from '../../../../../../core/public'; -import { SavedObjectLoader } from '../../../../../saved_objects/public'; -import { Field } from './field'; -import { ObjectField, FieldState, SubmittedFormData } from '../../types'; -import { createFieldList } from '../../../lib'; -import { SavedObjectWithMetadata } from '../../../types'; - -interface FormProps { - object: SavedObjectWithMetadata; - service: SavedObjectLoader; - savedObjectsClient: SavedObjectsClientContract; - editionEnabled: boolean; - onSave: (form: SubmittedFormData) => Promise; -} - -interface FormState { - fields: ObjectField[]; - fieldStates: Record; - submitting: boolean; -} - -export class Form extends Component { - constructor(props: FormProps) { - super(props); - this.state = { - fields: [], - fieldStates: {}, - submitting: false, - }; - } - - componentDidMount() { - const { object, service } = this.props; - - const fields = createFieldList(object, service); - - this.setState({ - fields, - }); - } - - render() { - const { editionEnabled, service } = this.props; - const { fields, fieldStates, submitting } = this.state; - const isValid = this.isFormValid(); - return ( - - {fields.map((field) => ( - - ))} - - - {editionEnabled && ( - - - - - - )} - - - - - - - - - ); - } - - handleFieldChange = (name: string, newState: FieldState) => { - this.setState({ - fieldStates: { - ...this.state.fieldStates, - [name]: newState, - }, - }); - }; - - isFormValid() { - const { fieldStates } = this.state; - return !Object.values(fieldStates).some((state) => state.invalid === true); - } - - onCancel = () => { - window.history.back(); - }; - - onSubmit = async () => { - const { object, onSave } = this.props; - const { fields, fieldStates } = this.state; - - if (!this.isFormValid()) { - return; - } - - this.setState({ - submitting: true, - }); - - const source = cloneDeep(object.attributes as any); - fields.forEach((field) => { - let value = fieldStates[field.name]?.value ?? field.value; - - if (field.type === 'array' && typeof value === 'string') { - value = JSON.parse(value); - } - - set(source, field.name, value); - }); - - // we extract the `references` field that does not belong to attributes - const { references, ...attributes } = source; - - await onSave({ attributes, references }); - - this.setState({ - submitting: false, - }); - }; -} diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx index dbbd2485096f9..796632f9747a9 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx @@ -19,6 +19,7 @@ describe('Intro component', () => { type: string; viewUrl: string; onDeleteClick: () => void; + title?: string; }) => mount( @@ -42,32 +43,11 @@ describe('Intro component', () => { expect(mounted).toMatchSnapshot(); }); - it('displays correct title depending on canEdit', () => { - let mounted = mountHeader({ - ...defaultProps, - canEdit: true, - }); - expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Edit search"`); - - mounted = mountHeader({ - ...defaultProps, - canEdit: false, - }); - expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"View search"`); - }); - - it('displays correct title depending on type', () => { - let mounted = mountHeader({ - ...defaultProps, - type: 'some-type', - }); - expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Edit some-type"`); - - mounted = mountHeader({ - ...defaultProps, - type: 'another-type', - }); - expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Edit another-type"`); + it('displays correct title if one is provided', () => { + let mounted = mountHeader({ ...defaultProps, title: 'my saved search' }); + expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Inspect my saved search"`); + mounted = mountHeader({ ...defaultProps, title: 'my other saved search' }); + expect(mounted.find('h1').text()).toMatchInlineSnapshot(`"Inspect my other saved search"`); }); it('only displays delete button if canDelete is true', () => { diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx index 9a13a1d232cb3..10374b839ca48 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx @@ -9,43 +9,24 @@ import React from 'react'; import { EuiButton, EuiPageHeader } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; interface HeaderProps { - canEdit: boolean; canDelete: boolean; canViewInApp: boolean; - type: string; viewUrl: string; onDeleteClick: () => void; + title?: string; } -const renderConditionalTitle = (canEdit: boolean, type: string) => - canEdit ? ( - - ) : ( - - ); - -export const Header = ({ - canEdit, - canDelete, - canViewInApp, - type, - viewUrl, - onDeleteClick, -}: HeaderProps) => { +export const Header = ({ canDelete, canViewInApp, viewUrl, onDeleteClick, title }: HeaderProps) => { return ( ), @@ -71,8 +52,7 @@ export const Header = ({ > ), diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts b/src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts index ffffd589d5ef8..55322afb3fabb 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts @@ -7,6 +7,5 @@ */ export { Header } from './header'; +export { Inspect } from './inspect'; export { NotFoundErrors } from './not_found_errors'; -export { Intro } from './intro'; -export { Form } from './form'; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/inspect.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/inspect.test.tsx new file mode 100644 index 0000000000000..433728baf6fc1 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/inspect.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { ShallowWrapper } from 'enzyme'; +import { shallowWithI18nProvider } from '@kbn/test/jest'; +import { Inspect, InspectProps } from './inspect'; +import { SavedObjectWithMetadata } from '../../../../common'; + +describe('Inspect component', () => { + let defaultProps: { object: SavedObjectWithMetadata }; + const shallowRender = (overrides: Partial = {}) => { + return shallowWithI18nProvider( + + ) as unknown as ShallowWrapper; + }; + beforeEach(() => { + defaultProps = { + object: { + id: '1', + type: 'index-pattern', + attributes: { + title: `MyIndexPattern*`, + }, + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/indexPatterns/patterns/1', + inAppUrl: { + path: '/management/kibana/indexPatterns/patterns/1', + uiCapabilitiesPath: 'management.kibana.indexPatterns', + }, + }, + references: [], + }, + }; + }); + + it('renders correctly', async () => { + const component = shallowRender(); + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + const codeEditorComponent = component.find('CodeEditor'); + expect(codeEditorComponent).toMatchSnapshot(); + }); + + it("does not include `meta` in the value that's rendered", async () => { + const component = shallowRender(); + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + const codeEditorComponent = component.find('CodeEditor'); + // find could return nothing + const editorValue = codeEditorComponent + ? (codeEditorComponent.prop('value') as unknown as string) + : ''; + // we assert against the expected object props rather than asserting that 'meta' is removed + expect(Object.keys(JSON.parse(editorValue))).toEqual([ + 'id', + 'type', + 'attributes', + 'references', + ]); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/inspect.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/inspect.tsx new file mode 100644 index 0000000000000..58d6da8ce935b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/inspect.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { XJsonLang } from '@kbn/monaco'; +import { omit } from 'lodash'; +import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '../../../../../kibana_react/public'; +import { SavedObjectWithMetadata } from '../../../../common'; + +export interface InspectProps { + object: SavedObjectWithMetadata; +} +const codeEditorAriaLabel = (title: string) => + i18n.translate('savedObjectsManagement.view.inspectCodeEditorAriaLabel', { + defaultMessage: 'inspect { title }', + values: { + title, + }, + }); +const copyToClipboardLabel = i18n.translate('savedObjectsManagement.view.copyToClipboardLabel', { + defaultMessage: 'Copy to clipboard', +}); + +export const Inspect: FC = ({ object }) => { + const title = object.meta.title || 'saved object'; + + const objectAsJsonString = useMemo(() => JSON.stringify(omit(object, 'meta'), null, 2), [object]); + + return ( + + +
+ + {(copy) => ( + + {copyToClipboardLabel} + + )} + + +
+ +
+
+ ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx deleted file mode 100644 index 0b869743f03c7..0000000000000 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@kbn/i18n/react'; -import { Intro } from './intro'; - -describe('Intro component', () => { - it('renders correctly', () => { - const mounted = mount( - - - - ); - expect(mounted.find('Intro')).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx deleted file mode 100644 index 0431208d34ad5..0000000000000 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const Intro = () => { - return ( - - } - iconType="alert" - color="warning" - > -
- -
-
- ); -}; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx index 767cc1ac59f47..5eab44cb416e9 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx @@ -10,44 +10,49 @@ import React from 'react'; import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; import { NotFoundErrors } from './not_found_errors'; +import { docLinksServiceMock } from '../../../../../../core/public/mocks'; describe('NotFoundErrors component', () => { const mountError = (type: string) => mount( - + ).find('NotFoundErrors'); it('renders correctly for search type', () => { const mounted = mountError('search'); - expect(mounted).toMatchSnapshot(); + const callOut = mounted.find('EuiCallOut'); + expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, go ahead and fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); it('renders correctly for index-pattern type', () => { const mounted = mountError('index-pattern'); - expect(mounted).toMatchSnapshot(); + const callOut = mounted.find('EuiCallOut'); + expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectThe index pattern associated with this object no longer exists.If you know what this error means, go ahead and fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectThe index pattern associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); it('renders correctly for index-pattern-field type', () => { const mounted = mountError('index-pattern-field'); - expect(mounted).toMatchSnapshot(); + const callOut = mounted.find('EuiCallOut'); + expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectA field associated with this object no longer exists in the index pattern.If you know what this error means, go ahead and fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectA field associated with this object no longer exists in the index pattern.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); it('renders correctly for unknown type', () => { const mounted = mountError('unknown'); - expect(mounted).toMatchSnapshot(); + const callOut = mounted.find('EuiCallOut'); + expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectIf you know what this error means, go ahead and fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectIf you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx index 2bce7b387a7e4..e3a349b1f4aa5 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx @@ -8,13 +8,23 @@ import React from 'react'; import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; +import { DocLinksStart } from '../../../../../../core/public'; interface NotFoundErrors { type: string; + docLinks: DocLinksStart['links']; } +const savedObjectsApisLinkText = i18n.translate( + 'savedObjectsManagement.view.howToFixErrorDescriptionLinkText', + { + defaultMessage: 'Saved objects APIs', + } +); -export const NotFoundErrors = ({ type }: NotFoundErrors) => { +export const NotFoundErrors = ({ type, docLinks }: NotFoundErrors) => { const getMessage = () => { switch (type) { case 'search': @@ -58,7 +68,18 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => {
+ {savedObjectsApisLinkText} + + ), + }} />
diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.scss b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.scss new file mode 100644 index 0000000000000..656f93468db90 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.scss @@ -0,0 +1,3 @@ +.savedObjectsManagementObjectView { + height: 100%; +} diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.test.mocks.ts new file mode 100644 index 0000000000000..7243955100690 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +jest.doMock('lodash', () => { + const original = jest.requireActual('lodash'); + return { + ...original, + get: (func: Function) => { + function get(this: any, args: any[]) { + return func.apply(this, args); + } + return get; + }, + }; +}); + +export const bulkGetObjectsMock = jest.fn(); +jest.doMock('../../lib/bulk_get_objects', () => ({ + bulkGetObjects: bulkGetObjectsMock, +})); diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.test.tsx new file mode 100644 index 0000000000000..13a806361e543 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.test.tsx @@ -0,0 +1,329 @@ +/* + * 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 { bulkGetObjectsMock } from './saved_object_view.test.mocks'; + +import React from 'react'; +import { ShallowWrapper } from 'enzyme'; +import { shallowWithI18nProvider } from '@kbn/test/jest'; + +import { + httpServiceMock, + overlayServiceMock, + notificationServiceMock, + savedObjectsServiceMock, + applicationServiceMock, + uiSettingsServiceMock, + scopedHistoryMock, + docLinksServiceMock, +} from '../../../../../core/public/mocks'; + +import { + SavedObjectEdition, + SavedObjectEditionProps, + SavedObjectEditionState, +} from './saved_object_view'; + +const resolvePromises = () => new Promise((resolve) => process.nextTick(resolve)); + +describe('SavedObjectEdition', () => { + let defaultProps: SavedObjectEditionProps; + let http: ReturnType; + let overlays: ReturnType; + let notifications: ReturnType; + let savedObjects: ReturnType; + let uiSettings: ReturnType; + let history: ReturnType; + let applications: ReturnType; + let docLinks: ReturnType; + + const shallowRender = (overrides: Partial = {}) => { + return shallowWithI18nProvider( + + ) as unknown as ShallowWrapper< + SavedObjectEditionProps, + SavedObjectEditionState, + SavedObjectEdition + >; + }; + + beforeEach(() => { + http = httpServiceMock.createStartContract(); + overlays = overlayServiceMock.createStartContract(); + notifications = notificationServiceMock.createStartContract(); + savedObjects = savedObjectsServiceMock.createStartContract(); + uiSettings = uiSettingsServiceMock.createStartContract(); + history = scopedHistoryMock.create(); + docLinks = docLinksServiceMock.createStartContract(); + applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + }; + + http.post.mockResolvedValue([]); + + defaultProps = { + id: '1', + savedObjectType: 'dashboard', + http, + capabilities: applications.capabilities, + overlays, + notifications, + savedObjectsClient: savedObjects.client, + history, + uiSettings, + docLinks: docLinks.links, + }; + + bulkGetObjectsMock.mockImplementation(() => [{}]); + }); + + it('should render normally', async () => { + bulkGetObjectsMock.mockImplementation(() => + Promise.resolve([ + { + id: '1', + type: 'dashboard', + attributes: { + title: `MyDashboard*`, + }, + meta: { + title: `MyDashboard*`, + icon: 'dashboardApp', + inAppUrl: { + path: '/app/dashboards#/view/1', + uiCapabilitiesPath: 'management.kibana.dashboard', + }, + }, + }, + ]) + ); + const component = shallowRender(); + // Ensure all promises resolve + await resolvePromises(); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should add danger toast when bulk get fails', async () => { + bulkGetObjectsMock.mockImplementation(() => + Promise.resolve([ + { + error: { + message: 'Not found', + }, + }, + ]) + ); + const component = shallowRender({ notFoundType: 'does_not_exist' }); + + await resolvePromises(); + + component.update(); + + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + }); + + it('should add danger toast when bulk get throws', async () => { + bulkGetObjectsMock.mockImplementation(() => Promise.reject(new Error('fail'))); + const component = shallowRender({ notFoundType: 'does_not_exist' }); + + await resolvePromises(); + + component.update(); + + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + }); + + it('should pass the correct props to the child components', async () => { + const savedObjectItem = { + id: '1', + type: 'index-pattern', + attributes: { + title: `MyIndexPattern*`, + }, + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/indexPatterns/patterns/1', + inAppUrl: { + path: '/management/kibana/indexPatterns/patterns/1', + uiCapabilitiesPath: 'management.kibana.indexPatterns', + }, + hiddenType: false, + }, + }; + bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem])); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: true, + }, + }; + const component = shallowRender({ + capabilities: applications.capabilities, + }); + + await resolvePromises(); + + component.update(); + const headerComponent = component.find('Header'); + expect(headerComponent.prop('canViewInApp')).toBe(true); + expect(headerComponent.prop('canDelete')).toBe(true); + expect(headerComponent.prop('viewUrl')).toEqual('/management/kibana/indexPatterns/patterns/1'); + const inspectComponent = component.find('Inspect'); + expect(inspectComponent.prop('object')).toEqual(savedObjectItem); + }); + + it("does not render Inspect if there isn't an object", async () => { + bulkGetObjectsMock.mockImplementation(() => Promise.resolve([])); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: true, + }, + }; + const component = shallowRender({ + capabilities: applications.capabilities, + }); + + await resolvePromises(); + + component.update(); + const inspectComponent = component.find('Inspect'); + expect(inspectComponent).toEqual({}); + }); + + describe('delete', () => { + const savedObjectItem = { + id: '1', + type: 'index-pattern', + attributes: { + title: `MyIndexPattern*`, + }, + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/indexPatterns/patterns/1', + inAppUrl: { + path: '/management/kibana/indexPatterns/patterns/1', + uiCapabilitiesPath: 'management.kibana.indexPatterns', + }, + hiddenType: false, + }, + }; + + it('should display a confirmation message on deleting the saved object', async () => { + bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem])); + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + delete: jest.fn().mockImplementation(() => ({})), + }; + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: true, + }, + }; + overlays.openConfirm.mockResolvedValue(false); + const component = shallowRender({ + capabilities: applications.capabilities, + savedObjectsClient: mockSavedObjectsClient, + overlays, + }); + + await resolvePromises(); + + component.update(); + component.instance().delete(); + expect(overlays.openConfirm).toHaveBeenCalledWith( + 'This action permanently removes the object from Kibana.', + { + buttonColor: 'danger', + confirmButtonText: 'Delete', + title: `Delete '${savedObjectItem.meta.title}'?`, + } + ); + }); + + it('should route back if action is confirm and user accepted', async () => { + bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem])); + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + delete: jest.fn().mockImplementation(() => ({})), + }; + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: true, + }, + }; + overlays.openConfirm.mockResolvedValue(true); + const component = shallowRender({ + capabilities: applications.capabilities, + savedObjectsClient: mockSavedObjectsClient, + overlays, + }); + + await resolvePromises(); + + component.update(); + component.instance().delete(); + expect(overlays.openConfirm).toHaveBeenCalledTimes(1); + expect(history.location.pathname).toEqual('/'); + }); + + it('should not enable delete if the saved object is hidden', async () => { + bulkGetObjectsMock.mockImplementation(() => + Promise.resolve([{ ...savedObjectItem, meta: { hiddenType: true } }]) + ); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: true, + }, + }; + const component = shallowRender({ + capabilities: applications.capabilities, + }); + + await resolvePromises(); + + component.update(); + expect(component.find('Header').prop('canDelete')).toBe(false); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 079a1c07da197..64b6e27309dd2 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -8,7 +8,9 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { get } from 'lodash'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; import { Capabilities, SavedObjectsClientContract, @@ -16,27 +18,27 @@ import { NotificationsStart, ScopedHistory, HttpSetup, + IUiSettingsClient, + DocLinksStart, } from '../../../../../core/public'; -import { ISavedObjectsManagementServiceRegistry } from '../../services'; -import { Header, NotFoundErrors, Intro, Form } from './components'; -import { canViewInApp, bulkGetObjects } from '../../lib'; -import { SubmittedFormData } from '../types'; +import { Header, Inspect, NotFoundErrors } from './components'; +import { bulkGetObjects } from '../../lib/bulk_get_objects'; import { SavedObjectWithMetadata } from '../../types'; - -interface SavedObjectEditionProps { +import './saved_object_view.scss'; +export interface SavedObjectEditionProps { id: string; + savedObjectType: string; http: HttpSetup; - serviceName: string; - serviceRegistry: ISavedObjectsManagementServiceRegistry; capabilities: Capabilities; overlays: OverlayStart; notifications: NotificationsStart; notFoundType?: string; savedObjectsClient: SavedObjectsClientContract; history: ScopedHistory; + uiSettings: IUiSettingsClient; + docLinks: DocLinksStart['links']; } - -interface SavedObjectEditionState { +export interface SavedObjectEditionState { type: string; object?: SavedObjectWithMetadata; } @@ -45,7 +47,6 @@ const unableFindSavedObjectNotificationMessage = i18n.translate( 'savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage', { defaultMessage: 'Unable to find saved object' } ); - export class SavedObjectEdition extends Component< SavedObjectEditionProps, SavedObjectEditionState @@ -53,8 +54,7 @@ export class SavedObjectEdition extends Component< constructor(props: SavedObjectEditionProps) { super(props); - const { serviceRegistry, serviceName } = props; - const type = serviceRegistry.get(serviceName)!.service.type; + const { savedObjectType: type } = props; this.state = { object: undefined, @@ -85,54 +85,46 @@ export class SavedObjectEdition extends Component< }); } + canViewInApp(capabilities: Capabilities, obj?: SavedObjectWithMetadata) { + return obj && obj.meta.inAppUrl + ? get(capabilities, obj?.meta.inAppUrl?.uiCapabilitiesPath, false) && + Boolean(obj?.meta.inAppUrl?.path) + : false; + } + render() { - const { capabilities, notFoundType, serviceRegistry, http, serviceName, savedObjectsClient } = - this.props; - const { type } = this.state; + const { capabilities, notFoundType, http, uiSettings, docLinks } = this.props; const { object } = this.state; - const { edit: canEdit, delete: canDelete } = capabilities.savedObjectsManagement as Record< - string, - boolean - >; - const canView = canViewInApp(capabilities, type) && Boolean(object?.meta.inAppUrl?.path); - const service = serviceRegistry.get(serviceName)!.service; - + const { delete: canDelete } = capabilities.savedObjectsManagement as Record; + const canView = this.canViewInApp(capabilities, object); return ( -
-
this.delete()} - viewUrl={http.basePath.prepend(object?.meta.inAppUrl?.path || '')} - /> - - {notFoundType && ( - <> - - - - )} - {canEdit && ( - <> - - - - )} - {object && ( - <> - -
+ + +
this.delete()} + viewUrl={http.basePath.prepend(object?.meta.inAppUrl?.path || '')} + title={object?.meta.title} /> - - )} -
+ + {notFoundType && ( + + + + )} + {object && ( + + + + )} + + ); } @@ -167,15 +159,6 @@ export class SavedObjectEdition extends Component< } } - saveChanges = async ({ attributes, references }: SubmittedFormData) => { - const { savedObjectsClient, notifications } = this.props; - const { object, type } = this.state; - - await savedObjectsClient.update(object!.type, object!.id, attributes, { references }); - notifications.toasts.addSuccess(`Updated '${attributes.title}' ${type} object`); - this.redirectToListing(); - }; - redirectToListing() { this.props.history.push('/'); } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 46ea319ebc168..327a9635462cc 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -192,7 +192,6 @@ exports[`SavedObjectsTable should render normally 1`] = ` Object { "id": "2", "meta": Object { - "editUrl": "/management/kibana/objects/savedSearches/2", "icon": "search", "inAppUrl": Object { "path": "/discover/2", @@ -205,7 +204,6 @@ exports[`SavedObjectsTable should render normally 1`] = ` Object { "id": "3", "meta": Object { - "editUrl": "/management/kibana/objects/savedDashboards/3", "icon": "dashboardApp", "inAppUrl": Object { "path": "/dashboard/3", @@ -218,7 +216,6 @@ exports[`SavedObjectsTable should render normally 1`] = ` Object { "id": "4", "meta": Object { - "editUrl": "/management/kibana/objects/savedVisualizations/4", "icon": "visualizeApp", "inAppUrl": Object { "path": "/edit/4", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index bb426c91e827c..8325e7dc886e8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -146,7 +146,6 @@ exports[`Table prevents saved objects from being deleted 1`] = ` Object { "actions": Array [ Object { - "available": [Function], "data-test-subj": "savedObjectsTableAction-inspect", "description": "Inspect this saved object", "icon": "inspect", @@ -362,7 +361,6 @@ exports[`Table should render normally 1`] = ` Object { "actions": Array [ Object { - "available": [Function], "data-test-subj": "savedObjectsTableAction-inspect", "description": "Inspect this saved object", "icon": "inspect", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 30d172b89256e..acdab1db40370 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -11,7 +11,6 @@ import { importFileMock, resolveImportErrorsMock } from './flyout.test.mocks'; import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test/jest'; import { coreMock, httpServiceMock } from '../../../../../../core/public/mocks'; -import { serviceRegistryMock } from '../../../services/service_registry.mock'; import { Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; import { dataPluginMock } from '../../../../../data/public/mocks'; @@ -49,7 +48,6 @@ describe('Flyout', () => { } as any, http, allowedTypes: ['search', 'index-pattern', 'visualization'], - serviceRegistry: serviceRegistryMock.create(), search, basePath, }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 26de8c5f8b25a..607b3aeeac275 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -43,7 +43,6 @@ import { processImportResponse, ProcessedImportResponse, } from '../../../lib'; -import { ISavedObjectsManagementServiceRegistry } from '../../../services'; import { FailedImportConflict, RetryDecision } from '../../../lib/resolve_import_errors'; import { OverwriteModal } from './overwrite_modal'; import { ImportModeControl, ImportMode } from './import_mode_control'; @@ -53,7 +52,6 @@ const CREATE_NEW_COPIES_DEFAULT = false; const OVERWRITE_ALL_DEFAULT = true; export interface FlyoutProps { - serviceRegistry: ISavedObjectsManagementServiceRegistry; allowedTypes: string[]; close: () => void; done: () => void; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx index 8b07351f6c2c2..c24faf4e12687 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import './import_summary.scss'; import _ from 'lodash'; import React, { Fragment, FC, useMemo } from 'react'; import { @@ -30,6 +29,7 @@ import type { IBasePath, } from 'kibana/public'; import { getDefaultTitle, getSavedObjectLabel, FailedImport } from '../../../lib'; +import './import_summary.scss'; const DEFAULT_ICON = 'apps'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index f9171c7928dbe..8eb48ac91da66 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -298,7 +298,7 @@ export class Relationships extends Component goInspectObject(object), - available: (object: SavedObjectWithMetadata) => !!object.meta.editUrl, + available: (object: SavedObjectWithMetadata) => !!(object.type && object.id), }, ], }, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 4e4bd51c4bb84..0645c0955f7ac 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -243,7 +243,6 @@ export class Table extends PureComponent { type: 'icon', icon: 'inspect', onClick: (object) => goInspectObject(object), - available: (object) => !!object.meta.editUrl, 'data-test-subj': 'savedObjectsTableAction-inspect', }, { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 21a629097cbb4..025a7a320327f 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -28,7 +28,6 @@ import { applicationServiceMock, } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../data/public/mocks'; -import { serviceRegistryMock } from '../../services/service_registry.mock'; import { actionServiceMock } from '../../services/action_service.mock'; import { columnServiceMock } from '../../services/column_service.mock'; import { @@ -122,7 +121,6 @@ describe('SavedObjectsTable', () => { defaultProps = { allowedTypes, - serviceRegistry: serviceRegistryMock.create(), actionRegistry: actionServiceMock.createStart(), columnRegistry: columnServiceMock.createStart(), savedObjectsClient: savedObjects.client, @@ -159,7 +157,6 @@ describe('SavedObjectsTable', () => { meta: { title: `MySearch`, icon: 'search', - editUrl: '/management/kibana/objects/savedSearches/2', inAppUrl: { path: '/discover/2', uiCapabilitiesPath: 'discover.show', @@ -172,7 +169,6 @@ describe('SavedObjectsTable', () => { meta: { title: `MyDashboard`, icon: 'dashboardApp', - editUrl: '/management/kibana/objects/savedDashboards/3', inAppUrl: { path: '/dashboard/3', uiCapabilitiesPath: 'dashboard.show', @@ -185,7 +181,6 @@ describe('SavedObjectsTable', () => { meta: { title: `MyViz`, icon: 'visualizeApp', - editUrl: '/management/kibana/objects/savedVisualizations/4', inAppUrl: { path: '/edit/4', uiCapabilitiesPath: 'visualize.show', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index d4067cc21c2be..5001b52e819c2 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -37,7 +37,6 @@ import { } from '../../lib'; import { SavedObjectWithMetadata } from '../../types'; import { - ISavedObjectsManagementServiceRegistry, SavedObjectsManagementActionServiceStart, SavedObjectsManagementColumnServiceStart, } from '../../services'; @@ -58,7 +57,6 @@ interface ExportAllOption { export interface SavedObjectsTableProps { allowedTypes: string[]; - serviceRegistry: ISavedObjectsManagementServiceRegistry; actionRegistry: SavedObjectsManagementActionServiceStart; columnRegistry: SavedObjectsManagementColumnServiceStart; savedObjectsClient: SavedObjectsClientContract; @@ -540,7 +538,6 @@ export class SavedObjectsTable extends Component void; history: ScopedHistory; }) => { - const { service: serviceName, id } = useParams<{ service: string; id: string }>(); + const { type, id } = useParams<{ type: string; id: string }>(); const capabilities = coreStart.application.capabilities; + const dockLinks = coreStart.docLinks.links; const { search } = useLocation(); const query = parse(search); - const service = serviceRegistry.get(serviceName); useEffect(() => { setBreadcrumbs([ @@ -42,27 +40,31 @@ const SavedObjectsEditionPage = ({ href: '/', }, { - text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { - defaultMessage: 'Edit {savedObjectType}', - values: { savedObjectType: service?.service.type ?? 'object' }, + text: i18n.translate('savedObjectsManagement.breadcrumb.inspect', { + defaultMessage: 'Inspect {savedObjectType}', + values: { savedObjectType: type }, }), }, ]); - }, [setBreadcrumbs, service]); + }, [setBreadcrumbs, type]); return ( - + ); diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index f22f0333ec229..dccf33efc5317 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -75,13 +75,11 @@ const SavedObjectsTablePage = ({ spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent, [spacesApi] ); - return ( { - const { editUrl } = savedObject.meta; - if (editUrl) { - return coreStart.application.navigateToUrl( - coreStart.http.basePath.prepend(`/app${editUrl}`) - ); - } + const savedObjectEditUrl = savedObject.meta.editUrl + ? `/app${savedObject.meta.editUrl}` + : `/app/management/kibana/objects/${savedObject.type}/${savedObject.id}`; + coreStart.application.navigateToUrl(coreStart.http.basePath.prepend(savedObjectEditUrl)); }} canGoInApp={(savedObject) => { const { inAppUrl } = savedObject.meta; diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index 880e277294fc3..53027d5d5046c 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -20,9 +20,6 @@ export const visualizationSavedObjectType: SavedObjectsType = { getTitle(obj) { return obj.attributes.title; }, - getEditUrl(obj) { - return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`; - }, getInAppUrl(obj) { return { path: `/app/visualize#/edit/${encodeURIComponent(obj.id)}`, diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index f3f4b56cdccf5..9a5f94f9d8b9d 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -180,8 +180,6 @@ export default function ({ getService }: FtrProviderContext) { icon: 'discoverApp', title: 'OneRecord', hiddenType: false, - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', @@ -200,8 +198,6 @@ export default function ({ getService }: FtrProviderContext) { icon: 'dashboardApp', title: 'Dashboard', hiddenType: false, - editUrl: - '/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'dashboard.show', @@ -220,8 +216,6 @@ export default function ({ getService }: FtrProviderContext) { icon: 'visualizeApp', title: 'VisualizationFromSavedSearch', hiddenType: false, - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -232,8 +226,6 @@ export default function ({ getService }: FtrProviderContext) { icon: 'visualizeApp', title: 'Visualization', hiddenType: false, - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index 5fbd5cad8ec84..8ee5005348bcd 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -21,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { meta: schema.object({ title: schema.string(), icon: schema.string(), - editUrl: schema.string(), + editUrl: schema.maybe(schema.string()), inAppUrl: schema.object({ path: schema.string(), uiCapabilitiesPath: schema.string(), @@ -103,8 +103,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { title: 'VisualizationFromSavedSearch', icon: 'visualizeApp', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -147,8 +145,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'visualizeApp', title: 'VisualizationFromSavedSearch', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -192,8 +188,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'visualizeApp', title: 'Visualization', - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -209,8 +203,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'visualizeApp', title: 'VisualizationFromSavedSearch', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -234,8 +226,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'visualizeApp', title: 'Visualization', - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -251,8 +241,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'visualizeApp', title: 'VisualizationFromSavedSearch', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -296,8 +284,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'discoverApp', title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', @@ -313,8 +299,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'dashboardApp', title: 'Dashboard', - editUrl: - '/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'dashboard.show', @@ -340,8 +324,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'discoverApp', title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', @@ -385,8 +367,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'discoverApp', title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', @@ -402,8 +382,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'visualizeApp', title: 'Visualization', - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', @@ -429,8 +407,6 @@ export default function ({ getService }: FtrProviderContext) { meta: { icon: 'discoverApp', title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', inAppUrl: { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', @@ -475,8 +451,6 @@ export default function ({ getService }: FtrProviderContext) { { 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', diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts deleted file mode 100644 index f4bf45c0b7f70..0000000000000 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']); - const browser = getService('browser'); - const find = getService('find'); - - const setFieldValue = async (fieldName: string, value: string) => { - return testSubjects.setValue(`savedObjects-editField-${fieldName}`, value); - }; - - const getFieldValue = async (fieldName: string) => { - return testSubjects.getAttribute(`savedObjects-editField-${fieldName}`, 'value'); - }; - - const setAceEditorFieldValue = async (fieldName: string, fieldValue: string) => { - const editorId = `savedObjects-editField-${fieldName}-aceEditor`; - await find.clickByCssSelector(`#${editorId}`); - return browser.execute( - (editor: string, value: string) => { - return (window as any).ace.edit(editor).setValue(value); - }, - editorId, - fieldValue - ); - }; - - const getAceEditorFieldValue = async (fieldName: string) => { - const editorId = `savedObjects-editField-${fieldName}-aceEditor`; - await find.clickByCssSelector(`#${editorId}`); - return browser.execute((editor: string) => { - return (window as any).ace.edit(editor).getValue() as string; - }, editorId); - }; - - const focusAndClickButton = async (buttonSubject: string) => { - const button = await testSubjects.find(buttonSubject); - await button.scrollIntoViewIfNecessary(); - await delay(10); - await button.focus(); - await delay(10); - await button.click(); - // Allow some time for the transition/animations to occur before assuming the click is done - await delay(10); - }; - - describe('saved objects edition page', () => { - beforeEach(async () => { - await esArchiver.load( - 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' - ); - }); - - afterEach(async () => { - await esArchiver.unload( - 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' - ); - }); - - it('allows to update the saved object when submitting', async () => { - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - - let objects = await PageObjects.savedObjects.getRowTitles(); - expect(objects.includes('A Dashboard')).to.be(true); - - await PageObjects.common.navigateToUrl( - 'management', - 'kibana/objects/savedDashboards/i-exist', - { - shouldUseHashForSubUrl: false, - } - ); - - await testSubjects.existOrFail('savedObjectEditSave'); - - expect(await getFieldValue('title')).to.eql('A Dashboard'); - - await setFieldValue('title', 'Edited Dashboard'); - await setFieldValue('description', 'Some description'); - - await focusAndClickButton('savedObjectEditSave'); - - objects = await PageObjects.savedObjects.getRowTitles(); - expect(objects.includes('A Dashboard')).to.be(false); - expect(objects.includes('Edited Dashboard')).to.be(true); - - await PageObjects.common.navigateToUrl( - 'management', - 'kibana/objects/savedDashboards/i-exist', - { - shouldUseHashForSubUrl: false, - } - ); - - expect(await getFieldValue('title')).to.eql('Edited Dashboard'); - expect(await getFieldValue('description')).to.eql('Some description'); - }); - - it('allows to delete a saved object', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'kibana/objects/savedDashboards/i-exist', - { - shouldUseHashForSubUrl: false, - } - ); - - await focusAndClickButton('savedObjectEditDelete'); - await PageObjects.common.clickConfirmOnModal(); - - const objects = await PageObjects.savedObjects.getRowTitles(); - expect(objects.includes('A Dashboard')).to.be(false); - }); - - it('preserves the object references when saving', async () => { - const testVisualizationUrl = - 'kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed'; - const visualizationRefs = [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'logstash-*', - }, - ]; - - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - - const objects = await PageObjects.savedObjects.getRowTitles(); - expect(objects.includes('A Pie')).to.be(true); - - await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { - shouldUseHashForSubUrl: false, - }); - - await testSubjects.existOrFail('savedObjectEditSave'); - - let displayedReferencesValue = await getAceEditorFieldValue('references'); - - expect(JSON.parse(displayedReferencesValue)).to.eql(visualizationRefs); - - await focusAndClickButton('savedObjectEditSave'); - - await PageObjects.savedObjects.getRowTitles(); - - await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { - shouldUseHashForSubUrl: false, - }); - - // Parsing to avoid random keys ordering issues in raw string comparison - expect(JSON.parse(await getAceEditorFieldValue('references'))).to.eql(visualizationRefs); - - await setAceEditorFieldValue('references', JSON.stringify([], undefined, 2)); - - await focusAndClickButton('savedObjectEditSave'); - - await PageObjects.savedObjects.getRowTitles(); - - await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { - shouldUseHashForSubUrl: false, - }); - - displayedReferencesValue = await getAceEditorFieldValue('references'); - - expect(JSON.parse(displayedReferencesValue)).to.eql([]); - }); - }); -} diff --git a/test/functional/apps/saved_objects_management/index.ts b/test/functional/apps/saved_objects_management/index.ts index 0b367b284e741..12e0cc8863f12 100644 --- a/test/functional/apps/saved_objects_management/index.ts +++ b/test/functional/apps/saved_objects_management/index.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) { describe('saved objects management', function savedObjectsManagementAppTestSuite() { this.tags('ciGroup7'); - loadTestFile(require.resolve('./edit_saved_object')); + loadTestFile(require.resolve('./inspect_saved_objects')); loadTestFile(require.resolve('./show_relationships')); }); } diff --git a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts new file mode 100644 index 0000000000000..839c262acffa0 --- /dev/null +++ b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts @@ -0,0 +1,87 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']); + const find = getService('find'); + + const focusAndClickButton = async (buttonSubject: string) => { + const button = await testSubjects.find(buttonSubject); + await button.scrollIntoViewIfNecessary(); + await delay(10); + await button.focus(); + await delay(10); + await button.click(); + // Allow some time for the transition/animations to occur before assuming the click is done + await delay(10); + }; + const textIncludesAll = (text: string, items: string[]) => { + const bools = items.map((item) => !!text.includes(item)); + return bools.every((currBool) => currBool === true); + }; + + describe('saved objects edition page', () => { + beforeEach(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' + ); + }); + + afterEach(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' + ); + }); + + it('allows to view the saved object', async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + const objects = await PageObjects.savedObjects.getRowTitles(); + expect(objects.includes('A Dashboard')).to.be(true); + await PageObjects.common.navigateToUrl('management', 'kibana/objects/dashboard/i-exist', { + shouldUseHashForSubUrl: false, + }); + const inspectContainer = await find.byClassName('kibanaCodeEditor'); + const visibleContainerText = await inspectContainer.getVisibleText(); + // ensure that something renders visibly + expect( + textIncludesAll(visibleContainerText, [ + 'A Dashboard', + 'title', + 'id', + 'type', + 'attributes', + 'references', + ]) + ).to.be(true); + }); + + it('allows to delete a saved object', async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + let objects = await PageObjects.savedObjects.getRowTitles(); + expect(objects.includes('A Dashboard')).to.be(true); + await PageObjects.savedObjects.clickInspectByTitle('A Dashboard'); + await PageObjects.common.navigateToUrl('management', 'kibana/objects/dashboard/i-exist', { + shouldUseHashForSubUrl: false, + }); + await focusAndClickButton('savedObjectEditDelete'); + await PageObjects.common.clickConfirmOnModal(); + + objects = await PageObjects.savedObjects.getRowTitles(); + expect(objects.includes('A Dashboard')).to.be(false); + }); + }); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9c69e4fa612f5..aac2d45ec2120 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4369,14 +4369,11 @@ "savedObjects.saveModalOrigin.originAfterSavingSwitchLabel": "保存後に{originVerb}から{origin}", "savedObjects.saveModalOrigin.returnToOriginLabel": "戻る", "savedObjects.saveModalOrigin.saveAndReturnLabel": "保存して戻る", - "savedObjectsManagement.breadcrumb.edit": "{savedObjectType}を編集", "savedObjectsManagement.breadcrumb.index": "保存されたオブジェクト", "savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel": "削除", "savedObjectsManagement.deleteConfirm.modalDescription": "このアクションはオブジェクトをKibanaから永久に削除します。", "savedObjectsManagement.deleteConfirm.modalTitle": "「{title}」を削除しますか?", "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "この操作は次の保存されたオブジェクトを削除します:", - "savedObjectsManagement.field.offLabel": "オフ", - "savedObjectsManagement.field.onLabel": "オン", "savedObjectsManagement.importSummary.createdCountHeader": "{createdCount}件の新規項目", "savedObjectsManagement.importSummary.createdOutcomeLabel": "作成済み", "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount}件のエラー", @@ -4478,21 +4475,10 @@ "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", - "savedObjectsManagement.view.cancelButtonAriaLabel": "キャンセル", - "savedObjectsManagement.view.cancelButtonLabel": "キャンセル", - "savedObjectsManagement.view.deleteItemButtonLabel": "{title}を削除", - "savedObjectsManagement.view.editItemTitle": "{title}の編集", "savedObjectsManagement.view.fieldDoesNotExistErrorMessage": "このオブジェクトに関連付けられたフィールドは、現在このインデックスパターンに存在しません。", - "savedObjectsManagement.view.howToFixErrorDescription": "このエラーの原因がわかる場合は修正してください。わからない場合は上の削除ボタンをクリックしてください。", - "savedObjectsManagement.view.howToModifyObjectDescription": "オブジェクトの編集は上級ユーザー向けです。オブジェクトのプロパティが検証されておらず、無効なオブジェクトはエラー、データ損失、またはそれ以上の問題の原因となります。コードを熟知した人に指示されていない限り、この設定は変更しない方が無難です。", - "savedObjectsManagement.view.howToModifyObjectTitle": "十分ご注意ください!", "savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "このオブジェクトに関連付けられたインデックスパターンは現在存在しません。", - "savedObjectsManagement.view.saveButtonAriaLabel": "{ title }オブジェクトを保存", - "savedObjectsManagement.view.saveButtonLabel": "{ title }オブジェクトを保存", "savedObjectsManagement.view.savedObjectProblemErrorMessage": "この保存されたオブジェクトに問題があります", "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "このオブジェクトに関連付けられた保存された検索は現在存在しません。", - "savedObjectsManagement.view.viewItemButtonLabel": "{title}を表示", - "savedObjectsManagement.view.viewItemTitle": "{title}を表示", "security.checkup.dismissButtonText": "閉じる", "security.checkup.dontShowAgain": "今後表示しない", "security.checkup.insecureClusterMessage": "1 ビットを失わないでください。Elastic では無料でデータを保護できます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1c91700a74e81..73ad85a85f427 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4409,14 +4409,11 @@ "savedObjects.saveModalOrigin.originAfterSavingSwitchLabel": "保存后{originVerb}至{origin}", "savedObjects.saveModalOrigin.returnToOriginLabel": "返回", "savedObjects.saveModalOrigin.saveAndReturnLabel": "保存并返回", - "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", "savedObjectsManagement.breadcrumb.index": "已保存对象", "savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel": "删除", "savedObjectsManagement.deleteConfirm.modalDescription": "此操作会将对象从 Kibana 永久移除。", "savedObjectsManagement.deleteConfirm.modalTitle": "删除“{title}”?", "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "此操作将删除以下已保存对象:", - "savedObjectsManagement.field.offLabel": "关闭", - "savedObjectsManagement.field.onLabel": "开启", "savedObjectsManagement.importSummary.createdCountHeader": "{createdCount} 个新", "savedObjectsManagement.importSummary.createdOutcomeLabel": "已创建", "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount} 个错误", @@ -4523,21 +4520,10 @@ "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "找不到已保存对象", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", "savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage": "找不到已保存对象", - "savedObjectsManagement.view.cancelButtonAriaLabel": "取消", - "savedObjectsManagement.view.cancelButtonLabel": "取消", - "savedObjectsManagement.view.deleteItemButtonLabel": "删除“{title}”", - "savedObjectsManagement.view.editItemTitle": "编辑“{title}”", "savedObjectsManagement.view.fieldDoesNotExistErrorMessage": "与此对象关联的字段在该索引模式中已不存在。", - "savedObjectsManagement.view.howToFixErrorDescription": "如果您清楚此错误的含义,请修复该错误 — 否则单击上面的删除按钮。", - "savedObjectsManagement.view.howToModifyObjectDescription": "修改对象仅适用于高级用户。对象属性未得到验证,无效的对象可能会导致错误、数据丢失或更坏的情况发生。除非熟悉该代码的人让您来这里,否则您可能不应到访此处。", - "savedObjectsManagement.view.howToModifyObjectTitle": "谨慎操作!", "savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "与此对象关联的索引模式已不存在。", - "savedObjectsManagement.view.saveButtonAriaLabel": "保存 { title } 对象", - "savedObjectsManagement.view.saveButtonLabel": "保存 { title } 对象", "savedObjectsManagement.view.savedObjectProblemErrorMessage": "此已保存对象有问题", "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。", - "savedObjectsManagement.view.viewItemButtonLabel": "查看“{title}”", - "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", "security.checkup.dismissButtonText": "关闭", "security.checkup.dontShowAgain": "不再显示", "security.checkup.insecureClusterMessage": "不要丢失一位。使用 Elastic,免费保护您的数据。", diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 3d4c30c1bfdd6..58f08a1dfb9f7 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -14,6 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'security', 'error', 'savedObjects']); const kibanaServer = getService('kibanaServer'); let version: string = ''; + const find = getService('find'); describe('feature controls saved objects management', () => { before(async () => { @@ -108,12 +109,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(actual).to.be(true); }); }); - - describe('edit visualization', () => { + // From https://github.com/elastic/kibana/issues/59588 edit view became read-only json view + // test description changed from "edit" to "inspect" + // Skipping the test to allow code owners to delete or modify the test. + describe('inspect visualization', () => { before(async () => { await PageObjects.common.navigateToUrl( 'management', - 'kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + 'kibana/objects/visualization/75c3e060-1e7c-11e9-8488-65449e65d0ed', { shouldLoginIfPrompted: false, shouldUseHashForSubUrl: false, @@ -125,11 +128,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('savedObjectEditDelete'); }); - it('shows save button', async () => { + // no longer a feature + it.skip('shows save button', async () => { await testSubjects.existOrFail('savedObjectEditSave'); }); - it('has inputs without readonly attributes', async () => { + // no longer a feature + it.skip('has inputs without readonly attributes', async () => { const form = await testSubjects.find('savedObjectEditForm'); const inputs = await form.findAllByCssSelector('input'); expect(inputs.length).to.be.greaterThan(0); @@ -223,17 +228,30 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('edit visualization', () => { + // From https://github.com/elastic/kibana/issues/59588 edit view became read-only json view + // test description changed from "edit" to "inspect" + // Skipping the test to allow code owners to delete or modify the test. + describe('inspect visualization', () => { before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + const objects = await PageObjects.savedObjects.getRowTitles(); + expect(objects.includes('A Pie')).to.be(true); + await PageObjects.savedObjects.clickInspectByTitle('A Pie'); await PageObjects.common.navigateToUrl( 'management', - 'kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + 'kibana/objects/visualization/75c3e060-1e7c-11e9-8488-65449e65d0ed', { shouldLoginIfPrompted: false, shouldUseHashForSubUrl: false, } ); - await testSubjects.existOrFail('savedObjectsEdit'); + }); + + it('allows viewing the object', async () => { + const inspectContainer = await find.byClassName('kibanaCodeEditor'); + const visibleContainerText = await inspectContainer.getVisibleText(); + expect(visibleContainerText.includes('A Pie')); }); it('does not show delete button', async () => { @@ -244,7 +262,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.missingOrFail('savedObjectEditSave'); }); - it('has inputs with only readonly attributes', async () => { + // No longer a feature + it.skip('has inputs with only readonly attributes', async () => { const form = await testSubjects.find('savedObjectEditForm'); const inputs = await form.findAllByCssSelector('input'); expect(inputs.length).to.be.greaterThan(0); @@ -309,11 +328,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('edit visualization', () => { + describe('inspect visualization', () => { it('redirects to management home', async () => { await PageObjects.common.navigateToUrl( 'management', - 'kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + 'kibana/objects/visualization/75c3e060-1e7c-11e9-8488-65449e65d0ed', { shouldLoginIfPrompted: false, ensureCurrentUrl: false, diff --git a/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts b/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts index 28d04c1f9c54c..e4da0b341dce9 100644 --- a/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts +++ b/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts @@ -14,7 +14,6 @@ const getSpacePrefix = (spaceId: string) => { export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'common', 'security', @@ -22,9 +21,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'spaceSelector', 'settings', ]); + const find = getService('find'); const spaceId = 'space_1'; + const textIncludesAll = (text: string, items: string[]) => { + const bools = items.map((item) => !!text.includes(item)); + return bools.every((currBool) => currBool === true); + }; + describe('spaces integration', () => { before(async () => { await esArchiver.load( @@ -54,9 +59,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.waitUntilUrlIncludes(getSpacePrefix(spaceId)); - expect(await testSubjects.getAttribute(`savedObjects-editField-title`, 'value')).to.eql( - 'A Pie' - ); + const inspectContainer = await find.byClassName('kibanaCodeEditor'); + const visibleContainerText = await inspectContainer.getVisibleText(); + expect( + textIncludesAll(visibleContainerText, [ + 'A Pie', + 'title', + 'id', + 'type', + 'attributes', + 'references', + ]) + ).to.be(true); + expect(visibleContainerText.includes('A Pie')); }); }); }