From 7ba504f13e0cb475b2d2011bfe8dda985c20ae10 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 2 Oct 2019 14:23:18 +0200 Subject: [PATCH 01/12] wip --- x-pack/legacy/plugins/graph/public/app.js | 1530 ++++++++--------- x-pack/legacy/plugins/graph/public/plugin.ts | 49 + .../legacy/plugins/graph/public/render_app.ts | 54 + 3 files changed, 862 insertions(+), 771 deletions(-) create mode 100644 x-pack/legacy/plugins/graph/public/plugin.ts create mode 100644 x-pack/legacy/plugins/graph/public/render_app.ts diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index a9b74bb4f6e67..731f38a4ffd0e 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -10,31 +10,7 @@ import { i18n } from '@kbn/i18n'; import 'ace'; import rison from 'rison-node'; import React from 'react'; - -// import the uiExports that we want to "use" -import 'uiExports/fieldFormats'; -import 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; -import 'ui/kbn_top_nav'; -import 'ui/directives/saved_object_finder'; -import 'ui/directives/input_focus'; -import 'ui/saved_objects/ui/saved_object_save_as_checkbox'; -import 'uiExports/autocompleteProviders'; -import chrome from 'ui/chrome'; -import { uiModules } from 'ui/modules'; -import uiRoutes from 'ui/routes'; -import { addAppRedirectMessageToUrl, fatalError, toastNotifications } from 'ui/notify'; -import { formatAngularHttpError } from 'ui/notify/lib'; -import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; -import { npStart } from 'ui/new_platform'; -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import { capabilities } from 'ui/capabilities'; -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; - -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import appTemplate from './angular/templates/index.html'; import listingTemplate from './angular/templates/listing_ng_wrapper.html'; @@ -47,7 +23,6 @@ import { Listing } from './components/listing'; import { Settings } from './components/settings'; import gws from './angular/graph_client_workspace.js'; -import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; import { iconChoices, colorChoices, @@ -75,849 +50,862 @@ import { import './angular/directives/graph_inspect'; -const app = uiModules.get('app/graph'); +export function initAngularModule(moduleName, deps) { + const { + xpackInfo, + addAppRedirectMessageToUrl, + fatalError, + chrome, + savedGraphWorkspaces, + toastNotifications, + savedObjectsClient, //Private(SavedObjectsClientProvider) + indexPatterns, //data.indexPatterns.indexPatterns + savedWorkspacesClient, //Private(SavedWorkspacesProvider) + kbnBaseUrl, + kbnUrl, + config, //uiSettings? + savedObjectRegistry, //Private(SavedObjectRegistryProvider) + capabilities, + formatAngularHttpError, + coreStart, //npStart.core + confirmModal, + http, //$http + showSaveModal, + } = deps; + + const app = angular.module(moduleName, ['ngRoute']); + + function checkLicense(Promise, kbnBaseUrl) { + const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && + xpackInfo.get('features.graph.enableAppLink'); + if (!licenseAllowsToShowThisPage) { + const message = xpackInfo.get('features.graph.message'); + const newUrl = addAppRedirectMessageToUrl(chrome.addBasePath(kbnBaseUrl), message); + window.location.href = newUrl; + return Promise.halt(); + } -function checkLicense(Promise, kbnBaseUrl) { - const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && - xpackInfo.get('features.graph.enableAppLink'); - if (!licenseAllowsToShowThisPage) { - const message = xpackInfo.get('features.graph.message'); - const newUrl = addAppRedirectMessageToUrl(chrome.addBasePath(kbnBaseUrl), message); - window.location.href = newUrl; - return Promise.halt(); + return Promise.resolve(); } - return Promise.resolve(); -} + app.directive('focusOn', function () { + return function (scope, elem, attr) { + scope.$on(attr.focusOn, function () { + elem[0].focus(); + }); + }; + }); -app.directive('focusOn', function () { - return function (scope, elem, attr) { - scope.$on(attr.focusOn, function () { - elem[0].focus(); - }); - }; -}); - -app.directive('vennDiagram', function (reactDirective) { - return reactDirective(VennDiagram); -}); - -app.directive('graphListing', function (reactDirective) { - return reactDirective(Listing); -}); - -app.directive('graphApp', function (reactDirective) { - return reactDirective(GraphApp, [ - ['state', { watchDepth: 'reference' }], - ['dispatch', { watchDepth: 'reference' }], - ['currentIndexPattern', { watchDepth: 'reference' }], - ['isLoading', { watchDepth: 'reference' }], - ['onIndexPatternSelected', { watchDepth: 'reference' }], - ['onQuerySubmit', { watchDepth: 'reference' }], - ['savedObjects', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ['http', { watchDepth: 'reference' }], - ['initialQuery', { watchDepth: 'reference' }], - ['overlays', { watchDepth: 'reference' }] - ]); -}); - -if (uiRoutes.enable) { - uiRoutes.enable(); -} + app.directive('vennDiagram', function (reactDirective) { + return reactDirective(VennDiagram); + }); -uiRoutes - .when('/home', { - template: listingTemplate, - badge: getReadonlyBadge, - controller($injector, $location, $scope, Private, config, Promise, kbnBaseUrl) { - checkLicense(Promise, kbnBaseUrl); - const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; - const graphService = services['Graph workspace']; - const kbnUrl = $injector.get('kbnUrl'); - - $scope.listingLimit = config.get('savedObjects:listingLimit'); - $scope.create = () => { - kbnUrl.redirect(getNewPath()); - }; - $scope.find = (search) => { - return graphService.find(search, $scope.listingLimit); - }; - $scope.editItem = (workspace) => { - kbnUrl.redirect(getEditPath(workspace)); - }; - $scope.getViewUrl = (workspace) => getEditUrl(chrome, workspace); - $scope.delete = (workspaces) => { - return graphService.delete(workspaces.map(({ id }) => id)); - }; - $scope.capabilities = capabilities.get().graph; - $scope.initialFilter = ($location.search()).filter || ''; - setBreadcrumbs({ chrome }); - } - }) - .when('/workspace/:id?', { - template: appTemplate, - badge: getReadonlyBadge, - resolve: { - savedWorkspace: function (savedGraphWorkspaces, courier, $route) { - return $route.current.params.id && savedGraphWorkspaces.get($route.current.params.id) - .catch( - function () { - toastNotifications.addDanger( - i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: 'Missing workspace', - }) - ); - } - ); + app.directive('graphListing', function (reactDirective) { + return reactDirective(Listing); + }); - }, - //Copied from example found in wizard.js ( Kibana TODO - can't - indexPatterns: function (Private) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - - return savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000 - }).then(response => response.savedObjects); - }, - GetIndexPatternProvider: function (Private) { - return data.indexPatterns.indexPatterns; - }, - SavedWorkspacesProvider: function (Private) { - return Private(SavedWorkspacesProvider); + app.directive('graphApp', function (reactDirective) { + return reactDirective(GraphApp, [ + ['state', { watchDepth: 'reference' }], + ['dispatch', { watchDepth: 'reference' }], + ['currentIndexPattern', { watchDepth: 'reference' }], + ['isLoading', { watchDepth: 'reference' }], + ['onIndexPatternSelected', { watchDepth: 'reference' }], + ['onQuerySubmit', { watchDepth: 'reference' }], + ['savedObjects', { watchDepth: 'reference' }], + ['uiSettings', { watchDepth: 'reference' }], + ['http', { watchDepth: 'reference' }], + ['initialQuery', { watchDepth: 'reference' }], + ['overlays', { watchDepth: 'reference' }] + ]); + }); + + app.config(function ($routeProvider, $locationProvider) { + $routeProvider.when('/home', { + template: listingTemplate, + badge: getReadonlyBadge, + controller($location, $scope) { + checkLicense(Promise, kbnBaseUrl); + const services = savedObjectRegistry.byLoaderPropertiesName; + const graphService = services['Graph workspace']; + + $scope.listingLimit = config.get('savedObjects:listingLimit'); + $scope.create = () => { + kbnUrl.redirect(getNewPath()); + }; + $scope.find = (search) => { + return graphService.find(search, $scope.listingLimit); + }; + $scope.editItem = (workspace) => { + kbnUrl.redirect(getEditPath(workspace)); + }; + $scope.getViewUrl = (workspace) => getEditUrl(chrome, workspace); + $scope.delete = (workspaces) => { + return graphService.delete(workspaces.map(({ id }) => id)); + }; + $scope.capabilities = capabilities; + $scope.initialFilter = ($location.search()).filter || ''; + setBreadcrumbs({ chrome }); } - } - }) - .otherwise({ - redirectTo: '/home' + }) + .when('/workspace/:id?', { + template: appTemplate, + badge: getReadonlyBadge, + resolve: { + savedWorkspace: function ($route) { + return $route.current.params.id && savedGraphWorkspaces.get($route.current.params.id) + .catch( + function () { + toastNotifications.addDanger( + i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: 'Missing workspace', + }) + ); + } + ); + + }, + //Copied from example found in wizard.js ( Kibana TODO - can't + indexPatterns: function () { + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title', 'type'], + perPage: 10000 + }).then(response => response.savedObjects); + }, + GetIndexPatternProvider: function () { + return indexPatterns; + }, + SavedWorkspacesProvider: function () { + return savedWorkspacesClient; + } + } + }) + .otherwise({ + redirectTo: '/home' + }); + + $locationProvider.html5Mode(false); + $locationProvider.hashPrefix(''); }); -//======== Controller for basic UI ================== -app.controller('graphuiPlugin', function ( - $scope, - $route, - $http, - kbnUrl, - Promise, - confirmModal, - kbnBaseUrl -) { - function handleSuccess(data) { - return checkLicense(Promise, kbnBaseUrl) - .then(() => data); - } + //======== Controller for basic UI ================== + app.controller('graphuiPlugin', function ($scope, $route) { + function handleSuccess(data) { + return checkLicense(Promise, kbnBaseUrl) + .then(() => data); + } - function handleError(err) { - return checkLicense(Promise, kbnBaseUrl) - .then(() => { - const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { - defaultMessage: 'Graph Error', - description: '"Graph" is a product name and should not be translated.', - }); - if (err instanceof Error) { - toastNotifications.addError(err, { - title: toastTitle, + function handleError(err) { + return checkLicense(Promise, kbnBaseUrl) + .then(() => { + const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { + defaultMessage: 'Graph Error', + description: '"Graph" is a product name and should not be translated.', }); - } else { - toastNotifications.addDanger({ - title: toastTitle, - text: String(err), + if (err instanceof Error) { + toastNotifications.addError(err, { + title: toastTitle, + }); + } else { + toastNotifications.addDanger({ + title: toastTitle, + text: String(err), + }); + } + }); + } + + function handleHttpError(error) { + return checkLicense(Promise, kbnBaseUrl) + .then(() => { + toastNotifications.addDanger(formatAngularHttpError(error)); + }); + } + + function updateBreadcrumbs() { + setBreadcrumbs({ + chrome, + savedWorkspace: $route.current.locals.savedWorkspace, + navigateTo: () => { + // TODO this should be wrapped into canWipeWorkspace, + // but the check is too simple right now. Change this + // once actual state-diffing is in place. + $scope.$evalAsync(() => { + kbnUrl.changePath(getHomePath()); }); } }); - } + } - function handleHttpError(error) { - return checkLicense(Promise, kbnBaseUrl) - .then(() => { - toastNotifications.addDanger(formatAngularHttpError(error)); - }); - } + const store = createGraphStore(); - function updateBreadcrumbs() { - setBreadcrumbs({ - chrome, - savedWorkspace: $route.current.locals.savedWorkspace, - navigateTo: () => { - // TODO this should be wrapped into canWipeWorkspace, - // but the check is too simple right now. Change this - // once actual state-diffing is in place. - $scope.$evalAsync(() => { - kbnUrl.changePath(getHomePath()); + $scope.title = 'Graph'; + $scope.spymode = 'request'; + + $scope.iconChoices = iconChoices; + $scope.drillDownIconChoices = urlTemplateIconChoices; + $scope.colors = colorChoices; + $scope.iconChoicesByClass = iconChoicesByClass; + + $scope.outlinkEncoders = outlinkEncoders; + + $scope.fields = []; + $scope.canEditDrillDownUrls = chrome.getInjected('canEditDrillDownUrls'); + + $scope.graphSavePolicy = chrome.getInjected('graphSavePolicy'); + $scope.allSavingDisabled = $scope.graphSavePolicy === 'none'; + $scope.searchTerm = ''; + + $scope.reduxDispatch = (action) => { + store.dispatch(action); + + // patch updated icons and fields on the nodes in the workspace state + // this workaround is necessary because the nodes are still managed by + // angular - once they are moved over to redux, this can be handled in + // the reducer + if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && + action.payload.fieldProperties.color && $scope.workspace) { + $scope.workspace.nodes.forEach(function (node) { + if (node.data.field === action.payload.fieldName) { + node.color = action.payload.fieldProperties.color; + } }); } - }); - } - const store = createGraphStore(); + if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && + action.payload.fieldProperties.icon && $scope.workspace) { + $scope.workspace.nodes.forEach(function (node) { + if (node.data.field === action.payload.fieldName) { + node.icon = action.payload.fieldProperties.icon; + } + }); + } + }; - $scope.title = 'Graph'; - $scope.spymode = 'request'; + $scope.pluginDependencies = coreStart; - $scope.iconChoices = iconChoices; - $scope.drillDownIconChoices = urlTemplateIconChoices; - $scope.colors = colorChoices; - $scope.iconChoicesByClass = iconChoicesByClass; + $scope.loading = false; - $scope.outlinkEncoders = outlinkEncoders; + const updateScope = () => { + const newState = store.getState(); + $scope.reduxState = newState; + $scope.allFields = fieldsSelector(newState); + $scope.selectedFields = selectedFieldsSelector(newState); + $scope.liveResponseFields = liveResponseFieldsSelector(newState); + if ($scope.workspace) { + $scope.workspace.options.vertex_fields = $scope.selectedFields; + } + }; + store.subscribe(updateScope); + updateScope(); - $scope.fields = []; - $scope.canEditDrillDownUrls = chrome.getInjected('canEditDrillDownUrls'); + //So scope properties can be used consistently with ng-model + $scope.grr = $scope; - $scope.graphSavePolicy = chrome.getInjected('graphSavePolicy'); - $scope.allSavingDisabled = $scope.graphSavePolicy === 'none'; - $scope.searchTerm = ''; + $scope.toggleDrillDownIcon = function (urlTemplate, icon) { + urlTemplate.icon === icon ? urlTemplate.icon = null : urlTemplate.icon = icon; + }; - $scope.reduxDispatch = (action) => { - store.dispatch(action); + $scope.nodeClick = function (n, $event) { - // patch updated icons and fields on the nodes in the workspace state - // this workaround is necessary because the nodes are still managed by - // angular - once they are moved over to redux, this can be handled in - // the reducer - if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && - action.payload.fieldProperties.color && $scope.workspace) { - $scope.workspace.nodes.forEach(function (node) { - if (node.data.field === action.payload.fieldName) { - node.color = action.payload.fieldProperties.color; - } - }); + //Selection logic - shift key+click helps selects multiple nodes + // Without the shift key we deselect all prior selections (perhaps not + // a great idea for touch devices with no concept of shift key) + if (!$event.shiftKey) { + const prevSelection = n.isSelected; + $scope.workspace.selectNone(); + n.isSelected = prevSelection; + } + + + if ($scope.workspace.toggleNodeSelection(n)) { + $scope.selectSelected(n); + } else { + $scope.detail = null; + } + }; + + function canWipeWorkspace(yesFn, noFn) { + if ($scope.selectedFields.length === 0 && $scope.workspace === null) { + yesFn(); + return; + } + const confirmModalOptions = { + onConfirm: yesFn, + onCancel: noFn || (() => {}), + confirmButtonText: i18n.translate('xpack.graph.clearWorkspace.confirmButtonLabel', { + defaultMessage: 'Continue', + }), + title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { + defaultMessage: 'Discard changes to workspace?', + }), + }; + confirmModal(i18n.translate('xpack.graph.clearWorkspace.confirmText', { + defaultMessage: 'Once you discard changes made to a workspace, there is no getting them back.', + }), confirmModalOptions); } - if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && - action.payload.fieldProperties.icon && $scope.workspace) { - $scope.workspace.nodes.forEach(function (node) { - if (node.data.field === action.payload.fieldName) { - node.icon = action.payload.fieldProperties.icon; - } + $scope.uiSelectIndex = function (proposedIndex) { + canWipeWorkspace(function () { + $scope.indexSelected(proposedIndex); }); - } - }; + }; - $scope.pluginDependencies = npStart.core; + $scope.indexSelected = function (selectedIndex) { + $scope.clearWorkspace(); + $scope.allFields = []; + $scope.selectedFields = []; + $scope.basicModeSelectedSingleField = null; + $scope.selectedField = null; + $scope.selectedFieldConfig = null; + + return $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id) + .then(handleSuccess) + .then(function (indexPattern) { + $scope.selectedIndex = indexPattern; + store.dispatch(loadFields(mapFields(indexPattern))); + $scope.$digest(); + }, handleError); + }; - $scope.loading = false; - const updateScope = () => { - const newState = store.getState(); - $scope.reduxState = newState; - $scope.allFields = fieldsSelector(newState); - $scope.selectedFields = selectedFieldsSelector(newState); - $scope.liveResponseFields = liveResponseFieldsSelector(newState); - if ($scope.workspace) { - $scope.workspace.options.vertex_fields = $scope.selectedFields; - } - }; - store.subscribe(updateScope); - updateScope(); - - //So scope properties can be used consistently with ng-model - $scope.grr = $scope; - - $scope.toggleDrillDownIcon = function (urlTemplate, icon) { - urlTemplate.icon === icon ? urlTemplate.icon = null : urlTemplate.icon = icon; - }; - - $scope.nodeClick = function (n, $event) { - - //Selection logic - shift key+click helps selects multiple nodes - // Without the shift key we deselect all prior selections (perhaps not - // a great idea for touch devices with no concept of shift key) - if (!$event.shiftKey) { - const prevSelection = n.isSelected; - $scope.workspace.selectNone(); - n.isSelected = prevSelection; + $scope.clickEdge = function (edge) { + if (edge.inferred) { + $scope.setDetail ({ 'inferredEdge': edge }); + }else { + $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); + } + }; + + // Replacement function for graphClientWorkspace's comms so + // that it works with Kibana. + function callNodeProxy(indexName, query, responseHandler) { + const request = { + index: indexName, + query: query + }; + $scope.loading = true; + return http.post('../api/graph/graphExplore', request) + .then(function (resp) { + if (resp.data.resp.timed_out) { + toastNotifications.addWarning( + i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { + defaultMessage: 'Exploration timed out', + }) + ); + } + responseHandler(resp.data.resp); + }) + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); } - if ($scope.workspace.toggleNodeSelection(n)) { - $scope.selectSelected(n); - } else { - $scope.detail = null; - } - }; + //Helper function for the graphClientWorkspace to perform a query + const callSearchNodeProxy = function (indexName, query, responseHandler) { + const request = { + index: indexName, + body: query + }; + $scope.loading = true; + http.post('../api/graph/searchProxy', request) + .then(function (resp) { + responseHandler(resp.data.resp); + }) + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); + }; - function canWipeWorkspace(yesFn, noFn) { - if ($scope.selectedFields.length === 0 && $scope.workspace === null) { - yesFn(); - return; - } - const confirmModalOptions = { - onConfirm: yesFn, - onCancel: noFn || (() => {}), - confirmButtonText: i18n.translate('xpack.graph.clearWorkspace.confirmButtonLabel', { - defaultMessage: 'Continue', - }), - title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { - defaultMessage: 'Discard changes to workspace?', - }), + $scope.submit = function (searchTerm) { + initWorkspaceIfRequired(); + const numHops = 2; + if (searchTerm.startsWith('{')) { + try { + const query = JSON.parse(searchTerm); + if (query.vertices) { + // Is a graph explore request + $scope.workspace.callElasticsearch(query); + }else { + // Is a regular query DSL query + $scope.workspace.search(query, $scope.liveResponseFields, numHops); + } + } + catch (err) { + handleError(err); + } + return; + } + $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); }; - confirmModal(i18n.translate('xpack.graph.clearWorkspace.confirmText', { - defaultMessage: 'Once you discard changes made to a workspace, there is no getting them back.', - }), confirmModalOptions); - } - $scope.uiSelectIndex = function (proposedIndex) { - canWipeWorkspace(function () { - $scope.indexSelected(proposedIndex); - }); - }; - - $scope.indexSelected = function (selectedIndex) { - $scope.clearWorkspace(); - $scope.allFields = []; - $scope.selectedFields = []; - $scope.basicModeSelectedSingleField = null; - $scope.selectedField = null; - $scope.selectedFieldConfig = null; - - return $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id) - .then(handleSuccess) - .then(function (indexPattern) { - $scope.selectedIndex = indexPattern; - store.dispatch(loadFields(mapFields(indexPattern))); - $scope.$digest(); - }, handleError); - }; + $scope.clearWorkspace = function () { + $scope.workspace = null; + $scope.detail = null; + if ($scope.closeMenus) $scope.closeMenus(); + }; - $scope.clickEdge = function (edge) { - if (edge.inferred) { - $scope.setDetail ({ 'inferredEdge': edge }); - }else { - $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); - } - }; - - // Replacement function for graphClientWorkspace's comms so - // that it works with Kibana. - function callNodeProxy(indexName, query, responseHandler) { - const request = { - index: indexName, - query: query + $scope.selectSelected = function (node) { + $scope.detail = { + latestNodeSelection: node + }; + return $scope.selectedSelectedVertex = node; }; - $scope.loading = true; - return $http.post('../api/graph/graphExplore', request) - .then(function (resp) { - if (resp.data.resp.timed_out) { - toastNotifications.addWarning( - i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { - defaultMessage: 'Exploration timed out', - }) - ); - } - responseHandler(resp.data.resp); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - }); - } - - //Helper function for the graphClientWorkspace to perform a query - const callSearchNodeProxy = function (indexName, query, responseHandler) { - const request = { - index: indexName, - body: query + $scope.isSelectedSelected = function (node) { + return $scope.selectedSelectedVertex === node; }; - $scope.loading = true; - $http.post('../api/graph/searchProxy', request) - .then(function (resp) { - responseHandler(resp.data.resp); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - }); - }; - - $scope.submit = function (searchTerm) { - initWorkspaceIfRequired(); - const numHops = 2; - if (searchTerm.startsWith('{')) { - try { - const query = JSON.parse(searchTerm); - if (query.vertices) { - // Is a graph explore request - $scope.workspace.callElasticsearch(query); - }else { - // Is a regular query DSL query - $scope.workspace.search(query, $scope.liveResponseFields, numHops); - } - } - catch (err) { - handleError(err); + + $scope.saveUrlTemplate = function (index, urlTemplate) { + const newTemplatesList = [...$scope.urlTemplates]; + if (index !== -1) { + newTemplatesList[index] = urlTemplate; + } else { + newTemplatesList.push(urlTemplate); } - return; - } - $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); - }; - $scope.clearWorkspace = function () { - $scope.workspace = null; - $scope.detail = null; - if ($scope.closeMenus) $scope.closeMenus(); - }; + $scope.urlTemplates = newTemplatesList; + }; + $scope.removeUrlTemplate = function (urlTemplate) { + const newTemplatesList = [...$scope.urlTemplates]; + const i = newTemplatesList.indexOf(urlTemplate); + newTemplatesList.splice(i, 1); + $scope.urlTemplates = newTemplatesList; + }; - $scope.selectSelected = function (node) { - $scope.detail = { - latestNodeSelection: node + $scope.openUrlTemplate = function (template) { + const url = template.url; + const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); + window.open(newUrl, '_blank'); }; - return $scope.selectedSelectedVertex = node; - }; - $scope.isSelectedSelected = function (node) { - return $scope.selectedSelectedVertex === node; - }; - $scope.saveUrlTemplate = function (index, urlTemplate) { - const newTemplatesList = [...$scope.urlTemplates]; - if (index !== -1) { - newTemplatesList[index] = urlTemplate; - } else { - newTemplatesList.push(urlTemplate); - } + //============================ - $scope.urlTemplates = newTemplatesList; - }; - - $scope.removeUrlTemplate = function (urlTemplate) { - const newTemplatesList = [...$scope.urlTemplates]; - const i = newTemplatesList.indexOf(urlTemplate); - newTemplatesList.splice(i, 1); - $scope.urlTemplates = newTemplatesList; - }; - - $scope.openUrlTemplate = function (template) { - const url = template.url; - const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); - window.open(newUrl, '_blank'); - }; - - - //============================ - - $scope.resetWorkspace = function () { - $scope.clearWorkspace(); - $scope.selectedIndex = null; - $scope.proposedIndex = null; - $scope.detail = null; - $scope.selectedSelectedVertex = null; - $scope.selectedField = null; - $scope.description = null; - $scope.allFields = []; - $scope.urlTemplates = []; - - $scope.fieldNamesFilterString = null; - $scope.filteredFields = []; - - $scope.selectedFields = []; - $scope.liveResponseFields = []; - - $scope.exploreControls = { - useSignificance: true, - sampleSize: 2000, - timeoutMillis: 5000, - sampleDiversityField: null, - maxValuesPerDoc: 1, - minDocCount: 3 + $scope.resetWorkspace = function () { + $scope.clearWorkspace(); + $scope.selectedIndex = null; + $scope.proposedIndex = null; + $scope.detail = null; + $scope.selectedSelectedVertex = null; + $scope.selectedField = null; + $scope.description = null; + $scope.allFields = []; + $scope.urlTemplates = []; + + $scope.fieldNamesFilterString = null; + $scope.filteredFields = []; + + $scope.selectedFields = []; + $scope.liveResponseFields = []; + + $scope.exploreControls = { + useSignificance: true, + sampleSize: 2000, + timeoutMillis: 5000, + sampleDiversityField: null, + maxValuesPerDoc: 1, + minDocCount: 3 + }; }; - }; - function initWorkspaceIfRequired() { - if ($scope.workspace) { - return; - } - const options = { - indexName: $scope.selectedIndex.title, - vertex_fields: $scope.selectedFields, - // Here we have the opportunity to look up labels for nodes... - nodeLabeller: function () { - // console.log(newNodes); - }, - changeHandler: function () { - //Allows DOM to update with graph layout changes. - $scope.$apply(); - }, - graphExploreProxy: callNodeProxy, - searchProxy: callSearchNodeProxy, - exploreControls: $scope.exploreControls - }; - $scope.workspace = gws.createWorkspace(options); - $scope.detail = null; + function initWorkspaceIfRequired() { + if ($scope.workspace) { + return; + } + const options = { + indexName: $scope.selectedIndex.title, + vertex_fields: $scope.selectedFields, + // Here we have the opportunity to look up labels for nodes... + nodeLabeller: function () { + // console.log(newNodes); + }, + changeHandler: function () { + //Allows DOM to update with graph layout changes. + $scope.$apply(); + }, + graphExploreProxy: callNodeProxy, + searchProxy: callSearchNodeProxy, + exploreControls: $scope.exploreControls + }; + $scope.workspace = gws.createWorkspace(options); + $scope.detail = null; - // filter out default url templates because they will get re-added - $scope.urlTemplates = $scope.urlTemplates.filter(template => !template.isDefault); + // filter out default url templates because they will get re-added + $scope.urlTemplates = $scope.urlTemplates.filter(template => !template.isDefault); - if ($scope.urlTemplates.length === 0) { - // url templates specified by users can include the `{{gquery}}` tag and - // will have the elasticsearch query for the graph nodes injected there - const tag = '{{gquery}}'; + if ($scope.urlTemplates.length === 0) { + // url templates specified by users can include the `{{gquery}}` tag and + // will have the elasticsearch query for the graph nodes injected there + const tag = '{{gquery}}'; - const kUrl = new KibanaParsedUrl({ - appId: 'kibana', - basePath: chrome.getBasePath(), - appPath: '/discover' - }); + const kUrl = new KibanaParsedUrl({ + appId: 'kibana', + basePath: chrome.getBasePath(), + appPath: '/discover' + }); - kUrl.addQueryParameter('_a', rison.encode({ - columns: ['_source'], - index: $scope.selectedIndex.id, - interval: 'auto', - query: { language: 'kuery', query: tag }, - sort: ['_score', 'desc'] - })); - - const discoverUrl = kUrl.getRootRelativePath() - // replace the URI encoded version of the tag with the unescaped version - // so it can be found with String.replace, regexp, etc. - .replace(encodeURIComponent(tag), tag); - - $scope.urlTemplates.push({ - url: discoverUrl, - description: i18n.translate('xpack.graph.settings.drillDowns.defaultUrlTemplateTitle', { - defaultMessage: 'Raw documents', - }), - encoder: $scope.outlinkEncoders[0], - isDefault: true - }); + kUrl.addQueryParameter('_a', rison.encode({ + columns: ['_source'], + index: $scope.selectedIndex.id, + interval: 'auto', + query: { language: 'kuery', query: tag }, + sort: ['_score', 'desc'] + })); + + const discoverUrl = kUrl.getRootRelativePath() + // replace the URI encoded version of the tag with the unescaped version + // so it can be found with String.replace, regexp, etc. + .replace(encodeURIComponent(tag), tag); + + $scope.urlTemplates.push({ + url: discoverUrl, + description: i18n.translate('xpack.graph.settings.drillDowns.defaultUrlTemplateTitle', { + defaultMessage: 'Raw documents', + }), + encoder: $scope.outlinkEncoders[0], + isDefault: true + }); + } } - } - $scope.setDetail = function (data) { - $scope.detail = data; - }; - - $scope.performMerge = function (parentId, childId) { - let found = true; - while (found) { - found = false; - for (const i in $scope.detail.mergeCandidates) { - const mc = $scope.detail.mergeCandidates[i]; - if ((mc.id1 === childId) || (mc.id2 === childId)) { - $scope.detail.mergeCandidates.splice(i, 1); - found = true; - break; + $scope.setDetail = function (data) { + $scope.detail = data; + }; + + $scope.performMerge = function (parentId, childId) { + let found = true; + while (found) { + found = false; + for (const i in $scope.detail.mergeCandidates) { + const mc = $scope.detail.mergeCandidates[i]; + if ((mc.id1 === childId) || (mc.id2 === childId)) { + $scope.detail.mergeCandidates.splice(i, 1); + found = true; + break; + } } } - } - $scope.workspace.mergeIds(parentId, childId); - $scope.detail = null; - }; - - - $scope.handleMergeCandidatesCallback = function (termIntersects) { - const mergeCandidates = []; - for (const i in termIntersects) { - const ti = termIntersects[i]; - mergeCandidates.push({ - 'id1': ti.id1, - 'id2': ti.id2, - 'term1': ti.term1, - 'term2': ti.term2, - 'v1': ti.v1, - 'v2': ti.v2, - 'overlap': ti.overlap - }); + $scope.workspace.mergeIds(parentId, childId); + $scope.detail = null; + }; - } - $scope.detail = { mergeCandidates }; - }; - - // Zoom functions for the SVG-based graph - const redraw = function () { - d3.select('#svgRootGroup') - .attr('transform', - 'translate(' + d3.event.translate + ')' + 'scale(' + d3.event.scale + ')') - .attr('style', 'stroke-width: ' + 1 / d3.event.scale); - //To make scale-dependent features possible.... - if ($scope.zoomLevel !== d3.event.scale) { - $scope.zoomLevel = d3.event.scale; - $scope.$apply(); - } - }; - //initialize all the state - $scope.resetWorkspace(); + $scope.handleMergeCandidatesCallback = function (termIntersects) { + const mergeCandidates = []; + for (const i in termIntersects) { + const ti = termIntersects[i]; + mergeCandidates.push({ + 'id1': ti.id1, + 'id2': ti.id2, + 'term1': ti.term1, + 'term2': ti.term2, + 'v1': ti.v1, + 'v2': ti.v2, + 'overlap': ti.overlap + }); + } + $scope.detail = { mergeCandidates }; + }; - const blockScroll = function () { - d3.event.preventDefault(); - }; - d3.select('#graphSvg') - .on('mousewheel', blockScroll) - .on('DOMMouseScroll', blockScroll) - .call(d3.behavior.zoom() - .on('zoom', redraw)); + // Zoom functions for the SVG-based graph + const redraw = function () { + d3.select('#svgRootGroup') + .attr('transform', + 'translate(' + d3.event.translate + ')' + 'scale(' + d3.event.scale + ')') + .attr('style', 'stroke-width: ' + 1 / d3.event.scale); + //To make scale-dependent features possible.... + if ($scope.zoomLevel !== d3.event.scale) { + $scope.zoomLevel = d3.event.scale; + $scope.$apply(); + } + }; + //initialize all the state + $scope.resetWorkspace(); - const managementUrl = npStart.core.chrome.navLinks.get('kibana:management').url; - const url = `${managementUrl}/kibana/index_patterns`; - if ($route.current.locals.indexPatterns.length === 0) { - toastNotifications.addWarning({ - title: i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', { - defaultMessage: 'No data source', - }), - text: ( -

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

- ), - }); - } + const blockScroll = function () { + d3.event.preventDefault(); + }; + d3.select('#graphSvg') + .on('mousewheel', blockScroll) + .on('DOMMouseScroll', blockScroll) + .call(d3.behavior.zoom() + .on('zoom', redraw)); - // ===== Menubar configuration ========= - $scope.topNavMenu = []; - $scope.topNavMenu.push({ - key: 'new', - label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { - defaultMessage: 'New', - }), - description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { - defaultMessage: 'New Workspace', - }), - tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { - defaultMessage: 'Create a new workspace', - }), - run: function () { - canWipeWorkspace(function () { - $scope.$evalAsync(() => { - kbnUrl.change('/workspace/', {}); - }); - }); }, - testId: 'graphNewButton', - }); + const managementUrl = chrome.navLinks.get('kibana:management').url; + const url = `${managementUrl}/kibana/index_patterns`; + + if ($route.current.locals.indexPatterns.length === 0) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', { + defaultMessage: 'No data source', + }), + text: ( +

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

+ ), + }); + } - // if saving is disabled using uiCapabilities, we don't want to render the save - // button so it's consistent with all of the other applications - if (capabilities.get().graph.save) { - // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + // ===== Menubar configuration ========= + $scope.topNavMenu = []; $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', + key: 'new', + label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { + defaultMessage: 'New', }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save workspace', + description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { + defaultMessage: 'New Workspace', }), - tooltip: () => { - if ($scope.allSavingDisabled) { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { + defaultMessage: 'Create a new workspace', + }), + run: function () { + canWipeWorkspace(function () { + $scope.$evalAsync(() => { + kbnUrl.change('/workspace/', {}); }); - } else { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', + }); }, + testId: 'graphNewButton', + }); + + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (capabilities.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + + $scope.topNavMenu.push({ + key: 'save', + label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save workspace', + }), + tooltip: () => { + if ($scope.allSavingDisabled) { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }); + } else { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }); + } + }, + disableButton: function () { + return $scope.allSavingDisabled || $scope.selectedFields.length === 0; + }, + run: () => { + openSaveModal({ + savePolicy: $scope.graphSavePolicy, + hasData: $scope.workspace && ($scope.workspace.nodes.length > 0 || $scope.workspace.blacklistedNodes.length > 0), + workspace: $scope.savedWorkspace, + saveWorkspace: $scope.saveWorkspace, + showSaveModal }); - } - }, - disableButton: function () { - return $scope.allSavingDisabled || $scope.selectedFields.length === 0; + }, + testId: 'graphSaveButton', + }); + } + $scope.topNavMenu.push({ + key: 'inspect', + disableButton: function () { return $scope.workspace === null; }, + label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { + defaultMessage: 'Inspect', + }), + run: () => { + $scope.$evalAsync(() => { + const curState = $scope.menus.showInspect; + $scope.closeMenus(); + $scope.menus.showInspect = !curState; + }); }, + }); + + let currentSettingsFlyout; + $scope.topNavMenu.push({ + key: 'settings', + disableButton: function () { return $scope.selectedIndex === null; }, + label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { + defaultMessage: 'Settings', + }), + description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { + defaultMessage: 'Settings', + }), run: () => { - openSaveModal({ - savePolicy: $scope.graphSavePolicy, - hasData: $scope.workspace && ($scope.workspace.nodes.length > 0 || $scope.workspace.blacklistedNodes.length > 0), - workspace: $scope.savedWorkspace, - saveWorkspace: $scope.saveWorkspace, - showSaveModal + if (currentSettingsFlyout) { + currentSettingsFlyout.close(); + return; + } + const settingsObservable = asAngularSyncedObservable(() => ({ + advancedSettings: { ...$scope.exploreControls }, + updateAdvancedSettings: (updatedSettings) => { + $scope.exploreControls = updatedSettings; + if ($scope.workspace) { + $scope.workspace.options.exploreControls = updatedSettings; + } + }, + blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, + unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, + urlTemplates: [...$scope.urlTemplates], + removeUrlTemplate: $scope.removeUrlTemplate, + saveUrlTemplate: $scope.saveUrlTemplate, + allFields: [...$scope.allFields], + canEditDrillDownUrls: $scope.canEditDrillDownUrls + }), $scope.$digest.bind($scope)); + currentSettingsFlyout = coreStart.overlays.openFlyout(, { + size: 'm', + closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }), + 'data-test-subj': 'graphSettingsFlyout', + ownFocus: true, + className: 'gphSettingsFlyout', + maxWidth: 520, }); + currentSettingsFlyout.onClose.then(() => { currentSettingsFlyout = null; }); }, - testId: 'graphSaveButton', }); - } - $scope.topNavMenu.push({ - key: 'inspect', - disableButton: function () { return $scope.workspace === null; }, - label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { - defaultMessage: 'Inspect', - }), - run: () => { - $scope.$evalAsync(() => { - const curState = $scope.menus.showInspect; - $scope.closeMenus(); - $scope.menus.showInspect = !curState; - }); - }, - }); - let currentSettingsFlyout; - $scope.topNavMenu.push({ - key: 'settings', - disableButton: function () { return $scope.selectedIndex === null; }, - label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { - defaultMessage: 'Settings', - }), - description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { - defaultMessage: 'Settings', - }), - run: () => { - if (currentSettingsFlyout) { - currentSettingsFlyout.close(); - return; - } - const settingsObservable = asAngularSyncedObservable(() => ({ - advancedSettings: { ...$scope.exploreControls }, - updateAdvancedSettings: (updatedSettings) => { - $scope.exploreControls = updatedSettings; - if ($scope.workspace) { - $scope.workspace.options.exploreControls = updatedSettings; - } - }, - blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, - unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, - urlTemplates: [...$scope.urlTemplates], - removeUrlTemplate: $scope.removeUrlTemplate, - saveUrlTemplate: $scope.saveUrlTemplate, - allFields: [...$scope.allFields], - canEditDrillDownUrls: $scope.canEditDrillDownUrls - }), $scope.$digest.bind($scope)); - currentSettingsFlyout = npStart.core.overlays.openFlyout(, { - size: 'm', - closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }), - 'data-test-subj': 'graphSettingsFlyout', - ownFocus: true, - className: 'gphSettingsFlyout', - maxWidth: 520, - }); - currentSettingsFlyout.onClose.then(() => { currentSettingsFlyout = null; }); - }, - }); + updateBreadcrumbs(); - updateBreadcrumbs(); + $scope.menus = { + showSettings: false, + }; - $scope.menus = { - showSettings: false, - }; + $scope.closeMenus = () => { + _.forOwn($scope.menus, function (_, key) { + $scope.menus[key] = false; + }); + }; - $scope.closeMenus = () => { - _.forOwn($scope.menus, function (_, key) { - $scope.menus[key] = false; - }); - }; - - // 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, $route.current.locals.indexPatterns); - if(!selectedIndex) { - toastNotifications.addDanger( - i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { - defaultMessage: 'Index pattern not found', - }) - ); - return; - } - $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id).then(indexPattern => { - $scope.selectedIndex = indexPattern; - initWorkspaceIfRequired(); - const { - urlTemplates, - advancedSettings, - allFields, - } = savedWorkspaceToAppState($scope.savedWorkspace, indexPattern, $scope.workspace); - - // wire up stuff to angular - store.dispatch(loadFields(allFields)); - $scope.exploreControls = advancedSettings; - $scope.workspace.options.exploreControls = advancedSettings; - $scope.urlTemplates = urlTemplates; - $scope.workspace.runLayout(); - // Allow URLs to include a user-defined text query - if ($route.current.params.query) { - $scope.initialQuery = $route.current.params.query; - $scope.submit($route.current.params.query); + // 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, $route.current.locals.indexPatterns); + if(!selectedIndex) { + toastNotifications.addDanger( + i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { + defaultMessage: 'Index pattern not found', + }) + ); + return; } + $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id).then(indexPattern => { + $scope.selectedIndex = indexPattern; + initWorkspaceIfRequired(); + const { + urlTemplates, + advancedSettings, + allFields, + } = savedWorkspaceToAppState($scope.savedWorkspace, indexPattern, $scope.workspace); + + // wire up stuff to angular + store.dispatch(loadFields(allFields)); + $scope.exploreControls = advancedSettings; + $scope.workspace.options.exploreControls = advancedSettings; + $scope.urlTemplates = urlTemplates; + $scope.workspace.runLayout(); + // Allow URLs to include a user-defined text query + if ($route.current.params.query) { + $scope.initialQuery = $route.current.params.query; + $scope.submit($route.current.params.query); + } - $scope.$digest(); - }); - } else { - $route.current.locals.SavedWorkspacesProvider.get().then(function (newWorkspace) { - $scope.savedWorkspace = newWorkspace; - openSourceModal(npStart.core, indexPattern => { - $scope.indexSelected(indexPattern); + $scope.$digest(); }); - }); - } + } else { + $route.current.locals.SavedWorkspacesProvider.get().then(function (newWorkspace) { + $scope.savedWorkspace = newWorkspace; + openSourceModal(coreStart, indexPattern => { + $scope.indexSelected(indexPattern); + }); + }); + } - $scope.saveWorkspace = function (saveOptions, userHasConfirmedSaveWorkspaceData) { - if ($scope.allSavingDisabled) { - // It should not be possible to navigate to this function if allSavingDisabled is set - // but adding check here as a safeguard. - toastNotifications.addWarning( - i18n.translate('xpack.graph.saveWorkspace.disabledWarning', { defaultMessage: 'Saving is disabled' }) + $scope.saveWorkspace = function (saveOptions, userHasConfirmedSaveWorkspaceData) { + if ($scope.allSavingDisabled) { + // It should not be possible to navigate to this function if allSavingDisabled is set + // but adding check here as a safeguard. + toastNotifications.addWarning( + i18n.translate('xpack.graph.saveWorkspace.disabledWarning', { defaultMessage: 'Saving is disabled' }) + ); + return; + } + initWorkspaceIfRequired(); + const canSaveData = $scope.graphSavePolicy === 'configAndData' || + ($scope.graphSavePolicy === 'configAndDataWithConsent' && userHasConfirmedSaveWorkspaceData); + + appStateToSavedWorkspace( + $scope.savedWorkspace, + { + workspace: $scope.workspace, + urlTemplates: $scope.urlTemplates, + advancedSettings: $scope.exploreControls, + selectedIndex: $scope.selectedIndex, + selectedFields: $scope.selectedFields + }, + canSaveData ); - return; - } - initWorkspaceIfRequired(); - const canSaveData = $scope.graphSavePolicy === 'configAndData' || - ($scope.graphSavePolicy === 'configAndDataWithConsent' && userHasConfirmedSaveWorkspaceData); - - appStateToSavedWorkspace( - $scope.savedWorkspace, - { - workspace: $scope.workspace, - urlTemplates: $scope.urlTemplates, - advancedSettings: $scope.exploreControls, - selectedIndex: $scope.selectedIndex, - selectedFields: $scope.selectedFields - }, - canSaveData - ); - - return $scope.savedWorkspace.save(saveOptions).then(function (id) { - if (id) { - const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', { - defaultMessage: 'Saved "{workspaceTitle}"', - values: { workspaceTitle: $scope.savedWorkspace.title }, - }); - let text; - if (!canSaveData && $scope.workspace.nodes.length > 0) { - text = i18n.translate('xpack.graph.saveWorkspace.successNotification.noDataSavedText', { - defaultMessage: 'The configuration was saved, but the data was not saved', + + return $scope.savedWorkspace.save(saveOptions).then(function (id) { + if (id) { + const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', { + defaultMessage: 'Saved "{workspaceTitle}"', + values: { workspaceTitle: $scope.savedWorkspace.title }, }); - } + let text; + if (!canSaveData && $scope.workspace.nodes.length > 0) { + text = i18n.translate('xpack.graph.saveWorkspace.successNotification.noDataSavedText', { + defaultMessage: 'The configuration was saved, but the data was not saved', + }); + } - toastNotifications.addSuccess({ - title, - text, - 'data-test-subj': 'saveGraphSuccess', - }); - if ($scope.savedWorkspace.id !== $route.current.params.id) { - kbnUrl.change(getEditPath($scope.savedWorkspace)); + toastNotifications.addSuccess({ + title, + text, + 'data-test-subj': 'saveGraphSuccess', + }); + if ($scope.savedWorkspace.id !== $route.current.params.id) { + kbnUrl.change(getEditPath($scope.savedWorkspace)); + } } - } - return { id }; - }, fatalError); + return { id }; + }, fatalError); - }; + }; -}); -//End controller + }); + //End controller +} diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts new file mode 100644 index 0000000000000..3455787289592 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -0,0 +1,49 @@ +/* + * 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 the uiExports that we want to "use" +import 'uiExports/fieldFormats'; +import 'uiExports/savedObjectTypes'; + +import 'ui/autoload/all'; +import 'ui/kbn_top_nav'; +import 'ui/directives/saved_object_finder'; +import 'ui/directives/input_focus'; +import 'ui/saved_objects/ui/saved_object_save_as_checkbox'; +import 'uiExports/autocompleteProviders'; +import chrome from 'ui/chrome'; +import { uiModules } from 'ui/modules'; +import uiRoutes from 'ui/routes'; +import { addAppRedirectMessageToUrl, fatalError, toastNotifications } from 'ui/notify'; +import { formatAngularHttpError } from 'ui/notify/lib'; +import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; +import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; +import { npStart } from 'ui/new_platform'; +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +import { capabilities } from 'ui/capabilities'; +import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; + +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; + +import { CoreSetup } from 'src/core/public'; + +export class GraphPlugin { + setup(core: CoreSetup) { + core.application.register({ + id: 'angularDemo', + title: 'Angular Demo', + async mount(context, params) { + const { renderApp } = await import('./render_app'); + return renderApp(context, params); + }, + }); + } + + start() {} + + stop() {} +} diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts new file mode 100644 index 0000000000000..236153a7661cf --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -0,0 +1,54 @@ +/* + * 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 angular from 'angular'; +// @ts-ignore +import { initAngularModule } from './app'; +import { AppMountContext } from 'kibana/public'; + +const mainTemplate = (basePath: string) => ` + + +
+ +`; + +const moduleName = 'app/graph'; + + // const { + // xpackInfo, + // addAppRedirectMessageToUrl, + // fatalError, + // chrome, + // savedGraphWorkspaces, + // toastNotifications, + // savedObjectsClient, //Private(SavedObjectsClientProvider) + // indexPatterns, //data.indexPatterns.indexPatterns + // savedWorkspacesClient, //Private(SavedWorkspacesProvider) + // kbnBaseUrl, + // kbnUrl, + // config, //uiSettings? + // savedObjectRegistry, //Private(SavedObjectRegistryProvider) + // X capabilities, + // formatAngularHttpError, + // X coreStart, //npStart.core + // confirmModal, + // http, //$http + // showSaveModal, + // } = deps; + +export const renderApp = ({ core }: AppMountContext, { element , appBasePath }: { element: HTMLElement, appBasePath: string }) => { + const deps = { + capabilities: core.application.capabilities.graph, + coreStart: core, + + } + initAngularModule(moduleName, deps); + // eslint-disable-next-line + element.innerHTML = mainTemplate(appBasePath); + const $injector = angular.bootstrap(element, [moduleName]); + return () => $injector.get('$rootScope').$destroy(); +}; From 9f1c141f5095798a41532e49c3083b9f3e312887 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 2 Oct 2019 15:58:29 +0200 Subject: [PATCH 02/12] wip --- .../show_saved_object_save_modal.tsx | 2 +- x-pack/legacy/plugins/graph/public/app.js | 4 +- .../legacy/plugins/graph/public/render_app.ts | 43 ++++++++++++------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx b/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx index 6aea3c72e0c34..3c691c692948a 100644 --- a/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx +++ b/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx @@ -34,7 +34,7 @@ function isSuccess(result: SaveResult): result is { id?: string } { return 'id' in result; } -interface MinimalSaveModalProps { +export interface MinimalSaveModalProps { onSave: (...args: any[]) => Promise; onClose: () => void; } diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 9a7575db74889..f7f5a3a80157c 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -66,7 +66,6 @@ export function initAngularModule(moduleName, deps) { config, //uiSettings? savedObjectRegistry, //Private(SavedObjectRegistryProvider) capabilities, - formatAngularHttpError, coreStart, //npStart.core confirmModal, http, //$http @@ -224,7 +223,8 @@ export function initAngularModule(moduleName, deps) { function handleHttpError(error) { return checkLicense(Promise, kbnBaseUrl) .then(() => { - toastNotifications.addDanger(formatAngularHttpError(error)); + // TODO format this + toastNotifications.addDanger(error); }); } diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 236153a7661cf..664fda94d2e4a 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -8,6 +8,8 @@ import angular from 'angular'; // @ts-ignore import { initAngularModule } from './app'; import { AppMountContext } from 'kibana/public'; +import { MinimalSaveModalProps } from 'ui/saved_objects/show_saved_object_save_modal'; +import { DataSetup } from 'src/legacy/core_plugins/data/public'; const mainTemplate = (basePath: string) => ` @@ -22,29 +24,38 @@ const moduleName = 'app/graph'; // xpackInfo, // addAppRedirectMessageToUrl, // fatalError, - // chrome, - // savedGraphWorkspaces, - // toastNotifications, - // savedObjectsClient, //Private(SavedObjectsClientProvider) - // indexPatterns, //data.indexPatterns.indexPatterns - // savedWorkspacesClient, //Private(SavedWorkspacesProvider) - // kbnBaseUrl, - // kbnUrl, - // config, //uiSettings? - // savedObjectRegistry, //Private(SavedObjectRegistryProvider) + // X chrome, + // I savedGraphWorkspaces, + // X toastNotifications, + // I savedObjectsClient, //Private(SavedObjectsClientProvider) + // X indexPatterns, //data.indexPatterns.indexPatterns + // I savedWorkspacesClient, //Private(SavedWorkspacesProvider) + // I kbnBaseUrl, + // I kbnUrl, + // X config, //uiSettings? + // I savedObjectRegistry, //Private(SavedObjectRegistryProvider) // X capabilities, - // formatAngularHttpError, // X coreStart, //npStart.core - // confirmModal, - // http, //$http - // showSaveModal, + // I confirmModal, + // X http, //$http + // X showSaveModal, // } = deps; -export const renderApp = ({ core }: AppMountContext, { element , appBasePath }: { element: HTMLElement, appBasePath: string }) => { +export interface GraphDeps { + showSaveModal: (saveModal: React.ReactElement) => void; + element: HTMLElement; appBasePath: string; data: DataSetup +} + +export const renderApp = ({ core }: AppMountContext, { element , appBasePath, showSaveModal, data }: GraphDeps) => { const deps = { capabilities: core.application.capabilities.graph, coreStart: core, - + http: core.http, + chrome: core.chrome, + config: core.uiSettings, + showSaveModal: showSaveModal, + toastNotifications: core.notifications.toasts, + indexPatterns: data.indexPatterns.indexPatterns } initAngularModule(moduleName, deps); // eslint-disable-next-line From cf06de8bc6451d6cb2369b8ed953520a4fae15e4 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 3 Oct 2019 09:21:53 +0200 Subject: [PATCH 03/12] wip --- x-pack/legacy/plugins/graph/index.js | 2 +- x-pack/legacy/plugins/graph/public/app.js | 26 +++--- x-pack/legacy/plugins/graph/public/index.ts | 31 +++++++ x-pack/legacy/plugins/graph/public/plugin.ts | 69 ++++++++++---- .../legacy/plugins/graph/public/render_app.ts | 89 ++++++++++++------- .../plugins/graph/public/services/url.ts | 12 +-- 6 files changed, 160 insertions(+), 69 deletions(-) create mode 100644 x-pack/legacy/plugins/graph/public/index.ts diff --git a/x-pack/legacy/plugins/graph/index.js b/x-pack/legacy/plugins/graph/index.js index 1a61caca7a7c1..9ece9966b7da4 100644 --- a/x-pack/legacy/plugins/graph/index.js +++ b/x-pack/legacy/plugins/graph/index.js @@ -23,7 +23,7 @@ export function graph(kibana) { order: 9000, icon: 'plugins/graph/icon.png', euiIconType: 'graphApp', - main: 'plugins/graph/app', + main: 'plugins/graph/index', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'], diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index f7f5a3a80157c..13a995fc3d5bd 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -10,6 +10,9 @@ import 'ace'; import rison from 'rison-node'; import React from 'react'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; +import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +import { formatAngularHttpError } from 'ui/notify/lib'; +import { addAppRedirectMessageToUrl } from 'ui/notify'; import appTemplate from './angular/templates/index.html'; import listingTemplate from './angular/templates/listing_ng_wrapper.html'; @@ -53,7 +56,6 @@ import './angular/directives/graph_inspect'; export function initAngularModule(moduleName, deps) { const { xpackInfo, - addAppRedirectMessageToUrl, fatalError, chrome, savedGraphWorkspaces, @@ -68,11 +70,12 @@ export function initAngularModule(moduleName, deps) { capabilities, coreStart, //npStart.core confirmModal, - http, //$http - showSaveModal, + $http, //$http + canEditDrillDownUrls, + graphSavePolicy } = deps; - const app = angular.module(moduleName, ['ngRoute']); + const app = angular.module(moduleName, ['ngRoute', 'react']); function checkLicense(Promise, kbnBaseUrl) { const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && @@ -134,13 +137,13 @@ export function initAngularModule(moduleName, deps) { $scope.listingLimit = config.get('savedObjects:listingLimit'); $scope.create = () => { - kbnUrl.redirect(getNewPath()); + $location.url(getNewPath()); }; $scope.find = (search) => { return graphService.find(search, $scope.listingLimit); }; $scope.editItem = (workspace) => { - kbnUrl.redirect(getEditPath(workspace)); + $location.url(getEditPath(workspace)); }; $scope.getViewUrl = (workspace) => getEditUrl(chrome, workspace); $scope.delete = (workspaces) => { @@ -223,8 +226,7 @@ export function initAngularModule(moduleName, deps) { function handleHttpError(error) { return checkLicense(Promise, kbnBaseUrl) .then(() => { - // TODO format this - toastNotifications.addDanger(error); + toastNotifications.addDanger(formatAngularHttpError(error)); }); } @@ -256,9 +258,9 @@ export function initAngularModule(moduleName, deps) { $scope.outlinkEncoders = outlinkEncoders; $scope.fields = []; - $scope.canEditDrillDownUrls = chrome.getInjected('canEditDrillDownUrls'); + $scope.canEditDrillDownUrls = canEditDrillDownUrls; - $scope.graphSavePolicy = chrome.getInjected('graphSavePolicy'); + $scope.graphSavePolicy = graphSavePolicy; $scope.allSavingDisabled = $scope.graphSavePolicy === 'none'; $scope.searchTerm = ''; @@ -391,7 +393,7 @@ export function initAngularModule(moduleName, deps) { query: query }; $scope.loading = true; - return http.post('../api/graph/graphExplore', request) + return $http.post('../api/graph/graphExplore', request) .then(function (resp) { if (resp.data.resp.timed_out) { toastNotifications.addWarning( @@ -416,7 +418,7 @@ export function initAngularModule(moduleName, deps) { body: query }; $scope.loading = true; - http.post('../api/graph/searchProxy', request) + $http.post('../api/graph/searchProxy', request) .then(function (resp) { responseHandler(resp.data.resp); }) diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts new file mode 100644 index 0000000000000..fe9295ad558be --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { GraphPlugin } from './plugin'; +import { npSetup, npStart } from 'ui/new_platform'; +import { IScope } from 'angular'; +import { App } from 'kibana/public'; + + + // Setup compatibility layer for AppService in legacy platform + npSetup.core.application.register = (app: App) => { + require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { + const element = $element[0]; + + // Root controller cannot return a Promise so use an internal async function and call it + (async () => { + const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + $scope.$on('$destroy', () => { + unmount(); + }); + })(); + }); + }; + + +const instance = new GraphPlugin(); +instance.setup(npSetup.core, { data }); \ No newline at end of file diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index 3455787289592..d9c5046f98594 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -// import the uiExports that we want to "use" +// legacy imports currently necessary to power Graph +// for a cutover all of these have to be resolved import 'uiExports/fieldFormats'; import 'uiExports/savedObjectTypes'; - import 'ui/autoload/all'; import 'ui/kbn_top_nav'; import 'ui/directives/saved_object_finder'; @@ -15,30 +15,67 @@ import 'ui/directives/input_focus'; import 'ui/saved_objects/ui/saved_object_save_as_checkbox'; import 'uiExports/autocompleteProviders'; import chrome from 'ui/chrome'; -import { uiModules } from 'ui/modules'; -import uiRoutes from 'ui/routes'; -import { addAppRedirectMessageToUrl, fatalError, toastNotifications } from 'ui/notify'; -import { formatAngularHttpError } from 'ui/notify/lib'; -import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { fatalError } from 'ui/notify'; +// @ts-ignore import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; -import { npStart } from 'ui/new_platform'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import { capabilities } from 'ui/capabilities'; -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; - +// @ts-ignore import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { IPrivate } from 'ui/private'; +// NP type imports import { CoreSetup } from 'src/core/public'; +import { DataSetup } from 'src/legacy/core_plugins/data/public'; +// @ts-ignore +import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularInjectedDependencies() { + const injector = await chrome.dangerouslyGetActiveInjector(); + + const Private = injector.get('Private'); + + return { + $http: injector.get('$http'), + confirmModal: injector.get('confirmModal'), + savedObjectRegisty: Private(SavedObjectRegistryProvider), + kbnUrl: injector.get('kbnUrl'), + kbnBaseUrl: injector.get('kbnBaseUrl'), + savedWorkspacesClient: Private(SavedWorkspacesProvider), + savedGraphWorkspaces: injector.get('savedGraphWorkspaces'), + savedObjectsClient: Private(SavedObjectsClientProvider), + savedObjectRegistry: Private(SavedObjectRegistryProvider), + canEditDrillDownUrls: chrome.getInjected('canEditDrillDownUrls'), + graphSavePolicy: chrome.getInjected('graphSavePolicy'), + }; +} + +export interface GraphPluginSetupDependencies { + data: DataSetup; +} export class GraphPlugin { - setup(core: CoreSetup) { + setup(core: CoreSetup, { data }: GraphPluginSetupDependencies) { core.application.register({ - id: 'angularDemo', - title: 'Angular Demo', + id: 'graph', + title: 'Graph', async mount(context, params) { const { renderApp } = await import('./render_app'); - return renderApp(context, params); + const angularDependencies = await getAngularInjectedDependencies(); + return renderApp( + context, + { + ...params, + data, + fatalError, + xpackInfo, + // TODO pass in npStart.core.http.basePath for getBasePath and addBasePath + }, + angularDependencies + ); }, }); } diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 664fda94d2e4a..0db14214f466a 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -5,13 +5,13 @@ */ import angular from 'angular'; -// @ts-ignore -import { initAngularModule } from './app'; import { AppMountContext } from 'kibana/public'; -import { MinimalSaveModalProps } from 'ui/saved_objects/show_saved_object_save_modal'; import { DataSetup } from 'src/legacy/core_plugins/data/public'; +import { AngularHttpError } from 'ui/notify/lib/format_angular_http_error'; +// @ts-ignore +import { initAngularModule } from './app'; -const mainTemplate = (basePath: string) => ` +const mainTemplate = (basePath: string) => `
@@ -20,46 +20,67 @@ const mainTemplate = (basePath: string) => ` const moduleName = 'app/graph'; - // const { - // xpackInfo, - // addAppRedirectMessageToUrl, - // fatalError, - // X chrome, - // I savedGraphWorkspaces, - // X toastNotifications, - // I savedObjectsClient, //Private(SavedObjectsClientProvider) - // X indexPatterns, //data.indexPatterns.indexPatterns - // I savedWorkspacesClient, //Private(SavedWorkspacesProvider) - // I kbnBaseUrl, - // I kbnUrl, - // X config, //uiSettings? - // I savedObjectRegistry, //Private(SavedObjectRegistryProvider) - // X capabilities, - // X coreStart, //npStart.core - // I confirmModal, - // X http, //$http - // X showSaveModal, - // } = deps; +export interface GraphDependencies { + element: HTMLElement; + appBasePath: string; + data: DataSetup; + fatalError: (error: AngularHttpError | Error | string, location?: string) => void; + xpackInfo: { get(path: string): unknown }; +} + +export interface LegacyAngularInjectedDependencies { + /** + * angular $http service + */ + $http: any; + /** + * src/legacy/ui/public/modals/confirm_modal.js + */ + confirmModal: any; + /** + * Private(SavedObjectRegistryProvider) + */ + savedObjectRegistry: any; + kbnUrl: any; + kbnBaseUrl: any; + /** + * Private(SavedWorkspacesProvider) + */ + + savedWorkspacesClient: any; + savedGraphWorkspaces: any; + /** + * Private(SavedObjectsClientProvider) + */ + + savedObjectsClient: any; -export interface GraphDeps { - showSaveModal: (saveModal: React.ReactElement) => void; - element: HTMLElement; appBasePath: string; data: DataSetup + // These are static values currently fetched from ui/chrome + canEditDrillDownUrls: string; + graphSavePolicy: string; } -export const renderApp = ({ core }: AppMountContext, { element , appBasePath, showSaveModal, data }: GraphDeps) => { +export const renderApp = ( + { core }: AppMountContext, + { element, appBasePath, data, fatalError, xpackInfo }: GraphDependencies, + angularDeps: LegacyAngularInjectedDependencies +) => { const deps = { capabilities: core.application.capabilities.graph, coreStart: core, - http: core.http, chrome: core.chrome, config: core.uiSettings, - showSaveModal: showSaveModal, toastNotifications: core.notifications.toasts, - indexPatterns: data.indexPatterns.indexPatterns - } + indexPatterns: data.indexPatterns.indexPatterns, + fatalError, + xpackInfo, + ...angularDeps, + }; initAngularModule(moduleName, deps); + const mountpoint = document.createElement('div'); // eslint-disable-next-line - element.innerHTML = mainTemplate(appBasePath); - const $injector = angular.bootstrap(element, [moduleName]); + mountpoint.innerHTML = mainTemplate(appBasePath); + const $injector = angular.bootstrap(mountpoint, [moduleName]); + element.appendChild(mountpoint); return () => $injector.get('$rootScope').$destroy(); }; diff --git a/x-pack/legacy/plugins/graph/public/services/url.ts b/x-pack/legacy/plugins/graph/public/services/url.ts index 97a30e26c25f3..069d07a2f626b 100644 --- a/x-pack/legacy/plugins/graph/public/services/url.ts +++ b/x-pack/legacy/plugins/graph/public/services/url.ts @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { Chrome } from 'ui/chrome'; import { GraphWorkspaceSavedObject } from '../types'; +import { ChromeStart } from 'kibana/public'; export function getHomePath() { return '/home'; @@ -20,23 +20,23 @@ export function getEditPath({ id }: GraphWorkspaceSavedObject) { return `/workspace/${id}`; } -export function getEditUrl(chrome: Chrome, workspace: GraphWorkspaceSavedObject) { +export function getEditUrl(chrome: ChromeStart, workspace: GraphWorkspaceSavedObject) { return chrome.addBasePath(`#${getEditPath(workspace)}`); } export type SetBreadcrumbOptions = | { - chrome: Chrome; + chrome: ChromeStart; } | { - chrome: Chrome; + chrome: ChromeStart; savedWorkspace?: GraphWorkspaceSavedObject; navigateTo: (path: string) => void; }; export function setBreadcrumbs(options: SetBreadcrumbOptions) { if ('savedWorkspace' in options) { - options.chrome.breadcrumbs.set([ + options.chrome.setBreadcrumbs([ { text: i18n.translate('xpack.graph.home.breadcrumb', { defaultMessage: 'Graph', @@ -56,7 +56,7 @@ export function setBreadcrumbs(options: SetBreadcrumbOptions) { }, ]); } else { - options.chrome.breadcrumbs.set([ + options.chrome.setBreadcrumbs([ { text: i18n.translate('xpack.graph.home.breadcrumb', { defaultMessage: 'Graph', From d06721a133fac3b027433e1c5f3219f8b977840c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 3 Oct 2019 13:56:10 +0200 Subject: [PATCH 04/12] wip --- x-pack/legacy/plugins/graph/public/app.js | 17 +++++++++-------- x-pack/legacy/plugins/graph/public/plugin.ts | 3 ++- .../legacy/plugins/graph/public/render_app.ts | 11 ++++++++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 13a995fc3d5bd..52703797c6123 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -64,7 +64,8 @@ export function initAngularModule(moduleName, deps) { indexPatterns, //data.indexPatterns.indexPatterns savedWorkspacesClient, //Private(SavedWorkspacesProvider) kbnBaseUrl, - kbnUrl, + addBasePath, + getBasePath, config, //uiSettings? savedObjectRegistry, //Private(SavedObjectRegistryProvider) capabilities, @@ -75,14 +76,14 @@ export function initAngularModule(moduleName, deps) { graphSavePolicy } = deps; - const app = angular.module(moduleName, ['ngRoute', 'react']); + const app = angular.module(moduleName, ['ngRoute', 'react', 'graphI18n', 'ngSanitize']); function checkLicense(Promise, kbnBaseUrl) { const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && xpackInfo.get('features.graph.enableAppLink'); if (!licenseAllowsToShowThisPage) { const message = xpackInfo.get('features.graph.message'); - const newUrl = addAppRedirectMessageToUrl(chrome.addBasePath(kbnBaseUrl), message); + const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message); window.location.href = newUrl; return Promise.halt(); } @@ -197,7 +198,7 @@ export function initAngularModule(moduleName, deps) { //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route) { + app.controller('graphuiPlugin', function ($scope, $route, $location) { function handleSuccess(data) { return checkLicense(Promise, kbnBaseUrl) .then(() => data); @@ -239,7 +240,7 @@ export function initAngularModule(moduleName, deps) { // but the check is too simple right now. Change this // once actual state-diffing is in place. $scope.$evalAsync(() => { - kbnUrl.changePath(getHomePath()); + $location.url(getHomePath()); }); } }); @@ -555,7 +556,7 @@ export function initAngularModule(moduleName, deps) { const kUrl = new KibanaParsedUrl({ appId: 'kibana', - basePath: chrome.getBasePath(), + basePath: getBasePath(), appPath: '/discover' }); @@ -676,7 +677,7 @@ export function initAngularModule(moduleName, deps) { run: function () { canWipeWorkspace(function () { $scope.$evalAsync(() => { - kbnUrl.change('/workspace/', {}); + $location.url('/workspace/'); }); }); }, testId: 'graphNewButton', @@ -882,7 +883,7 @@ export function initAngularModule(moduleName, deps) { 'data-test-subj': 'saveGraphSuccess', }); if ($scope.savedWorkspace.id !== $route.current.params.id) { - kbnUrl.change(getEditPath($scope.savedWorkspace)); + $location.url(getEditPath($scope.savedWorkspace)); } } return { id }; diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index d9c5046f98594..c8cbba4ab62de 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -72,7 +72,8 @@ export class GraphPlugin { data, fatalError, xpackInfo, - // TODO pass in npStart.core.http.basePath for getBasePath and addBasePath + addBasePath: core.http.basePath.prepend, + getBasePath: core.http.basePath.get }, angularDependencies ); diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 0db14214f466a..b5000b4badf7c 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -6,6 +6,7 @@ import angular from 'angular'; import { AppMountContext } from 'kibana/public'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { DataSetup } from 'src/legacy/core_plugins/data/public'; import { AngularHttpError } from 'ui/notify/lib/format_angular_http_error'; // @ts-ignore @@ -26,6 +27,8 @@ export interface GraphDependencies { data: DataSetup; fatalError: (error: AngularHttpError | Error | string, location?: string) => void; xpackInfo: { get(path: string): unknown }; + addBasePath: (url: string) => string; + getBasePath: () => string; } export interface LegacyAngularInjectedDependencies { @@ -62,7 +65,7 @@ export interface LegacyAngularInjectedDependencies { export const renderApp = ( { core }: AppMountContext, - { element, appBasePath, data, fatalError, xpackInfo }: GraphDependencies, + { element, appBasePath, data, fatalError, xpackInfo, addBasePath, getBasePath }: GraphDependencies, angularDeps: LegacyAngularInjectedDependencies ) => { const deps = { @@ -74,8 +77,14 @@ export const renderApp = ( indexPatterns: data.indexPatterns.indexPatterns, fatalError, xpackInfo, + addBasePath, + getBasePath, ...angularDeps, }; + angular.module('graphI18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); initAngularModule(moduleName, deps); const mountpoint = document.createElement('div'); // eslint-disable-next-line From 0b2c0c0b64fac75ab12a5e61688c10362ac2c474 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 7 Oct 2019 16:25:20 +0200 Subject: [PATCH 05/12] hook up everything --- .../ui/public/kbn_top_nav/kbn_top_nav.js | 12 +- .../public/legacy_compat/angular_config.tsx | 6 +- src/legacy/ui/public/private/private.js | 146 +++++++++--------- src/legacy/ui/public/promises/promises.js | 10 +- .../state_management/config_provider.js | 32 ++-- x-pack/legacy/plugins/graph/public/app.js | 30 ++-- x-pack/legacy/plugins/graph/public/index.ts | 34 ++-- x-pack/legacy/plugins/graph/public/plugin.ts | 6 +- .../legacy/plugins/graph/public/render_app.ts | 93 ++++++++++- .../plugins/graph/public/services/url.ts | 9 +- 10 files changed, 235 insertions(+), 143 deletions(-) diff --git a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js index f2faeee75810e..e76de09cbe858 100644 --- a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js @@ -27,7 +27,7 @@ import { start as data } from '../../../core_plugins/data/public/legacy'; const module = uiModules.get('kibana'); -module.directive('kbnTopNav', () => { +export function createTopNavDirective() { return { restrict: 'E', template: '', @@ -89,9 +89,11 @@ module.directive('kbnTopNav', () => { return linkFn; } }; -}); +} -module.directive('kbnTopNavHelper', (reactDirective) => { +module.directive('kbnTopNav', createTopNavDirective); + +export function createTopNavHelper(reactDirective) { return reactDirective( wrapInI18nContext(TopNavMenu), [ @@ -139,4 +141,6 @@ module.directive('kbnTopNavHelper', (reactDirective) => { 'showAutoRefreshOnly', ], ); -}); +} + +module.directive('kbnTopNavHelper', createTopNavHelper); diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 28d57e9f8e8c9..ece1ce6c4997d 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -287,14 +287,12 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( $location: ILocationService, - $rootScope: IRootScopeService, - Private: any, - config: any + $rootScope: IRootScopeService ) => { const urlOverflow = new UrlOverflowService(); const check = () => { // disable long url checks when storing state in session storage - if (config.get('state:storeInSessionStorage')) { + if (newPlatform.uiSettings.get('state:storeInSessionStorage')) { return; } diff --git a/src/legacy/ui/public/private/private.js b/src/legacy/ui/public/private/private.js index ef5c59c21dd7a..6257c12ecf696 100644 --- a/src/legacy/ui/public/private/private.js +++ b/src/legacy/ui/public/private/private.js @@ -108,98 +108,100 @@ function name(fn) { return fn.name || fn.toString().split('\n').shift(); } -uiModules.get('kibana/private') - .provider('Private', function () { - const provider = this; - - // one cache/swaps per Provider - const cache = {}; - const swaps = {}; +export function PrivateProvider() { + const provider = this; - // return the uniq id for this function - function identify(fn) { - if (typeof fn !== 'function') { - throw new TypeError('Expected private module "' + fn + '" to be a function'); - } + // one cache/swaps per Provider + const cache = {}; + const swaps = {}; - if (fn.$$id) return fn.$$id; - else return (fn.$$id = nextId()); + // return the uniq id for this function + function identify(fn) { + if (typeof fn !== 'function') { + throw new TypeError('Expected private module "' + fn + '" to be a function'); } - provider.stub = function (fn, instance) { - cache[identify(fn)] = instance; - return instance; - }; + if (fn.$$id) return fn.$$id; + else return (fn.$$id = nextId()); + } - provider.swap = function (fn, prov) { - const id = identify(fn); - swaps[id] = prov; - }; + provider.stub = function (fn, instance) { + cache[identify(fn)] = instance; + return instance; + }; - provider.$get = ['$injector', function PrivateFactory($injector) { + provider.swap = function (fn, prov) { + const id = identify(fn); + swaps[id] = prov; + }; - // prevent circular deps by tracking where we came from - const privPath = []; - const pathToString = function () { - return privPath.map(name).join(' -> '); - }; + provider.$get = ['$injector', function PrivateFactory($injector) { - // call a private provider and return the instance it creates - function instantiate(prov, locals) { - if (~privPath.indexOf(prov)) { - throw new Error( - 'Circular reference to "' + name(prov) + '"' + - ' found while resolving private deps: ' + pathToString() - ); - } + // prevent circular deps by tracking where we came from + const privPath = []; + const pathToString = function () { + return privPath.map(name).join(' -> '); + }; - privPath.push(prov); + // call a private provider and return the instance it creates + function instantiate(prov, locals) { + if (~privPath.indexOf(prov)) { + throw new Error( + 'Circular reference to "' + name(prov) + '"' + + ' found while resolving private deps: ' + pathToString() + ); + } - const context = {}; - let instance = $injector.invoke(prov, context, locals); - if (!_.isObject(instance)) instance = context; + privPath.push(prov); - privPath.pop(); - return instance; - } + const context = {}; + let instance = $injector.invoke(prov, context, locals); + if (!_.isObject(instance)) instance = context; - // retrieve an instance from cache or create and store on - function get(id, prov, $delegateId, $delegateProv) { - if (cache[id]) return cache[id]; + privPath.pop(); + return instance; + } - let instance; + // retrieve an instance from cache or create and store on + function get(id, prov, $delegateId, $delegateProv) { + if (cache[id]) return cache[id]; - if ($delegateId != null && $delegateProv != null) { - instance = instantiate(prov, { - $decorate: _.partial(get, $delegateId, $delegateProv) - }); - } else { - instance = instantiate(prov); - } + let instance; - return (cache[id] = instance); + if ($delegateId != null && $delegateProv != null) { + instance = instantiate(prov, { + $decorate: _.partial(get, $delegateId, $delegateProv) + }); + } else { + instance = instantiate(prov); } - // main api, get the appropriate instance for a provider - function Private(prov) { - let id = identify(prov); - let $delegateId; - let $delegateProv; + return (cache[id] = instance); + } - if (swaps[id]) { - $delegateId = id; - $delegateProv = prov; + // main api, get the appropriate instance for a provider + function Private(prov) { + let id = identify(prov); + let $delegateId; + let $delegateProv; - prov = swaps[$delegateId]; - id = identify(prov); - } + if (swaps[id]) { + $delegateId = id; + $delegateProv = prov; - return get(id, prov, $delegateId, $delegateProv); + prov = swaps[$delegateId]; + id = identify(prov); } - Private.stub = provider.stub; - Private.swap = provider.swap; + return get(id, prov, $delegateId, $delegateProv); + } + + Private.stub = provider.stub; + Private.swap = provider.swap; + + return Private; + }]; +} - return Private; - }]; - }); +uiModules.get('kibana/private') + .provider('Private', PrivateProvider); diff --git a/src/legacy/ui/public/promises/promises.js b/src/legacy/ui/public/promises/promises.js index 99c9a11be7431..af8a5081e0c55 100644 --- a/src/legacy/ui/public/promises/promises.js +++ b/src/legacy/ui/public/promises/promises.js @@ -22,9 +22,7 @@ import { uiModules } from '../modules'; const module = uiModules.get('kibana'); -// Provides a tiny subset of the excellent API from -// bluebird, reimplemented using the $q service -module.service('Promise', function ($q, $timeout) { +export function PromiseServiceCreator($q, $timeout) { function Promise(fn) { if (typeof this === 'undefined') throw new Error('Promise constructor must be called with "new"'); @@ -122,4 +120,8 @@ module.service('Promise', function ($q, $timeout) { }; return Promise; -}); +} + +// Provides a tiny subset of the excellent API from +// bluebird, reimplemented using the $q service +module.service('Promise', PromiseServiceCreator); diff --git a/src/legacy/ui/public/state_management/config_provider.js b/src/legacy/ui/public/state_management/config_provider.js index 090210cc8723e..ec770e7fef6ca 100644 --- a/src/legacy/ui/public/state_management/config_provider.js +++ b/src/legacy/ui/public/state_management/config_provider.js @@ -25,21 +25,23 @@ import { uiModules } from '../modules'; -uiModules.get('kibana/state_management') - .provider('stateManagementConfig', class StateManagementConfigProvider { - _enabled = true +export class StateManagementConfigProvider { + _enabled = true + + $get(/* inject stuff */) { + return { + enabled: this._enabled, + }; + } - $get(/* inject stuff */) { - return { - enabled: this._enabled, - }; - } + disable() { + this._enabled = false; + } - disable() { - this._enabled = false; - } + enable() { + this._enabled = true; + } +} - enable() { - this._enabled = true; - } - }); +uiModules.get('kibana/state_management') + .provider('stateManagementConfig', StateManagementConfigProvider); diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 52703797c6123..61e2829766eff 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -53,7 +53,7 @@ import { import './angular/directives/graph_inspect'; -export function initAngularModule(moduleName, deps) { +export function initAngularModule(angularModule, deps) { const { xpackInfo, fatalError, @@ -72,11 +72,12 @@ export function initAngularModule(moduleName, deps) { coreStart, //npStart.core confirmModal, $http, //$http + KbnUrlProvider, canEditDrillDownUrls, - graphSavePolicy + graphSavePolicy, } = deps; - const app = angular.module(moduleName, ['ngRoute', 'react', 'graphI18n', 'ngSanitize']); + const app = angularModule; function checkLicense(Promise, kbnBaseUrl) { const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && @@ -127,26 +128,27 @@ export function initAngularModule(moduleName, deps) { ]); }); - app.config(function ($routeProvider, $locationProvider) { + app.config(function ($routeProvider) { $routeProvider.when('/home', { template: listingTemplate, badge: getReadonlyBadge, - controller($location, $scope) { + controller($location, $scope, Private) { + const kbnUrl = Private(KbnUrlProvider); checkLicense(Promise, kbnBaseUrl); const services = savedObjectRegistry.byLoaderPropertiesName; const graphService = services['Graph workspace']; $scope.listingLimit = config.get('savedObjects:listingLimit'); $scope.create = () => { - $location.url(getNewPath()); + kbnUrl.change(getNewPath()); }; $scope.find = (search) => { return graphService.find(search, $scope.listingLimit); }; $scope.editItem = (workspace) => { - $location.url(getEditPath(workspace)); + kbnUrl.change(getEditPath(workspace)); }; - $scope.getViewUrl = (workspace) => getEditUrl(chrome, workspace); + $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); $scope.delete = (workspaces) => { return graphService.delete(workspaces.map(({ id }) => id)); }; @@ -191,14 +193,12 @@ export function initAngularModule(moduleName, deps) { .otherwise({ redirectTo: '/home' }); - - $locationProvider.html5Mode(false); - $locationProvider.hashPrefix(''); }); //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route, $location) { + app.controller('graphuiPlugin', function ($scope, $route, $location, Private) { + const kbnUrl = Private(KbnUrlProvider); function handleSuccess(data) { return checkLicense(Promise, kbnBaseUrl) .then(() => data); @@ -240,7 +240,7 @@ export function initAngularModule(moduleName, deps) { // but the check is too simple right now. Change this // once actual state-diffing is in place. $scope.$evalAsync(() => { - $location.url(getHomePath()); + kbnUrl.change(getHomePath()); }); } }); @@ -677,7 +677,7 @@ export function initAngularModule(moduleName, deps) { run: function () { canWipeWorkspace(function () { $scope.$evalAsync(() => { - $location.url('/workspace/'); + kbnUrl.change('/workspace/'); }); }); }, testId: 'graphNewButton', @@ -883,7 +883,7 @@ export function initAngularModule(moduleName, deps) { 'data-test-subj': 'saveGraphSuccess', }); if ($scope.savedWorkspace.id !== $route.current.params.id) { - $location.url(getEditPath($scope.savedWorkspace)); + kbnUrl.change(getEditPath($scope.savedWorkspace)); } } return { id }; diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index fe9295ad558be..8f6084056f5a7 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -4,28 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; -import { GraphPlugin } from './plugin'; import { npSetup, npStart } from 'ui/new_platform'; import { IScope } from 'angular'; import { App } from 'kibana/public'; +import { GraphPlugin } from './plugin'; +import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +// Setup compatibility layer for AppService in legacy platform +npSetup.core.application.register = (app: App) => { + require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { + const element = $element[0]; - // Setup compatibility layer for AppService in legacy platform - npSetup.core.application.register = (app: App) => { - require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = $element[0]; - - // Root controller cannot return a Promise so use an internal async function and call it - (async () => { - const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); - $scope.$on('$destroy', () => { - unmount(); - }); - })(); - }); - }; - + // Root controller cannot return a Promise so use an internal async function and call it + (async () => { + const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + $scope.$on('$destroy', () => { + unmount(); + }); + })(); + }); +}; const instance = new GraphPlugin(); -instance.setup(npSetup.core, { data }); \ No newline at end of file +instance.setup(npSetup.core, { data }); diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index c8cbba4ab62de..b8a99a13f32d8 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -17,6 +17,8 @@ import 'uiExports/autocompleteProviders'; import chrome from 'ui/chrome'; import { fatalError } from 'ui/notify'; // @ts-ignore +import { KbnUrlProvider } from 'ui/url'; +// @ts-ignore import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; // @ts-ignore @@ -42,7 +44,6 @@ async function getAngularInjectedDependencies() { $http: injector.get('$http'), confirmModal: injector.get('confirmModal'), savedObjectRegisty: Private(SavedObjectRegistryProvider), - kbnUrl: injector.get('kbnUrl'), kbnBaseUrl: injector.get('kbnBaseUrl'), savedWorkspacesClient: Private(SavedWorkspacesProvider), savedGraphWorkspaces: injector.get('savedGraphWorkspaces'), @@ -72,8 +73,9 @@ export class GraphPlugin { data, fatalError, xpackInfo, + KbnUrlProvider, addBasePath: core.http.basePath.prepend, - getBasePath: core.http.basePath.get + getBasePath: core.http.basePath.get, }, angularDependencies ); diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index b5000b4badf7c..01e72b8676246 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -9,13 +9,29 @@ import { AppMountContext } from 'kibana/public'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { DataSetup } from 'src/legacy/core_plugins/data/public'; import { AngularHttpError } from 'ui/notify/lib/format_angular_http_error'; +import { configureAppAngularModule } from 'ui/legacy_compat'; + +// @ts-ignore +import { GlobalStateProvider } from 'ui/state_management/global_state'; +// @ts-ignore +import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; +// @ts-ignore +import { PrivateProvider } from 'ui/private/private'; +// @ts-ignore +import { EventsProvider } from 'ui/events'; +// @ts-ignore +import { PersistedState } from 'ui/persisted_state'; +// @ts-ignore +import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore +import { PromiseServiceCreator } from 'ui/promises/promises'; import { initAngularModule } from './app'; +// @ts-ignore -const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
-
+
`; @@ -29,6 +45,7 @@ export interface GraphDependencies { xpackInfo: { get(path: string): unknown }; addBasePath: (url: string) => string; getBasePath: () => string; + KbnUrlProvider: any; } export interface LegacyAngularInjectedDependencies { @@ -44,7 +61,6 @@ export interface LegacyAngularInjectedDependencies { * Private(SavedObjectRegistryProvider) */ savedObjectRegistry: any; - kbnUrl: any; kbnBaseUrl: any; /** * Private(SavedWorkspacesProvider) @@ -65,7 +81,16 @@ export interface LegacyAngularInjectedDependencies { export const renderApp = ( { core }: AppMountContext, - { element, appBasePath, data, fatalError, xpackInfo, addBasePath, getBasePath }: GraphDependencies, + { + element, + appBasePath, + data, + fatalError, + xpackInfo, + addBasePath, + getBasePath, + KbnUrlProvider, + }: GraphDependencies, angularDeps: LegacyAngularInjectedDependencies ) => { const deps = { @@ -79,17 +104,73 @@ export const renderApp = ( xpackInfo, addBasePath, getBasePath, + KbnUrlProvider, ...angularDeps, }; - angular.module('graphI18n', []) + angular + .module('graphI18n', []) .provider('i18n', I18nProvider) .filter('i18n', i18nFilter) .directive('i18nId', i18nDirective); - initAngularModule(moduleName, deps); + angular.module('graphPrivate', []).provider('Private', PrivateProvider); + angular.module('graphPromise', []).service('Promise', PromiseServiceCreator); + angular + .module('graphGlobalStateDeps', ['graphPrivate']) + .provider('stateManagementConfig', StateManagementConfigProvider) + .provider('config', () => { + return { + $get: () => ({ + get: core.uiSettings.get.bind(core.uiSettings), + }), + }; + }) + .service('kbnUrl', (Private: any) => Private(KbnUrlProvider)); + angular + .module('graphUrlStuff', ['graphPrivate', 'graphPromise']) + .factory('PersistedState', ($injector: any) => { + const Private = $injector.get('Private'); + const Events = Private(EventsProvider); + + // Extend PersistedState to override the EmitterClass class with + // our Angular friendly version. + return class AngularPersistedState extends PersistedState { + constructor(value: any, path: any) { + super(value, path, Events); + } + }; + }); + angular + .module('graphTopNav', ['react']) + .directive('kbnTopNav', createTopNavDirective) + .directive('kbnTopNavHelper', createTopNavHelper); + // global state is only here because of legacy reasons, it's not actually used. + // But it is helpful as a reference for Discover + angular + .module('graphGlobalState', ['graphPrivate', 'graphGlobalStateDeps', 'graphPromise']) + .service('globalState', function(Private: any) { + return Private(GlobalStateProvider); + }); + const graphAngularModule = angular.module(moduleName, [ + 'ngSanitize', + 'ngRoute', + 'react', + 'graphI18n', + 'graphPrivate', + 'graphUrlStuff', + 'graphTopNav', + 'graphGlobalState', + ]); + configureAppAngularModule(graphAngularModule); + initAngularModule(graphAngularModule, deps); const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%'); // eslint-disable-next-line mountpoint.innerHTML = mainTemplate(appBasePath); + // bootstrap angular into detached element and attach it later to + // make angular-within-angular possible const $injector = angular.bootstrap(mountpoint, [moduleName]); + // initialize global state handler + $injector.get('globalState'); element.appendChild(mountpoint); return () => $injector.get('$rootScope').$destroy(); }; diff --git a/x-pack/legacy/plugins/graph/public/services/url.ts b/x-pack/legacy/plugins/graph/public/services/url.ts index 069d07a2f626b..ecc2f8998b1dc 100644 --- a/x-pack/legacy/plugins/graph/public/services/url.ts +++ b/x-pack/legacy/plugins/graph/public/services/url.ts @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { GraphWorkspaceSavedObject } from '../types'; import { ChromeStart } from 'kibana/public'; +import { GraphWorkspaceSavedObject } from '../types'; export function getHomePath() { return '/home'; @@ -20,8 +20,11 @@ export function getEditPath({ id }: GraphWorkspaceSavedObject) { return `/workspace/${id}`; } -export function getEditUrl(chrome: ChromeStart, workspace: GraphWorkspaceSavedObject) { - return chrome.addBasePath(`#${getEditPath(workspace)}`); +export function getEditUrl( + addBasePath: (url: string) => string, + workspace: GraphWorkspaceSavedObject +) { + return addBasePath(`#${getEditPath(workspace)}`); } export type SetBreadcrumbOptions = From adc67ce96bd018e113fc40de18b980ead0e04c61 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 14 Oct 2019 15:08:39 +0200 Subject: [PATCH 06/12] change indentation to reduce merge conflicts --- x-pack/legacy/plugins/graph/public/app.js | 1444 ++++++++++----------- 1 file changed, 722 insertions(+), 722 deletions(-) diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 61e2829766eff..46fe40bdf5608 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -77,822 +77,822 @@ export function initAngularModule(angularModule, deps) { graphSavePolicy, } = deps; - const app = angularModule; - - function checkLicense(Promise, kbnBaseUrl) { - const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && - xpackInfo.get('features.graph.enableAppLink'); - if (!licenseAllowsToShowThisPage) { - const message = xpackInfo.get('features.graph.message'); - const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message); - window.location.href = newUrl; - return Promise.halt(); - } - - return Promise.resolve(); +const app = angularModule; + +function checkLicense(Promise, kbnBaseUrl) { + const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && + xpackInfo.get('features.graph.enableAppLink'); + if (!licenseAllowsToShowThisPage) { + const message = xpackInfo.get('features.graph.message'); + const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message); + window.location.href = newUrl; + return Promise.halt(); } - app.directive('focusOn', function () { - return function (scope, elem, attr) { - scope.$on(attr.focusOn, function () { - elem[0].focus(); - }); - }; - }); - - app.directive('vennDiagram', function (reactDirective) { - return reactDirective(VennDiagram); - }); - - app.directive('graphVisualization', function (reactDirective) { - return reactDirective(GraphVisualization); - }); - - app.directive('graphListing', function (reactDirective) { - return reactDirective(Listing); - }); - - app.directive('graphApp', function (reactDirective) { - return reactDirective(GraphApp, [ - ['state', { watchDepth: 'reference' }], - ['dispatch', { watchDepth: 'reference' }], - ['currentIndexPattern', { watchDepth: 'reference' }], - ['isLoading', { watchDepth: 'reference' }], - ['onIndexPatternSelected', { watchDepth: 'reference' }], - ['onQuerySubmit', { watchDepth: 'reference' }], - ['savedObjects', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ['http', { watchDepth: 'reference' }], - ['initialQuery', { watchDepth: 'reference' }], - ['overlays', { watchDepth: 'reference' }] - ]); - }); + return Promise.resolve(); +} - app.config(function ($routeProvider) { - $routeProvider.when('/home', { - template: listingTemplate, +app.directive('focusOn', function () { + return function (scope, elem, attr) { + scope.$on(attr.focusOn, function () { + elem[0].focus(); + }); + }; +}); + +app.directive('vennDiagram', function (reactDirective) { + return reactDirective(VennDiagram); +}); + +app.directive('graphVisualization', function (reactDirective) { + return reactDirective(GraphVisualization); +}); + +app.directive('graphListing', function (reactDirective) { + return reactDirective(Listing); +}); + +app.directive('graphApp', function (reactDirective) { + return reactDirective(GraphApp, [ + ['state', { watchDepth: 'reference' }], + ['dispatch', { watchDepth: 'reference' }], + ['currentIndexPattern', { watchDepth: 'reference' }], + ['isLoading', { watchDepth: 'reference' }], + ['onIndexPatternSelected', { watchDepth: 'reference' }], + ['onQuerySubmit', { watchDepth: 'reference' }], + ['savedObjects', { watchDepth: 'reference' }], + ['uiSettings', { watchDepth: 'reference' }], + ['http', { watchDepth: 'reference' }], + ['initialQuery', { watchDepth: 'reference' }], + ['overlays', { watchDepth: 'reference' }] + ]); +}); + +app.config(function ($routeProvider) { + $routeProvider.when('/home', { + template: listingTemplate, + badge: getReadonlyBadge, + controller($location, $scope, Private) { + const kbnUrl = Private(KbnUrlProvider); + checkLicense(Promise, kbnBaseUrl); + const services = savedObjectRegistry.byLoaderPropertiesName; + const graphService = services['Graph workspace']; + + $scope.listingLimit = config.get('savedObjects:listingLimit'); + $scope.create = () => { + kbnUrl.change(getNewPath()); + }; + $scope.find = (search) => { + return graphService.find(search, $scope.listingLimit); + }; + $scope.editItem = (workspace) => { + kbnUrl.change(getEditPath(workspace)); + }; + $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); + $scope.delete = (workspaces) => { + return graphService.delete(workspaces.map(({ id }) => id)); + }; + $scope.capabilities = capabilities; + $scope.initialFilter = ($location.search()).filter || ''; + setBreadcrumbs({ chrome }); + } + }) + .when('/workspace/:id?', { + template: appTemplate, badge: getReadonlyBadge, - controller($location, $scope, Private) { - const kbnUrl = Private(KbnUrlProvider); - checkLicense(Promise, kbnBaseUrl); - const services = savedObjectRegistry.byLoaderPropertiesName; - const graphService = services['Graph workspace']; - - $scope.listingLimit = config.get('savedObjects:listingLimit'); - $scope.create = () => { - kbnUrl.change(getNewPath()); - }; - $scope.find = (search) => { - return graphService.find(search, $scope.listingLimit); - }; - $scope.editItem = (workspace) => { - kbnUrl.change(getEditPath(workspace)); - }; - $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); - $scope.delete = (workspaces) => { - return graphService.delete(workspaces.map(({ id }) => id)); - }; - $scope.capabilities = capabilities; - $scope.initialFilter = ($location.search()).filter || ''; - setBreadcrumbs({ chrome }); + resolve: { + savedWorkspace: function ($route) { + return $route.current.params.id && savedGraphWorkspaces.get($route.current.params.id) + .catch( + function () { + toastNotifications.addDanger( + i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: 'Missing workspace', + }) + ); + } + ); + + }, + //Copied from example found in wizard.js ( Kibana TODO - can't + indexPatterns: function () { + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title', 'type'], + perPage: 10000 + }).then(response => response.savedObjects); + }, + GetIndexPatternProvider: function () { + return indexPatterns; + }, + SavedWorkspacesProvider: function () { + return savedWorkspacesClient; + } } }) - .when('/workspace/:id?', { - template: appTemplate, - badge: getReadonlyBadge, - resolve: { - savedWorkspace: function ($route) { - return $route.current.params.id && savedGraphWorkspaces.get($route.current.params.id) - .catch( - function () { - toastNotifications.addDanger( - i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: 'Missing workspace', - }) - ); - } - ); - - }, - //Copied from example found in wizard.js ( Kibana TODO - can't - indexPatterns: function () { - return savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000 - }).then(response => response.savedObjects); - }, - GetIndexPatternProvider: function () { - return indexPatterns; - }, - SavedWorkspacesProvider: function () { - return savedWorkspacesClient; - } - } - }) - .otherwise({ - redirectTo: '/home' - }); - }); - + .otherwise({ + redirectTo: '/home' + }); +}); - //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route, $location, Private) { - const kbnUrl = Private(KbnUrlProvider); - function handleSuccess(data) { - return checkLicense(Promise, kbnBaseUrl) - .then(() => data); - } - function handleError(err) { - return checkLicense(Promise, kbnBaseUrl) - .then(() => { - const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { - defaultMessage: 'Graph Error', - description: '"Graph" is a product name and should not be translated.', - }); - if (err instanceof Error) { - toastNotifications.addError(err, { - title: toastTitle, - }); - } else { - toastNotifications.addDanger({ - title: toastTitle, - text: String(err), - }); - } - }); - } +//======== Controller for basic UI ================== +app.controller('graphuiPlugin', function ($scope, $route, $location, Private) { + const kbnUrl = Private(KbnUrlProvider); + function handleSuccess(data) { + return checkLicense(Promise, kbnBaseUrl) + .then(() => data); + } - function handleHttpError(error) { - return checkLicense(Promise, kbnBaseUrl) - .then(() => { - toastNotifications.addDanger(formatAngularHttpError(error)); + function handleError(err) { + return checkLicense(Promise, kbnBaseUrl) + .then(() => { + const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { + defaultMessage: 'Graph Error', + description: '"Graph" is a product name and should not be translated.', }); - } - - function updateBreadcrumbs() { - setBreadcrumbs({ - chrome, - savedWorkspace: $route.current.locals.savedWorkspace, - navigateTo: () => { - // TODO this should be wrapped into canWipeWorkspace, - // but the check is too simple right now. Change this - // once actual state-diffing is in place. - $scope.$evalAsync(() => { - kbnUrl.change(getHomePath()); + if (err instanceof Error) { + toastNotifications.addError(err, { + title: toastTitle, + }); + } else { + toastNotifications.addDanger({ + title: toastTitle, + text: String(err), }); } }); - } - - const store = createGraphStore(); - - $scope.title = 'Graph'; - $scope.spymode = 'request'; - - $scope.iconChoices = iconChoices; - $scope.drillDownIconChoices = urlTemplateIconChoices; - $scope.colors = colorChoices; - $scope.iconChoicesByClass = iconChoicesByClass; - - $scope.outlinkEncoders = outlinkEncoders; - - $scope.fields = []; - $scope.canEditDrillDownUrls = canEditDrillDownUrls; - - $scope.graphSavePolicy = graphSavePolicy; - $scope.allSavingDisabled = $scope.graphSavePolicy === 'none'; - $scope.searchTerm = ''; - - $scope.reduxDispatch = (action) => { - store.dispatch(action); + } - // patch updated icons and fields on the nodes in the workspace state - // this workaround is necessary because the nodes are still managed by - // angular - once they are moved over to redux, this can be handled in - // the reducer - if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && - action.payload.fieldProperties.color && $scope.workspace) { - $scope.workspace.nodes.forEach(function (node) { - if (node.data.field === action.payload.fieldName) { - node.color = action.payload.fieldProperties.color; - } - }); - } + function handleHttpError(error) { + return checkLicense(Promise, kbnBaseUrl) + .then(() => { + toastNotifications.addDanger(formatAngularHttpError(error)); + }); + } - if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && - action.payload.fieldProperties.icon && $scope.workspace) { - $scope.workspace.nodes.forEach(function (node) { - if (node.data.field === action.payload.fieldName) { - node.icon = action.payload.fieldProperties.icon; - } + function updateBreadcrumbs() { + setBreadcrumbs({ + chrome, + savedWorkspace: $route.current.locals.savedWorkspace, + navigateTo: () => { + // TODO this should be wrapped into canWipeWorkspace, + // but the check is too simple right now. Change this + // once actual state-diffing is in place. + $scope.$evalAsync(() => { + kbnUrl.change(getHomePath()); }); } - }; - - $scope.pluginDependencies = coreStart; + }); + } - $scope.loading = false; + const store = createGraphStore(); - const updateScope = () => { - const newState = store.getState(); - $scope.reduxState = newState; - $scope.allFields = fieldsSelector(newState); - $scope.selectedFields = selectedFieldsSelector(newState); - $scope.liveResponseFields = liveResponseFieldsSelector(newState); - if ($scope.workspace) { - $scope.workspace.options.vertex_fields = $scope.selectedFields; - } - }; - store.subscribe(updateScope); - updateScope(); + $scope.title = 'Graph'; + $scope.spymode = 'request'; - //So scope properties can be used consistently with ng-model - $scope.grr = $scope; + $scope.iconChoices = iconChoices; + $scope.drillDownIconChoices = urlTemplateIconChoices; + $scope.colors = colorChoices; + $scope.iconChoicesByClass = iconChoicesByClass; - $scope.toggleDrillDownIcon = function (urlTemplate, icon) { - urlTemplate.icon === icon ? urlTemplate.icon = null : urlTemplate.icon = icon; - }; + $scope.outlinkEncoders = outlinkEncoders; - $scope.nodeClick = function (n, $event) { + $scope.fields = []; + $scope.canEditDrillDownUrls = canEditDrillDownUrls; - //Selection logic - shift key+click helps selects multiple nodes - // Without the shift key we deselect all prior selections (perhaps not - // a great idea for touch devices with no concept of shift key) - if (!$event.shiftKey) { - const prevSelection = n.isSelected; - $scope.workspace.selectNone(); - n.isSelected = prevSelection; - } + $scope.graphSavePolicy = graphSavePolicy; + $scope.allSavingDisabled = $scope.graphSavePolicy === 'none'; + $scope.searchTerm = ''; + $scope.reduxDispatch = (action) => { + store.dispatch(action); - if ($scope.workspace.toggleNodeSelection(n)) { - $scope.selectSelected(n); - } else { - $scope.detail = null; - } - }; - - function canWipeWorkspace(yesFn, noFn) { - if ($scope.selectedFields.length === 0 && $scope.workspace === null) { - yesFn(); - return; - } - const confirmModalOptions = { - onConfirm: yesFn, - onCancel: noFn || (() => {}), - confirmButtonText: i18n.translate('xpack.graph.clearWorkspace.confirmButtonLabel', { - defaultMessage: 'Continue', - }), - title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { - defaultMessage: 'Discard changes to workspace?', - }), - }; - confirmModal(i18n.translate('xpack.graph.clearWorkspace.confirmText', { - defaultMessage: 'Once you discard changes made to a workspace, there is no getting them back.', - }), confirmModalOptions); + // patch updated icons and fields on the nodes in the workspace state + // this workaround is necessary because the nodes are still managed by + // angular - once they are moved over to redux, this can be handled in + // the reducer + if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && + action.payload.fieldProperties.color && $scope.workspace) { + $scope.workspace.nodes.forEach(function (node) { + if (node.data.field === action.payload.fieldName) { + node.color = action.payload.fieldProperties.color; + } + }); } - $scope.uiSelectIndex = function (proposedIndex) { - canWipeWorkspace(function () { - $scope.indexSelected(proposedIndex); + if (action.type === 'x-pack/graph/fields/UPDATE_FIELD_PROPERTIES' && + action.payload.fieldProperties.icon && $scope.workspace) { + $scope.workspace.nodes.forEach(function (node) { + if (node.data.field === action.payload.fieldName) { + node.icon = action.payload.fieldProperties.icon; + } }); - }; + } + }; - $scope.indexSelected = function (selectedIndex) { - $scope.clearWorkspace(); - $scope.allFields = []; - $scope.selectedFields = []; - $scope.basicModeSelectedSingleField = null; - $scope.selectedField = null; - $scope.selectedFieldConfig = null; - - return $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id) - .then(handleSuccess) - .then(function (indexPattern) { - $scope.selectedIndex = indexPattern; - store.dispatch(loadFields(mapFields(indexPattern))); - $scope.$digest(); - }, handleError); - }; + $scope.pluginDependencies = coreStart; + $scope.loading = false; - $scope.clickEdge = function (edge) { - if (edge.inferred) { - $scope.setDetail ({ 'inferredEdge': edge }); - }else { - $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); - } - }; - - // Replacement function for graphClientWorkspace's comms so - // that it works with Kibana. - function callNodeProxy(indexName, query, responseHandler) { - const request = { - index: indexName, - query: query - }; - $scope.loading = true; - return $http.post('../api/graph/graphExplore', request) - .then(function (resp) { - if (resp.data.resp.timed_out) { - toastNotifications.addWarning( - i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { - defaultMessage: 'Exploration timed out', - }) - ); - } - responseHandler(resp.data.resp); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - }); + const updateScope = () => { + const newState = store.getState(); + $scope.reduxState = newState; + $scope.allFields = fieldsSelector(newState); + $scope.selectedFields = selectedFieldsSelector(newState); + $scope.liveResponseFields = liveResponseFieldsSelector(newState); + if ($scope.workspace) { + $scope.workspace.options.vertex_fields = $scope.selectedFields; + } + }; + store.subscribe(updateScope); + updateScope(); + + //So scope properties can be used consistently with ng-model + $scope.grr = $scope; + + $scope.toggleDrillDownIcon = function (urlTemplate, icon) { + urlTemplate.icon === icon ? urlTemplate.icon = null : urlTemplate.icon = icon; + }; + + $scope.nodeClick = function (n, $event) { + + //Selection logic - shift key+click helps selects multiple nodes + // Without the shift key we deselect all prior selections (perhaps not + // a great idea for touch devices with no concept of shift key) + if (!$event.shiftKey) { + const prevSelection = n.isSelected; + $scope.workspace.selectNone(); + n.isSelected = prevSelection; } - //Helper function for the graphClientWorkspace to perform a query - const callSearchNodeProxy = function (indexName, query, responseHandler) { - const request = { - index: indexName, - body: query - }; - $scope.loading = true; - $http.post('../api/graph/searchProxy', request) - .then(function (resp) { - responseHandler(resp.data.resp); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - }); - }; + if ($scope.workspace.toggleNodeSelection(n)) { + $scope.selectSelected(n); + } else { + $scope.detail = null; + } + }; - $scope.submit = function (searchTerm) { - initWorkspaceIfRequired(); - const numHops = 2; - if (searchTerm.startsWith('{')) { - try { - const query = JSON.parse(searchTerm); - if (query.vertices) { - // Is a graph explore request - $scope.workspace.callElasticsearch(query); - }else { - // Is a regular query DSL query - $scope.workspace.search(query, $scope.liveResponseFields, numHops); - } - } - catch (err) { - handleError(err); - } - return; - } - $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); + function canWipeWorkspace(yesFn, noFn) { + if ($scope.selectedFields.length === 0 && $scope.workspace === null) { + yesFn(); + return; + } + const confirmModalOptions = { + onConfirm: yesFn, + onCancel: noFn || (() => {}), + confirmButtonText: i18n.translate('xpack.graph.clearWorkspace.confirmButtonLabel', { + defaultMessage: 'Continue', + }), + title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { + defaultMessage: 'Discard changes to workspace?', + }), }; + confirmModal(i18n.translate('xpack.graph.clearWorkspace.confirmText', { + defaultMessage: 'Once you discard changes made to a workspace, there is no getting them back.', + }), confirmModalOptions); + } - $scope.clearWorkspace = function () { - $scope.workspace = null; - $scope.detail = null; - if ($scope.closeMenus) $scope.closeMenus(); - }; + $scope.uiSelectIndex = function (proposedIndex) { + canWipeWorkspace(function () { + $scope.indexSelected(proposedIndex); + }); + }; + + $scope.indexSelected = function (selectedIndex) { + $scope.clearWorkspace(); + $scope.allFields = []; + $scope.selectedFields = []; + $scope.basicModeSelectedSingleField = null; + $scope.selectedField = null; + $scope.selectedFieldConfig = null; + + return $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id) + .then(handleSuccess) + .then(function (indexPattern) { + $scope.selectedIndex = indexPattern; + store.dispatch(loadFields(mapFields(indexPattern))); + $scope.$digest(); + }, handleError); + }; - $scope.selectSelected = function (node) { - $scope.detail = { - latestNodeSelection: node - }; - return $scope.selectedSelectedVertex = node; + $scope.clickEdge = function (edge) { + if (edge.inferred) { + $scope.setDetail ({ 'inferredEdge': edge }); + }else { + $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); + } + }; + + // Replacement function for graphClientWorkspace's comms so + // that it works with Kibana. + function callNodeProxy(indexName, query, responseHandler) { + const request = { + index: indexName, + query: query }; + $scope.loading = true; + return $http.post('../api/graph/graphExplore', request) + .then(function (resp) { + if (resp.data.resp.timed_out) { + toastNotifications.addWarning( + i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { + defaultMessage: 'Exploration timed out', + }) + ); + } + responseHandler(resp.data.resp); + }) + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); + } - $scope.isSelectedSelected = function (node) { - return $scope.selectedSelectedVertex === node; - }; - $scope.saveUrlTemplate = function (index, urlTemplate) { - const newTemplatesList = [...$scope.urlTemplates]; - if (index !== -1) { - newTemplatesList[index] = urlTemplate; - } else { - newTemplatesList.push(urlTemplate); + //Helper function for the graphClientWorkspace to perform a query + const callSearchNodeProxy = function (indexName, query, responseHandler) { + const request = { + index: indexName, + body: query + }; + $scope.loading = true; + $http.post('../api/graph/searchProxy', request) + .then(function (resp) { + responseHandler(resp.data.resp); + }) + .catch(handleHttpError) + .finally(() => { + $scope.loading = false; + }); + }; + + $scope.submit = function (searchTerm) { + initWorkspaceIfRequired(); + const numHops = 2; + if (searchTerm.startsWith('{')) { + try { + const query = JSON.parse(searchTerm); + if (query.vertices) { + // Is a graph explore request + $scope.workspace.callElasticsearch(query); + }else { + // Is a regular query DSL query + $scope.workspace.search(query, $scope.liveResponseFields, numHops); + } + } + catch (err) { + handleError(err); } + return; + } + $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); + }; - $scope.urlTemplates = newTemplatesList; - }; + $scope.clearWorkspace = function () { + $scope.workspace = null; + $scope.detail = null; + if ($scope.closeMenus) $scope.closeMenus(); + }; - $scope.removeUrlTemplate = function (urlTemplate) { - const newTemplatesList = [...$scope.urlTemplates]; - const i = newTemplatesList.indexOf(urlTemplate); - newTemplatesList.splice(i, 1); - $scope.urlTemplates = newTemplatesList; - }; - $scope.openUrlTemplate = function (template) { - const url = template.url; - const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); - window.open(newUrl, '_blank'); + $scope.selectSelected = function (node) { + $scope.detail = { + latestNodeSelection: node }; + return $scope.selectedSelectedVertex = node; + }; + $scope.isSelectedSelected = function (node) { + return $scope.selectedSelectedVertex === node; + }; - //============================ + $scope.saveUrlTemplate = function (index, urlTemplate) { + const newTemplatesList = [...$scope.urlTemplates]; + if (index !== -1) { + newTemplatesList[index] = urlTemplate; + } else { + newTemplatesList.push(urlTemplate); + } - $scope.resetWorkspace = function () { - $scope.clearWorkspace(); - $scope.selectedIndex = null; - $scope.proposedIndex = null; - $scope.detail = null; - $scope.selectedSelectedVertex = null; - $scope.selectedField = null; - $scope.description = null; - $scope.allFields = []; - $scope.urlTemplates = []; - - $scope.fieldNamesFilterString = null; - $scope.filteredFields = []; - - $scope.selectedFields = []; - $scope.liveResponseFields = []; - - $scope.exploreControls = { - useSignificance: true, - sampleSize: 2000, - timeoutMillis: 5000, - sampleDiversityField: null, - maxValuesPerDoc: 1, - minDocCount: 3 - }; + $scope.urlTemplates = newTemplatesList; + }; + + $scope.removeUrlTemplate = function (urlTemplate) { + const newTemplatesList = [...$scope.urlTemplates]; + const i = newTemplatesList.indexOf(urlTemplate); + newTemplatesList.splice(i, 1); + $scope.urlTemplates = newTemplatesList; + }; + + $scope.openUrlTemplate = function (template) { + const url = template.url; + const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); + window.open(newUrl, '_blank'); + }; + + + //============================ + + $scope.resetWorkspace = function () { + $scope.clearWorkspace(); + $scope.selectedIndex = null; + $scope.proposedIndex = null; + $scope.detail = null; + $scope.selectedSelectedVertex = null; + $scope.selectedField = null; + $scope.description = null; + $scope.allFields = []; + $scope.urlTemplates = []; + + $scope.fieldNamesFilterString = null; + $scope.filteredFields = []; + + $scope.selectedFields = []; + $scope.liveResponseFields = []; + + $scope.exploreControls = { + useSignificance: true, + sampleSize: 2000, + timeoutMillis: 5000, + sampleDiversityField: null, + maxValuesPerDoc: 1, + minDocCount: 3 }; + }; - function initWorkspaceIfRequired() { - if ($scope.workspace) { - return; - } - const options = { - indexName: $scope.selectedIndex.title, - vertex_fields: $scope.selectedFields, - // Here we have the opportunity to look up labels for nodes... - nodeLabeller: function () { - // console.log(newNodes); - }, - changeHandler: function () { - //Allows DOM to update with graph layout changes. - $scope.$apply(); - }, - graphExploreProxy: callNodeProxy, - searchProxy: callSearchNodeProxy, - exploreControls: $scope.exploreControls - }; - $scope.workspace = gws.createWorkspace(options); - $scope.detail = null; + function initWorkspaceIfRequired() { + if ($scope.workspace) { + return; + } + const options = { + indexName: $scope.selectedIndex.title, + vertex_fields: $scope.selectedFields, + // Here we have the opportunity to look up labels for nodes... + nodeLabeller: function () { + // console.log(newNodes); + }, + changeHandler: function () { + //Allows DOM to update with graph layout changes. + $scope.$apply(); + }, + graphExploreProxy: callNodeProxy, + searchProxy: callSearchNodeProxy, + exploreControls: $scope.exploreControls + }; + $scope.workspace = gws.createWorkspace(options); + $scope.detail = null; - // filter out default url templates because they will get re-added - $scope.urlTemplates = $scope.urlTemplates.filter(template => !template.isDefault); + // filter out default url templates because they will get re-added + $scope.urlTemplates = $scope.urlTemplates.filter(template => !template.isDefault); - if ($scope.urlTemplates.length === 0) { - // url templates specified by users can include the `{{gquery}}` tag and - // will have the elasticsearch query for the graph nodes injected there - const tag = '{{gquery}}'; + if ($scope.urlTemplates.length === 0) { + // url templates specified by users can include the `{{gquery}}` tag and + // will have the elasticsearch query for the graph nodes injected there + const tag = '{{gquery}}'; - const kUrl = new KibanaParsedUrl({ - appId: 'kibana', - basePath: getBasePath(), - appPath: '/discover' - }); + const kUrl = new KibanaParsedUrl({ + appId: 'kibana', + basePath: getBasePath(), + appPath: '/discover' + }); - kUrl.addQueryParameter('_a', rison.encode({ - columns: ['_source'], - index: $scope.selectedIndex.id, - interval: 'auto', - query: { language: 'kuery', query: tag }, - sort: ['_score', 'desc'] - })); - - const discoverUrl = kUrl.getRootRelativePath() - // replace the URI encoded version of the tag with the unescaped version - // so it can be found with String.replace, regexp, etc. - .replace(encodeURIComponent(tag), tag); - - $scope.urlTemplates.push({ - url: discoverUrl, - description: i18n.translate('xpack.graph.settings.drillDowns.defaultUrlTemplateTitle', { - defaultMessage: 'Raw documents', - }), - encoder: $scope.outlinkEncoders[0], - isDefault: true - }); - } + kUrl.addQueryParameter('_a', rison.encode({ + columns: ['_source'], + index: $scope.selectedIndex.id, + interval: 'auto', + query: { language: 'kuery', query: tag }, + sort: ['_score', 'desc'] + })); + + const discoverUrl = kUrl.getRootRelativePath() + // replace the URI encoded version of the tag with the unescaped version + // so it can be found with String.replace, regexp, etc. + .replace(encodeURIComponent(tag), tag); + + $scope.urlTemplates.push({ + url: discoverUrl, + description: i18n.translate('xpack.graph.settings.drillDowns.defaultUrlTemplateTitle', { + defaultMessage: 'Raw documents', + }), + encoder: $scope.outlinkEncoders[0], + isDefault: true + }); } + } - $scope.setDetail = function (data) { - $scope.detail = data; - }; - - $scope.aceLoaded = (editor) => { - editor.$blockScrolling = Infinity; - }; - - $scope.performMerge = function (parentId, childId) { - let found = true; - while (found) { - found = false; - for (const i in $scope.detail.mergeCandidates) { - const mc = $scope.detail.mergeCandidates[i]; - if ((mc.id1 === childId) || (mc.id2 === childId)) { - $scope.detail.mergeCandidates.splice(i, 1); - found = true; - break; - } + $scope.setDetail = function (data) { + $scope.detail = data; + }; + + $scope.aceLoaded = (editor) => { + editor.$blockScrolling = Infinity; + }; + + $scope.performMerge = function (parentId, childId) { + let found = true; + while (found) { + found = false; + for (const i in $scope.detail.mergeCandidates) { + const mc = $scope.detail.mergeCandidates[i]; + if ((mc.id1 === childId) || (mc.id2 === childId)) { + $scope.detail.mergeCandidates.splice(i, 1); + found = true; + break; } } - $scope.workspace.mergeIds(parentId, childId); - $scope.detail = null; - }; - - $scope.handleMergeCandidatesCallback = function (termIntersects) { - const mergeCandidates = []; - for (const i in termIntersects) { - const ti = termIntersects[i]; - mergeCandidates.push({ - 'id1': ti.id1, - 'id2': ti.id2, - 'term1': ti.term1, - 'term2': ti.term2, - 'v1': ti.v1, - 'v2': ti.v2, - 'overlap': ti.overlap - }); - - } - $scope.detail = { mergeCandidates }; - }; - + } + $scope.workspace.mergeIds(parentId, childId); + $scope.detail = null; + }; + + $scope.handleMergeCandidatesCallback = function (termIntersects) { + const mergeCandidates = []; + for (const i in termIntersects) { + const ti = termIntersects[i]; + mergeCandidates.push({ + 'id1': ti.id1, + 'id2': ti.id2, + 'term1': ti.term1, + 'term2': ti.term2, + 'v1': ti.v1, + 'v2': ti.v2, + 'overlap': ti.overlap + }); - //initialize all the state - $scope.resetWorkspace(); + } + $scope.detail = { mergeCandidates }; + }; - const managementUrl = chrome.navLinks.get('kibana:management').url; - const url = `${managementUrl}/kibana/index_patterns`; - if ($route.current.locals.indexPatterns.length === 0) { - toastNotifications.addWarning({ - title: i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', { - defaultMessage: 'No data source', - }), - text: ( -

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

- ), - }); - } + //initialize all the state + $scope.resetWorkspace(); + const managementUrl = chrome.navLinks.get('kibana:management').url; + const url = `${managementUrl}/kibana/index_patterns`; - // ===== Menubar configuration ========= - $scope.topNavMenu = []; - $scope.topNavMenu.push({ - key: 'new', - label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { - defaultMessage: 'New', - }), - description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { - defaultMessage: 'New Workspace', + if ($route.current.locals.indexPatterns.length === 0) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', { + defaultMessage: 'No data source', }), - tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { - defaultMessage: 'Create a new workspace', - }), - run: function () { - canWipeWorkspace(function () { - $scope.$evalAsync(() => { - kbnUrl.change('/workspace/'); - }); - }); }, - testId: 'graphNewButton', + text: ( +

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

+ ), }); + } - // if saving is disabled using uiCapabilities, we don't want to render the save - // button so it's consistent with all of the other applications - if (capabilities.save) { - // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality - $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', - }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save workspace', - }), - tooltip: () => { - if ($scope.allSavingDisabled) { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }); - } else { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', - }); - } - }, - disableButton: function () { - return $scope.allSavingDisabled || $scope.selectedFields.length === 0; - }, - run: () => { - openSaveModal({ - savePolicy: $scope.graphSavePolicy, - hasData: $scope.workspace && ($scope.workspace.nodes.length > 0 || $scope.workspace.blacklistedNodes.length > 0), - workspace: $scope.savedWorkspace, - saveWorkspace: $scope.saveWorkspace, - showSaveModal - }); - }, - testId: 'graphSaveButton', - }); - } - $scope.topNavMenu.push({ - key: 'inspect', - disableButton: function () { return $scope.workspace === null; }, - label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { - defaultMessage: 'Inspect', - }), - run: () => { + // ===== Menubar configuration ========= + $scope.topNavMenu = []; + $scope.topNavMenu.push({ + key: 'new', + label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { + defaultMessage: 'New', + }), + description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { + defaultMessage: 'New Workspace', + }), + tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { + defaultMessage: 'Create a new workspace', + }), + run: function () { + canWipeWorkspace(function () { $scope.$evalAsync(() => { - const curState = $scope.menus.showInspect; - $scope.closeMenus(); - $scope.menus.showInspect = !curState; + kbnUrl.change('/workspace/'); }); - }, - }); + }); }, + testId: 'graphNewButton', + }); + + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (capabilities.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality - let currentSettingsFlyout; $scope.topNavMenu.push({ - key: 'settings', - disableButton: function () { return $scope.selectedIndex === null; }, - label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { - defaultMessage: 'Settings', + key: 'save', + label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', }), - description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { - defaultMessage: 'Settings', + description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save workspace', }), - run: () => { - if (currentSettingsFlyout) { - currentSettingsFlyout.close(); - return; + tooltip: () => { + if ($scope.allSavingDisabled) { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }); + } else { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }); } - const settingsObservable = asAngularSyncedObservable(() => ({ - advancedSettings: { ...$scope.exploreControls }, - updateAdvancedSettings: (updatedSettings) => { - $scope.exploreControls = updatedSettings; - if ($scope.workspace) { - $scope.workspace.options.exploreControls = updatedSettings; - } - }, - blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, - unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, - urlTemplates: [...$scope.urlTemplates], - removeUrlTemplate: $scope.removeUrlTemplate, - saveUrlTemplate: $scope.saveUrlTemplate, - allFields: [...$scope.allFields], - canEditDrillDownUrls: $scope.canEditDrillDownUrls - }), $scope.$digest.bind($scope)); - currentSettingsFlyout = coreStart.overlays.openFlyout(, { - size: 'm', - closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }), - 'data-test-subj': 'graphSettingsFlyout', - ownFocus: true, - className: 'gphSettingsFlyout', - maxWidth: 520, + }, + disableButton: function () { + return $scope.allSavingDisabled || $scope.selectedFields.length === 0; + }, + run: () => { + openSaveModal({ + savePolicy: $scope.graphSavePolicy, + hasData: $scope.workspace && ($scope.workspace.nodes.length > 0 || $scope.workspace.blacklistedNodes.length > 0), + workspace: $scope.savedWorkspace, + saveWorkspace: $scope.saveWorkspace, + showSaveModal }); - currentSettingsFlyout.onClose.then(() => { currentSettingsFlyout = null; }); }, + testId: 'graphSaveButton', }); + } + $scope.topNavMenu.push({ + key: 'inspect', + disableButton: function () { return $scope.workspace === null; }, + label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { + defaultMessage: 'Inspect', + }), + run: () => { + $scope.$evalAsync(() => { + const curState = $scope.menus.showInspect; + $scope.closeMenus(); + $scope.menus.showInspect = !curState; + }); + }, + }); - updateBreadcrumbs(); + let currentSettingsFlyout; + $scope.topNavMenu.push({ + key: 'settings', + disableButton: function () { return $scope.selectedIndex === null; }, + label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { + defaultMessage: 'Settings', + }), + description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { + defaultMessage: 'Settings', + }), + run: () => { + if (currentSettingsFlyout) { + currentSettingsFlyout.close(); + return; + } + const settingsObservable = asAngularSyncedObservable(() => ({ + advancedSettings: { ...$scope.exploreControls }, + updateAdvancedSettings: (updatedSettings) => { + $scope.exploreControls = updatedSettings; + if ($scope.workspace) { + $scope.workspace.options.exploreControls = updatedSettings; + } + }, + blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, + unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, + urlTemplates: [...$scope.urlTemplates], + removeUrlTemplate: $scope.removeUrlTemplate, + saveUrlTemplate: $scope.saveUrlTemplate, + allFields: [...$scope.allFields], + canEditDrillDownUrls: $scope.canEditDrillDownUrls + }), $scope.$digest.bind($scope)); + currentSettingsFlyout = coreStart.overlays.openFlyout(, { + size: 'm', + closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }), + 'data-test-subj': 'graphSettingsFlyout', + ownFocus: true, + className: 'gphSettingsFlyout', + maxWidth: 520, + }); + currentSettingsFlyout.onClose.then(() => { currentSettingsFlyout = null; }); + }, + }); - $scope.menus = { - showSettings: false, - }; + updateBreadcrumbs(); - $scope.closeMenus = () => { - _.forOwn($scope.menus, function (_, key) { - $scope.menus[key] = false; - }); - }; + $scope.menus = { + showSettings: false, + }; - // 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, $route.current.locals.indexPatterns); - if(!selectedIndex) { - toastNotifications.addDanger( - i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { - defaultMessage: 'Index pattern not found', - }) - ); - return; + $scope.closeMenus = () => { + _.forOwn($scope.menus, function (_, key) { + $scope.menus[key] = false; + }); + }; + + // 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, $route.current.locals.indexPatterns); + if(!selectedIndex) { + toastNotifications.addDanger( + i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { + defaultMessage: 'Index pattern not found', + }) + ); + return; + } + $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id).then(indexPattern => { + $scope.selectedIndex = indexPattern; + initWorkspaceIfRequired(); + const { + urlTemplates, + advancedSettings, + allFields, + } = savedWorkspaceToAppState($scope.savedWorkspace, indexPattern, $scope.workspace); + + // wire up stuff to angular + store.dispatch(loadFields(allFields)); + $scope.exploreControls = advancedSettings; + $scope.workspace.options.exploreControls = advancedSettings; + $scope.urlTemplates = urlTemplates; + $scope.workspace.runLayout(); + // Allow URLs to include a user-defined text query + if ($route.current.params.query) { + $scope.initialQuery = $route.current.params.query; + $scope.submit($route.current.params.query); } - $route.current.locals.GetIndexPatternProvider.get(selectedIndex.id).then(indexPattern => { - $scope.selectedIndex = indexPattern; - initWorkspaceIfRequired(); - const { - urlTemplates, - advancedSettings, - allFields, - } = savedWorkspaceToAppState($scope.savedWorkspace, indexPattern, $scope.workspace); - - // wire up stuff to angular - store.dispatch(loadFields(allFields)); - $scope.exploreControls = advancedSettings; - $scope.workspace.options.exploreControls = advancedSettings; - $scope.urlTemplates = urlTemplates; - $scope.workspace.runLayout(); - // Allow URLs to include a user-defined text query - if ($route.current.params.query) { - $scope.initialQuery = $route.current.params.query; - $scope.submit($route.current.params.query); - } - $scope.$digest(); - }); - } else { - $route.current.locals.SavedWorkspacesProvider.get().then(function (newWorkspace) { - $scope.savedWorkspace = newWorkspace; - openSourceModal(coreStart, indexPattern => { - $scope.indexSelected(indexPattern); - }); + $scope.$digest(); + }); + } else { + $route.current.locals.SavedWorkspacesProvider.get().then(function (newWorkspace) { + $scope.savedWorkspace = newWorkspace; + openSourceModal(coreStart, indexPattern => { + $scope.indexSelected(indexPattern); }); - } + }); + } - $scope.saveWorkspace = function (saveOptions, userHasConfirmedSaveWorkspaceData) { - if ($scope.allSavingDisabled) { - // It should not be possible to navigate to this function if allSavingDisabled is set - // but adding check here as a safeguard. - toastNotifications.addWarning( - i18n.translate('xpack.graph.saveWorkspace.disabledWarning', { defaultMessage: 'Saving is disabled' }) - ); - return; - } - initWorkspaceIfRequired(); - const canSaveData = $scope.graphSavePolicy === 'configAndData' || - ($scope.graphSavePolicy === 'configAndDataWithConsent' && userHasConfirmedSaveWorkspaceData); - - appStateToSavedWorkspace( - $scope.savedWorkspace, - { - workspace: $scope.workspace, - urlTemplates: $scope.urlTemplates, - advancedSettings: $scope.exploreControls, - selectedIndex: $scope.selectedIndex, - selectedFields: $scope.selectedFields - }, - canSaveData + $scope.saveWorkspace = function (saveOptions, userHasConfirmedSaveWorkspaceData) { + if ($scope.allSavingDisabled) { + // It should not be possible to navigate to this function if allSavingDisabled is set + // but adding check here as a safeguard. + toastNotifications.addWarning( + i18n.translate('xpack.graph.saveWorkspace.disabledWarning', { defaultMessage: 'Saving is disabled' }) ); - - return $scope.savedWorkspace.save(saveOptions).then(function (id) { - if (id) { - const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', { - defaultMessage: 'Saved "{workspaceTitle}"', - values: { workspaceTitle: $scope.savedWorkspace.title }, + return; + } + initWorkspaceIfRequired(); + const canSaveData = $scope.graphSavePolicy === 'configAndData' || + ($scope.graphSavePolicy === 'configAndDataWithConsent' && userHasConfirmedSaveWorkspaceData); + + appStateToSavedWorkspace( + $scope.savedWorkspace, + { + workspace: $scope.workspace, + urlTemplates: $scope.urlTemplates, + advancedSettings: $scope.exploreControls, + selectedIndex: $scope.selectedIndex, + selectedFields: $scope.selectedFields + }, + canSaveData + ); + + return $scope.savedWorkspace.save(saveOptions).then(function (id) { + if (id) { + const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', { + defaultMessage: 'Saved "{workspaceTitle}"', + values: { workspaceTitle: $scope.savedWorkspace.title }, + }); + let text; + if (!canSaveData && $scope.workspace.nodes.length > 0) { + text = i18n.translate('xpack.graph.saveWorkspace.successNotification.noDataSavedText', { + defaultMessage: 'The configuration was saved, but the data was not saved', }); - let text; - if (!canSaveData && $scope.workspace.nodes.length > 0) { - text = i18n.translate('xpack.graph.saveWorkspace.successNotification.noDataSavedText', { - defaultMessage: 'The configuration was saved, but the data was not saved', - }); - } + } - toastNotifications.addSuccess({ - title, - text, - 'data-test-subj': 'saveGraphSuccess', - }); - if ($scope.savedWorkspace.id !== $route.current.params.id) { - kbnUrl.change(getEditPath($scope.savedWorkspace)); - } + toastNotifications.addSuccess({ + title, + text, + 'data-test-subj': 'saveGraphSuccess', + }); + if ($scope.savedWorkspace.id !== $route.current.params.id) { + kbnUrl.change(getEditPath($scope.savedWorkspace)); } - return { id }; - }, fatalError); + } + return { id }; + }, fatalError); - }; + }; - }); - //End controller +}); +//End controller } From b058df63dd66661bf11942e842ed94b0b3c3bd12 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 14 Oct 2019 18:22:58 +0200 Subject: [PATCH 07/12] finalize shim --- x-pack/legacy/plugins/graph/index.js | 2 +- .../angular/directives/graph_inspect.js | 17 -- .../angular/services/saved_workspaces.js | 5 - .../graph/public/angular/templates/index.html | 44 +++- .../public/angular/templates/inspect.html | 43 ---- x-pack/legacy/plugins/graph/public/app.js | 18 +- x-pack/legacy/plugins/graph/public/index.ts | 22 +- x-pack/legacy/plugins/graph/public/plugin.ts | 54 +++-- .../legacy/plugins/graph/public/render_app.ts | 218 +++++++++++------- .../public/state_management/meta_data.test.ts | 8 +- .../graph/public/state_management/mocks.ts | 4 +- .../graph/public/state_management/store.ts | 4 +- 12 files changed, 234 insertions(+), 205 deletions(-) delete mode 100644 x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js delete mode 100644 x-pack/legacy/plugins/graph/public/angular/templates/inspect.html diff --git a/x-pack/legacy/plugins/graph/index.js b/x-pack/legacy/plugins/graph/index.js index 9ece9966b7da4..db08125d1e459 100644 --- a/x-pack/legacy/plugins/graph/index.js +++ b/x-pack/legacy/plugins/graph/index.js @@ -26,7 +26,7 @@ export function graph(kibana) { main: 'plugins/graph/index', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'], + hacks: ['plugins/graph/hacks/toggle_app_link_in_nav', 'plugins/graph/index'], home: ['plugins/graph/register_feature'], mappings, migrations, diff --git a/x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js b/x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js deleted file mode 100644 index 1e6622fd796fb..0000000000000 --- a/x-pack/legacy/plugins/graph/public/angular/directives/graph_inspect.js +++ /dev/null @@ -1,17 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from '../templates/inspect.html'; -const app = uiModules.get('app/graph'); - -app.directive('graphInspect', function () { - return { - replace: true, - restrict: 'E', - template, - }; -}); diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js index 7af0dad3c704c..1fef4b7c38c07 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js @@ -6,7 +6,6 @@ import _ from 'lodash'; -import { uiModules } from 'ui/modules'; import chrome from 'ui/chrome'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; @@ -87,9 +86,5 @@ export function SavedWorkspacesProvider(kbnUrl, Private, Promise) { }); }; } -// This is the only thing that gets injected into controllers -uiModules.get('app/graph').service('savedGraphWorkspaces', function (Private) { - return Private(SavedWorkspacesProvider); -}); SavedObjectRegistryProvider.register(SavedWorkspacesProvider); 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 a3b025ff9d6df..686f0f590a19c 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -4,7 +4,49 @@
- +
+
+ +
+ http://host:port/{{ selectedIndex.name }}/_graph/explore + + +
+
+
+
-
- -
- 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 d7789db78103a..08b6d1701e225 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -27,7 +27,6 @@ import { Settings } from './components/settings'; import { GraphVisualization } from './components/graph_visualization'; import gws from './angular/graph_client_workspace.js'; -import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs } from './services/url'; import { createCachedIndexPatternProvider } from './services/index_pattern_cache'; import { urlTemplateRegex } from './helpers/url_template'; @@ -41,9 +40,7 @@ import { hasFieldsSelector } from './state_management'; -import './angular/directives/graph_inspect'; - -export function initAngularModule(angularModule, deps) { +export function initGraphApp(angularModule, deps) { const { xpackInfo, fatalError, @@ -52,17 +49,17 @@ export function initAngularModule(angularModule, deps) { toastNotifications, savedObjectsClient, //Private(SavedObjectsClientProvider) indexPatterns, //data.indexPatterns.indexPatterns - savedWorkspacesClient, //Private(SavedWorkspacesProvider) kbnBaseUrl, addBasePath, getBasePath, - data, + npData, config, //uiSettings? savedObjectRegistry, //Private(SavedObjectRegistryProvider) capabilities, coreStart, //coreStart confirmModal, $http, //$http + Storage, KbnUrlProvider, canEditDrillDownUrls, graphSavePolicy, @@ -160,7 +157,7 @@ export function initAngularModule(angularModule, deps) { badge: getReadonlyBadge, resolve: { savedWorkspace: function ($route) { - return $route.current.params.id && savedGraphWorkspaces.get($route.current.params.id) + return $route.current.params.id ? savedGraphWorkspaces.get($route.current.params.id) .catch( function () { toastNotifications.addDanger( @@ -169,7 +166,7 @@ export function initAngularModule(angularModule, deps) { }) ); } - ); + ) : savedGraphWorkspaces.get(); }, //Copied from example found in wizard.js ( Kibana TODO - can't @@ -183,9 +180,6 @@ export function initAngularModule(angularModule, deps) { GetIndexPatternProvider: function () { return indexPatterns; }, - SavedWorkspacesProvider: function () { - return savedWorkspacesClient; - } } }) .otherwise({ @@ -320,7 +314,7 @@ export function initAngularModule(angularModule, deps) { }); // register things on scope passed down to react components - $scope.pluginDataStart = data; + $scope.pluginDataStart = npData; $scope.store = new Storage(window.localStorage); $scope.coreStart = coreStart; $scope.loading = false; diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 8f6084056f5a7..0442d27219d0c 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -5,25 +5,9 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { IScope } from 'angular'; -import { App } from 'kibana/public'; +import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { GraphPlugin } from './plugin'; -import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; - -// Setup compatibility layer for AppService in legacy platform -npSetup.core.application.register = (app: App) => { - require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = $element[0]; - - // Root controller cannot return a Promise so use an internal async function and call it - (async () => { - const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); - $scope.$on('$destroy', () => { - unmount(); - }); - })(); - }); -}; const instance = new GraphPlugin(); -instance.setup(npSetup.core, { data }); +instance.setup(npSetup.core); +instance.start(npStart.core, { data, npData: npStart.plugins.data }); diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index b8a99a13f32d8..ea0e4cf0dcac4 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -4,30 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +// LP type imports +import { IPrivate } from 'ui/private'; + +// NP type imports +import { CoreSetup, CoreStart } from 'src/core/public'; +import { DataStart } from 'src/legacy/core_plugins/data/public'; +import { Plugin as DataPlugin } from 'src/plugins/data/public'; + // legacy imports currently necessary to power Graph // for a cutover all of these have to be resolved import 'uiExports/fieldFormats'; import 'uiExports/savedObjectTypes'; -import 'ui/autoload/all'; -import 'ui/kbn_top_nav'; -import 'ui/directives/saved_object_finder'; -import 'ui/directives/input_focus'; -import 'ui/saved_objects/ui/saved_object_save_as_checkbox'; import 'uiExports/autocompleteProviders'; +import 'ui/autoload/all'; import chrome from 'ui/chrome'; import { fatalError } from 'ui/notify'; // @ts-ignore -import { KbnUrlProvider } from 'ui/url'; +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { Storage } from 'ui/storage'; // @ts-ignore import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; // @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { IPrivate } from 'ui/private'; - -// NP type imports -import { CoreSetup } from 'src/core/public'; -import { DataSetup } from 'src/legacy/core_plugins/data/public'; +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; // @ts-ignore import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; @@ -45,8 +44,7 @@ async function getAngularInjectedDependencies() { confirmModal: injector.get('confirmModal'), savedObjectRegisty: Private(SavedObjectRegistryProvider), kbnBaseUrl: injector.get('kbnBaseUrl'), - savedWorkspacesClient: Private(SavedWorkspacesProvider), - savedGraphWorkspaces: injector.get('savedGraphWorkspaces'), + savedGraphWorkspaces: Private(SavedWorkspacesProvider), savedObjectsClient: Private(SavedObjectsClientProvider), savedObjectRegistry: Private(SavedObjectRegistryProvider), canEditDrillDownUrls: chrome.getInjected('canEditDrillDownUrls'), @@ -54,28 +52,36 @@ async function getAngularInjectedDependencies() { }; } -export interface GraphPluginSetupDependencies { - data: DataSetup; +export interface GraphPluginStartDependencies { + data: DataStart; + npData: ReturnType; } export class GraphPlugin { - setup(core: CoreSetup, { data }: GraphPluginSetupDependencies) { + private dataStart: DataStart | null = null; + private npDataStart: ReturnType | null = null; + + setup(core: CoreSetup) { core.application.register({ id: 'graph', title: 'Graph', - async mount(context, params) { + order: 9000, + icon: 'plugins/graph/icon.png', + euiIconType: 'graphApp', + mount: async (context, params) => { const { renderApp } = await import('./render_app'); const angularDependencies = await getAngularInjectedDependencies(); return renderApp( context, { ...params, - data, + data: this.dataStart!, + npData: this.npDataStart!, fatalError, xpackInfo, - KbnUrlProvider, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, + Storage, }, angularDependencies ); @@ -83,7 +89,11 @@ export class GraphPlugin { }); } - start() {} + start(_core: CoreStart, { data, npData }: GraphPluginStartDependencies) { + // TODO is this really the right way? I though the app context would give us those + this.dataStart = data; + this.npDataStart = npData; + } stop() {} } diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index c9edb4b2be0b8..aab29ffa6d61a 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +// inner angular imports +// these are necessary to bootstrap the local angular. +// They can stay even after NP cutover import angular from 'angular'; -import { AppMountContext } from 'kibana/public'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { DataSetup } from 'src/legacy/core_plugins/data/public'; -import { AngularHttpError } from 'ui/notify/lib/format_angular_http_error'; +import 'ui/angular-bootstrap'; +import 'ace'; +import 'ui/kbn_top_nav'; import { configureAppAngularModule } from 'ui/legacy_compat'; - // @ts-ignore import { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore @@ -26,56 +28,68 @@ import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_to // @ts-ignore import { PromiseServiceCreator } from 'ui/promises/promises'; // @ts-ignore -import { initAngularModule } from './app'; - -const mainTemplate = (basePath: string) => `
- - -
-
-`; +import { KbnUrlProvider } from 'ui/url'; -const moduleName = 'app/graph'; +// type imports +import { IPrivate } from 'ui/private'; +import { DataStart } from 'src/legacy/core_plugins/data/public'; +import { AppMountContext } from 'kibana/public'; +import { AngularHttpError } from 'ui/notify/lib/format_angular_http_error'; +// @ts-ignore +import { initGraphApp } from './app'; +import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public'; +/** + * These are dependencies of the Graph app besides the base dependencies + * provided by the application service. Some of those still rely on non-shimmed + * plugins in LP-world, but if they are migrated only the import path in the plugin + * itself changes + */ export interface GraphDependencies { element: HTMLElement; appBasePath: string; - data: DataSetup; + data: DataStart; + npData: ReturnType; fatalError: (error: AngularHttpError | Error | string, location?: string) => void; xpackInfo: { get(path: string): unknown }; addBasePath: (url: string) => string; getBasePath: () => string; - KbnUrlProvider: any; + Storage: any; } +/** + * Dependencies of the Graph app which rely on the global angular instance. + * These dependencies have to be migrated to their NP counterparts. + */ export interface LegacyAngularInjectedDependencies { /** * angular $http service */ $http: any; /** - * src/legacy/ui/public/modals/confirm_modal.js + * Instance of `src/legacy/ui/public/modals/confirm_modal.js` */ confirmModal: any; /** - * Private(SavedObjectRegistryProvider) + * Instance of SavedObjectRegistryProvider */ savedObjectRegistry: any; kbnBaseUrl: any; /** - * Private(SavedWorkspacesProvider) + * Instance of SavedWorkspacesProvider */ - - savedWorkspacesClient: any; savedGraphWorkspaces: any; /** * Private(SavedObjectsClientProvider) */ - savedObjectsClient: any; - - // These are static values currently fetched from ui/chrome + /** + * Injected variable + */ canEditDrillDownUrls: string; + /** + * Injected variable + */ graphSavePolicy: string; } @@ -85,11 +99,12 @@ export const renderApp = ( element, appBasePath, data, + npData, fatalError, xpackInfo, addBasePath, getBasePath, - KbnUrlProvider, + Storage, }: GraphDependencies, angularDeps: LegacyAngularInjectedDependencies ) => { @@ -100,23 +115,98 @@ export const renderApp = ( config: core.uiSettings, toastNotifications: core.notifications.toasts, indexPatterns: data.indexPatterns.indexPatterns, - data, + npData, fatalError, xpackInfo, addBasePath, getBasePath, KbnUrlProvider, + Storage, ...angularDeps, }; + + const graphAngularModule = createLocalAngularModule(core); + configureAppAngularModule(graphAngularModule); + initGraphApp(graphAngularModule, deps); + const $injector = mountGraphApp(appBasePath, element); + return () => $injector.get('$rootScope').$destroy(); +}; + +const mainTemplate = (basePath: string) => `
+ +
+
+`; + +const moduleName = 'app/graph'; + +const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.bootstrap', 'ui.ace']; + +function mountGraphApp(appBasePath: string, element: HTMLElement) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%'); + // eslint-disable-next-line + mountpoint.innerHTML = mainTemplate(appBasePath); + // bootstrap angular into detached element and attach it later to + // make angular-within-angular possible + const $injector = angular.bootstrap(mountpoint, [moduleName]); + // initialize global state handler + $injector.get('globalState'); + element.appendChild(mountpoint); + return $injector; +} + +function createLocalAngularModule(core: AppMountContext['core']) { + createLocalI18nModule(); + createLocalPrivateModule(); + createLocalPromiseModule(); + createLocalConfigModule(core); + createLocalKbnUrlModule(); + createLocalPersistedStateModule(); + createLocalTopNavModule(); + createLocalGlobalStateModule(); + + const graphAngularModule = angular.module(moduleName, [ + ...thirdPartyAngularDependencies, + 'graphI18n', + 'graphPrivate', + 'graphPersistedState', + 'graphTopNav', + 'graphGlobalState', + ]); + return graphAngularModule; +} + +function createLocalGlobalStateModule() { angular - .module('graphI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); - angular.module('graphPrivate', []).provider('Private', PrivateProvider); - angular.module('graphPromise', []).service('Promise', PromiseServiceCreator); + .module('graphGlobalState', ['graphPrivate', 'graphConfig', 'graphKbnUrl', 'graphPromise']) + .service('globalState', function(Private: any) { + return Private(GlobalStateProvider); + }); +} + +function createLocalPersistedStateModule() { + angular + .module('graphPersistedState', ['graphPrivate', 'graphPromise']) + .factory('PersistedState', (Private: IPrivate) => { + const Events = Private(EventsProvider); + return class AngularPersistedState extends PersistedState { + constructor(value: any, path: any) { + super(value, path, Events); + } + }; + }); +} + +function createLocalKbnUrlModule() { angular - .module('graphGlobalStateDeps', ['graphPrivate']) + .module('graphKbnUrl', ['graphPrivate']) + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); +} + +function createLocalConfigModule(core: AppMountContext['core']) { + angular + .module('graphConfig', ['graphPrivate']) .provider('stateManagementConfig', StateManagementConfigProvider) .provider('config', () => { return { @@ -124,54 +214,28 @@ export const renderApp = ( get: core.uiSettings.get.bind(core.uiSettings), }), }; - }) - .service('kbnUrl', (Private: any) => Private(KbnUrlProvider)); - angular - .module('graphUrlStuff', ['graphPrivate', 'graphPromise']) - .factory('PersistedState', ($injector: any) => { - const Private = $injector.get('Private'); - const Events = Private(EventsProvider); - - // Extend PersistedState to override the EmitterClass class with - // our Angular friendly version. - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: any) { - super(value, path, Events); - } - }; }); +} + +function createLocalPromiseModule() { + angular.module('graphPromise', []).service('Promise', PromiseServiceCreator); +} + +function createLocalPrivateModule() { + angular.module('graphPrivate', []).provider('Private', PrivateProvider); +} + +function createLocalTopNavModule() { angular .module('graphTopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) .directive('kbnTopNavHelper', createTopNavHelper); - // global state is only here because of legacy reasons, it's not actually used. - // But it is helpful as a reference for Discover +} + +function createLocalI18nModule() { angular - .module('graphGlobalState', ['graphPrivate', 'graphGlobalStateDeps', 'graphPromise']) - .service('globalState', function(Private: any) { - return Private(GlobalStateProvider); - }); - const graphAngularModule = angular.module(moduleName, [ - 'ngSanitize', - 'ngRoute', - 'react', - 'graphI18n', - 'graphPrivate', - 'graphUrlStuff', - 'graphTopNav', - 'graphGlobalState', - ]); - configureAppAngularModule(graphAngularModule); - initAngularModule(graphAngularModule, deps); - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%'); - // eslint-disable-next-line - mountpoint.innerHTML = mainTemplate(appBasePath); - // bootstrap angular into detached element and attach it later to - // make angular-within-angular possible - const $injector = angular.bootstrap(mountpoint, [moduleName]); - // initialize global state handler - $injector.get('globalState'); - element.appendChild(mountpoint); - return () => $injector.get('$rootScope').$destroy(); -}; + .module('graphI18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); +} diff --git a/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts b/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts index 8ac3746158f3f..7125e1fcec1ca 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts @@ -6,7 +6,7 @@ import { createMockGraphStore, MockedGraphEnvironment } from './mocks'; import { syncBreadcrumbSaga, updateMetaData } from './meta_data'; -import { Chrome } from 'ui/chrome'; +import { ChromeStart } from 'kibana/public'; describe('breadcrumb sync saga', () => { let env: MockedGraphEnvironment; @@ -19,17 +19,17 @@ describe('breadcrumb sync saga', () => { breadcrumbs: { set: jest.fn(), }, - } as unknown) as Chrome, + } as unknown) as ChromeStart, }, }); }); it('syncs breadcrumb initially', () => { - expect(env.mockedDeps.chrome.breadcrumbs.set).toHaveBeenCalled(); + expect(env.mockedDeps.chrome.setBreadcrumbs).toHaveBeenCalled(); }); it('syncs breadcrumb with each change to meta data', () => { env.store.dispatch(updateMetaData({})); - expect(env.mockedDeps.chrome.breadcrumbs.set).toHaveBeenCalledTimes(2); + expect(env.mockedDeps.chrome.setBreadcrumbs).toHaveBeenCalledTimes(2); }); }); diff --git a/x-pack/legacy/plugins/graph/public/state_management/mocks.ts b/x-pack/legacy/plugins/graph/public/state_management/mocks.ts index 11cbf84e759ea..c5eda91bf9b41 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/mocks.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Chrome } from 'ui/chrome'; import { IndexPattern } from 'src/legacy/core_plugins/data/public'; import { NotificationsStart, HttpStart } from 'kibana/public'; import createSagaMiddleware from 'redux-saga'; import { createStore, applyMiddleware, AnyAction } from 'redux'; +import { ChromeStart } from 'kibana/public'; import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } from './store'; import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; @@ -55,7 +55,7 @@ export function createMockGraphStore({ breadcrumbs: { set: jest.fn(), }, - } as unknown) as Chrome, + } as unknown) as ChromeStart, createWorkspace: jest.fn(), getWorkspace: jest.fn(() => workspaceMock), getSavedWorkspace: jest.fn(() => savedWorkspace), diff --git a/x-pack/legacy/plugins/graph/public/state_management/store.ts b/x-pack/legacy/plugins/graph/public/state_management/store.ts index 752483e65d8cc..bb01f20196f87 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/store.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/store.ts @@ -6,8 +6,8 @@ import createSagaMiddleware, { SagaMiddleware } from 'redux-saga'; import { combineReducers, createStore, Store, AnyAction, Dispatch, applyMiddleware } from 'redux'; +import { ChromeStart } from 'kibana/public'; import { CoreStart } from 'src/core/public'; -import { Chrome } from 'ui/chrome'; import { fieldsReducer, FieldsState, @@ -61,7 +61,7 @@ export interface GraphStoreDependencies { setLiveResponseFields: (fields: WorkspaceField[]) => void; setUrlTemplates: (templates: UrlTemplate[]) => void; setWorkspaceInitialized: () => void; - chrome: Chrome; + chrome: ChromeStart; } export function createRootReducer(basePath: string) { From 21d173c6aa7d30eac9b9269dac4b43c72b8b3aea Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 16 Oct 2019 10:42:39 +0200 Subject: [PATCH 08/12] fix shim --- x-pack/legacy/plugins/graph/index.js | 2 +- x-pack/legacy/plugins/graph/public/render_app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/graph/index.js b/x-pack/legacy/plugins/graph/index.js index db08125d1e459..9ece9966b7da4 100644 --- a/x-pack/legacy/plugins/graph/index.js +++ b/x-pack/legacy/plugins/graph/index.js @@ -26,7 +26,7 @@ export function graph(kibana) { main: 'plugins/graph/index', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/graph/hacks/toggle_app_link_in_nav', 'plugins/graph/index'], + hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'], home: ['plugins/graph/register_feature'], mappings, migrations, diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index aab29ffa6d61a..62bbd39acbd05 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -200,7 +200,7 @@ function createLocalPersistedStateModule() { function createLocalKbnUrlModule() { angular - .module('graphKbnUrl', ['graphPrivate']) + .module('graphKbnUrl', ['graphPrivate', 'ngRoute']) .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } From 9276ba5b1362be2e24b4bfd619018a2663267cc0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 17 Oct 2019 14:23:12 +0200 Subject: [PATCH 09/12] get rid of ui modules and fix build --- src/dev/i18n/extractors/code.js | 2 +- .../new_platform/new_platform.karma_mock.js | 4 ++++ .../angular/services/saved_workspace.js | 8 ------- .../public/hacks/toggle_app_link_in_nav.js | 22 ++++++++----------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/dev/i18n/extractors/code.js b/src/dev/i18n/extractors/code.js index 58ca059be5491..fa0d834824e97 100644 --- a/src/dev/i18n/extractors/code.js +++ b/src/dev/i18n/extractors/code.js @@ -67,7 +67,7 @@ export function* extractCodeMessages(buffer, reporter) { try { ast = parse(buffer.toString(), { sourceType: 'module', - plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators'], + plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators', 'dynamicImport'], }); } catch (error) { if (error instanceof SyntaxError) { diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index ae5c0a83bd781..20ea9c9141aca 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -96,6 +96,10 @@ export const npStart = { export function __setup__(coreSetup) { npSetup.core = coreSetup; + + // no-op application register calls (this is overwritten to + // bootstrap an LP plugin outside of tests) + npSetup.core.application.register = () => {}; } export function __start__(coreStart) { diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js index 1d28b4ba5ab30..444e68dd03520 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiModules } from 'ui/modules'; import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; import { i18n } from '@kbn/i18n'; import { @@ -12,8 +11,6 @@ import { injectReferences, } from './saved_workspace_references'; -const module = uiModules.get('app/dashboard'); - export function SavedWorkspaceProvider(Private) { // SavedWorkspace constructor. Usually you'd interact with an instance of this. // ID is option, without it one will be generated on save. @@ -68,8 +65,3 @@ export function SavedWorkspaceProvider(Private) { SavedWorkspace.searchsource = false; return SavedWorkspace; } - -// Used only by the savedDashboards service, usually no reason to change this -module.factory('SavedGraphWorkspace', function (Private) { - return Private(SavedWorkspaceProvider); -}); diff --git a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js index 11b02354a97c5..5b1eb6181fd61 100644 --- a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiModules } from 'ui/modules'; import { npStart } from 'ui/new_platform'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -uiModules.get('xpack/graph') - .run(() => { - const navLinkUpdates = {}; - navLinkUpdates.hidden = true; - const showAppLink = xpackInfo.get('features.graph.showAppLink', false); - navLinkUpdates.hidden = !showAppLink; - if (showAppLink) { - navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); - navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); - } +const navLinkUpdates = {}; +navLinkUpdates.hidden = true; +const showAppLink = xpackInfo.get('features.graph.showAppLink', false); +navLinkUpdates.hidden = !showAppLink; +if (showAppLink) { + navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); + navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); +} - npStart.core.chrome.navLinks.update('graph', navLinkUpdates); - }); +npStart.core.chrome.navLinks.update('graph', navLinkUpdates); From 4eee3cd049a7f181e9e8fc7d2bda3ae751b920e5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 18 Oct 2019 12:17:00 +0200 Subject: [PATCH 10/12] formalize shim --- x-pack/legacy/plugins/graph/public/index.ts | 62 +++++++++++++++- x-pack/legacy/plugins/graph/public/plugin.ts | 73 +++++++------------ .../public/state_management/meta_data.test.ts | 8 -- .../graph/public/state_management/mocks.ts | 4 +- 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 0442d27219d0c..5279894cc7fa2 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -4,10 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ +// legacy imports currently necessary to power Graph +// for a cutover all of these have to be resolved +import 'uiExports/fieldFormats'; +import 'uiExports/savedObjectTypes'; +import 'uiExports/autocompleteProviders'; +import 'ui/autoload/all'; +import chrome from 'ui/chrome'; +import { IPrivate } from 'ui/private'; +import { fatalError } from 'ui/notify'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { Storage } from 'ui/storage'; +// @ts-ignore +import { SavedObjectsClientProvider } from 'ui/saved_objects'; +// @ts-ignore +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; + import { npSetup, npStart } from 'ui/new_platform'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { GraphPlugin } from './plugin'; -const instance = new GraphPlugin(); -instance.setup(npSetup.core); -instance.start(npStart.core, { data, npData: npStart.plugins.data }); +// @ts-ignore +import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; +import { LegacyAngularInjectedDependencies } from './render_app'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularInjectedDependencies(): Promise { + const injector = await chrome.dangerouslyGetActiveInjector(); + + const Private = injector.get('Private'); + + return { + $http: injector.get('$http'), + confirmModal: injector.get('confirmModal'), + savedObjectRegistry: Private(SavedObjectRegistryProvider), + kbnBaseUrl: injector.get('kbnBaseUrl'), + savedGraphWorkspaces: Private(SavedWorkspacesProvider), + savedObjectsClient: Private(SavedObjectsClientProvider), + canEditDrillDownUrls: chrome.getInjected('canEditDrillDownUrls'), + graphSavePolicy: chrome.getInjected('graphSavePolicy'), + }; +} + +(async () => { + const instance = new GraphPlugin(); + instance.setup(npSetup.core, { + __LEGACY: { + xpackInfo, + Storage, + fatalError, + }, + }); + instance.start(npStart.core, { + data, + npData: npStart.plugins.data, + __LEGACY: { + angularDependencies: await getAngularInjectedDependencies(), + }, + }); +})(); diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index ea0e4cf0dcac4..a3f79c6731957 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -4,64 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -// LP type imports -import { IPrivate } from 'ui/private'; - // NP type imports -import { CoreSetup, CoreStart } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { DataStart } from 'src/legacy/core_plugins/data/public'; import { Plugin as DataPlugin } from 'src/plugins/data/public'; +import { LegacyAngularInjectedDependencies } from './render_app'; -// legacy imports currently necessary to power Graph -// for a cutover all of these have to be resolved -import 'uiExports/fieldFormats'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/autocompleteProviders'; -import 'ui/autoload/all'; -import chrome from 'ui/chrome'; -import { fatalError } from 'ui/notify'; -// @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { Storage } from 'ui/storage'; -// @ts-ignore -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -// @ts-ignore -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -// @ts-ignore -import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; - -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularInjectedDependencies() { - const injector = await chrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); +export interface GraphPluginStartDependencies { + data: DataStart; + npData: ReturnType; +} - return { - $http: injector.get('$http'), - confirmModal: injector.get('confirmModal'), - savedObjectRegisty: Private(SavedObjectRegistryProvider), - kbnBaseUrl: injector.get('kbnBaseUrl'), - savedGraphWorkspaces: Private(SavedWorkspacesProvider), - savedObjectsClient: Private(SavedObjectsClientProvider), - savedObjectRegistry: Private(SavedObjectRegistryProvider), - canEditDrillDownUrls: chrome.getInjected('canEditDrillDownUrls'), - graphSavePolicy: chrome.getInjected('graphSavePolicy'), +export interface GraphPluginSetupDependencies { + __LEGACY: { + Storage: any; + xpackInfo: any; + fatalError: any; }; } export interface GraphPluginStartDependencies { - data: DataStart; - npData: ReturnType; + __LEGACY: { + angularDependencies: LegacyAngularInjectedDependencies; + }; } -export class GraphPlugin { +export class GraphPlugin implements Plugin { private dataStart: DataStart | null = null; private npDataStart: ReturnType | null = null; + private angularDependencies: LegacyAngularInjectedDependencies | null = null; - setup(core: CoreSetup) { + setup( + core: CoreSetup, + { __LEGACY: { fatalError, xpackInfo, Storage } }: GraphPluginSetupDependencies + ) { core.application.register({ id: 'graph', title: 'Graph', @@ -70,7 +46,6 @@ export class GraphPlugin { euiIconType: 'graphApp', mount: async (context, params) => { const { renderApp } = await import('./render_app'); - const angularDependencies = await getAngularInjectedDependencies(); return renderApp( context, { @@ -83,16 +58,20 @@ export class GraphPlugin { getBasePath: core.http.basePath.get, Storage, }, - angularDependencies + this.angularDependencies! ); }, }); } - start(_core: CoreStart, { data, npData }: GraphPluginStartDependencies) { + start( + _core: CoreStart, + { data, npData, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies + ) { // TODO is this really the right way? I though the app context would give us those this.dataStart = data; this.npDataStart = npData; + this.angularDependencies = angularDependencies; } stop() {} diff --git a/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts b/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts index 7125e1fcec1ca..25be050edfc13 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/meta_data.test.ts @@ -6,7 +6,6 @@ import { createMockGraphStore, MockedGraphEnvironment } from './mocks'; import { syncBreadcrumbSaga, updateMetaData } from './meta_data'; -import { ChromeStart } from 'kibana/public'; describe('breadcrumb sync saga', () => { let env: MockedGraphEnvironment; @@ -14,13 +13,6 @@ describe('breadcrumb sync saga', () => { beforeEach(() => { env = createMockGraphStore({ sagas: [syncBreadcrumbSaga], - mockedDepsOverwrites: { - chrome: ({ - breadcrumbs: { - set: jest.fn(), - }, - } as unknown) as ChromeStart, - }, }); }); diff --git a/x-pack/legacy/plugins/graph/public/state_management/mocks.ts b/x-pack/legacy/plugins/graph/public/state_management/mocks.ts index c5eda91bf9b41..9672edef31d6f 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/mocks.ts @@ -52,9 +52,7 @@ export function createMockGraphStore({ basePath: 'basepath', changeUrl: jest.fn(), chrome: ({ - breadcrumbs: { - set: jest.fn(), - }, + setBreadcrumbs: jest.fn(), } as unknown) as ChromeStart, createWorkspace: jest.fn(), getWorkspace: jest.fn(() => workspaceMock), From 4776eb1d20c98c674cb20fb7f9de5195293f80b9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 18 Oct 2019 12:40:53 +0200 Subject: [PATCH 11/12] simplify graph shim --- x-pack/legacy/plugins/graph/public/app.js | 23 +++-- x-pack/legacy/plugins/graph/public/index.ts | 2 - x-pack/legacy/plugins/graph/public/plugin.ts | 4 + .../legacy/plugins/graph/public/render_app.ts | 84 ++----------------- 4 files changed, 24 insertions(+), 89 deletions(-) diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 58ec295dc2eb3..a6b731ef4153a 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -126,21 +126,20 @@ export function initGraphApp(angularModule, deps) { $routeProvider.when('/home', { template: listingTemplate, badge: getReadonlyBadge, - controller($location, $scope, Private) { - const kbnUrl = Private(KbnUrlProvider); + controller($location, $scope) { checkLicense(kbnBaseUrl); const services = savedObjectRegistry.byLoaderPropertiesName; const graphService = services['Graph workspace']; $scope.listingLimit = config.get('savedObjects:listingLimit'); $scope.create = () => { - kbnUrl.change(getNewPath()); + $location.url(getNewPath()); }; $scope.find = (search) => { return graphService.find(search, $scope.listingLimit); }; $scope.editItem = (workspace) => { - kbnUrl.change(getEditPath(workspace)); + $location.url(getEditPath(workspace)); }; $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); $scope.delete = (workspaces) => { @@ -189,8 +188,7 @@ export function initGraphApp(angularModule, deps) { //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route, $location, Private) { - const kbnUrl = Private(KbnUrlProvider); + app.controller('graphuiPlugin', function ($scope, $route, $location) { checkLicense(kbnBaseUrl); function handleError(err) { @@ -304,7 +302,7 @@ export function initGraphApp(angularModule, deps) { savePolicy: graphSavePolicy, changeUrl: (newUrl) => { $scope.$evalAsync(() => { - kbnUrl.change(newUrl, {}); + $location.url(newUrl); }); }, notifyAngular: () => { @@ -472,8 +470,15 @@ export function initGraphApp(angularModule, deps) { }), run: function () { canWipeWorkspace(function () { - kbnUrl.change('/workspace/', {}); - }); }, + $scope.$evalAsync(() => { + if ($location.url() === '/workspace/') { + $route.reload(); + } else { + $location.url('/workspace/'); + } + }); + }); + }, testId: 'graphNewButton', }); diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 5279894cc7fa2..685b9a9007833 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -45,8 +45,6 @@ async function getAngularInjectedDependencies(): Promise string; getBasePath: () => string; Storage: any; + canEditDrillDownUrls: boolean; + graphSavePolicy: string; } /** @@ -83,14 +74,6 @@ export interface LegacyAngularInjectedDependencies { * Private(SavedObjectsClientProvider) */ savedObjectsClient: any; - /** - * Injected variable - */ - canEditDrillDownUrls: string; - /** - * Injected variable - */ - graphSavePolicy: string; } export const renderApp = ( @@ -105,6 +88,8 @@ export const renderApp = ( addBasePath, getBasePath, Storage, + canEditDrillDownUrls, + graphSavePolicy, }: GraphDependencies, angularDeps: LegacyAngularInjectedDependencies ) => { @@ -122,6 +107,8 @@ export const renderApp = ( getBasePath, KbnUrlProvider, Storage, + canEditDrillDownUrls, + graphSavePolicy, ...angularDeps, }; @@ -150,81 +137,22 @@ function mountGraphApp(appBasePath: string, element: HTMLElement) { // bootstrap angular into detached element and attach it later to // make angular-within-angular possible const $injector = angular.bootstrap(mountpoint, [moduleName]); - // initialize global state handler - $injector.get('globalState'); element.appendChild(mountpoint); return $injector; } function createLocalAngularModule(core: AppMountContext['core']) { createLocalI18nModule(); - createLocalPrivateModule(); - createLocalPromiseModule(); - createLocalConfigModule(core); - createLocalKbnUrlModule(); - createLocalPersistedStateModule(); createLocalTopNavModule(); - createLocalGlobalStateModule(); const graphAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, 'graphI18n', - 'graphPrivate', - 'graphPersistedState', 'graphTopNav', - 'graphGlobalState', ]); return graphAngularModule; } -function createLocalGlobalStateModule() { - angular - .module('graphGlobalState', ['graphPrivate', 'graphConfig', 'graphKbnUrl', 'graphPromise']) - .service('globalState', function(Private: any) { - return Private(GlobalStateProvider); - }); -} - -function createLocalPersistedStateModule() { - angular - .module('graphPersistedState', ['graphPrivate', 'graphPromise']) - .factory('PersistedState', (Private: IPrivate) => { - const Events = Private(EventsProvider); - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: any) { - super(value, path, Events); - } - }; - }); -} - -function createLocalKbnUrlModule() { - angular - .module('graphKbnUrl', ['graphPrivate', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); -} - -function createLocalConfigModule(core: AppMountContext['core']) { - angular - .module('graphConfig', ['graphPrivate']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: core.uiSettings.get.bind(core.uiSettings), - }), - }; - }); -} - -function createLocalPromiseModule() { - angular.module('graphPromise', []).service('Promise', PromiseServiceCreator); -} - -function createLocalPrivateModule() { - angular.module('graphPrivate', []).provider('Private', PrivateProvider); -} - function createLocalTopNavModule() { angular .module('graphTopNav', ['react']) From c8160503fd33701bfc0ac8591a10f6cb828e5fde Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 21 Oct 2019 14:10:01 +0200 Subject: [PATCH 12/12] simplify shim and pull confirmModal in --- src/legacy/ui/public/modals/confirm_modal.js | 24 +++--- x-pack/legacy/plugins/graph/public/app.js | 17 ++-- x-pack/legacy/plugins/graph/public/index.ts | 6 -- x-pack/legacy/plugins/graph/public/plugin.ts | 55 ++++++------- .../legacy/plugins/graph/public/render_app.ts | 82 +++++++------------ 5 files changed, 75 insertions(+), 109 deletions(-) diff --git a/src/legacy/ui/public/modals/confirm_modal.js b/src/legacy/ui/public/modals/confirm_modal.js index 6d5abfca64aaf..9c3f46da4e927 100644 --- a/src/legacy/ui/public/modals/confirm_modal.js +++ b/src/legacy/ui/public/modals/confirm_modal.js @@ -36,16 +36,7 @@ export const ConfirmationButtonTypes = { CANCEL: CANCEL_BUTTON }; -/** - * @typedef {Object} ConfirmModalOptions - * @property {String} confirmButtonText - * @property {String=} cancelButtonText - * @property {function} onConfirm - * @property {function=} onCancel - * @property {String=} title - If given, shows a title on the confirm modal. - */ - -module.factory('confirmModal', function ($rootScope, $compile) { +export function confirmModalFactory($rootScope, $compile) { let modalPopover; const confirmQueue = []; @@ -114,4 +105,15 @@ module.factory('confirmModal', function ($rootScope, $compile) { } } }; -}); +} + +/** + * @typedef {Object} ConfirmModalOptions + * @property {String} confirmButtonText + * @property {String=} cancelButtonText + * @property {function} onConfirm + * @property {function=} onCancel + * @property {String=} title - If given, shows a title on the confirm modal. + */ + +module.factory('confirmModal', confirmModalFactory); diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index a6b731ef4153a..fb089cbff14cf 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -43,24 +43,21 @@ import { export function initGraphApp(angularModule, deps) { const { xpackInfo, - fatalError, chrome, savedGraphWorkspaces, toastNotifications, - savedObjectsClient, //Private(SavedObjectsClientProvider) - indexPatterns, //data.indexPatterns.indexPatterns + savedObjectsClient, + indexPatterns, kbnBaseUrl, addBasePath, getBasePath, npData, - config, //uiSettings? - savedObjectRegistry, //Private(SavedObjectRegistryProvider) + config, + savedObjectRegistry, capabilities, - coreStart, //coreStart - confirmModal, - $http, //$http + coreStart, + $http, Storage, - KbnUrlProvider, canEditDrillDownUrls, graphSavePolicy, } = deps; @@ -188,7 +185,7 @@ export function initGraphApp(angularModule, deps) { //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route, $location) { + app.controller('graphuiPlugin', function ($scope, $route, $location, confirmModal) { checkLicense(kbnBaseUrl); function handleError(err) { diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 685b9a9007833..0249ca74035d6 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -12,13 +12,10 @@ import 'uiExports/autocompleteProviders'; import 'ui/autoload/all'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; -import { fatalError } from 'ui/notify'; // @ts-ignore import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { Storage } from 'ui/storage'; // @ts-ignore -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -// @ts-ignore import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { npSetup, npStart } from 'ui/new_platform'; @@ -40,11 +37,9 @@ async function getAngularInjectedDependencies(): Promise | null = null; + private savedObjectsClient: SavedObjectsClientContract | null = null; private angularDependencies: LegacyAngularInjectedDependencies | null = null; - setup( - core: CoreSetup, - { __LEGACY: { fatalError, xpackInfo, Storage } }: GraphPluginSetupDependencies - ) { + setup(core: CoreSetup, { __LEGACY: { xpackInfo, Storage } }: GraphPluginSetupDependencies) { core.application.register({ id: 'graph', title: 'Graph', - order: 9000, - icon: 'plugins/graph/icon.png', - euiIconType: 'graphApp', - mount: async (context, params) => { + mount: async ({ core: contextCore }, params) => { const { renderApp } = await import('./render_app'); - return renderApp( - context, - { - ...params, - data: this.dataStart!, - npData: this.npDataStart!, - fatalError, - xpackInfo, - addBasePath: core.http.basePath.prepend, - getBasePath: core.http.basePath.get, - canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( - 'canEditDrillDownUrls' - ) as boolean, - graphSavePolicy: core.injectedMetadata.getInjectedVar('graphSavePolicy') as string, - Storage, - }, - this.angularDependencies! - ); + return renderApp({ + ...params, + npData: this.npDataStart!, + savedObjectsClient: this.savedObjectsClient!, + xpackInfo, + addBasePath: core.http.basePath.prepend, + getBasePath: core.http.basePath.get, + canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( + 'canEditDrillDownUrls' + ) as boolean, + graphSavePolicy: core.injectedMetadata.getInjectedVar('graphSavePolicy') as string, + Storage, + capabilities: contextCore.application.capabilities.graph, + coreStart: contextCore, + chrome: contextCore.chrome, + config: contextCore.uiSettings, + toastNotifications: contextCore.notifications.toasts, + indexPatterns: this.dataStart!.indexPatterns.indexPatterns, + ...this.angularDependencies!, + }); }, }); } start( - _core: CoreStart, + core: CoreStart, { data, npData, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies ) { // TODO is this really the right way? I though the app context would give us those this.dataStart = data; this.npDataStart = npData; this.angularDependencies = angularDependencies; + this.savedObjectsClient = core.savedObjects.client; } stop() {} diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 51004c9c0410e..8625e20ab9c52 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiConfirmModal } from '@elastic/eui'; + // inner angular imports // these are necessary to bootstrap the local angular. // They can stay even after NP cutover @@ -16,14 +18,17 @@ import { configureAppAngularModule } from 'ui/legacy_compat'; // @ts-ignore import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore -import { PromiseServiceCreator } from 'ui/promises/promises'; -// @ts-ignore -import { KbnUrlProvider } from 'ui/url'; +import { confirmModalFactory } from 'ui/modals/confirm_modal'; // type imports import { DataStart } from 'src/legacy/core_plugins/data/public'; -import { AppMountContext } from 'kibana/public'; -import { AngularHttpError } from 'ui/notify/lib/format_angular_http_error'; +import { + AppMountContext, + ChromeStart, + SavedObjectsClientContract, + ToastsStart, + UiSettingsClientContract, +} from 'kibana/public'; // @ts-ignore import { initGraphApp } from './app'; import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public'; @@ -34,12 +39,17 @@ import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public'; * plugins in LP-world, but if they are migrated only the import path in the plugin * itself changes */ -export interface GraphDependencies { +export interface GraphDependencies extends LegacyAngularInjectedDependencies { element: HTMLElement; appBasePath: string; - data: DataStart; + capabilities: Record>; + coreStart: AppMountContext['core']; + chrome: ChromeStart; + config: UiSettingsClientContract; + toastNotifications: ToastsStart; + indexPatterns: DataStart['indexPatterns']['indexPatterns']; npData: ReturnType; - fatalError: (error: AngularHttpError | Error | string, location?: string) => void; + savedObjectsClient: SavedObjectsClientContract; xpackInfo: { get(path: string): unknown }; addBasePath: (url: string) => string; getBasePath: () => string; @@ -57,10 +67,6 @@ export interface LegacyAngularInjectedDependencies { * angular $http service */ $http: any; - /** - * Instance of `src/legacy/ui/public/modals/confirm_modal.js` - */ - confirmModal: any; /** * Instance of SavedObjectRegistryProvider */ @@ -70,49 +76,10 @@ export interface LegacyAngularInjectedDependencies { * Instance of SavedWorkspacesProvider */ savedGraphWorkspaces: any; - /** - * Private(SavedObjectsClientProvider) - */ - savedObjectsClient: any; } -export const renderApp = ( - { core }: AppMountContext, - { - element, - appBasePath, - data, - npData, - fatalError, - xpackInfo, - addBasePath, - getBasePath, - Storage, - canEditDrillDownUrls, - graphSavePolicy, - }: GraphDependencies, - angularDeps: LegacyAngularInjectedDependencies -) => { - const deps = { - capabilities: core.application.capabilities.graph, - coreStart: core, - chrome: core.chrome, - config: core.uiSettings, - toastNotifications: core.notifications.toasts, - indexPatterns: data.indexPatterns.indexPatterns, - npData, - fatalError, - xpackInfo, - addBasePath, - getBasePath, - KbnUrlProvider, - Storage, - canEditDrillDownUrls, - graphSavePolicy, - ...angularDeps, - }; - - const graphAngularModule = createLocalAngularModule(core); +export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { + const graphAngularModule = createLocalAngularModule(deps.coreStart); configureAppAngularModule(graphAngularModule); initGraphApp(graphAngularModule, deps); const $injector = mountGraphApp(appBasePath, element); @@ -144,15 +111,24 @@ function mountGraphApp(appBasePath: string, element: HTMLElement) { function createLocalAngularModule(core: AppMountContext['core']) { createLocalI18nModule(); createLocalTopNavModule(); + createLocalConfirmModalModule(); const graphAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, 'graphI18n', 'graphTopNav', + 'graphConfirmModal', ]); return graphAngularModule; } +function createLocalConfirmModalModule() { + angular + .module('graphConfirmModal', ['react']) + .factory('confirmModal', confirmModalFactory) + .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); +} + function createLocalTopNavModule() { angular .module('graphTopNav', ['react'])