From c739cb62671df46abc20f1501d300dd8d2948afb Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 19 Sep 2019 11:20:10 +0200 Subject: [PATCH] [Graph] EUI-ification of search bar (#45351) --- .../saved_object_finder.test.tsx | 4 +- .../saved_objects/saved_object_finder.tsx | 5 +- .../public/angular/templates/_graph.scss | 2 +- .../graph/public/angular/templates/index.html | 37 +++---- .../public/angular/templates/inspect.html | 2 +- x-pack/legacy/plugins/graph/public/app.js | 64 ++++++++---- .../graph/public/components/_index.scss | 1 + .../graph/public/components/_search_bar.scss | 11 +++ .../public/components/search_bar.test.tsx | 63 ++++++++++++ .../graph/public/components/search_bar.tsx | 97 +++++++++++++++++++ .../graph/public/components/source_modal.tsx | 29 ++++++ .../graph/public/components/source_picker.tsx | 55 +++++++++++ x-pack/legacy/plugins/graph/public/index.scss | 1 + .../graph/public/services/source_modal.tsx | 34 +++++++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 16 files changed, 355 insertions(+), 54 deletions(-) create mode 100644 x-pack/legacy/plugins/graph/public/components/_search_bar.scss create mode 100644 x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx create mode 100644 x-pack/legacy/plugins/graph/public/components/search_bar.tsx create mode 100644 x-pack/legacy/plugins/graph/public/components/source_modal.tsx create mode 100644 x-pack/legacy/plugins/graph/public/components/source_picker.tsx create mode 100644 x-pack/legacy/plugins/graph/public/services/source_modal.tsx diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx index 6b0728e73c691..5c8103cd4fe90 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx @@ -135,7 +135,9 @@ describe('SavedObjectsFinder', () => { .find(EuiListGroupItem) .first() .simulate('click'); - expect(chooseStub.calledWith('1', 'search', `${doc.attributes.title} (Search)`)).toEqual(true); + expect(chooseStub.calledWith('1', 'search', `${doc.attributes.title} (Search)`, doc)).toEqual( + true + ); }); describe('sorting', () => { 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 index d34185731f3cc..05f02fe972d46 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx @@ -86,7 +86,8 @@ interface BaseSavedObjectFinder { onChoose?: ( id: SimpleSavedObject['id'], type: SimpleSavedObject['type'], - name: string + name: string, + savedObject: SimpleSavedObject ) => void; noItemsMessage?: React.ReactNode; savedObjectMetaData: Array>; @@ -470,7 +471,7 @@ class SavedObjectFinder extends React.Component { - onChoose(item.id, item.type, fullName); + onChoose(item.id, item.type, fullName, item.savedObject); } : undefined } diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss b/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss index e739091771518..58b5db1e6cd7d 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss +++ b/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss @@ -75,7 +75,7 @@ } .gphGraph__menus, .gphGraph__bar { - margin: $euiSizeM $euiSizeM 0 $euiSizeM; + margin: $euiSizeS; } .gphGraph__flexGroup { diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/index.html b/x-pack/legacy/plugins/graph/public/angular/templates/index.html index 234ea2ecb5b4f..a10ca777295f4 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -6,19 +6,20 @@
+ +
- - - - - -
-
- - -
-
diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/inspect.html b/x-pack/legacy/plugins/graph/public/angular/templates/inspect.html index fdcaeab95b51d..a0c4f9803d844 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/inspect.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/inspect.html @@ -6,7 +6,7 @@ >
http://host:port/{{ selectedIndex.name }}/_graph/explore diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 624bdc474b50a..9c2080c9c4a82 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -41,6 +41,7 @@ import listingTemplate from './angular/templates/listing_ng_wrapper.html'; import { getReadonlyBadge } from './badge'; import { FormattedMessage } from '@kbn/i18n/react'; +import { SearchBar } from './components/search_bar'; import { VennDiagram } from './components/venn_diagram'; import { Listing } from './components/listing'; import { Settings } from './components/settings'; @@ -57,8 +58,9 @@ import { outlinkEncoders, } from './helpers/outlink_encoders'; import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs, getHomePath } from './services/url'; -import { appStateToSavedWorkspace, savedWorkspaceToAppState, lookupIndexPattern, mapFields } from './services/persistence'; +import { openSourceModal } from './services/source_modal'; import { openSaveModal } from './services/save_modal'; +import { appStateToSavedWorkspace, savedWorkspaceToAppState, lookupIndexPattern, mapFields } from './services/persistence'; import { urlTemplateRegex } from './helpers/url_template'; import { asAngularSyncedObservable, @@ -97,6 +99,18 @@ app.directive('graphListing', function (reactDirective) { return reactDirective(Listing); }); +app.directive('graphSearchBar', function (reactDirective) { + return reactDirective(SearchBar, [ + ['currentIndexPattern', { watchDepth: 'reference' }], + ['isLoading', { watchDepth: 'reference' }], + ['onIndexPatternSelected', { watchDepth: 'reference' }], + ['onQuerySubmit', { watchDepth: 'reference' }], + ['savedObjects', { watchDepth: 'reference' }], + ['uiSettings', { watchDepth: 'reference' }], + ['overlays', { watchDepth: 'reference' }] + ]); +}); + if (uiRoutes.enable) { uiRoutes.enable(); } @@ -244,6 +258,10 @@ app.controller('graphuiPlugin', function ( $scope.allSavingDisabled = $scope.graphSavePolicy === 'none'; $scope.searchTerm = ''; + $scope.pluginDependencies = npStart.core; + + $scope.loading = false; + //So scope properties can be used consistently with ng-model $scope.grr = $scope; @@ -369,11 +387,9 @@ app.controller('graphuiPlugin', function ( }), confirmModalOptions); } - $scope.uiSelectIndex = function () { + $scope.uiSelectIndex = function (proposedIndex) { canWipeWorkspace(function () { - $scope.indexSelected($scope.proposedIndex); - }, function () { - $scope.proposedIndex = $scope.selectedIndex; + $scope.indexSelected(proposedIndex); }); }; @@ -414,6 +430,7 @@ app.controller('graphuiPlugin', function ( index: indexName, query: query }; + $scope.loading = true; return $http.post('../api/graph/graphExplore', request) .then(function (resp) { if (resp.data.resp.timed_out) { @@ -425,7 +442,10 @@ app.controller('graphuiPlugin', function ( } responseHandler(resp.data.resp); }) - .catch(handleHttpError); + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); } @@ -435,20 +455,24 @@ app.controller('graphuiPlugin', function ( index: indexName, body: query }; + $scope.loading = true; $http.post('../api/graph/searchProxy', request) .then(function (resp) { responseHandler(resp.data.resp); }) - .catch(handleHttpError); + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); }; - $scope.submit = function () { + $scope.submit = function (searchTerm) { $scope.hideAllConfigPanels(); initWorkspaceIfRequired(); const numHops = 2; - if ($scope.searchTerm.startsWith('{')) { + if (searchTerm.startsWith('{')) { try { - const query = JSON.parse($scope.searchTerm); + const query = JSON.parse(searchTerm); if (query.vertices) { // Is a graph explore request $scope.workspace.callElasticsearch(query); @@ -462,7 +486,7 @@ app.controller('graphuiPlugin', function ( } return; } - $scope.workspace.simpleSearch($scope.searchTerm, $scope.liveResponseFields, numHops); + $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); }; $scope.clearWorkspace = function () { @@ -633,9 +657,6 @@ app.controller('graphuiPlugin', function ( } } - $scope.indices = $route.current.locals.indexPatterns.filter(indexPattern => !indexPattern.attributes.type); - - $scope.setDetail = function (data) { $scope.detail = data; }; @@ -706,7 +727,7 @@ app.controller('graphuiPlugin', function ( const managementUrl = npStart.core.chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; - if ($scope.indices.length === 0) { + if ($route.current.locals.indexPatterns.length === 0) { toastNotifications.addWarning({ title: i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', { defaultMessage: 'No data source', @@ -748,7 +769,9 @@ app.controller('graphuiPlugin', function ( }), run: function () { canWipeWorkspace(function () { - kbnUrl.change('/workspace/', {}); + $scope.$evalAsync(() => { + kbnUrl.change('/workspace/', {}); + }); }); }, testId: 'graphNewButton', }); @@ -868,7 +891,7 @@ app.controller('graphuiPlugin', function ( // Deal with situation of request to open saved workspace if ($route.current.locals.savedWorkspace) { $scope.savedWorkspace = $route.current.locals.savedWorkspace; - const selectedIndex = lookupIndexPattern($scope.savedWorkspace, $scope.indices); + const selectedIndex = lookupIndexPattern($scope.savedWorkspace, $route.current.locals.indexPatterns); if(!selectedIndex) { toastNotifications.addDanger( i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { @@ -902,13 +925,16 @@ app.controller('graphuiPlugin', function ( } // Allow URLs to include a user-defined text query if ($route.current.params.query) { - $scope.searchTerm = $route.current.params.query; - $scope.submit(); + $scope.initialQuery = $route.current.params.query; + $scope.submit($route.current.params.query); } }); } else { $route.current.locals.SavedWorkspacesProvider.get().then(function (newWorkspace) { $scope.savedWorkspace = newWorkspace; + openSourceModal(npStart.core, indexPattern => { + $scope.indexSelected(indexPattern); + }); }); } diff --git a/x-pack/legacy/plugins/graph/public/components/_index.scss b/x-pack/legacy/plugins/graph/public/components/_index.scss index 8fc1885968c87..abd6440c5a686 100644 --- a/x-pack/legacy/plugins/graph/public/components/_index.scss +++ b/x-pack/legacy/plugins/graph/public/components/_index.scss @@ -1,2 +1,3 @@ +@import './search_bar'; @import './venn_diagram/index'; @import './settings/index'; diff --git a/x-pack/legacy/plugins/graph/public/components/_search_bar.scss b/x-pack/legacy/plugins/graph/public/components/_search_bar.scss new file mode 100644 index 0000000000000..32186df2031b2 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/_search_bar.scss @@ -0,0 +1,11 @@ +.gphSearchBar { + padding: 0 $euiSizeS; +} + +.gphSearchBar__datasourceButton { + height: 100% !important; +} + +.gphSearchBar__datasourceButtonTooltip { + padding: 0; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx new file mode 100644 index 0000000000000..a8551fc33b6e6 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchBar } from './search_bar'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React, { ReactElement } from 'react'; +import { CoreStart } from 'src/core/public'; +import { IndexPatternSavedObject } from '../types'; +import { act } from 'react-dom/test-utils'; +import { EuiFieldText } from '@elastic/eui'; +import { openSourceModal } from '../services/source_modal'; + +jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); + +describe('search_bar', () => { + it('should render search bar and submit queryies', () => { + const querySubmit = jest.fn(); + const instance = shallowWithIntl( + {}} + onQuerySubmit={querySubmit} + savedObjects={{} as CoreStart['savedObjects']} + uiSettings={{} as CoreStart['uiSettings']} + overlays={{} as CoreStart['overlays']} + currentIndexPattern={{ attributes: { title: 'Testpattern' } } as IndexPatternSavedObject} + /> + ); + act(() => { + instance.find(EuiFieldText).simulate('change', { target: { value: 'testQuery' } }); + }); + + act(() => { + instance.find('form').simulate('submit', { preventDefault: () => {} }); + }); + + expect(querySubmit).toHaveBeenCalledWith('testQuery'); + }); + + it('should render index pattern picker', () => { + const indexPatternSelected = jest.fn(); + const instance = shallowWithIntl( + {}} + savedObjects={{} as CoreStart['savedObjects']} + uiSettings={{} as CoreStart['uiSettings']} + overlays={{} as CoreStart['overlays']} + currentIndexPattern={{ attributes: { title: 'Testpattern' } } as IndexPatternSavedObject} + /> + ); + + // pick the button component out of the tree because + // it's part of a popover and thus not covered by enzyme + (instance.find(EuiFieldText).prop('prepend') as ReactElement).props.children.props.onClick(); + + expect(openSourceModal).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx new file mode 100644 index 0000000000000..d3fc2922a6098 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiButton, + EuiButtonEmpty, + EuiToolTip, +} from '@elastic/eui'; +import React, { useState } from 'react'; + +import { CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import { IndexPatternSavedObject } from '../types'; +import { openSourceModal } from '../services/source_modal'; + +interface SearchBarProps { + isLoading: boolean; + initialQuery?: string; + currentIndexPattern?: IndexPatternSavedObject; + onIndexPatternSelected: (indexPattern: IndexPatternSavedObject) => void; + onQuerySubmit: (query: string) => void; + savedObjects: CoreStart['savedObjects']; + uiSettings: CoreStart['uiSettings']; + overlays: CoreStart['overlays']; +} + +export function SearchBar({ + currentIndexPattern, + onQuerySubmit, + isLoading, + onIndexPatternSelected, + initialQuery, + ...sourcePickerProps +}: SearchBarProps) { + const [query, setQuery] = useState(initialQuery || ''); + return ( +
{ + e.preventDefault(); + if (!isLoading && currentIndexPattern) { + onQuerySubmit(query); + } + }} + > + + + + { + openSourceModal(sourcePickerProps, onIndexPatternSelected); + }} + > + {currentIndexPattern + ? currentIndexPattern.attributes.title + : // This branch will be shown if the user exits the + // initial picker modal + i18n.translate('xpack.graph.bar.pickSourceLabel', { + defaultMessage: 'Click here to pick a data source', + })} + + + } + value={query} + onChange={({ target: { value } }) => setQuery(value)} + /> + + + + {i18n.translate('xpack.graph.bar.exploreLabel', { defaultMessage: 'Explore' })} + + + +
+ ); +} diff --git a/x-pack/legacy/plugins/graph/public/components/source_modal.tsx b/x-pack/legacy/plugins/graph/public/components/source_modal.tsx new file mode 100644 index 0000000000000..4c3b3c8be9110 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/source_modal.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +import { SourcePicker, SourcePickerProps } from './source_picker'; + +export function SourceModal(props: SourcePickerProps) { + return ( + <> + + + + + + + + + + ); +} diff --git a/x-pack/legacy/plugins/graph/public/components/source_picker.tsx b/x-pack/legacy/plugins/graph/public/components/source_picker.tsx new file mode 100644 index 0000000000000..08ccef8399c74 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/source_picker.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +import { CoreStart } from 'src/core/public'; +import { SavedObjectFinder } from '../../../../../../src/plugins/kibana_react/public'; +import { IndexPatternSavedObject } from '../types'; + +export interface SourcePickerProps { + currentIndexPattern?: IndexPatternSavedObject; + onIndexPatternSelected: (indexPattern: IndexPatternSavedObject) => void; + savedObjects: CoreStart['savedObjects']; + uiSettings: CoreStart['uiSettings']; +} + +const fixedPageSize = 8; + +export function SourcePicker({ + savedObjects, + uiSettings, + currentIndexPattern, + onIndexPatternSelected, +}: SourcePickerProps) { + return ( + { + onIndexPatternSelected(indexPattern as IndexPatternSavedObject); + }} + showFilter={false} + noItemsMessage={i18n.translate('xpack.graph.sourceModal.notFoundLabel', { + defaultMessage: 'No matching indices found.', + })} + savedObjectMetaData={[ + { + type: 'index-pattern', + getIconForSavedObject: () => 'indexPatternApp', + name: i18n.translate('xpack.graph.sourceModal.savedObjectType.indexPattern', { + defaultMessage: 'Index pattern', + }), + showSavedObject: indexPattern => + !indexPattern.attributes.type && + (!currentIndexPattern || currentIndexPattern.id !== indexPattern.id), + }, + ]} + fixedPageSize={fixedPageSize} + /> + ); +} diff --git a/x-pack/legacy/plugins/graph/public/index.scss b/x-pack/legacy/plugins/graph/public/index.scss index b0148b5662205..d53a702833ea5 100644 --- a/x-pack/legacy/plugins/graph/public/index.scss +++ b/x-pack/legacy/plugins/graph/public/index.scss @@ -12,4 +12,5 @@ @import './main'; @import './angular/templates/index'; + @import './components/index'; diff --git a/x-pack/legacy/plugins/graph/public/services/source_modal.tsx b/x-pack/legacy/plugins/graph/public/services/source_modal.tsx new file mode 100644 index 0000000000000..c985271f4dfe0 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/services/source_modal.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'src/core/public'; +import React from 'react'; +import { SourceModal } from '../components/source_modal'; +import { IndexPatternSavedObject } from '../types'; + +export function openSourceModal( + { + overlays, + savedObjects, + uiSettings, + }: { + overlays: CoreStart['overlays']; + savedObjects: CoreStart['savedObjects']; + uiSettings: CoreStart['uiSettings']; + }, + onSelected: (indexPattern: IndexPatternSavedObject) => void +) { + const modalRef = overlays.openModal( + { + onSelected(indexPattern); + modalRef.close(); + }} + /> + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 48a42bf321573..d2c2446b09777 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4664,8 +4664,6 @@ "xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel": "ワークスペースを保存", "xpack.graph.topNavMenu.saveWorkspace.enabledLabel": "保存", "xpack.graph.topNavMenu.saveWorkspace.enabledTooltip": "このワークスペースを保存します", - "xpack.graph.topNavMenu.searchButtonAriaLabel": "検索", - "xpack.graph.topNavMenu.selectIndexPatternOptionLabel": "インデックスパターンを選択してください…", "xpack.graph.topNavMenu.settingsAriaLabel": "設定", "xpack.graph.topNavMenu.settingsLabel": "設定", "xpack.graph.errorToastTitle": "Graph エラー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d976b872201bc..a0cfdbe2c03e9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4667,8 +4667,6 @@ "xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel": "保存工作空间", "xpack.graph.topNavMenu.saveWorkspace.enabledLabel": "保存", "xpack.graph.topNavMenu.saveWorkspace.enabledTooltip": "保存此工作空间", - "xpack.graph.topNavMenu.searchButtonAriaLabel": "搜索", - "xpack.graph.topNavMenu.selectIndexPatternOptionLabel": "选择索引模式......", "xpack.graph.topNavMenu.settingsAriaLabel": "设置", "xpack.graph.topNavMenu.settingsLabel": "设置", "xpack.graph.errorToastTitle": "Graph 错误",