From a5705f8dd0423ffcc1c29703dd305889bee2d66d Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 6 Feb 2017 14:41:21 -0800 Subject: [PATCH] Add landing page with table to Visualize app. (#9605) * Refactor Visualize Wizard to use explicit controller names, instead of defining them dynamically. * Add landing page with table to Visualize app. * Update Visualize wizard UI. * Add kuiIcon--basic and support icons in Table component. Display icons in Visualize landing page. * Refactor Visualize Wizard templates to use import syntax. * Set kuiViewContent width to 100% to avoid it shrink wrapping its content. * Move ToolBar buttons to the right side. Remove labels and add tooltips. * Remove unused Visualize load menu template. * Disable timepicker in Visualization listing. * Change Visualize route names. Add omitPages attribute to breadcrumbs directive. Make Visualize breadcrumbs into links. * Remove Open and New menu buttons from Visualize. * Add ConfirmationModal for deleting visualizations. * Implement sorting for name and type columns in Visualize listing. * Refactor Visualize routes into VisualizeConstants file. Fix functional tests. * Add pager_controls directive and pager service. Add pagination to Dashboard and Visualize landing pages. Change Dashboard listing to use hrefs for each dashboard. * Use ng-if instead of ng-hide to hide/reveal Table action buttons in Dashboard listing. * Redirect from old Visualize wizard routes to new ones. * Use ViewContent and Title components in Visualize wizard. * Fix Visualize and Dashboard listing table logic so that selection only applies to the current page of items. * Paging clears the selection. * Searching clears the selection. * Sorting clears the selection. * Use consistent "Create" terminology in both Visualize and Dashboard for creating new items. * Use NoItems and PromptForItems components in Visualize listing view. * Use $injector to inject dependencies in Visualize and Dashboard listing. --- .../public/dashboard/dashboard_constants.js | 2 +- .../kibana/public/dashboard/index.js | 2 +- .../dashboard/listing/dashboard_listing.html | 49 +++-- .../dashboard/listing/dashboard_listing.js | 98 ++++++--- .../public/visualize/editor/editor.html | 20 +- .../kibana/public/visualize/editor/editor.js | 25 +-- .../public/visualize/editor/panels/load.html | 2 - .../kibana/public/visualize/index.js | 9 +- .../visualize/listing/visualize_listing.html | 197 ++++++++++++++++++ .../visualize/listing/visualize_listing.js | 178 ++++++++++++++++ .../kibana/public/visualize/styles/main.less | 71 +++---- .../public/visualize/visualize_constants.js | 7 + .../public/visualize/wizard/step_1.html | 50 +++-- .../public/visualize/wizard/step_2.html | 54 +++-- .../kibana/public/visualize/wizard/wizard.js | 57 +++-- .../public/visualize/wizard/wizard.less | 6 - .../kbn_top_nav/bread_crumbs/bread_crumbs.js | 11 + src/ui/public/kbn_top_nav/kbn_top_nav.js | 2 +- src/ui/public/pager/index.js | 1 + src/ui/public/pager/pager.js | 58 ++++++ src/ui/public/pager/pager_factory.js | 12 ++ .../tool_bar_pager_buttons.html | 16 ++ .../tool_bar_pager_buttons.js | 29 +++ .../tool_bar_pager_text.html | 3 + .../tool_bar_pager_text.js | 21 ++ src/ui/public/pager_control/index.js | 2 + src/ui/public/styles/base.less | 1 + test/functional/apps/visualize/_area_chart.js | 2 +- .../functional/apps/visualize/_chart_types.js | 2 +- test/functional/apps/visualize/_data_table.js | 2 +- .../apps/visualize/_heatmap_chart.js | 2 +- test/functional/apps/visualize/_line_chart.js | 2 +- .../apps/visualize/_metric_chart.js | 2 +- test/functional/apps/visualize/_pie_chart.js | 2 +- test/functional/apps/visualize/_tile_map.js | 2 +- .../apps/visualize/_vertical_bar_chart.js | 2 +- test/support/page_objects/common.js | 9 + test/support/page_objects/visualize_page.js | 27 +-- ui_framework/components/icon/_icon.scss | 4 + ui_framework/components/table/_table.scss | 2 +- ui_framework/dist/ui_framework.css | 5 +- .../doc_site/src/views/icon/icon_basic.html | 1 + .../doc_site/src/views/icon/icon_example.jsx | 6 + 43 files changed, 850 insertions(+), 205 deletions(-) delete mode 100644 src/core_plugins/kibana/public/visualize/editor/panels/load.html create mode 100644 src/core_plugins/kibana/public/visualize/listing/visualize_listing.html create mode 100644 src/core_plugins/kibana/public/visualize/listing/visualize_listing.js create mode 100644 src/core_plugins/kibana/public/visualize/visualize_constants.js delete mode 100644 src/core_plugins/kibana/public/visualize/wizard/wizard.less create mode 100644 src/ui/public/pager/index.js create mode 100644 src/ui/public/pager/pager.js create mode 100644 src/ui/public/pager/pager_factory.js create mode 100644 src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.html create mode 100644 src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.js create mode 100644 src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.html create mode 100644 src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.js create mode 100644 src/ui/public/pager_control/index.js create mode 100644 ui_framework/doc_site/src/views/icon/icon_basic.html diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js index 79305daf3c209..d7b4d32bfb898 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js @@ -2,5 +2,5 @@ export const DashboardConstants = { ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard', NEW_VISUALIZATION_ID_PARAM: 'addVisualization', - LANDING_PAGE_URL: '/dashboard' + LANDING_PAGE_PATH: '/dashboard' }; diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index 7bea2c0edcb44..a90ad4275a9ee 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -14,7 +14,7 @@ uiRoutes .defaults(/dashboard/, { requireDefaultIndex: true }) - .when(DashboardConstants.LANDING_PAGE_URL, { + .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, controller: DashboardListingController, controllerAs: 'listingController' diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html index 45ce0f34fd1e5..b5c49ad7eb7f1 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html @@ -31,17 +31,13 @@ -
- -
-
+ +
+ + + +
- +
- Looks like you don’t have any dashboards. Let’s add some! + Looks like you don’t have any dashboards. Let’s create some!
@@ -87,7 +98,7 @@ href="#/dashboard/create" > - Add a dashboard + Create a dashboard
@@ -118,7 +129,7 @@ @@ -132,7 +143,7 @@ @@ -149,8 +160,20 @@ {{ listingController.getSelectedItemsCount() }} selected
-
- + +
+ + +
diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 3abec72de0128..1ae98b0637589 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -1,17 +1,21 @@ import SavedObjectRegistryProvider from 'ui/saved_objects/saved_object_registry'; +import 'ui/pager_control'; +import 'ui/pager'; import { DashboardConstants } from '../dashboard_constants'; import _ from 'lodash'; -export function DashboardListingController( - $scope, - kbnUrl, - Notifier, - Private, - timefilter, - confirmModal -) { +export function DashboardListingController($injector, $scope) { + const $filter = $injector.get('$filter'); + const confirmModal = $injector.get('confirmModal'); + const kbnUrl = $injector.get('kbnUrl'); + const Notifier = $injector.get('Notifier'); + const pagerFactory = $injector.get('pagerFactory'); + const Private = $injector.get('Private'); + const timefilter = $injector.get('timefilter'); + timefilter.enabled = false; + const limitTo = $filter('limitTo'); // TODO: Extract this into an external service. const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const dashboardService = services.dashboards; @@ -19,17 +23,51 @@ export function DashboardListingController( let selectedItems = []; + /** + * Sorts hits either ascending or descending + * @param {Array} hits Array of saved finder object hits + * @return {Array} Array sorted either ascending or descending + */ + const sortItems = () => { + this.items = + this.isAscending + ? _.sortBy(this.items, 'title') + : _.sortBy(this.items, 'title').reverse(); + }; + + const calculateItemsOnPage = () => { + sortItems(); + this.pager.setTotalItems(this.items.length); + this.pageOfItems = limitTo(this.items, this.pager.pageSize, this.pager.startIndex); + }; + const fetchObjects = () => { dashboardService.find(this.filter) .then(result => { this.items = result.hits; - this.sortItems(); + calculateItemsOnPage(); }); }; + const deselectAll = () => { + selectedItems = []; + }; + + const selectAll = () => { + selectedItems = this.pageOfItems.slice(0); + }; + this.items = []; + this.pageOfItems = []; this.filter = ''; + this.pager = pagerFactory.create(this.items.length, 20, 1); + + $scope.$watch(() => this.filter, () => { + deselectAll(); + fetchObjects(); + }); + /** * Boolean that keeps track of whether hits are sorted ascending (true) * or descending (false) by title @@ -37,28 +75,17 @@ export function DashboardListingController( */ this.isAscending = true; - /** - * Sorts hits either ascending or descending - * @param {Array} hits Array of saved finder object hits - * @return {Array} Array sorted either ascending or descending - */ - this.sortItems = function sortItems() { - this.items = - this.isAscending - ? _.sortBy(this.items, 'title') - : _.sortBy(this.items, 'title').reverse(); - }; - this.toggleSort = function toggleSort() { this.isAscending = !this.isAscending; - this.sortItems(); + deselectAll(); + calculateItemsOnPage(); }; this.toggleAll = function toggleAll() { if (this.areAllItemsChecked()) { - selectedItems = []; + deselectAll(); } else { - selectedItems = this.items.slice(0); + selectAll(); } }; @@ -76,7 +103,7 @@ export function DashboardListingController( }; this.areAllItemsChecked = function areAllItemsChecked() { - return this.getSelectedItemsCount() === this.items.length; + return this.getSelectedItemsCount() === this.pageOfItems.length; }; this.getSelectedItemsCount = function getSelectedItemsCount() { @@ -90,10 +117,11 @@ export function DashboardListingController( dashboardService.delete(selectedIds) .then(fetchObjects) .then(() => { - selectedItems = []; + deselectAll(); }) .catch(error => notify.error(error)); }; + confirmModal( 'Are you sure you want to delete the selected dashboards? This action is irreversible!', { @@ -102,11 +130,19 @@ export function DashboardListingController( }); }; - this.open = function open(item) { - kbnUrl.change(item.url.substr(1)); + this.onPageNext = () => { + deselectAll(); + this.pager.nextPage(); + calculateItemsOnPage(); }; - $scope.$watch(() => this.filter, () => { - fetchObjects(); - }); + this.onPagePrevious = () => { + deselectAll(); + this.pager.previousPage(); + calculateItemsOnPage(); + }; + + this.getUrlForItem = function getUrlForItem(item) { + return `#/dashboard/${item.id}`; + }; } diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.html b/src/core_plugins/kibana/public/visualize/editor/editor.html index d675297daffa6..93021aaf27f49 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.html +++ b/src/core_plugins/kibana/public/visualize/editor/editor.html @@ -3,16 +3,14 @@
- -
+ - -
+ page-title="getVisualizationTitle()" + use-links="true" + omit-current-page="true" + omit-pages="['edit']" + >
- +
- +
Open Visualization
- diff --git a/src/core_plugins/kibana/public/visualize/index.js b/src/core_plugins/kibana/public/visualize/index.js index 67cf63db22f75..d8c3e45870ecf 100644 --- a/src/core_plugins/kibana/public/visualize/index.js +++ b/src/core_plugins/kibana/public/visualize/index.js @@ -18,13 +18,18 @@ import 'plugins/kibana/visualize/saved_visualizations/_saved_vis'; import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; import uiRoutes from 'ui/routes'; +import visualizeListingTemplate from './listing/visualize_listing.html'; +import { VisualizeListingController } from './listing/visualize_listing'; +import { VisualizeConstants } from './visualize_constants'; uiRoutes .defaults(/visualize/, { requireDefaultIndex: true }) -.when('/visualize', { - redirectTo: '/visualize/step/1' +.when(VisualizeConstants.LANDING_PAGE_PATH, { + template: visualizeListingTemplate, + controller: VisualizeListingController, + controllerAs: 'listingController', }); // preloading diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html new file mode 100644 index 0000000000000..a9885114f9b27 --- /dev/null +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html @@ -0,0 +1,197 @@ + + + +
+ +
+ Visualize +
+
+
+ +
+ +
+ +
+
+
+ + +
+
+ +
+ + + + + + + +
+ +
+ + + +
+
+ + +
+
+ No visualizations matched your search. +
+
+ + +
+
+
+ Looks like you don’t have any visualizations. Let’s create some! +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + Name + + + Type + +
+ + + + +
+ + + {{ item.type.title }} + +
+
+ + +
+
+
+ {{ listingController.getSelectedItemsCount() }} selected +
+
+ +
+ + + +
+
+
+
diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js new file mode 100644 index 0000000000000..0cd96bee764d9 --- /dev/null +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js @@ -0,0 +1,178 @@ +import SavedObjectRegistryProvider from 'ui/saved_objects/saved_object_registry'; +import 'ui/pager_control'; +import 'ui/pager'; +import _ from 'lodash'; + +export function VisualizeListingController($injector, $scope) { + const $filter = $injector.get('$filter'); + const confirmModal = $injector.get('confirmModal'); + const kbnUrl = $injector.get('kbnUrl'); + const Notifier = $injector.get('Notifier'); + const pagerFactory = $injector.get('pagerFactory'); + const Private = $injector.get('Private'); + const timefilter = $injector.get('timefilter'); + + timefilter.enabled = false; + + const limitTo = $filter('limitTo'); + // TODO: Extract this into an external service. + const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; + const visualizationService = services.visualizations; + const notify = new Notifier({ location: 'Visualize' }); + + let selectedItems = []; + + /** + * Sorts hits either ascending or descending + * @param {Array} hits Array of saved finder object hits + * @return {Array} Array sorted either ascending or descending + */ + const sortItems = () => { + const sortProperty = this.getSortProperty(); + + this.items = + sortProperty.isAscending + ? _.sortBy(this.items, sortProperty.getValue) + : _.sortBy(this.items, sortProperty.getValue).reverse(); + }; + + const calculateItemsOnPage = () => { + sortItems(); + this.pager.setTotalItems(this.items.length); + this.pageOfItems = limitTo(this.items, this.pager.pageSize, this.pager.startIndex); + }; + + const fetchObjects = () => { + visualizationService.find(this.filter) + .then(result => { + this.items = result.hits; + calculateItemsOnPage(); + }); + }; + + const deselectAll = () => { + selectedItems = []; + }; + + const selectAll = () => { + selectedItems = this.pageOfItems.slice(0); + }; + + this.items = []; + this.pageOfItems = []; + this.filter = ''; + + this.pager = pagerFactory.create(this.items.length, 20, 1); + + $scope.$watch(() => this.filter, () => { + deselectAll(); + fetchObjects(); + }); + + /** + * Remember sort direction per property. + */ + this.sortProperties = [{ + name: 'title', + getValue: item => item.title, + isSelected: true, + isAscending: true, + }, { + name: 'type', + getValue: item => item.type.title, + isSelected: false, + isAscending: true, + }]; + + this.getSortProperty = function getSortProperty() { + return this.sortProperties.find(property => property.isSelected); + }; + + this.getSortPropertyByName = function getSortPropertyByName(name) { + return this.sortProperties.find(property => property.name === name); + }; + + this.isAscending = function isAscending() { + const sortProperty = this.getSortProperty(); + return sortProperty.isAscending; + }; + + this.sortOn = function sortOn(propertyName) { + const sortProperty = this.getSortProperty(); + + if (sortProperty.name === propertyName) { + sortProperty.isAscending = !sortProperty.isAscending; + } else { + sortProperty.isSelected = false; + this.getSortPropertyByName(propertyName).isSelected = true; + } + + deselectAll(); + calculateItemsOnPage(); + }; + + this.toggleAll = function toggleAll() { + if (this.areAllItemsChecked()) { + deselectAll(); + } else { + selectAll(); + } + }; + + this.toggleItem = function toggleItem(item) { + if (this.isItemChecked(item)) { + const index = selectedItems.indexOf(item); + selectedItems.splice(index, 1); + } else { + selectedItems.push(item); + } + }; + + this.isItemChecked = function isItemChecked(item) { + return selectedItems.includes(item); + }; + + this.areAllItemsChecked = function areAllItemsChecked() { + return this.getSelectedItemsCount() === this.pageOfItems.length; + }; + + this.getSelectedItemsCount = function getSelectedItemsCount() { + return selectedItems.length; + }; + + this.deleteSelectedItems = function deleteSelectedItems() { + const doDelete = () => { + const selectedIds = selectedItems.map(item => item.id); + + visualizationService.delete(selectedIds) + .then(fetchObjects) + .then(() => { + deselectAll(); + }) + .catch(error => notify.error(error)); + }; + + confirmModal( + 'Are you sure you want to delete the selected visualizations? This action is irreversible!', + { + confirmButtonText: 'Delete', + onConfirm: doDelete + }); + }; + + this.onPageNext = () => { + deselectAll(); + this.pager.nextPage(); + calculateItemsOnPage(); + }; + + this.onPagePrevious = () => { + deselectAll(); + this.pager.previousPage(); + calculateItemsOnPage(); + }; + + this.getUrlForItem = function getUrlForItem(item) { + return `#/visualize/edit/${item.id}`; + }; +} diff --git a/src/core_plugins/kibana/public/visualize/styles/main.less b/src/core_plugins/kibana/public/visualize/styles/main.less index e14b6d8d5e010..f230f71ab3f19 100644 --- a/src/core_plugins/kibana/public/visualize/styles/main.less +++ b/src/core_plugins/kibana/public/visualize/styles/main.less @@ -2,59 +2,50 @@ @import (reference) "~ui/styles/bootstrap/list-group"; @import (reference) "~ui/styles/list-group-menu"; -/** - * 1. Push down the breadcrumbs a bit. - */ -.vis-wizard { - margin: 0; - padding: 12px 0 0; // 1 +.wizard-sub-title { + margin-top: 0px; + margin-bottom: 8px; + padding: 0px 5px; } - .wizard-sub-title { - margin-top: 0px; - margin-bottom: 8px; - padding: 0px 5px; - } +.wizard-type { + flex: 1; - .wizard-type { - flex: 1; + // TODO: When we migrate off Bootstrap, we can eliminate these "mixins". + .list-group-item(); + .list-group-menu .list-group-menu-item(); - // TODO: When we migrate off Bootstrap, we can eliminate these "mixins". - .list-group-item(); - .list-group-menu .list-group-menu-item(); + border: none; + border-radius: 0; + background-color: @kibanaGray6; +} - border: none; - border-radius: 0; - background-color: @kibanaGray6; + .wizard-type-heading { + flex: 0 0 200px; + display: flex; + align-items: center; + font-size: 1.2em; } - .wizard-type-heading { - flex: 0 0 200px; - display: flex; - align-items: center; - font-size: 1.2em; + .wizard-type-heading-icon { + flex: 0 0 auto; + margin-right: @padding-base-horizontal; + font-size: 1.5em; + text-align: center; + color: @saved-object-finder-icon-color; } - .wizard-type-heading-icon { - flex: 0 0 auto; - margin-right: @padding-base-horizontal; - font-size: 1.5em; - text-align: center; - color: @saved-object-finder-icon-color; - } - - .wizard-type-heading-text { - flex: 1 0 auto; - } - - .wizard-type-description { - flex: 1 1 auto; - color: @wizard-vis-type-description-color; + .wizard-type-heading-text { + flex: 1 0 auto; } + .wizard-type-description { + flex: 1 1 auto; + color: @wizard-vis-type-description-color; + } + @media (min-width: @screen-lg) { .wizard { - padding: 0; display: flex; } diff --git a/src/core_plugins/kibana/public/visualize/visualize_constants.js b/src/core_plugins/kibana/public/visualize/visualize_constants.js new file mode 100644 index 0000000000000..eddc7bac7daed --- /dev/null +++ b/src/core_plugins/kibana/public/visualize/visualize_constants.js @@ -0,0 +1,7 @@ +export const VisualizeConstants = { + LANDING_PAGE_PATH: '/visualize', + WIZARD_STEP_1_PAGE_PATH: '/visualize/new', + WIZARD_STEP_2_PAGE_PATH: '/visualize/new/configure', + CREATE_PATH: '/visualize/create', + EDIT_PATH: '/visualize/edit', +}; diff --git a/src/core_plugins/kibana/public/visualize/wizard/step_1.html b/src/core_plugins/kibana/public/visualize/wizard/step_1.html index 49baecc110961..ebc04c4218792 100644 --- a/src/core_plugins/kibana/public/visualize/wizard/step_1.html +++ b/src/core_plugins/kibana/public/visualize/wizard/step_1.html @@ -1,33 +1,47 @@ -
- -
-
-
-

Create New Visualization

-
+ + + +
+ + +
+
+ +
+ + -
-

Or, Open a Saved Visualization

- -
diff --git a/src/core_plugins/kibana/public/visualize/wizard/step_2.html b/src/core_plugins/kibana/public/visualize/wizard/step_2.html index 43fc64bdb6468..b818992084101 100644 --- a/src/core_plugins/kibana/public/visualize/wizard/step_2.html +++ b/src/core_plugins/kibana/public/visualize/wizard/step_2.html @@ -1,24 +1,44 @@ -
- -
-
-
-

From a New Search, Select Index

- + + + +
+ +
-
-

Or, From a Saved Search

- - + +
+
+
+

+ From a New Search, Select Index +

+ + +
+ +
+

+ Or, From a Saved Search +

+ + + +
diff --git a/src/core_plugins/kibana/public/visualize/wizard/wizard.js b/src/core_plugins/kibana/public/visualize/wizard/wizard.js index 98fb790a50264..55b7e2c5fc7ec 100644 --- a/src/core_plugins/kibana/public/visualize/wizard/wizard.js +++ b/src/core_plugins/kibana/public/visualize/wizard/wizard.js @@ -1,25 +1,29 @@ - import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; import 'ui/directives/saved_object_finder'; import 'ui/directives/paginated_selectable_list'; import 'plugins/kibana/discover/saved_searches/saved_searches'; import { DashboardConstants } from 'plugins/kibana/dashboard/dashboard_constants'; +import { VisualizeConstants } from '../visualize_constants'; import routes from 'ui/routes'; import RegistryVisTypesProvider from 'ui/registry/vis_types'; import uiModules from 'ui/modules'; -import './wizard.less'; - -const templateStep = function (num, txt) { - return '
' + txt + '
'; -}; +import visualizeWizardStep1Template from './step_1.html'; +import visualizeWizardStep2Template from './step_2.html'; const module = uiModules.get('app/visualize', ['kibana/courier']); /******** /** Wizard Step 1 /********/ + +// Redirect old route to new route. routes.when('/visualize/step/1', { - template: templateStep(1, require('plugins/kibana/visualize/wizard/step_1.html')) + redirectTo: VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, +}); + +routes.when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { + template: visualizeWizardStep1Template, + controller: 'VisualizeWizardStep1', }); module.controller('VisualizeWizardStep1', function ($scope, $route, kbnUrl, timefilter, Private) { @@ -29,9 +33,15 @@ module.controller('VisualizeWizardStep1', function ($scope, $route, kbnUrl, time kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); $scope.visTypes = Private(RegistryVisTypesProvider); + $scope.visTypeUrl = function (visType) { - const baseUrl = visType.requiresSearch ? '#/visualize/step/2?' : '#/visualize/create?'; + const baseUrl = + visType.requiresSearch + ? `#${VisualizeConstants.WIZARD_STEP_2_PAGE_PATH}?` + : `#${VisualizeConstants.CREATE_PATH}?`; + const params = [`type=${encodeURIComponent(visType.name)}`]; + if (addToDashMode) { params.push(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); } @@ -43,8 +53,17 @@ module.controller('VisualizeWizardStep1', function ($scope, $route, kbnUrl, time /******** /** Wizard Step 2 /********/ + +// Redirect old route to new route. +// NOTE: Accessing this route directly means the user has entered into the wizard UX without +// selecting a Visualization type in step 1. So we want to redirect them to step 1, not step 2. routes.when('/visualize/step/2', { - template: templateStep(2, require('plugins/kibana/visualize/wizard/step_2.html')), + redirectTo: VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, +}); + +routes.when(VisualizeConstants.WIZARD_STEP_2_PAGE_PATH, { + template: visualizeWizardStep2Template, + controller: 'VisualizeWizardStep2', resolve: { indexPatternIds: function (courier) { return courier.indexPatterns.getIds(); @@ -60,10 +79,17 @@ module.controller('VisualizeWizardStep2', function ($route, $scope, timefilter, $scope.step2WithSearchUrl = function (hit) { if (addToDashMode) { return kbnUrl.eval( - `#/visualize/create?&type={{type}}&savedSearchId={{id}}&${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`, - { type: type, id: hit.id }); + `#${VisualizeConstants.CREATE_PATH}` + + `?type={{type}}&savedSearchId={{id}}` + + `&${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`, + { type: type, id: hit.id } + ); } - return kbnUrl.eval('#/visualize/create?&type={{type}}&savedSearchId={{id}}', { type: type, id: hit.id }); + + return kbnUrl.eval( + `#${VisualizeConstants.CREATE_PATH}?type={{type}}&savedSearchId={{id}}`, + { type: type, id: hit.id } + ); }; timefilter.enabled = false; @@ -77,8 +103,11 @@ module.controller('VisualizeWizardStep2', function ($route, $scope, timefilter, if (!pattern) return; if (addToDashMode) { - return `#/visualize/create?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}&type=${type}&indexPattern=${pattern}`; + return `#${VisualizeConstants.CREATE_PATH}` + + `?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}` + + `&type=${type}&indexPattern=${pattern}`; } - return `#/visualize/create?type=${type}&indexPattern=${pattern}`; + + return `#${VisualizeConstants.CREATE_PATH}?type=${type}&indexPattern=${pattern}`; }; }); diff --git a/src/core_plugins/kibana/public/visualize/wizard/wizard.less b/src/core_plugins/kibana/public/visualize/wizard/wizard.less deleted file mode 100644 index 5b61fe6a166e3..0000000000000 --- a/src/core_plugins/kibana/public/visualize/wizard/wizard.less +++ /dev/null @@ -1,6 +0,0 @@ -.visualizeWizardBreadcrumbs { - display: flex; - align-items: center; - justify-content: space-between; - height: 32px; -} diff --git a/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js index f032005490a62..bb02fef4971e0 100644 --- a/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js +++ b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js @@ -11,6 +11,11 @@ module.directive('breadCrumbs', function ($location) { replace: true, scope: { omitCurrentPage: '=', + /** + * Pages to omit from the breadcrumbs. Should be lower-case. + * @type {Array} + */ + omitPages: '=', /** * Optional title to append at the end of the breadcrumbs. Note that this can't just be * 'title', because that will be interpreted by browsers as an actual 'title' HTML attribute. @@ -32,6 +37,12 @@ module.directive('breadCrumbs', function ($location) { $scope.breadcrumbs.pop(); } + if ($scope.omitPages) { + $scope.breadcrumbs = $scope.breadcrumbs.filter(breadcrumb => + !$scope.omitPages.includes(breadcrumb.toLowerCase()) + ); + } + if ($scope.useLinks) { const url = '#' + $location.path(); $scope.breadCrumbUrls = getBreadCrumbUrls($scope.breadcrumbs, url); diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav.js b/src/ui/public/kbn_top_nav/kbn_top_nav.js index 2d4cd3ef91476..840b5a1cca090 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav.js @@ -100,8 +100,8 @@ module.directive('kbnTopNav', function (Private) { }); const extensions = getNavbarExtensions($attrs.name); - let controls = _.get($scope, $attrs.config, []); + if (controls instanceof KbnTopNavController) { controls.addItems(extensions); } else { diff --git a/src/ui/public/pager/index.js b/src/ui/public/pager/index.js new file mode 100644 index 0000000000000..3f8e47d5d122a --- /dev/null +++ b/src/ui/public/pager/index.js @@ -0,0 +1 @@ +import './pager_factory'; diff --git a/src/ui/public/pager/pager.js b/src/ui/public/pager/pager.js new file mode 100644 index 0000000000000..6a8057d77628d --- /dev/null +++ b/src/ui/public/pager/pager.js @@ -0,0 +1,58 @@ +function clamp(val, min, max) { + return Math.min(Math.max(min, val), max); +} + +export class Pager { + constructor(totalItems, pageSize, startingPage) { + this.currentPage = startingPage; + this.totalItems = totalItems; + this.pageSize = pageSize; + this.startIndex = 0; + this.updateMeta(); + } + + get pageCount() { + return Math.ceil(this.totalItems / this.pageSize); + } + + get hasNextPage() { + return this.currentPage < this.totalPages; + } + + get hasPreviousPage() { + return this.currentPage > 1; + } + + nextPage() { + this.currentPage += 1; + this.updateMeta(); + } + + previousPage() { + this.currentPage -= 1; + this.updateMeta(); + } + + setTotalItems(count) { + this.totalItems = count; + this.updateMeta(); + } + + setPageSize(count) { + this.pageSize = count; + this.updateMeta(); + } + + updateMeta() { + this.totalPages = Math.ceil(this.totalItems / this.pageSize); + this.currentPage = clamp(this.currentPage, 1, this.totalPages); + + this.startItem = ((this.currentPage - 1) * this.pageSize) + 1; + this.startItem = clamp(this.startItem, 0, this.totalItems); + + this.endItem = (this.startItem - 1) + this.pageSize; + this.endItem = clamp(this.endItem, 0, this.totalItems); + + this.startIndex = this.startItem - 1; + } +} diff --git a/src/ui/public/pager/pager_factory.js b/src/ui/public/pager/pager_factory.js new file mode 100644 index 0000000000000..957c219fe3478 --- /dev/null +++ b/src/ui/public/pager/pager_factory.js @@ -0,0 +1,12 @@ +import uiModules from 'ui/modules'; +import { Pager } from './pager'; + +const app = uiModules.get('kibana'); + +app.factory('pagerFactory', () => { + return { + create(...args) { + return new Pager(...args); + } + }; +}); diff --git a/src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.html b/src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.html new file mode 100644 index 0000000000000..361d67848e58e --- /dev/null +++ b/src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.html @@ -0,0 +1,16 @@ +
+ + +
diff --git a/src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.js b/src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.js new file mode 100644 index 0000000000000..302f1382357af --- /dev/null +++ b/src/ui/public/pager_control/components/tool_bar_pager_buttons/tool_bar_pager_buttons.js @@ -0,0 +1,29 @@ +import uiModules from 'ui/modules'; +import template from './tool_bar_pager_buttons.html'; + +const app = uiModules.get('kibana'); + +app.directive('toolBarPagerButtons', function () { + return { + restrict: 'E', + replace: true, + template: template, + scope: { + hasNextPage: '=', + hasPreviousPage: '=', + onPageNext: '=', + onPagePrevious: '=', + }, + controllerAs: 'toolBarPagerButtons', + bindToController: true, + controller: class ToolBarPagerButtonsController { + nextPage = () => { + this.onPageNext(); + }; + + previousPage = () => { + this.onPagePrevious(); + }; + } + }; +}); diff --git a/src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.html b/src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.html new file mode 100644 index 0000000000000..36f7675b8db32 --- /dev/null +++ b/src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.html @@ -0,0 +1,3 @@ +
+ {{ toolBarPagerText.startItem | number }}–{{ toolBarPagerText.endItem | number }} of {{ toolBarPagerText.totalItems | number }} +
\ No newline at end of file diff --git a/src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.js b/src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.js new file mode 100644 index 0000000000000..873bdb597d0fb --- /dev/null +++ b/src/ui/public/pager_control/components/tool_bar_pager_text/tool_bar_pager_text.js @@ -0,0 +1,21 @@ +import uiModules from 'ui/modules'; +import template from './tool_bar_pager_text.html'; + +const app = uiModules.get('kibana'); + +app.directive('toolBarPagerText', function () { + return { + restrict: 'E', + replace: true, + template: template, + scope: { + startItem: '=', + endItem: '=', + totalItems: '=', + }, + controllerAs: 'toolBarPagerText', + bindToController: true, + controller: class ToolBarPagerTextController { + } + }; +}); diff --git a/src/ui/public/pager_control/index.js b/src/ui/public/pager_control/index.js new file mode 100644 index 0000000000000..9f0f3b8ea66d5 --- /dev/null +++ b/src/ui/public/pager_control/index.js @@ -0,0 +1,2 @@ +import './components/tool_bar_pager_text/tool_bar_pager_text'; +import './components/tool_bar_pager_buttons/tool_bar_pager_buttons'; diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less index 1c28e5bd95f32..4da92ce29b6d4 100644 --- a/src/ui/public/styles/base.less +++ b/src/ui/public/styles/base.less @@ -621,6 +621,7 @@ fieldset { .kuiViewContent { padding-top: 20px; padding-bottom: 20px; + width: 100%; } .kuiViewContent--constrainedWidth { diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 908462a993bdb..fa1593c2d9ff1 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { const toTime = '2015-09-23 18:31:44.000'; PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickAreaChart'); return PageObjects.visualize.clickAreaChart(); diff --git a/test/functional/apps/visualize/_chart_types.js b/test/functional/apps/visualize/_chart_types.js index 374b53c8f8e62..76e265c99fbd1 100644 --- a/test/functional/apps/visualize/_chart_types.js +++ b/test/functional/apps/visualize/_chart_types.js @@ -10,7 +10,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.before(function () { PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize'); + return PageObjects.common.navigateToUrl('visualize', 'new'); }); bdd.describe('chart types', function indexPatternCreation() { diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js index 9d5fbe3c270ba..07d1bd274fb06 100644 --- a/test/functional/apps/visualize/_data_table.js +++ b/test/functional/apps/visualize/_data_table.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.before(function () { PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickDataTable'); return PageObjects.visualize.clickDataTable(); diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index 85a63d0452651..cd4bd06d111a3 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.before(function () { PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickHeatmapChart'); return PageObjects.visualize.clickHeatmapChart(); diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 34cc5f88672ac..db324e28a5b0b 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { const toTime = '2015-09-23 18:31:44.000'; PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickLineChart'); return PageObjects.visualize.clickLineChart(); diff --git a/test/functional/apps/visualize/_metric_chart.js b/test/functional/apps/visualize/_metric_chart.js index dc861efb07bda..2a864126435c8 100644 --- a/test/functional/apps/visualize/_metric_chart.js +++ b/test/functional/apps/visualize/_metric_chart.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.before(function () { PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickMetric'); return PageObjects.visualize.clickMetric(); diff --git a/test/functional/apps/visualize/_pie_chart.js b/test/functional/apps/visualize/_pie_chart.js index 5c0770f2e9556..7636062cdb162 100644 --- a/test/functional/apps/visualize/_pie_chart.js +++ b/test/functional/apps/visualize/_pie_chart.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { const toTime = '2015-09-23 18:31:44.000'; PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickPieChart'); return PageObjects.visualize.clickPieChart(); diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index e0c61592cc151..610cb88f4b480 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -15,7 +15,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.before(function () { PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickTileMap'); return PageObjects.visualize.clickTileMap(); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index 2b702fa929c4d..047ea2ecbece2 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -14,7 +14,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.before(function () { PageObjects.common.debug('navigateToApp visualize'); - return PageObjects.common.navigateToApp('visualize') + return PageObjects.common.navigateToUrl('visualize', 'new') .then(function () { PageObjects.common.debug('clickVerticalBarChart'); return PageObjects.visualize.clickVerticalBarChart(); diff --git a/test/support/page_objects/common.js b/test/support/page_objects/common.js index 073c4b0ad979d..287f6ef5e470d 100644 --- a/test/support/page_objects/common.js +++ b/test/support/page_objects/common.js @@ -80,6 +80,15 @@ export default class Common { return getUrl.baseUrl(config.servers.elasticsearch); } + navigateToUrl(appName, subUrl) { + const appConfig = Object.assign({}, config.apps[appName], { + // Overwrite the default hash with the URL we really want. + hash: `${appName}/${subUrl}`, + }); + const appUrl = getUrl.noAuth(config.servers.kibana, appConfig); + return this.remote.get(appUrl); + } + navigateToApp(appName, testStatusPage) { const self = this; const appUrl = getUrl.noAuth(config.servers.kibana, config.apps[appName]); diff --git a/test/support/page_objects/visualize_page.js b/test/support/page_objects/visualize_page.js index 22c7b902c4086..263de4eea6b83 100644 --- a/test/support/page_objects/visualize_page.js +++ b/test/support/page_objects/visualize_page.js @@ -82,10 +82,7 @@ export default class VisualizePage { } getChartTypes() { - - return this.remote - .setFindTimeout(defaultFindTimeout) - .findAllByCssSelector('.wizard-type-heading h4') + return PageObjects.common.findAllTestSubjects('visualizeWizardChartTypeTitle') .then(function (chartTypes) { function getChartType(chart) { return chart.getVisibleText(); @@ -299,11 +296,6 @@ export default class VisualizePage { }); } - clickNewVisualization() { - return PageObjects.common.findTestSubject('visualizeNewButton') - .click(); - } - saveVisualization(vizName) { return PageObjects.common.findTestSubject('visualizeSaveButton') .click() @@ -340,7 +332,11 @@ export default class VisualizePage { } clickLoadSavedVisButton() { - return PageObjects.common.findTestSubject('visualizeOpenButton') + // TODO: Use a test subject selector once we rewrite breadcrumbs to accept each breadcrumb + // element as a child instead of building the breadcrumbs dynamically. + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector('[href="#/visualize"]') .click(); } @@ -376,17 +372,8 @@ export default class VisualizePage { }); } - // this is for starting on the - // bottom half of the "Create a new visualization Step 1" page openSavedVisualization(vizName) { - const self = this; - return self.filterVisByName(vizName) - .then(() => { - return PageObjects.common.sleep(1000); - }) - .then(function clickDashboardByLinkedText() { - return self.clickVisualizationByLinkText(vizName); - }); + return this.clickVisualizationByLinkText(vizName); } getXAxisLabels() { diff --git a/ui_framework/components/icon/_icon.scss b/ui_framework/components/icon/_icon.scss index fc8426f4266c3..f104b63059e6a 100644 --- a/ui_framework/components/icon/_icon.scss +++ b/ui_framework/components/icon/_icon.scss @@ -30,3 +30,7 @@ .kuiIcon--inactive { color: $inactiveColor; } + +.kuiIcon--basic { + color: #565656; +} diff --git a/ui_framework/components/table/_table.scss b/ui_framework/components/table/_table.scss index 9a5341a30e0c7..66402eb89aa6a 100644 --- a/ui_framework/components/table/_table.scss +++ b/ui_framework/components/table/_table.scss @@ -70,7 +70,7 @@ overflow: hidden; text-overflow: ellipsis; - * > { + & > * { vertical-align: middle; /* 1 */ } } diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index dad77d110a2f5..87b70556c9e44 100644 --- a/ui_framework/dist/ui_framework.css +++ b/ui_framework/dist/ui_framework.css @@ -542,6 +542,9 @@ body { .kuiIcon--inactive { color: #c3c3c3; } +.kuiIcon--basic { + color: #565656; } + .kuiInfoPanel { padding: 14px 20px 18px; line-height: 1.5; @@ -1407,7 +1410,7 @@ body { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .kuiTableRowCell__liner * > { + .kuiTableRowCell__liner > * { vertical-align: middle; /* 1 */ } diff --git a/ui_framework/doc_site/src/views/icon/icon_basic.html b/ui_framework/doc_site/src/views/icon/icon_basic.html new file mode 100644 index 0000000000000..78f198e68b543 --- /dev/null +++ b/ui_framework/doc_site/src/views/icon/icon_basic.html @@ -0,0 +1 @@ +
diff --git a/ui_framework/doc_site/src/views/icon/icon_example.jsx b/ui_framework/doc_site/src/views/icon/icon_example.jsx index 3085ea62b0ff1..7aee7f935504f 100644 --- a/ui_framework/doc_site/src/views/icon/icon_example.jsx +++ b/ui_framework/doc_site/src/views/icon/icon_example.jsx @@ -17,6 +17,12 @@ export default createExample([{

Use this Icon to denote useful information.

), html: require('./icon_info.html'), +}, { + title: 'Basic', + description: ( +

Use this Icon when you don't want to communicate any particular meaning with the icon's color.

+ ), + html: require('./icon_basic.html'), hasDarkTheme: false, }, { title: 'Success',