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.
This commit is contained in:
CJ Cenizal 2017-02-06 14:41:21 -08:00 committed by GitHub
parent c8c03e5fec
commit a5705f8dd0
43 changed files with 853 additions and 208 deletions

View file

@ -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'
};

View file

@ -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'

View file

@ -31,17 +31,13 @@
</div>
</div>
<div class="kuiToolBarSection">
<!-- We need an empty section for the buttons to be positioned consistently. -->
</div>
<div class="kuiToolBarSection">
<!-- Bulk delete button -->
<button
class="kuiButton kuiButton--danger"
ng-click="listingController.deleteSelectedItems()"
aria-label="Delete selected objects"
ng-hide="listingController.getSelectedItemsCount() === 0"
ng-if="listingController.getSelectedItemsCount() > 0"
tooltip="Delete selected dashboards"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-trash"></span>
@ -53,15 +49,30 @@
href="#/dashboard/create"
aria-label="Create new dashboard"
data-test-subj="newDashboardLink"
ng-hide="listingController.getSelectedItemsCount() > 0"
ng-if="listingController.getSelectedItemsCount() === 0"
tooltip="Create new dashboard"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-plus"></span>
</a>
</div>
<div class="kuiToolBarSection">
<!-- Pagination -->
<tool-bar-pager-text
start-item="listingController.pager.startItem"
end-item="listingController.pager.endItem"
total-items="listingController.pager.totalItems"
></tool-bar-pager-text>
<tool-bar-pager-buttons
has-previous-page="listingController.pager.hasPreviousPage"
has-next-page="listingController.pager.hasNextPage"
on-page-next="listingController.onPageNext"
on-page-previous="listingController.onPagePrevious"
></tool-bar-pager-buttons>
</div>
</div>
<!-- NoResults -->
<!-- NoItems -->
<div
class="kuiPanel kuiPanel--centered kuiPanel--withHeader"
ng-if="!listingController.items.length && listingController.filter"
@ -78,7 +89,7 @@
>
<div class="kuiPromptForItems">
<div class="kuiPromptForItems__message">
Looks like you don&rsquo;t have any dashboards. Let&rsquo;s add some!
Looks like you don&rsquo;t have any dashboards. Let&rsquo;s create some!
</div>
<div class="kuiPromptForItems__actions">
@ -87,7 +98,7 @@
href="#/dashboard/create"
>
<span class="kuiButton__icon kuiIcon fa-plus"></span>
Add a dashboard
Create a dashboard
</a>
</div>
</div>
@ -118,7 +129,7 @@
<tbody>
<tr
ng-repeat="item in listingController.items track by item.id"
ng-repeat="item in listingController.pageOfItems track by item.id"
class="kuiTableRow"
>
<td class="kuiTableRowCell kuiTableRowCell--checkBox">
@ -132,7 +143,7 @@
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<a class="kuiLink" ng-click="listingController.open(item)">
<a class="kuiLink" ng-href="{{ listingController.getUrlForItem(item) }}">
{{ item.title }}
</a>
</div>
@ -149,8 +160,20 @@
{{ listingController.getSelectedItemsCount() }} selected
</div>
</div>
<div class="kuiToolBarFooterSection">
<!-- We need an empty section for the buttons to be positioned consistently. -->
<div class="kuiToolBarSection">
<!-- Pagination -->
<tool-bar-pager-text
start-item="listingController.pager.startItem"
end-item="listingController.pager.endItem"
total-items="listingController.pager.totalItems"
></tool-bar-pager-text>
<tool-bar-pager-buttons
has-previous-page="listingController.pager.hasPreviousPage"
has-next-page="listingController.pager.hasNextPage"
on-page-next="listingController.onPageNext"
on-page-previous="listingController.onPagePrevious"
></tool-bar-pager-buttons>
</div>
</div>
</div>

View file

@ -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}`;
};
}

View file

@ -3,16 +3,14 @@
<kbn-top-nav name="visualize" config="topNavMenu">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Title. -->
<div
<!-- Breadcrumbs. -->
<bread-crumbs
data-transclude-slot="topLeftCorner"
class="kuiLocalTitle"
>
<span
ng-show="savedVis.id"
ng-bind="savedVis.lastSavedTitle"
></span>
</div>
page-title="getVisualizationTitle()"
use-links="true"
omit-current-page="true"
omit-pages="['edit']"
></bread-crumbs>
<!-- Search. -->
<div
@ -29,11 +27,11 @@
ng-dblclick="unlink()"
tooltip="Double click to unlink from Saved Search"
>
<i aria-hidden="true" class="fa fa-chain-broken"></i>
<span aria-hidden="true" class="fa fa-chain-broken"></span>
</a>
</div>
<!-- Allow searching if there is no linked Saved Searc. -->
<!-- Allow searching if there is no linked Saved Search. -->
<form
ng-if="vis.type.requiresSearch && !$state.linked"
name="queryInput"

View file

@ -18,9 +18,10 @@ import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import editorTemplate from 'plugins/kibana/visualize/editor/editor.html';
import { DashboardConstants } from 'plugins/kibana/dashboard/dashboard_constants';
import { VisualizeConstants } from '../visualize_constants';
uiRoutes
.when('/visualize/create', {
.when(VisualizeConstants.CREATE_PATH, {
template: editorTemplate,
resolve: {
savedVis: function (savedVisualizations, courier, $route, Private) {
@ -37,7 +38,7 @@ uiRoutes
}
}
})
.when('/visualize/edit/:id', {
.when(`${VisualizeConstants.EDIT_PATH}/:id`, {
template: editorTemplate,
resolve: {
savedVis: function (savedVisualizations, courier, $route) {
@ -104,20 +105,10 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
const searchSource = savedVis.searchSource;
$scope.topNavMenu = [{
key: 'new',
description: 'New Visualization',
run: function () { kbnUrl.change('/visualize', {}); },
testId: 'visualizeNewButton',
}, {
key: 'save',
description: 'Save Visualization',
template: require('plugins/kibana/visualize/editor/panels/save.html'),
testId: 'visualizeSaveButton',
}, {
key: 'open',
description: 'Open Saved Visualization',
template: require('plugins/kibana/visualize/editor/panels/load.html'),
testId: 'visualizeOpenButton',
}, {
key: 'share',
description: 'Share Visualization',
@ -224,6 +215,10 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
$state.replace();
$scope.getVisualizationTitle = function getVisualizationTitle() {
return savedVis.lastSavedTitle || `${savedVis.title} (unsaved)`;
};
$scope.$watch('searchSource.get("index").timeFieldName', function (timeField) {
timefilter.enabled = !!timeField;
});
@ -292,10 +287,6 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
}
};
$scope.startOver = function () {
kbnUrl.change('/visualize', {});
};
/**
* Called when the user clicks "Save" button.
*/
@ -321,7 +312,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
} else if (savedVis.id === $route.current.params.id) {
docTitle.change(savedVis.lastSavedTitle);
} else {
kbnUrl.change('/visualize/edit/{{id}}', { id: savedVis.id });
kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id });
}
}
}, notify.fatal);

View file

@ -1,2 +0,0 @@
<div class="kuiLocalDropdownTitle">Open Visualization</div>
<saved-object-finder type="visualizations"></saved-object-finder>

View file

@ -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

View file

@ -0,0 +1,197 @@
<!-- Local nav. -->
<kbn-top-nav name="visualize">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Title. -->
<div
data-transclude-slot="topLeftCorner"
class="kuiLocalTitle"
>
Visualize
</div>
</div>
</kbn-top-nav>
<div class="kuiViewContent kuiViewContent--constrainedWidth">
<!-- ControlledTable -->
<div class="kuiViewContentItem kuiControlledTable kuiVerticalRhythm">
<!-- ToolBar -->
<div class="kuiToolBar">
<div class="kuiToolBarSearch">
<div class="kuiToolBarSearchBox">
<div class="kuiToolBarSearchBox__icon kuiIcon fa-search"></div>
<input
class="kuiToolBarSearchBox__input"
type="text"
placeholder="Search..."
aria-label="Filter"
ng-model="listingController.filter"
>
</div>
</div>
<div class="kuiToolBarSection">
<!-- Bulk delete button -->
<button
class="kuiButton kuiButton--danger"
aria-label="Delete selected objects"
ng-if="listingController.getSelectedItemsCount() > 0"
ng-click="listingController.deleteSelectedItems()"
tooltip="Delete selected visualizations"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-trash"></span>
</button>
<!-- Create visualization button -->
<a
class="kuiButton kuiButton--primary"
href="#/visualize/new"
aria-label="Create new visualization"
ng-if="listingController.getSelectedItemsCount() === 0"
tooltip="Create new visualization"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-plus"></span>
</a>
</div>
<div class="kuiToolBarSection">
<!-- Pagination -->
<tool-bar-pager-text
start-item="listingController.pager.startItem"
end-item="listingController.pager.endItem"
total-items="listingController.pager.totalItems"
></tool-bar-pager-text>
<tool-bar-pager-buttons
has-previous-page="listingController.pager.hasPreviousPage"
has-next-page="listingController.pager.hasNextPage"
on-page-next="listingController.onPageNext"
on-page-previous="listingController.onPagePrevious"
></tool-bar-pager-buttons>
</div>
</div>
<!-- NoItems -->
<div
class="kuiPanel kuiPanel--centered kuiPanel--withHeader"
ng-if="!listingController.items.length && listingController.filter"
>
<div class="kuiNoItems">
No visualizations matched your search.
</div>
</div>
<!-- PromptForItems -->
<div
class="kuiPanel kuiPanel--centered kuiPanel--withHeader"
ng-if="!listingController.items.length && !listingController.filter"
>
<div class="kuiPromptForItems">
<div class="kuiPromptForItems__message">
Looks like you don&rsquo;t have any visualizations. Let&rsquo;s create some!
</div>
<div class="kuiPromptForItems__actions">
<a
class="kuiButton kuiButton--primary kuiButton--iconText"
href="#/visualize/new"
>
<span class="kuiButton__icon kuiIcon fa-plus"></span>
Create a visualization
</a>
</div>
</div>
</div>
<!-- Table -->
<table class="kuiTable" ng-if="listingController.items.length">
<thead>
<tr>
<th class="kuiTableHeaderCell kuiTableHeaderCell--checkBox">
<input
type="checkbox"
class="kuiCheckBox"
ng-checked="listingController.areAllItemsChecked()"
ng-click="listingController.toggleAll()"
>
</th>
<th class="kuiTableHeaderCell" ng-click="listingController.sortOn('title')">
Name
<span
class="kuiIcon"
ng-show="listingController.getSortProperty().name === 'title'"
ng-class="listingController.isAscending() ? 'fa-caret-up' : 'fa-caret-down'"
></span>
</th>
<th class="kuiTableHeaderCell" ng-click="listingController.sortOn('type')">
Type
<span
class="kuiIcon"
ng-show="listingController.getSortProperty().name === 'type'"
ng-class="listingController.isAscending() ? 'fa-caret-up' : 'fa-caret-down'"
></span>
</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="item in listingController.pageOfItems track by item.id"
class="kuiTableRow"
>
<td class="kuiTableRowCell kuiTableRowCell--checkBox">
<input
type="checkbox"
class="kuiCheckBox"
ng-click="listingController.toggleItem(item)"
ng-checked="listingController.isItemChecked(item)"
>
</td>
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<a class="kuiLink" ng-href="{{ listingController.getUrlForItem(item) }}">
{{ item.title }}
</a>
</div>
</td>
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<span class="kuiStatusText">
<span class="kuiStatusText__icon kuiIcon {{ item.icon }}"></span>
{{ item.type.title }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
<!-- ToolBarFooter -->
<div class="kuiToolBarFooter">
<div class="kuiToolBarFooterSection">
<div class="kuiToolBarText" ng-hide="listingController.getSelectedItemsCount() === 0">
{{ listingController.getSelectedItemsCount() }} selected
</div>
</div>
<div class="kuiToolBarFooterSection">
<!-- Pagination -->
<tool-bar-pager-text
start-item="listingController.pager.startItem"
end-item="listingController.pager.endItem"
total-items="listingController.pager.totalItems"
></tool-bar-pager-text>
<tool-bar-pager-buttons
has-previous-page="listingController.pager.hasPreviousPage"
has-next-page="listingController.pager.hasNextPage"
on-page-next="listingController.onPageNext"
on-page-previous="listingController.onPagePrevious"
></tool-bar-pager-buttons>
</div>
</div>
</div>
</div>

View file

@ -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}`;
};
}

View file

@ -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;
// 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;
}
.wizard-type-heading {
flex: 0 0 200px;
display: flex;
align-items: center;
font-size: 1.2em;
}
.wizard-type {
flex: 1;
.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;
}
// TODO: When we migrate off Bootstrap, we can eliminate these "mixins".
.list-group-item();
.list-group-menu .list-group-menu-item();
.wizard-type-heading-text {
flex: 1 0 auto;
}
border: none;
border-radius: 0;
background-color: @kibanaGray6;
.wizard-type-description {
flex: 1 1 auto;
color: @wizard-vis-type-description-color;
}
.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-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;
}

View file

@ -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',
};

View file

@ -1,33 +1,47 @@
<div class="visualizeWizardBreadcrumbs">
<bread-crumbs></bread-crumbs>
</div>
<div class="wizard">
<div class="wizard-column">
<h3 class="wizard-sub-title">Create New Visualization</h3>
<div class="wizard-row">
<!-- Local nav. -->
<kbn-top-nav name="visualize">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Breadcrumbs. -->
<bread-crumbs
data-transclude-slot="topLeftCorner"
use-links="true"
omit-current-page="true"
page-title="'New'"
></bread-crumbs>
</div>
</kbn-top-nav>
<div class="kuiViewContent kuiViewContent--constrainedWidth">
<!-- Header -->
<div class="kuiViewContentItem">
<h1 class="kuiTitle kuiVerticalRhythm">
Select visualization type
</h1>
<div class="kuiVerticalRhythm">
<a
class="wizard-type"
ng-repeat="type in visTypes.inTitleOrder"
ng-href="{{ visTypeUrl(type) }}"
>
<div class="wizard-type-heading">
<i
<span
aria-hidden="true"
class="wizard-type-heading-icon fa fa-fw"
ng-class="type.icon"
></i>
<h4 class="wizard-type-heading-text">{{type.title}}</h4>
></span>
<h4
data-test-subj="visualizeWizardChartTypeTitle"
class="wizard-type-heading-text"
>
{{type.title}}
</h4>
</div>
<p class="wizard-type-description">{{type.description}}</p>
</a>
</div>
</div>
<div class="wizard-column">
<h3 class="wizard-sub-title">Or, Open a Saved Visualization</h3>
<saved-object-finder
title="Saved Visualizations"
type="visualizations"
class="wizard-row"
></saved-object-finder>
</div>
</div>

View file

@ -1,24 +1,44 @@
<div class="visualizeWizardBreadcrumbs">
<bread-crumbs></bread-crumbs>
</div>
<div class="wizard">
<div class="wizard-column wizard-column--small">
<h3 class="wizard-sub-title">From a New Search, Select Index</h3>
<paginated-selectable-list
per-page="20"
list="indexPattern.list"
user-make-url="makeUrl"
class="wizard-row"
></paginated-selectable-list>
<!-- Local nav. -->
<kbn-top-nav name="visualize">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Breadcrumbs. -->
<bread-crumbs
data-transclude-slot="topLeftCorner"
use-links="true"
omit-current-page="true"
page-title="'Choose search source'"
></bread-crumbs>
</div>
<div class="wizard-column wizard-column--large">
<h3 class="wizard-sub-title">Or, From a Saved Search</h3>
<!-- Saved searches -->
<saved-object-finder
</kbn-top-nav>
<div class="kuiViewContent kuiViewContent--constrainedWidth">
<div class="wizard kuiViewContentItem">
<div class="wizard-column wizard-column--small">
<h1 class="kuiTitle kuiVerticalRhythm">
From a New Search, Select Index
</h1>
<paginated-selectable-list
per-page="20"
list="indexPattern.list"
user-make-url="makeUrl"
class="wizard-row kuiVerticalRhythm"
></paginated-selectable-list>
</div>
<div class="wizard-column wizard-column--large">
<h2 class="kuiTitle kuiVerticalRhythm">
Or, From a Saved Search
</h2>
<!-- Saved searches -->
<saved-object-finder
title="Saved Searches"
type="searches"
class="wizard-row"
class="wizard-row kuiVerticalRhythm"
make-url="step2WithSearchUrl"
></saved-object-finder>
</div>
</div>
</div>

View file

@ -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 '<div ng-controller="VisualizeWizardStep' + num + '" class="container-fluid vis-wizard">' + txt + '</div>';
};
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}`;
};
});

View file

@ -1,6 +0,0 @@
.visualizeWizardBreadcrumbs {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
}

View file

@ -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);

View file

@ -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 {

View file

@ -0,0 +1 @@
import './pager_factory';

View file

@ -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;
}
}

View file

@ -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);
}
};
});

View file

@ -0,0 +1,16 @@
<div class="kuiButtonGroup">
<button
class="kuiButton kuiButton--basic kuiButton--icon"
ng-click="toolBarPagerButtons.previousPage()"
ng-disabled="!toolBarPagerButtons.hasPreviousPage"
>
<span class="kuiButton__icon kuiIcon fa-chevron-left"></span>
</button>
<button
class="kuiButton kuiButton--basic kuiButton--icon"
ng-click="toolBarPagerButtons.nextPage()"
ng-disabled="!toolBarPagerButtons.hasNextPage"
>
<span class="kuiButton__icon kuiIcon fa-chevron-right"></span>
</button>
</div>

View file

@ -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();
};
}
};
});

View file

@ -0,0 +1,3 @@
<div class="kuiToolBarText">
{{ toolBarPagerText.startItem | number }}&ndash;{{ toolBarPagerText.endItem | number }} of {{ toolBarPagerText.totalItems | number }}
</div>

View file

@ -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 {
}
};
});

View file

@ -0,0 +1,2 @@
import './components/tool_bar_pager_text/tool_bar_pager_text';
import './components/tool_bar_pager_buttons/tool_bar_pager_buttons';

View file

@ -621,6 +621,7 @@ fieldset {
.kuiViewContent {
padding-top: 20px;
padding-bottom: 20px;
width: 100%;
}
.kuiViewContent--constrainedWidth {

View file

@ -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();

View file

@ -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() {

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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]);

View file

@ -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() {

View file

@ -30,3 +30,7 @@
.kuiIcon--inactive {
color: $inactiveColor;
}
.kuiIcon--basic {
color: #565656;
}

View file

@ -70,7 +70,7 @@
overflow: hidden;
text-overflow: ellipsis;
* > {
& > * {
vertical-align: middle; /* 1 */
}
}

View file

@ -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 */ }

View file

@ -0,0 +1 @@
<div class="kuiIcon kuiIcon--basic fa-train"></div>

View file

@ -17,6 +17,12 @@ export default createExample([{
<p>Use this Icon to denote useful information.</p>
),
html: require('./icon_info.html'),
}, {
title: 'Basic',
description: (
<p>Use this Icon when you don't want to communicate any particular meaning with the icon's color.</p>
),
html: require('./icon_basic.html'),
hasDarkTheme: false,
}, {
title: 'Success',