diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx index 9169286fb41718..5b787eb2655093 100644 --- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx @@ -17,46 +17,17 @@ * under the License. */ -import _ from 'lodash'; -import PropTypes from 'prop-types'; import React from 'react'; -import chrome from 'ui/chrome'; - -import { - CommonProps, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiContextMenuPanelProps, - EuiEmptyPrompt, - EuiFieldSearch, - EuiFilterButton, - EuiFilterGroup, - EuiFlexGroup, - EuiFlexItem, - EuiListGroup, - EuiListGroupItem, - EuiLoadingSpinner, - EuiPagination, - EuiPopover, - EuiSpacer, - EuiTablePagination, - IconType, -} from '@elastic/eui'; -import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; -import { i18n } from '@kbn/i18n'; - +import { npStart } from 'ui/new_platform'; +import { IconType } from '@elastic/eui'; import { SavedObjectAttributes } from 'src/core/server'; import { SimpleSavedObject } from 'src/core/public'; +import { SavedObjectFinder as SavedObjectFinderNP } from '../../../../../plugins/kibana_react/public'; -// TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted -const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent< - CommonProps & { maxWidth: boolean } ->; - -// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted -const FixedEuiContextMenuPanel = (EuiContextMenuPanel as any) as React.FunctionComponent< - EuiContextMenuPanelProps & { watchedItemProps: string[] } ->; +/** + * DO NOT USE THIS COMPONENT, IT IS DEPRECATED. + * Use the one in `src/plugins/kibana_react` instead. + */ export interface SavedObjectMetaData { type: string; @@ -66,452 +37,72 @@ export interface SavedObjectMetaData { showSavedObject?(savedObject: SimpleSavedObject): boolean; } -interface SavedObjectFinderState { - items: Array<{ - title: string | null; - id: SimpleSavedObject['id']; - type: SimpleSavedObject['type']; - savedObject: SimpleSavedObject; - }>; - query: string; - isFetchingItems: boolean; - page: number; - perPage: number; - sortDirection?: Direction; - sortOpen: boolean; - filterOpen: boolean; - filteredTypes: string[]; -} - interface BaseSavedObjectFinder { + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ onChoose?: ( id: SimpleSavedObject['id'], type: SimpleSavedObject['type'], name: string ) => void; + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ noItemsMessage?: React.ReactNode; + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ savedObjectMetaData: Array>; + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ showFilter?: boolean; } interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder { + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ initialPageSize?: undefined; + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ fixedPageSize: number; } interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ initialPageSize?: 5 | 10 | 15 | 25; + /** + * @deprecated + * + * Use component in `src/plugins/kibana_react` instead. + */ fixedPageSize?: undefined; } type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; -class SavedObjectFinder extends React.Component { - public static propTypes = { - onChoose: PropTypes.func, - noItemsMessage: PropTypes.node, - savedObjectMetaData: PropTypes.array.isRequired, - initialPageSize: PropTypes.oneOf([5, 10, 15, 25]), - fixedPageSize: PropTypes.number, - showFilter: PropTypes.bool, - }; - - private isComponentMounted: boolean = false; - - private debouncedFetch = _.debounce(async (query: string) => { - const metaDataMap = this.getSavedObjectMetaDataMap(); - - const resp = await chrome.getSavedObjectsClient().find({ - type: Object.keys(metaDataMap), - fields: ['title', 'visState'], - search: query ? `${query}*` : undefined, - page: 1, - perPage: chrome.getUiSettingsClient().get('savedObjects:listingLimit'), - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - }); - - resp.savedObjects = resp.savedObjects.filter(savedObject => { - const metaData = metaDataMap[savedObject.type]; - if (metaData.showSavedObject) { - return metaData.showSavedObject(savedObject); - } else { - return true; - } - }); - - if (!this.isComponentMounted) { - return; - } - - // We need this check to handle the case where search results come back in a different - // order than they were sent out. Only load results for the most recent search. - if (query === this.state.query) { - this.setState({ - isFetchingItems: false, - items: resp.savedObjects.map(savedObject => { - const { - attributes: { title }, - id, - type, - } = savedObject; - return { - title: typeof title === 'string' ? title : '', - id, - type, - savedObject, - }; - }), - }); - } - }, 300); - - constructor(props: SavedObjectFinderProps) { - super(props); - - this.state = { - items: [], - isFetchingItems: false, - page: 0, - perPage: props.initialPageSize || props.fixedPageSize || 10, - query: '', - filterOpen: false, - filteredTypes: [], - sortOpen: false, - }; - } - - public componentWillUnmount() { - this.isComponentMounted = false; - this.debouncedFetch.cancel(); - } - - public componentDidMount() { - this.isComponentMounted = true; - this.fetchItems(); - } - - public render() { - return ( - - {this.renderSearchBar()} - {this.renderListing()} - - ); - } - - private getSavedObjectMetaDataMap(): Record> { - return this.props.savedObjectMetaData.reduce( - (map, metaData) => ({ ...map, [metaData.type]: metaData }), - {} - ); - } - - private getPageCount() { - return Math.ceil( - (this.state.filteredTypes.length === 0 - ? this.state.items.length - : this.state.items.filter( - item => - this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type) - ).length) / this.state.perPage - ); - } - - // server-side paging not supported - // 1) saved object client does not support sorting by title because title is only mapped as analyzed - // 2) can not search on anything other than title because all other fields are stored in opaque JSON strings, - // for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side - // with the current mappings - private getPageOfItems = () => { - // do not sort original list to preserve elasticsearch ranking order - const items = this.state.items.slice(); - const { sortDirection } = this.state; - - if (sortDirection || !this.state.query) { - items.sort(({ title: titleA }, { title: titleB }) => { - let order = 1; - if (sortDirection === 'desc') { - order = -1; - } - return order * (titleA || '').toLowerCase().localeCompare((titleB || '').toLowerCase()); - }); - } - - // If begin is greater than the length of the sequence, an empty array is returned. - const startIndex = this.state.page * this.state.perPage; - // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). - const lastIndex = startIndex + this.state.perPage; - return items - .filter( - item => - this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type) - ) - .slice(startIndex, lastIndex); - }; - - private fetchItems = () => { - this.setState( - { - isFetchingItems: true, - }, - this.debouncedFetch.bind(null, this.state.query) - ); - }; - - private getAvailableSavedObjectMetaData() { - const typesInItems = new Set(); - this.state.items.forEach(item => { - typesInItems.add(item.type); - }); - return this.props.savedObjectMetaData.filter(metaData => typesInItems.has(metaData.type)); - } - - private getSortOptions() { - const sortOptions = [ - { - this.setState({ - sortDirection: 'asc', - }); - }} - > - {i18n.translate('common.ui.savedObjects.finder.sortAsc', { - defaultMessage: 'Ascending', - })} - , - { - this.setState({ - sortDirection: 'desc', - }); - }} - > - {i18n.translate('common.ui.savedObjects.finder.sortDesc', { - defaultMessage: 'Descending', - })} - , - ]; - if (this.state.query) { - sortOptions.push( - { - this.setState({ - sortDirection: undefined, - }); - }} - > - {i18n.translate('common.ui.savedObjects.finder.sortAuto', { - defaultMessage: 'Best match', - })} - - ); - } - return sortOptions; - } - - private renderSearchBar() { - const availableSavedObjectMetaData = this.getAvailableSavedObjectMetaData(); - - return ( - - - { - this.setState( - { - query: e.target.value, - }, - this.fetchItems - ); - }} - data-test-subj="savedObjectFinderSearchInput" - isLoading={this.state.isFetchingItems} - /> - - - - this.setState({ sortOpen: false })} - button={ - - this.setState(({ sortOpen }) => ({ - sortOpen: !sortOpen, - })) - } - iconType="arrowDown" - isSelected={this.state.sortOpen} - data-test-subj="savedObjectFinderSortButton" - > - {i18n.translate('common.ui.savedObjects.finder.sortButtonLabel', { - defaultMessage: 'Sort', - })} - - } - > - - - {this.props.showFilter && ( - this.setState({ filterOpen: false })} - button={ - - this.setState(({ filterOpen }) => ({ - filterOpen: !filterOpen, - })) - } - iconType="arrowDown" - data-test-subj="savedObjectFinderFilterButton" - isSelected={this.state.filterOpen} - numFilters={this.props.savedObjectMetaData.length} - hasActiveFilters={this.state.filteredTypes.length > 0} - numActiveFilters={this.state.filteredTypes.length} - > - {i18n.translate('common.ui.savedObjects.finder.filterButtonLabel', { - defaultMessage: 'Types', - })} - - } - > - ( - { - this.setState(({ filteredTypes }) => ({ - filteredTypes: filteredTypes.includes(metaData.type) - ? filteredTypes.filter(t => t !== metaData.type) - : [...filteredTypes, metaData.type], - page: 0, - })); - }} - > - {metaData.name} - - ))} - /> - - )} - - - - ); - } - - private renderListing() { - const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); - const { onChoose, savedObjectMetaData } = this.props; - - return ( - <> - {this.state.isFetchingItems && this.state.items.length === 0 && ( - - - - - - - )} - {items.length > 0 ? ( - - {items.map(item => { - const currentSavedObjectMetaData = savedObjectMetaData.find( - metaData => metaData.type === item.type - )!; - const fullName = currentSavedObjectMetaData.getTooltipForSavedObject - ? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject) - : `${item.title} (${currentSavedObjectMetaData!.name})`; - const iconType = ( - currentSavedObjectMetaData || - ({ - getIconForSavedObject: () => 'document', - } as Pick, 'getIconForSavedObject'>) - ).getIconForSavedObject(item.savedObject); - return ( - { - onChoose(item.id, item.type, fullName); - } - : undefined - } - title={fullName} - data-test-subj={`savedObjectTitle${(item.title || '').split(' ').join('-')}`} - /> - ); - })} - - ) : ( - !this.state.isFetchingItems && - )} - {this.getPageCount() > 1 && - (this.props.fixedPageSize ? ( - { - this.setState({ - page, - }); - }} - /> - ) : ( - { - this.setState({ - page, - }); - }} - onChangeItemsPerPage={perPage => { - this.setState({ - perPage, - }); - }} - itemsPerPage={this.state.perPage} - itemsPerPageOptions={[5, 10, 15, 25]} - /> - ))} - - ); - } -} - -export { SavedObjectFinder }; +export const SavedObjectFinder: React.FC = props => ( + +); diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx index b6802758b958ac..131f28059cebd0 100644 --- a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx +++ b/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx @@ -16,256 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFieldText, - EuiForm, - EuiFormRow, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, - EuiSpacer, - EuiSwitch, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Fragment } from 'react'; -import { EuiText } from '@elastic/eui'; -interface OnSaveProps { - newTitle: string; - newCopyOnSave: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; -} - -interface Props { - onSave: (props: OnSaveProps) => void; - onClose: () => void; - title: string; - showCopyOnSave: boolean; - objectType: string; - confirmButtonLabel?: React.ReactNode; - options?: React.ReactNode; - description?: string; -} - -interface State { - title: string; - copyOnSave: boolean; - isTitleDuplicateConfirmed: boolean; - hasTitleDuplicate: boolean; - isLoading: boolean; -} - -export class SavedObjectSaveModal extends React.Component { - public readonly state = { - title: this.props.title, - copyOnSave: false, - isTitleDuplicateConfirmed: false, - hasTitleDuplicate: false, - isLoading: false, - }; - - public render() { - const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, isLoading } = this.state; - - return ( - -
- - - - - - - - - {this.renderDuplicateTitleCallout()} - - - {this.props.description && ( - - {this.props.description} - - )} - {this.renderCopyOnSave()} - - - } - > - - - - {this.props.options} - - - - - - - - - - {this.props.confirmButtonLabel ? ( - this.props.confirmButtonLabel - ) : ( - - )} - - - -
-
- ); - } - - private onTitleDuplicate = () => { - this.setState({ - isLoading: false, - isTitleDuplicateConfirmed: true, - hasTitleDuplicate: true, - }); - }; - - private saveSavedObject = async () => { - if (this.state.isLoading) { - // ignore extra clicks - return; - } - - this.setState({ - isLoading: true, - }); - - await this.props.onSave({ - newTitle: this.state.title, - newCopyOnSave: this.state.copyOnSave, - isTitleDuplicateConfirmed: this.state.isTitleDuplicateConfirmed, - onTitleDuplicate: this.onTitleDuplicate, - }); - }; - - private onTitleChange = (event: React.ChangeEvent) => { - this.setState({ - title: event.target.value, - isTitleDuplicateConfirmed: false, - hasTitleDuplicate: false, - }); - }; - - private onCopyOnSaveChange = (event: React.ChangeEvent) => { - this.setState({ - copyOnSave: event.target.checked, - }); - }; - - private onFormSubmit = (event: React.FormEvent) => { - event.preventDefault(); - this.saveSavedObject(); - }; - - private renderDuplicateTitleCallout = () => { - if (!this.state.hasTitleDuplicate) { - return; - } - - return ( - - - } - color="warning" - data-test-subj="titleDupicateWarnMsg" - > -

- - - - ), - }} - /> -

-
- -
- ); - }; - - private renderCopyOnSave = () => { - if (!this.props.showCopyOnSave) { - return; - } - - return ( - - - } - /> - - - ); - }; -} +/** + * @deprecated + * + * Do not import this component from here. Import from `src/plugins/kibana_react` instead. + */ +export { SavedObjectSaveModal } from '../../../../../plugins/kibana_react/public'; diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index b9a223a29c17c8..0e98d689884887 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -17,6 +17,7 @@ * under the License. */ +export * from './saved_objects'; export * from './exit_full_screen_button'; export * from './context'; export * from './overlays'; diff --git a/src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap similarity index 88% rename from src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.tsx.snap rename to src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap index aabf9d8d1d1e72..47b4a5219068f4 100644 --- a/src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -15,7 +15,7 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` } @@ -62,7 +62,7 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` > @@ -78,7 +78,7 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` > diff --git a/src/plugins/kibana_react/public/saved_objects/index.ts b/src/plugins/kibana_react/public/saved_objects/index.ts new file mode 100644 index 00000000000000..ade80d2cd2a92b --- /dev/null +++ b/src/plugins/kibana_react/public/saved_objects/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './saved_object_finder'; +export * from './saved_object_save_modal'; diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.test.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx similarity index 53% rename from src/legacy/ui/public/saved_objects/components/saved_object_finder.test.tsx rename to src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx index 0170efbd56aee6..6b0728e73c6910 100644 --- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.test.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx @@ -17,12 +17,6 @@ * under the License. */ -jest.mock('ui/chrome', () => ({ - getUiSettingsClient: () => ({ - get: () => 10, - }), -})); - jest.mock('lodash', () => ({ debounce: (fn: any) => fn, })); @@ -42,10 +36,10 @@ import { shallow } from 'enzyme'; import React from 'react'; import * as sinon from 'sinon'; import { SavedObjectFinder } from './saved_object_finder'; +// eslint-disable-next-line +import { coreMock } from '../../../../core/public/mocks'; describe('SavedObjectsFinder', () => { - let objectsClientStub: sinon.SinonStub; - const doc = { id: '1', type: 'search', @@ -69,38 +63,48 @@ describe('SavedObjectsFinder', () => { }, ]; - beforeEach(() => { - objectsClientStub = sinon.stub(); - objectsClientStub.returns(Promise.resolve({ savedObjects: [] })); - require('ui/chrome').getSavedObjectsClient = () => ({ - find: async (...args: any[]) => { - return objectsClientStub(...args); - }, - }); - }); - it('should call saved object client on startup', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc] }) + ); + core.uiSettings.get.mockImplementation(() => 10); - const wrapper = shallow(); + const wrapper = shallow( + + ); wrapper.instance().componentDidMount!(); - expect( - objectsClientStub.calledWith({ - type: ['search'], - fields: ['title', 'visState'], - search: undefined, - page: 1, - perPage: 10, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - }) - ).toBe(true); + + expect(core.savedObjects.client.find).toHaveBeenCalledWith({ + type: ['search'], + fields: ['title', 'visState'], + search: undefined, + page: 1, + perPage: 10, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + }); }); it('should list initial items', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc] }) + ); + core.uiSettings.get.mockImplementation(() => 10); + + const wrapper = shallow( + + ); - const wrapper = shallow(); wrapper.instance().componentDidMount!(); await nextTick(); expect( @@ -110,11 +114,21 @@ describe('SavedObjectsFinder', () => { it('should call onChoose on item click', async () => { const chooseStub = sinon.stub(); - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc] }) + ); + core.uiSettings.get.mockImplementation(() => 10); const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); wrapper @@ -126,9 +140,19 @@ describe('SavedObjectsFinder', () => { describe('sorting', () => { it('should list items ascending', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); - const wrapper = shallow(); + const wrapper = shallow( + + ); wrapper.instance().componentDidMount!(); await nextTick(); const list = wrapper.find(EuiListGroup); @@ -137,9 +161,20 @@ describe('SavedObjectsFinder', () => { }); it('should list items descending', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); + + const wrapper = shallow( + + ); - const wrapper = shallow(); wrapper.instance().componentDidMount!(); await nextTick(); wrapper.setState({ sortDirection: 'desc' }); @@ -150,10 +185,16 @@ describe('SavedObjectsFinder', () => { }); it('should not show the saved objects which get filtered by showSavedObject', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); const wrapper = shallow( { ]} /> ); + wrapper.instance().componentDidMount!(); await nextTick(); const list = wrapper.find(EuiListGroup); @@ -173,9 +215,19 @@ describe('SavedObjectsFinder', () => { describe('search', () => { it('should request filtered list on search input', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); - const wrapper = shallow(); + const wrapper = shallow( + + ); wrapper.instance().componentDidMount!(); await nextTick(); wrapper @@ -183,23 +235,32 @@ describe('SavedObjectsFinder', () => { .first() .simulate('change', { target: { value: 'abc' } }); - expect( - objectsClientStub.calledWith({ - type: ['search'], - fields: ['title', 'visState'], - search: 'abc*', - page: 1, - perPage: 10, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - }) - ).toBe(true); + expect(core.savedObjects.client.find).toHaveBeenCalledWith({ + type: ['search'], + fields: ['title', 'visState'], + search: 'abc*', + page: 1, + perPage: 10, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + }); }); it('should respect response order on search input', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); + + const wrapper = shallow( + + ); - const wrapper = shallow(); wrapper.instance().componentDidMount!(); await nextTick(); wrapper @@ -214,8 +275,16 @@ describe('SavedObjectsFinder', () => { }); it('should request multiple saved object types at once', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( { ); wrapper.instance().componentDidMount!(); - expect( - objectsClientStub.calledWith({ - type: ['search', 'vis'], - fields: ['title', 'visState'], - search: undefined, - page: 1, - perPage: 10, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - }) - ).toBe(true); + expect(core.savedObjects.client.find).toHaveBeenCalledWith({ + type: ['search', 'vis'], + fields: ['title', 'visState'], + search: undefined, + page: 1, + perPage: 10, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + }); }); describe('filter', () => { @@ -260,14 +327,23 @@ describe('SavedObjectsFinder', () => { ]; it('should not render filter buttons if disabled', async () => { - objectsClientStub.returns( + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => Promise.resolve({ savedObjects: [doc, doc2, doc3], }) ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); expect(wrapper.find('[data-test-subj="savedObjectFinderFilter-search"]').exists()).toBe( @@ -276,14 +352,23 @@ describe('SavedObjectsFinder', () => { }); it('should not render filter buttons if there is only one type in the list', async () => { - objectsClientStub.returns( + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => Promise.resolve({ savedObjects: [doc, doc2], }) ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); expect(wrapper.find('[data-test-subj="savedObjectFinderFilter-search"]').exists()).toBe( @@ -292,14 +377,23 @@ describe('SavedObjectsFinder', () => { }); it('should apply filter if selected', async () => { - objectsClientStub.returns( + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => Promise.resolve({ savedObjects: [doc, doc2, doc3], }) ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); wrapper.setState({ filteredTypes: ['vis'] }); @@ -313,10 +407,20 @@ describe('SavedObjectsFinder', () => { }); it('should display no items message if there are no items', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [] }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const noItemsMessage = ; const wrapper = shallow( - + ); wrapper.instance().componentDidMount!(); await nextTick(); @@ -338,14 +442,22 @@ describe('SavedObjectsFinder', () => { }, })); - beforeEach(() => { - objectsClientStub.returns(Promise.resolve({ savedObjects: longItemList })); - }); - it('should show a table pagination with initial per page', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: longItemList }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); expect( @@ -358,9 +470,21 @@ describe('SavedObjectsFinder', () => { }); it('should allow switching the page size', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: longItemList }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); wrapper @@ -371,9 +495,21 @@ describe('SavedObjectsFinder', () => { }); it('should switch page correctly', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: longItemList }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); wrapper @@ -390,9 +526,21 @@ describe('SavedObjectsFinder', () => { }); it('should show an ordinary pagination for fixed page sizes', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: longItemList }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); expect( @@ -405,9 +553,21 @@ describe('SavedObjectsFinder', () => { }); it('should switch page correctly for fixed page sizes', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: longItemList }) + ); + core.uiSettings.get.mockImplementation(() => 10); + const wrapper = shallow( - + ); + wrapper.instance().componentDidMount!(); await nextTick(); wrapper @@ -426,16 +586,29 @@ describe('SavedObjectsFinder', () => { describe('loading state', () => { it('should display a spinner during initial loading', () => { - const wrapper = shallow(); + const core = coreMock.createStart(); + + const wrapper = shallow( + + ); expect(wrapper.containsMatchingElement()).toBe(true); }); it('should hide the spinner if data is shown', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc] }) + ); const wrapper = shallow( { ]} /> ); + wrapper.instance().componentDidMount!(); await nextTick(); expect(wrapper.containsMatchingElement()).toBe(false); }); it('should not show the spinner if there are already items', async () => { - objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] })); + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc] }) + ); + + const wrapper = shallow( + + ); - const wrapper = shallow(); wrapper.instance().componentDidMount!(); await nextTick(); wrapper diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx new file mode 100644 index 00000000000000..d34185731f3cc4 --- /dev/null +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx @@ -0,0 +1,520 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import { + CommonProps, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiContextMenuPanelProps, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiListGroup, + EuiListGroupItem, + EuiLoadingSpinner, + EuiPagination, + EuiPopover, + EuiSpacer, + EuiTablePagination, + IconType, +} from '@elastic/eui'; +import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; +import { i18n } from '@kbn/i18n'; + +import { SavedObjectAttributes } from '../../../../core/server'; +import { SimpleSavedObject, CoreStart } from '../../../../core/public'; + +// TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted +const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent< + CommonProps & { maxWidth: boolean } +>; + +// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted +const FixedEuiContextMenuPanel = (EuiContextMenuPanel as any) as React.FunctionComponent< + EuiContextMenuPanelProps & { watchedItemProps: string[] } +>; + +export interface SavedObjectMetaData { + type: string; + name: string; + getIconForSavedObject(savedObject: SimpleSavedObject): IconType; + getTooltipForSavedObject?(savedObject: SimpleSavedObject): string; + showSavedObject?(savedObject: SimpleSavedObject): boolean; +} + +interface SavedObjectFinderState { + items: Array<{ + title: string | null; + id: SimpleSavedObject['id']; + type: SimpleSavedObject['type']; + savedObject: SimpleSavedObject; + }>; + query: string; + isFetchingItems: boolean; + page: number; + perPage: number; + sortDirection?: Direction; + sortOpen: boolean; + filterOpen: boolean; + filteredTypes: string[]; +} + +interface BaseSavedObjectFinder { + onChoose?: ( + id: SimpleSavedObject['id'], + type: SimpleSavedObject['type'], + name: string + ) => void; + noItemsMessage?: React.ReactNode; + savedObjectMetaData: Array>; + showFilter?: boolean; +} + +interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder { + initialPageSize?: undefined; + fixedPageSize: number; +} + +interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { + initialPageSize?: 5 | 10 | 15 | 25; + fixedPageSize?: undefined; +} +type SavedObjectFinderProps = { + savedObjects: CoreStart['savedObjects']; + uiSettings: CoreStart['uiSettings']; +} & (SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize); + +class SavedObjectFinder extends React.Component { + public static propTypes = { + onChoose: PropTypes.func, + noItemsMessage: PropTypes.node, + savedObjectMetaData: PropTypes.array.isRequired, + initialPageSize: PropTypes.oneOf([5, 10, 15, 25]), + fixedPageSize: PropTypes.number, + showFilter: PropTypes.bool, + }; + + private isComponentMounted: boolean = false; + + private debouncedFetch = _.debounce(async (query: string) => { + const metaDataMap = this.getSavedObjectMetaDataMap(); + + const perPage = this.props.uiSettings.get('savedObjects:listingLimit'); + const resp = await this.props.savedObjects.client.find({ + type: Object.keys(metaDataMap), + fields: ['title', 'visState'], + search: query ? `${query}*` : undefined, + page: 1, + perPage, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + }); + + resp.savedObjects = resp.savedObjects.filter(savedObject => { + const metaData = metaDataMap[savedObject.type]; + if (metaData.showSavedObject) { + return metaData.showSavedObject(savedObject); + } else { + return true; + } + }); + + if (!this.isComponentMounted) { + return; + } + + // We need this check to handle the case where search results come back in a different + // order than they were sent out. Only load results for the most recent search. + if (query === this.state.query) { + this.setState({ + isFetchingItems: false, + items: resp.savedObjects.map(savedObject => { + const { + attributes: { title }, + id, + type, + } = savedObject; + return { + title: typeof title === 'string' ? title : '', + id, + type, + savedObject, + }; + }), + }); + } + }, 300); + + constructor(props: SavedObjectFinderProps) { + super(props); + + this.state = { + items: [], + isFetchingItems: false, + page: 0, + perPage: props.initialPageSize || props.fixedPageSize || 10, + query: '', + filterOpen: false, + filteredTypes: [], + sortOpen: false, + }; + } + + public componentWillUnmount() { + this.isComponentMounted = false; + this.debouncedFetch.cancel(); + } + + public componentDidMount() { + this.isComponentMounted = true; + this.fetchItems(); + } + + public render() { + return ( + + {this.renderSearchBar()} + {this.renderListing()} + + ); + } + + private getSavedObjectMetaDataMap(): Record> { + return this.props.savedObjectMetaData.reduce( + (map, metaData) => ({ ...map, [metaData.type]: metaData }), + {} + ); + } + + private getPageCount() { + return Math.ceil( + (this.state.filteredTypes.length === 0 + ? this.state.items.length + : this.state.items.filter( + item => + this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type) + ).length) / this.state.perPage + ); + } + + // server-side paging not supported + // 1) saved object client does not support sorting by title because title is only mapped as analyzed + // 2) can not search on anything other than title because all other fields are stored in opaque JSON strings, + // for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side + // with the current mappings + private getPageOfItems = () => { + // do not sort original list to preserve elasticsearch ranking order + const items = this.state.items.slice(); + const { sortDirection } = this.state; + + if (sortDirection || !this.state.query) { + items.sort(({ title: titleA }, { title: titleB }) => { + let order = 1; + if (sortDirection === 'desc') { + order = -1; + } + return order * (titleA || '').toLowerCase().localeCompare((titleB || '').toLowerCase()); + }); + } + + // If begin is greater than the length of the sequence, an empty array is returned. + const startIndex = this.state.page * this.state.perPage; + // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). + const lastIndex = startIndex + this.state.perPage; + return items + .filter( + item => + this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type) + ) + .slice(startIndex, lastIndex); + }; + + private fetchItems = () => { + this.setState( + { + isFetchingItems: true, + }, + this.debouncedFetch.bind(null, this.state.query) + ); + }; + + private getAvailableSavedObjectMetaData() { + const typesInItems = new Set(); + this.state.items.forEach(item => { + typesInItems.add(item.type); + }); + return this.props.savedObjectMetaData.filter(metaData => typesInItems.has(metaData.type)); + } + + private getSortOptions() { + const sortOptions = [ + { + this.setState({ + sortDirection: 'asc', + }); + }} + > + {i18n.translate('kibana-react.savedObjects.finder.sortAsc', { + defaultMessage: 'Ascending', + })} + , + { + this.setState({ + sortDirection: 'desc', + }); + }} + > + {i18n.translate('kibana-react.savedObjects.finder.sortDesc', { + defaultMessage: 'Descending', + })} + , + ]; + if (this.state.query) { + sortOptions.push( + { + this.setState({ + sortDirection: undefined, + }); + }} + > + {i18n.translate('kibana-react.savedObjects.finder.sortAuto', { + defaultMessage: 'Best match', + })} + + ); + } + return sortOptions; + } + + private renderSearchBar() { + const availableSavedObjectMetaData = this.getAvailableSavedObjectMetaData(); + + return ( + + + { + this.setState( + { + query: e.target.value, + }, + this.fetchItems + ); + }} + data-test-subj="savedObjectFinderSearchInput" + isLoading={this.state.isFetchingItems} + /> + + + + this.setState({ sortOpen: false })} + button={ + + this.setState(({ sortOpen }) => ({ + sortOpen: !sortOpen, + })) + } + iconType="arrowDown" + isSelected={this.state.sortOpen} + data-test-subj="savedObjectFinderSortButton" + > + {i18n.translate('kibana-react.savedObjects.finder.sortButtonLabel', { + defaultMessage: 'Sort', + })} + + } + > + + + {this.props.showFilter && ( + this.setState({ filterOpen: false })} + button={ + + this.setState(({ filterOpen }) => ({ + filterOpen: !filterOpen, + })) + } + iconType="arrowDown" + data-test-subj="savedObjectFinderFilterButton" + isSelected={this.state.filterOpen} + numFilters={this.props.savedObjectMetaData.length} + hasActiveFilters={this.state.filteredTypes.length > 0} + numActiveFilters={this.state.filteredTypes.length} + > + {i18n.translate('kibana-react.savedObjects.finder.filterButtonLabel', { + defaultMessage: 'Types', + })} + + } + > + ( + { + this.setState(({ filteredTypes }) => ({ + filteredTypes: filteredTypes.includes(metaData.type) + ? filteredTypes.filter(t => t !== metaData.type) + : [...filteredTypes, metaData.type], + page: 0, + })); + }} + > + {metaData.name} + + ))} + /> + + )} + + + + ); + } + + private renderListing() { + const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); + const { onChoose, savedObjectMetaData } = this.props; + + return ( + <> + {this.state.isFetchingItems && this.state.items.length === 0 && ( + + + + + + + )} + {items.length > 0 ? ( + + {items.map(item => { + const currentSavedObjectMetaData = savedObjectMetaData.find( + metaData => metaData.type === item.type + )!; + const fullName = currentSavedObjectMetaData.getTooltipForSavedObject + ? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject) + : `${item.title} (${currentSavedObjectMetaData!.name})`; + const iconType = ( + currentSavedObjectMetaData || + ({ + getIconForSavedObject: () => 'document', + } as Pick, 'getIconForSavedObject'>) + ).getIconForSavedObject(item.savedObject); + return ( + { + onChoose(item.id, item.type, fullName); + } + : undefined + } + title={fullName} + data-test-subj={`savedObjectTitle${(item.title || '').split(' ').join('-')}`} + /> + ); + })} + + ) : ( + !this.state.isFetchingItems && + )} + {this.getPageCount() > 1 && + (this.props.fixedPageSize ? ( + { + this.setState({ + page, + }); + }} + /> + ) : ( + { + this.setState({ + page, + }); + }} + onChangeItemsPerPage={perPage => { + this.setState({ + perPage, + }); + }} + itemsPerPage={this.state.perPage} + itemsPerPageOptions={[5, 10, 15, 25]} + /> + ))} + + ); + } +} + +export { SavedObjectFinder }; diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.test.tsx similarity index 100% rename from src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.tsx rename to src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.test.tsx diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx new file mode 100644 index 00000000000000..27da5d90646b3b --- /dev/null +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx @@ -0,0 +1,271 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { EuiText } from '@elastic/eui'; + +interface OnSaveProps { + newTitle: string; + newCopyOnSave: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; +} + +interface Props { + onSave: (props: OnSaveProps) => void; + onClose: () => void; + title: string; + showCopyOnSave: boolean; + objectType: string; + confirmButtonLabel?: React.ReactNode; + options?: React.ReactNode; + description?: string; +} + +interface State { + title: string; + copyOnSave: boolean; + isTitleDuplicateConfirmed: boolean; + hasTitleDuplicate: boolean; + isLoading: boolean; +} + +export class SavedObjectSaveModal extends React.Component { + public readonly state = { + title: this.props.title, + copyOnSave: false, + isTitleDuplicateConfirmed: false, + hasTitleDuplicate: false, + isLoading: false, + }; + + public render() { + const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, isLoading } = this.state; + + return ( + +
+ + + + + + + + + {this.renderDuplicateTitleCallout()} + + + {this.props.description && ( + + {this.props.description} + + )} + {this.renderCopyOnSave()} + + + } + > + + + + {this.props.options} + + + + + + + + + + {this.props.confirmButtonLabel ? ( + this.props.confirmButtonLabel + ) : ( + + )} + + + +
+
+ ); + } + + private onTitleDuplicate = () => { + this.setState({ + isLoading: false, + isTitleDuplicateConfirmed: true, + hasTitleDuplicate: true, + }); + }; + + private saveSavedObject = async () => { + if (this.state.isLoading) { + // ignore extra clicks + return; + } + + this.setState({ + isLoading: true, + }); + + await this.props.onSave({ + newTitle: this.state.title, + newCopyOnSave: this.state.copyOnSave, + isTitleDuplicateConfirmed: this.state.isTitleDuplicateConfirmed, + onTitleDuplicate: this.onTitleDuplicate, + }); + }; + + private onTitleChange = (event: React.ChangeEvent) => { + this.setState({ + title: event.target.value, + isTitleDuplicateConfirmed: false, + hasTitleDuplicate: false, + }); + }; + + private onCopyOnSaveChange = (event: React.ChangeEvent) => { + this.setState({ + copyOnSave: event.target.checked, + }); + }; + + private onFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + this.saveSavedObject(); + }; + + private renderDuplicateTitleCallout = () => { + if (!this.state.hasTitleDuplicate) { + return; + } + + return ( + <> + + } + color="warning" + data-test-subj="titleDupicateWarnMsg" + > +

+ + + + ), + }} + /> +

+
+ + + ); + }; + + private renderCopyOnSave = () => { + if (!this.props.showCopyOnSave) { + return; + } + + return ( + <> + + } + /> + + + ); + }; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c447c72d66bf4d..c07cf455dcd8b4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -506,24 +506,24 @@ "common.ui.savedObjects.confirmModal.overwriteTitle": "{name} を上書きしますか?", "common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel": "{name} を保存", "common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage": "「{title}」というタイトルの {name} が既に存在します。保存を続けますか?", - "common.ui.savedObjects.finder.filterButtonLabel": "タイプ", - "common.ui.savedObjects.finder.searchPlaceholder": "検索…", - "common.ui.savedObjects.finder.sortAsc": "昇順", - "common.ui.savedObjects.finder.sortAuto": "ベストマッチ", - "common.ui.savedObjects.finder.sortButtonLabel": "並べ替え", - "common.ui.savedObjects.finder.sortDesc": "降順", + "kibana-react.savedObjects.finder.filterButtonLabel": "タイプ", + "kibana-react.savedObjects.finder.searchPlaceholder": "検索…", + "kibana-react.savedObjects.finder.sortAsc": "昇順", + "kibana-react.savedObjects.finder.sortAuto": "ベストマッチ", + "kibana-react.savedObjects.finder.sortButtonLabel": "並べ替え", + "kibana-react.savedObjects.finder.sortDesc": "降順", "common.ui.savedObjects.howToSaveAsNewDescription": "Kibana の以前のバージョンでは、{savedObjectName} の名前を変更すると新しい名前でコピーが作成されました。今後この操作を行うには、「新規 {savedObjectName} として保存」を使用します。", "common.ui.savedObjects.overwriteRejectedDescription": "上書き確認が拒否されました", "common.ui.savedObjects.saveAsNewLabel": "新規 {savedObjectName} として保存", "common.ui.savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", - "common.ui.savedObjects.saveModal.cancelButtonLabel": "キャンセル", - "common.ui.savedObjects.saveModal.confirmSaveButtonLabel": "保存の確認", - "common.ui.savedObjects.saveModal.duplicateTitleDescription": "重複タイトルで {objectType} を保存するには {confirmSaveLabel} をクリックしてください。", - "common.ui.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "保存の確認", - "common.ui.savedObjects.saveModal.duplicateTitleLabel": "「{title}」というタイトルの {objectType} が既に存在します。", - "common.ui.savedObjects.saveModal.saveAsNewLabel": "新規 {objectType} として保存", - "common.ui.savedObjects.saveModal.saveTitle": "{objectType} を保存", - "common.ui.savedObjects.saveModal.titleLabel": "タイトル", + "kibana-react.savedObjects.saveModal.cancelButtonLabel": "キャンセル", + "kibana-react.savedObjects.saveModal.confirmSaveButtonLabel": "保存の確認", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription": "重複タイトルで {objectType} を保存するには {confirmSaveLabel} をクリックしてください。", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "保存の確認", + "kibana-react.savedObjects.saveModal.duplicateTitleLabel": "「{title}」というタイトルの {objectType} が既に存在します。", + "kibana-react.savedObjects.saveModal.saveAsNewLabel": "新規 {objectType} として保存", + "kibana-react.savedObjects.saveModal.saveTitle": "{objectType} を保存", + "kibana-react.savedObjects.saveModal.titleLabel": "タイトル", "common.ui.scriptingLanguages.errorFetchingToastDescription": "Elasticsearch から利用可能なスクリプト言語の取得中にエラーが発生しました", "common.ui.share.contextMenu.embedCodeLabel": "埋め込みコード", "common.ui.share.contextMenu.embedCodePanelTitle": "埋め込みコード", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d02898938ce171..dedbfb98402bec 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -506,24 +506,24 @@ "common.ui.savedObjects.confirmModal.overwriteTitle": "覆盖“{name}”?", "common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel": "保存“{name}”", "common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage": "具有标题 “{title}” 的 “{name}” 已存在。是否确定要保存?", - "common.ui.savedObjects.finder.filterButtonLabel": "类型", - "common.ui.savedObjects.finder.searchPlaceholder": "搜索……", - "common.ui.savedObjects.finder.sortAsc": "升序", - "common.ui.savedObjects.finder.sortAuto": "最佳匹配", - "common.ui.savedObjects.finder.sortButtonLabel": "排序", - "common.ui.savedObjects.finder.sortDesc": "降序", + "kibana-react.savedObjects.finder.filterButtonLabel": "类型", + "kibana-react.savedObjects.finder.searchPlaceholder": "搜索……", + "kibana-react.savedObjects.finder.sortAsc": "升序", + "kibana-react.savedObjects.finder.sortAuto": "最佳匹配", + "kibana-react.savedObjects.finder.sortButtonLabel": "排序", + "kibana-react.savedObjects.finder.sortDesc": "降序", "common.ui.savedObjects.howToSaveAsNewDescription": "在 Kibana 的以前版本中,更改 {savedObjectName} 的名称将创建具有新名称的副本。使用“另存为新的 {savedObjectName}” 复选框可立即达到此目的。", "common.ui.savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认", "common.ui.savedObjects.saveAsNewLabel": "另存为新的 {savedObjectName}", "common.ui.savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", - "common.ui.savedObjects.saveModal.cancelButtonLabel": "取消", - "common.ui.savedObjects.saveModal.confirmSaveButtonLabel": "确认保存", - "common.ui.savedObjects.saveModal.duplicateTitleDescription": "单击 “{confirmSaveLabel}” 以保存标题重复的{objectType}", - "common.ui.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "确认保存", - "common.ui.savedObjects.saveModal.duplicateTitleLabel": "具有标题 “{title}” 的 {objectType} 已存在。", - "common.ui.savedObjects.saveModal.saveAsNewLabel": "另存为新的{objectType}", - "common.ui.savedObjects.saveModal.saveTitle": "保存{objectType}", - "common.ui.savedObjects.saveModal.titleLabel": "标题", + "kibana-react.savedObjects.saveModal.cancelButtonLabel": "取消", + "kibana-react.savedObjects.saveModal.confirmSaveButtonLabel": "确认保存", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription": "单击 “{confirmSaveLabel}” 以保存标题重复的{objectType}", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "确认保存", + "kibana-react.savedObjects.saveModal.duplicateTitleLabel": "具有标题 “{title}” 的 {objectType} 已存在。", + "kibana-react.savedObjects.saveModal.saveAsNewLabel": "另存为新的{objectType}", + "kibana-react.savedObjects.saveModal.saveTitle": "保存{objectType}", + "kibana-react.savedObjects.saveModal.titleLabel": "标题", "common.ui.scriptingLanguages.errorFetchingToastDescription": "从 Elasticsearch 获取可用的脚本语言时出错", "common.ui.share.contextMenu.embedCodeLabel": "嵌入代码", "common.ui.share.contextMenu.embedCodePanelTitle": "嵌入代码",