diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js
index 4711fb1edb018..377fd72e9c771 100644
--- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js
+++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js
@@ -554,8 +554,8 @@ function discoverController(
$scope.$watchCollection('state.sort', function (sort) {
if (!sort) return;
- // get the current sort from {key: val} to ["key", "val"];
- const currentSort = _.pairs($scope.searchSource.getField('sort')).pop();
+ // get the current sort from searchSource as array of arrays
+ const currentSort = getSort.array($scope.searchSource.getField('sort'), $scope.indexPattern);
// if the searchSource doesn't know, tell it so
if (!angular.equals(sort, currentSort)) $scope.fetch();
@@ -862,8 +862,8 @@ function discoverController(
.setField('filter', queryFilter.getFilters());
});
- $scope.setSortOrder = function setSortOrder(columnName, direction) {
- $scope.state.sort = [columnName, direction];
+ $scope.setSortOrder = function setSortOrder(sortPair) {
+ $scope.state.sort = sortPair;
};
// TODO: On array fields, negating does not negate the combination, rather all terms
diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js
index bec26d38d0fa7..b8b962b9f92d7 100644
--- a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js
+++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js
@@ -23,7 +23,7 @@ import ngMock from 'ng_mock';
import { getSort } from '../../lib/get_sort';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
-const defaultSort = { time: 'desc' };
+const defaultSort = [{ time: 'desc' }];
let indexPattern;
describe('docTable', function () {
@@ -38,11 +38,11 @@ describe('docTable', function () {
expect(getSort).to.be.a(Function);
});
- it('should return an object if passed a 2 item array', function () {
- expect(getSort(['bytes', 'desc'], indexPattern)).to.eql({ bytes: 'desc' });
+ it('should return an array of objects if passed a 2 item array', function () {
+ expect(getSort(['bytes', 'desc'], indexPattern)).to.eql([{ bytes: 'desc' }]);
delete indexPattern.timeFieldName;
- expect(getSort(['bytes', 'desc'], indexPattern)).to.eql({ bytes: 'desc' });
+ expect(getSort(['bytes', 'desc'], indexPattern)).to.eql([{ bytes: 'desc' }]);
});
it('should sort by the default when passed an unsortable field', function () {
@@ -50,7 +50,7 @@ describe('docTable', function () {
expect(getSort(['lol_nope', 'asc'], indexPattern)).to.eql(defaultSort);
delete indexPattern.timeFieldName;
- expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql({ _score: 'desc' });
+ expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql([{ _score: 'desc' }]);
});
it('should sort in reverse chrono order otherwise on time based patterns', function () {
@@ -62,9 +62,9 @@ describe('docTable', function () {
it('should sort by score on non-time patterns', function () {
delete indexPattern.timeFieldName;
- expect(getSort([], indexPattern)).to.eql({ _score: 'desc' });
- expect(getSort(['foo'], indexPattern)).to.eql({ _score: 'desc' });
- expect(getSort({ foo: 'bar' }, indexPattern)).to.eql({ _score: 'desc' });
+ expect(getSort([], indexPattern)).to.eql([{ _score: 'desc' }]);
+ expect(getSort(['foo'], indexPattern)).to.eql([{ _score: 'desc' }]);
+ expect(getSort({ foo: 'bar' }, indexPattern)).to.eql([{ _score: 'desc' }]);
});
});
@@ -73,8 +73,8 @@ describe('docTable', function () {
expect(getSort.array).to.be.a(Function);
});
- it('should return an array for sortable fields', function () {
- expect(getSort.array(['bytes', 'desc'], indexPattern)).to.eql([ 'bytes', 'desc' ]);
+ it('should return an array of arrays for sortable fields', function () {
+ expect(getSort.array(['bytes', 'desc'], indexPattern)).to.eql([[ 'bytes', 'desc' ]]);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx
index c294d43e4ff4b..ea2c65b1b8487 100644
--- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx
+++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.test.tsx
@@ -59,7 +59,7 @@ function getMockProps(props = {}) {
indexPattern: getMockIndexPattern(),
hideTimeColumn: false,
columns: ['first', 'middle', 'last'],
- sortOrder: ['time', 'asc'] as SortOrder,
+ sortOrder: [['time', 'asc']] as SortOrder[],
isShortDots: true,
onRemoveColumn: jest.fn(),
onChangeSortOrder: jest.fn(),
@@ -89,7 +89,7 @@ describe('TableHeader with time column', () => {
test('time column is sortable with button, cycling sort direction', () => {
findTestSubject(wrapper, 'docTableHeaderFieldSort_time').simulate('click');
- expect(props.onChangeSortOrder).toHaveBeenCalledWith('time', 'desc');
+ expect(props.onChangeSortOrder).toHaveBeenCalledWith([['time', 'desc']]);
});
test('time column is not removeable, no button displayed', () => {
diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx
index f67f3babe2b42..abc8077afb669 100644
--- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx
+++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/table_header.tsx
@@ -28,10 +28,10 @@ interface Props {
hideTimeColumn: boolean;
indexPattern: IndexPattern;
isShortDots: boolean;
- onChangeSortOrder?: (name: string, direction: 'asc' | 'desc') => void;
+ onChangeSortOrder?: (sortOrder: SortOrder[]) => void;
onMoveColumn?: (name: string, index: number) => void;
onRemoveColumn?: (name: string) => void;
- sortOrder: SortOrder;
+ sortOrder: SortOrder[];
}
export function TableHeader({
@@ -45,7 +45,6 @@ export function TableHeader({
sortOrder,
}: Props) {
const displayedColumns = getDisplayedColumns(columns, indexPattern, hideTimeColumn, isShortDots);
- const [currColumnName, currDirection = 'asc'] = sortOrder;
return (
@@ -55,7 +54,7 @@ export function TableHeader({
void;
+ onChangeSortOrder?: (sortOrder: SortOrder[]) => void;
onMoveColumn?: (name: string, idx: number) => void;
onRemoveColumn?: (name: string) => void;
- sortDirection: 'asc' | 'desc' | ''; // asc|desc -> field is sorted in this direction, else ''
+ sortOrder: SortOrder[];
}
+const sortDirectionToIcon = {
+ desc: 'fa fa-sort-down',
+ asc: 'fa fa-sort-up',
+ '': 'fa fa-sort',
+};
+
export function TableHeaderColumn({
colLeftIdx,
colRightIdx,
@@ -46,43 +53,78 @@ export function TableHeaderColumn({
onChangeSortOrder,
onMoveColumn,
onRemoveColumn,
- sortDirection,
+ sortOrder,
}: Props) {
- const btnSortIcon = sortDirection === 'desc' ? 'fa fa-sort-down' : 'fa fa-sort-up';
+ const [, sortDirection = ''] = sortOrder.find(sortPair => name === sortPair[0]) || [];
+ const currentSortWithoutColumn = sortOrder.filter(pair => pair[0] !== name);
+ const currentColumnSort = sortOrder.find(pair => pair[0] === name);
+ const currentColumnSortDirection = (currentColumnSort && currentColumnSort[1]) || '';
+
+ const btnSortIcon = sortDirectionToIcon[sortDirection];
const btnSortClassName =
sortDirection !== '' ? btnSortIcon : `kbnDocTableHeader__sortChange ${btnSortIcon}`;
+ const handleChangeSortOrder = () => {
+ if (!onChangeSortOrder) return;
+
+ // Cycle goes Unsorted -> Asc -> Desc -> Unsorted
+ if (currentColumnSort === undefined) {
+ onChangeSortOrder([...currentSortWithoutColumn, [name, 'asc']]);
+ } else if (currentColumnSortDirection === 'asc') {
+ onChangeSortOrder([...currentSortWithoutColumn, [name, 'desc']]);
+ } else if (currentColumnSortDirection === 'desc' && currentSortWithoutColumn.length === 0) {
+ // If we're at the end of the cycle and this is the only existing sort, we switch
+ // back to ascending sort instead of removing it.
+ onChangeSortOrder([[name, 'asc']]);
+ } else {
+ onChangeSortOrder(currentSortWithoutColumn);
+ }
+ };
+
+ const getSortButtonAriaLabel = () => {
+ const sortAscendingMessage = i18n.translate(
+ 'kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel',
+ {
+ defaultMessage: 'Sort {columnName} ascending',
+ values: { columnName: name },
+ }
+ );
+ const sortDescendingMessage = i18n.translate(
+ 'kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel',
+ {
+ defaultMessage: 'Sort {columnName} descending',
+ values: { columnName: name },
+ }
+ );
+ const stopSortingMessage = i18n.translate(
+ 'kbn.docTable.tableHeader.sortByColumnUnsortedAriaLabel',
+ {
+ defaultMessage: 'Stop sorting on {columnName}',
+ values: { columnName: name },
+ }
+ );
+
+ if (currentColumnSort === undefined) {
+ return sortAscendingMessage;
+ } else if (sortDirection === 'asc') {
+ return sortDescendingMessage;
+ } else if (sortDirection === 'desc' && currentSortWithoutColumn.length === 0) {
+ return sortAscendingMessage;
+ } else {
+ return stopSortingMessage;
+ }
+ };
+
// action buttons displayed on the right side of the column name
const buttons = [
// Sort Button
{
active: isSortable && typeof onChangeSortOrder === 'function',
- ariaLabel:
- sortDirection === 'asc'
- ? i18n.translate('kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel', {
- defaultMessage: 'Sort {columnName} descending',
- values: { columnName: name },
- })
- : i18n.translate('kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel', {
- defaultMessage: 'Sort {columnName} ascending',
- values: { columnName: name },
- }),
+ ariaLabel: getSortButtonAriaLabel(),
className: btnSortClassName,
- onClick: () => {
- /**
- * cycle sorting direction
- * asc -> desc, desc -> asc, default: asc
- */
- if (typeof onChangeSortOrder === 'function') {
- const newDirection = sortDirection === 'asc' ? 'desc' : 'asc';
- onChangeSortOrder(name, newDirection);
- }
- },
+ onClick: handleChangeSortOrder,
testSubject: `docTableHeaderFieldSort_${name}`,
- tooltip: i18n.translate('kbn.docTable.tableHeader.sortByColumnTooltip', {
- defaultMessage: 'Sort by {columnName}',
- values: { columnName: name },
- }),
+ tooltip: getSortButtonAriaLabel(),
},
// Remove Button
{
diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html b/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html
index 075468b76090b..b73f204626b9c 100644
--- a/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html
+++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html
@@ -46,7 +46,6 @@
filters="filters"
class="kbnDocTable__row"
on-add-column="onAddColumn"
- on-change-sort-order="onChangeSortOrder"
on-remove-column="onRemoveColumn"
>
@@ -98,7 +97,6 @@
ng-class="{'kbnDocTable__row--highlight': row['$$_isAnchor']}"
data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}"
on-add-column="onAddColumn"
- on-change-sort-order="onChangeSortOrder"
on-remove-column="onRemoveColumn"
>
diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js
index 82195823c6749..9aba887569dbf 100644
--- a/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js
+++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js
@@ -19,42 +19,49 @@
import _ from 'lodash';
+function isSortable(field, indexPattern) {
+ return (indexPattern.fields.byName[field] && indexPattern.fields.byName[field].sortable);
+}
+
+function createSortObject(sortPair, indexPattern) {
+
+ if (Array.isArray(sortPair) && sortPair.length === 2 && isSortable(sortPair[0], indexPattern)) {
+ const [ field, direction ] = sortPair;
+ return { [field]: direction };
+ }
+ else {
+ return undefined;
+ }
+}
+
/**
* Take a sorting array and make it into an object
- * @param {array} 2 item array [fieldToSort, directionToSort]
+ * @param {array} sort 2 item array [fieldToSort, directionToSort]
* @param {object} indexPattern used for determining default sort
* @returns {object} a sort object suitable for returning to elasticsearch
*/
export function getSort(sort, indexPattern, defaultSortOrder = 'desc') {
- const sortObj = {};
- let field;
- let direction;
- function isSortable(field) {
- return (indexPattern.fields.byName[field] && indexPattern.fields.byName[field].sortable);
+ let sortObjects;
+ if (Array.isArray(sort)) {
+ if (sort.length > 0 && !Array.isArray(sort[0])) {
+ sort = [sort];
+ }
+ sortObjects = _.compact(sort.map((sortPair) => createSortObject(sortPair, indexPattern)));
}
- if (Array.isArray(sort) && sort.length === 2 && isSortable(sort[0])) {
- // At some point we need to refactor the sorting logic, this array sucks.
- field = sort[0];
- direction = sort[1];
- } else if (indexPattern.timeFieldName && isSortable(indexPattern.timeFieldName)) {
- field = indexPattern.timeFieldName;
- direction = defaultSortOrder;
+ if (!_.isEmpty(sortObjects)) {
+ return sortObjects;
}
-
- if (field) {
- sortObj[field] = direction;
- } else {
- sortObj._score = 'desc';
+ else if (indexPattern.timeFieldName && isSortable(indexPattern.timeFieldName, indexPattern)) {
+ return [{ [indexPattern.timeFieldName]: defaultSortOrder }];
+ }
+ else {
+ return [{ _score: 'desc' }];
}
-
-
-
- return sortObj;
}
getSort.array = function (sort, indexPattern, defaultSortOrder) {
- return _(getSort(sort, indexPattern, defaultSortOrder)).pairs().pop();
+ return getSort(sort, indexPattern, defaultSortOrder).map((sortPair) => _(sortPair).pairs().pop());
};
diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts
index 7dec893f68025..56f368f2a5161 100644
--- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts
@@ -45,11 +45,11 @@ import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
interface SearchScope extends ng.IScope {
columns?: string[];
description?: string;
- sort?: string[];
+ sort?: string[] | string[][];
searchSource?: SearchSource;
sharedItemTitle?: string;
inspectorAdapters?: Adapters;
- setSortOrder?: (column: string, columnDirection: string) => void;
+ setSortOrder?: (sortPair: [string, string]) => void;
removeColumn?: (column: string) => void;
addColumn?: (column: string) => void;
moveColumn?: (column: string, index: number) => void;
@@ -201,8 +201,8 @@ export class SearchEmbeddable extends Embeddable
this.pushContainerStateParamsToScope(searchScope);
- searchScope.setSortOrder = (columnName, direction) => {
- searchScope.sort = [columnName, direction];
+ searchScope.setSortOrder = sortPair => {
+ searchScope.sort = sortPair;
this.updateInput({ sort: searchScope.sort });
};
@@ -263,6 +263,9 @@ export class SearchEmbeddable extends Embeddable
// been overridden in a dashboard.
searchScope.columns = this.input.columns || this.savedSearch.columns;
searchScope.sort = this.input.sort || this.savedSearch.sort;
+ if (searchScope.sort.length && !Array.isArray(searchScope.sort[0])) {
+ searchScope.sort = [searchScope.sort];
+ }
searchScope.sharedItemTitle = this.panelTitle;
if (
diff --git a/src/legacy/ui/public/styles/_legacy/components/_table.scss b/src/legacy/ui/public/styles/_legacy/components/_table.scss
index 94f30fa1f3498..e7c1bda829f0e 100644
--- a/src/legacy/ui/public/styles/_legacy/components/_table.scss
+++ b/src/legacy/ui/public/styles/_legacy/components/_table.scss
@@ -33,14 +33,14 @@ table {
button.fa-sort-down,
i.fa-sort-asc,
i.fa-sort-down {
- color: $euiColorLightShade;
+ color: $euiColorPrimary;
}
button.fa-sort-desc,
button.fa-sort-up,
i.fa-sort-desc,
i.fa-sort-up {
- color: $euiColorLightShade;
+ color: $euiColorPrimary;
}
}
}
diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js
index 4b85593d1f02c..f0d34207a87a2 100644
--- a/test/functional/apps/discover/_shared_links.js
+++ b/test/functional/apps/discover/_shared_links.js
@@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }) {
':(from:\'2015-09-19T06:31:44.000Z\',to:\'2015-09' +
'-23T18:31:44.000Z\'))&_a=(columns:!(_source),index:\'logstash-' +
'*\',interval:auto,query:(language:kuery,query:\'\')' +
- ',sort:!(\'@timestamp\',desc))';
+ ',sort:!(!(\'@timestamp\',desc)))';
const actualUrl = await PageObjects.share.getSharedUrl();
// strip the timestamp out of each URL
expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be(
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 0a2eab567d208..93a8e0e831ec8 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -1568,7 +1568,6 @@
"kbn.docTable.tableHeader.removeColumnButtonTooltip": "列を削除",
"kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel": "{columnName} を昇順に並べ替える",
"kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel": "{columnName} を降順に並べ替える",
- "kbn.docTable.tableHeader.sortByColumnTooltip": "{columnName} で並べ替えます",
"kbn.docTable.tableRow.detailHeading": "拡張ドキュメント",
"kbn.docTable.tableRow.filterForValueButtonAriaLabel": "値でフィルタリング",
"kbn.docTable.tableRow.filterForValueButtonTooltip": "値でフィルタリング",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1b4dbfbf0d5d8..bc14e4220bb14 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -1568,7 +1568,6 @@
"kbn.docTable.tableHeader.removeColumnButtonTooltip": "删除列",
"kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel": "升序排序 {columnName}",
"kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel": "降序排序 {columnName}",
- "kbn.docTable.tableHeader.sortByColumnTooltip": "按“{columnName}”排序",
"kbn.docTable.tableRow.detailHeading": "已展开文档",
"kbn.docTable.tableRow.filterForValueButtonAriaLabel": "筛留值",
"kbn.docTable.tableRow.filterForValueButtonTooltip": "筛留值",