De-angularize discover kbnTableHeader (#41259) (#41979)

* Add TableHeader + TableHeaderColumn react components

* Migration of kbnTableHeader to use react component

* Disable mocha tests

* Add jest tests
This commit is contained in:
Matthias Wilhelm 2019-07-25 19:50:13 +02:00 committed by GitHub
parent f57a2a774e
commit 509531fc64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 763 additions and 361 deletions

View file

@ -27,9 +27,6 @@ import $ from 'jquery';
import 'plugins/kibana/discover/index';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
const SORTABLE_FIELDS = ['bytes', '@timestamp'];
const UNSORTABLE_FIELDS = ['request_body'];
describe('Doc Table', function () {
let $parentScope;
let $scope;
@ -119,155 +116,6 @@ describe('Doc Table', function () {
});
};
describe('kbnTableHeader', function () {
const $elem = angular.element(`
<thead
kbn-table-header
columns="columns"
index-pattern="indexPattern"
sort-order="sortOrder"
on-change-sort-order="onChangeSortOrder"
on-move-column="moveColumn"
on-remove-column="removeColumn"
></thead>
`);
beforeEach(function () {
init($elem, {
columns: [],
sortOrder: [],
onChangeSortOrder: sinon.stub(),
moveColumn: sinon.spy(),
removeColumn: sinon.spy(),
});
});
afterEach(function () {
destroy();
});
describe('adding and removing columns', function () {
columnTests('[data-test-subj~="docTableHeaderField"]', $elem);
});
describe('sorting button', function () {
beforeEach(function () {
$parentScope.columns = ['bytes', '_source'];
$elem.scope().$digest();
});
it('should show for sortable columns', function () {
expect($elem.find(`[data-test-subj="docTableHeaderFieldSort_bytes"]`).length).to.be(1);
});
it('should not be shown for unsortable columns', function () {
expect($elem.find(`[data-test-subj="docTableHeaderFieldSort__source"]`).length).to.be(0);
});
});
describe('cycleSortOrder function', function () {
it('should exist', function () {
expect($scope.cycleSortOrder).to.be.a(Function);
});
it('should call onChangeSortOrder with ascending order for a sortable field without sort order', function () {
$scope.sortOrder = [];
$scope.cycleSortOrder(SORTABLE_FIELDS[0]);
expect($scope.onChangeSortOrder.callCount).to.be(1);
expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'asc']);
});
it('should call onChangeSortOrder with ascending order for a sortable field already sorted by in descending order', function () {
$scope.sortOrder = [SORTABLE_FIELDS[0], 'desc'];
$scope.cycleSortOrder(SORTABLE_FIELDS[0]);
expect($scope.onChangeSortOrder.callCount).to.be(1);
expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'asc']);
});
it('should call onChangeSortOrder with ascending order for a sortable field when already sorted by an different field', function () {
$scope.sortOrder = [SORTABLE_FIELDS[1], 'asc'];
$scope.cycleSortOrder(SORTABLE_FIELDS[0]);
expect($scope.onChangeSortOrder.callCount).to.be(1);
expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'asc']);
});
it('should call onChangeSortOrder with descending order for a sortable field already sorted by in ascending order', function () {
$scope.sortOrder = [SORTABLE_FIELDS[0], 'asc'];
$scope.cycleSortOrder(SORTABLE_FIELDS[0]);
expect($scope.onChangeSortOrder.callCount).to.be(1);
expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'desc']);
});
it('should not call onChangeSortOrder for an unsortable field', function () {
$scope.sortOrder = [];
$scope.cycleSortOrder(UNSORTABLE_FIELDS[0]);
expect($scope.onChangeSortOrder.callCount).to.be(0);
});
it('should not try to call onChangeSortOrder when it is not defined', function () {
$scope.onChangeSortOrder = undefined;
expect(() => $scope.cycleSortOrder(SORTABLE_FIELDS[0])).to.not.throwException();
});
});
describe('headerClass function', function () {
it('should exist', function () {
expect($scope.headerClass).to.be.a(Function);
});
it('should return list including kbnDocTableHeader__sortChange for a sortable field not currently sorted by', function () {
expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('kbnDocTableHeader__sortChange');
});
it('should return undefined for an unsortable field', function () {
expect($scope.headerClass(UNSORTABLE_FIELDS[0])).to.be(undefined);
});
it('should return list including fa-sort-up for a sortable field not currently sorted by', function () {
expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('fa-sort-up');
});
it('should return list including fa-sort-up for a sortable field currently sorted by in ascending order', function () {
$scope.sortOrder = [SORTABLE_FIELDS[0], 'asc'];
expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('fa-sort-up');
});
it('should return list including fa-sort-down for a sortable field currently sorted by in descending order', function () {
$scope.sortOrder = [SORTABLE_FIELDS[0], 'desc'];
expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('fa-sort-down');
});
});
describe('moving columns', function () {
beforeEach(function () {
$parentScope.columns = ['bytes', 'request_body', '@timestamp', 'point'];
$elem.scope().$digest();
});
it('should move columns to the right', function () {
$scope.moveColumnRight('bytes');
expect($scope.onMoveColumn.callCount).to.be(1);
expect($scope.onMoveColumn.firstCall.args).to.eql(['bytes', 1]);
});
it('shouldnt move the last column to the right', function () {
$scope.moveColumnRight('point');
expect($scope.onMoveColumn.callCount).to.be(0);
});
it('should move columns to the left', function () {
$scope.moveColumnLeft('@timestamp');
expect($scope.onMoveColumn.callCount).to.be(1);
expect($scope.onMoveColumn.firstCall.args).to.eql(['@timestamp', 1]);
});
it('shouldnt move the first column to the left', function () {
$scope.moveColumnLeft('bytes');
expect($scope.onMoveColumn.callCount).to.be(0);
});
});
});
describe('kbnTableRow', function () {
const $elem = angular.element(
'<tr kbn-table-row="row" ' +

View file

@ -1,7 +1,9 @@
.kbnDocTableHeader button {
margin-left: $euiSizeXS;
}
.kbnDocTableHeader__move,
.kbnDocTableHeader__sortChange {
opacity: 0;
th:hover &,
&:focus {
opacity: 1;

View file

@ -1,79 +0,0 @@
<tr>
<td width="1%"></td>
<th
ng-if="indexPattern.timeFieldName && !hideTimeColumn"
data-test-subj="docTableHeaderField"
scope="col"
>
<span>
<span
i18n-id="kbn.docTable.tableHeader.timeHeaderCellTitle"
i18n-default-message="Time"
></span>
<button
id="docTableHeaderFieldSort{{indexPattern.timeFieldName}}"
tabindex="0"
aria-label="{{ getAriaLabelForColumn(indexPattern.timeFieldName) }}"
ng-class="headerClass(indexPattern.timeFieldName)"
role="button"
ng-click="cycleSortOrder(indexPattern.timeFieldName)"
tooltip="{{ ::'kbn.docTable.tableHeader.sortByTimeTooltip' | i18n: {defaultMessage: 'Sort by time'} }}"
></button>
</span>
</th>
<th
ng-repeat="name in columns"
data-test-subj="docTableHeaderField"
scope="col"
>
<span
data-test-subj="docTableHeader-{{name}}"
>
{{getShortDotsName(name)}}
<button
data-test-subj="docTableHeaderFieldSort_{{name}}"
id="docTableHeaderFieldSort{{name}}"
ng-if="isSortableColumn(name)"
aria-label="{{ getAriaLabelForColumn(name) }}"
ng-class="headerClass(name)"
ng-click="cycleSortOrder(name)"
tooltip="{{tooltip(name)}}"
tooltip-append-to-body="1"
></button>
</span>
<button
class="fa fa-remove kbnDocTableHeader__move"
ng-click="onRemoveColumn(name)"
ng-if="canRemoveColumn(name)"
tooltip-append-to-body="1"
tooltip="{{ ::'kbn.docTable.tableHeader.removeColumnButtonTooltip' | i18n: {defaultMessage: 'Remove column'} }}"
aria-label="{{ 'kbn.docTable.tableHeader.removeColumnButtonAriaLabel' | i18n: {
defaultMessage: 'Remove {columnName} column',
values: {columnName: name}
} }}"
data-test-subj="docTableRemoveHeader-{{name}}"
></button>
<button
class="fa fa-angle-double-left kbnDocTableHeader__move"
ng-click="moveColumnLeft(name)"
ng-if="canMoveColumnLeft(name)"
tooltip-append-to-body="1"
tooltip="{{ ::'kbn.docTable.tableHeader.moveColumnLeftButtonTooltip' | i18n: {defaultMessage: 'Move column to the left'} }}"
aria-label="{{ 'kbn.docTable.tableHeader.moveColumnLeftButtonAriaLabel' | i18n: {
defaultMessage: 'Move {columnName} column to the left',
values: {columnName: name}
} }}"
></button>
<button
class="fa fa-angle-double-right kbnDocTableHeader__move"
ng-click="moveColumnRight(name)"
ng-if="canMoveColumnRight(name)"
tooltip-append-to-body="1"
tooltip="{{ ::'kbn.docTable.tableHeader.moveColumnRightButtonTooltip' | i18n: {defaultMessage: 'Move column to the right'} }}"
aria-label="{{ 'kbn.docTable.tableHeader.moveColumnRightButtonAriaLabel' | i18n: {
defaultMessage: 'Move {columnName} column to the right',
values: {columnName: name}
} }}"
></button>
</th>
</tr>

View file

@ -16,133 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { shortenDottedString } from '../../../../common/utils/shorten_dotted_string';
import headerHtml from './table_header.html';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
import { TableHeader } from './table_header/table_header';
const module = uiModules.get('app/discover');
module.directive('kbnTableHeader', function () {
return {
restrict: 'A',
scope: {
columns: '=',
sortOrder: '=',
indexPattern: '=',
onChangeSortOrder: '=?',
onRemoveColumn: '=?',
onMoveColumn: '=?',
},
template: headerHtml,
controller: function ($scope, config) {
$scope.hideTimeColumn = config.get('doc_table:hideTimeColumn');
$scope.isShortDots = config.get('shortDots:enable');
$scope.getShortDotsName = function getShortDotsName(columnName) {
return $scope.isShortDots ? shortenDottedString(columnName) : columnName;
};
$scope.isSortableColumn = function isSortableColumn(columnName) {
return (
!!$scope.indexPattern
&& _.isFunction($scope.onChangeSortOrder)
&& _.get($scope, ['indexPattern', 'fields', 'byName', columnName, 'sortable'], false)
);
};
$scope.tooltip = function (column) {
if (!$scope.isSortableColumn(column)) return '';
const name = $scope.isShortDots ? shortenDottedString(column) : column;
return i18n.translate('kbn.docTable.tableHeader.sortByColumnTooltip', {
defaultMessage: 'Sort by {columnName}',
values: { columnName: name },
});
};
$scope.canMoveColumnLeft = function canMoveColumn(columnName) {
return (
_.isFunction($scope.onMoveColumn)
&& $scope.columns.indexOf(columnName) > 0
);
};
$scope.canMoveColumnRight = function canMoveColumn(columnName) {
return (
_.isFunction($scope.onMoveColumn)
&& $scope.columns.indexOf(columnName) < $scope.columns.length - 1
);
};
$scope.canRemoveColumn = function canRemoveColumn(columnName) {
return (
_.isFunction($scope.onRemoveColumn)
&& (columnName !== '_source' || $scope.columns.length > 1)
);
};
$scope.headerClass = function (column) {
if (!$scope.isSortableColumn(column)) return;
const sortOrder = $scope.sortOrder;
const defaultClass = ['fa', 'fa-sort-up', 'kbnDocTableHeader__sortChange'];
if (!sortOrder || column !== sortOrder[0]) return defaultClass;
return ['fa', sortOrder[1] === 'asc' ? 'fa-sort-up' : 'fa-sort-down'];
};
$scope.moveColumnLeft = function moveLeft(columnName) {
const newIndex = $scope.columns.indexOf(columnName) - 1;
if (newIndex < 0) {
return;
}
$scope.onMoveColumn(columnName, newIndex);
};
$scope.moveColumnRight = function moveRight(columnName) {
const newIndex = $scope.columns.indexOf(columnName) + 1;
if (newIndex >= $scope.columns.length) {
return;
}
$scope.onMoveColumn(columnName, newIndex);
};
$scope.cycleSortOrder = function cycleSortOrder(columnName) {
if (!$scope.isSortableColumn(columnName)) {
return;
}
const [currentColumnName, currentDirection = 'asc'] = $scope.sortOrder;
const newDirection = (
(columnName === currentColumnName && currentDirection === 'asc')
? 'desc'
: 'asc'
);
$scope.onChangeSortOrder(columnName, newDirection);
};
$scope.getAriaLabelForColumn = function getAriaLabelForColumn(name) {
if (!$scope.isSortableColumn(name)) return null;
const [currentColumnName, currentDirection = 'asc'] = $scope.sortOrder;
if(name === currentColumnName && currentDirection === 'asc') {
return i18n.translate('kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel', {
defaultMessage: 'Sort {columnName} descending',
values: { columnName: name },
});
}
return i18n.translate('kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel', {
defaultMessage: 'Sort {columnName} ascending',
values: { columnName: name },
});
};
module.directive('kbnTableHeader', function (reactDirective, config) {
return reactDirective(
wrapInI18nContext(TableHeader),
undefined,
{ restrict: 'A' },
{
hideTimeColumn: config.get('doc_table:hideTimeColumn'),
isShortDots: config.get('shortDots:enable'),
}
};
);
});

View file

@ -0,0 +1,236 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TableHeader with time column renders correctly 1`] = `
<tr
class="kbnDocTableHeader"
data-test-subj="docTableHeader"
>
<th
style="width: 24px;"
/>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-time"
>
Time
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-time-tt"
aria-label="Sort time descending"
class="fa fa-sort-up"
data-test-subj="docTableHeaderFieldSort_time"
/>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-first"
>
first
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-first-tt"
aria-label="Remove first column"
class="fa fa-remove kbnDocTableHeader__move"
data-test-subj="docTableRemoveHeader-first"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-first-tt"
aria-label="Move first column to the right"
class="fa fa-angle-double-right kbnDocTableHeader__move"
data-test-subj="docTableMoveRightHeader-first"
/>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-middle"
>
middle
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-middle-tt"
aria-label="Remove middle column"
class="fa fa-remove kbnDocTableHeader__move"
data-test-subj="docTableRemoveHeader-middle"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-middle-tt"
aria-label="Move middle column to the left"
class="fa fa-angle-double-left kbnDocTableHeader__move"
data-test-subj="docTableMoveLeftHeader-middle"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-middle-tt"
aria-label="Move middle column to the right"
class="fa fa-angle-double-right kbnDocTableHeader__move"
data-test-subj="docTableMoveRightHeader-middle"
/>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-last"
>
last
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-last-tt"
aria-label="Remove last column"
class="fa fa-remove kbnDocTableHeader__move"
data-test-subj="docTableRemoveHeader-last"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-last-tt"
aria-label="Move last column to the left"
class="fa fa-angle-double-left kbnDocTableHeader__move"
data-test-subj="docTableMoveLeftHeader-last"
/>
</span>
</span>
</th>
</tr>
`;
exports[`TableHeader without time column renders correctly 1`] = `
<tr
class="kbnDocTableHeader"
data-test-subj="docTableHeader"
>
<th
style="width: 24px;"
/>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-first"
>
first
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-first-tt"
aria-label="Remove first column"
class="fa fa-remove kbnDocTableHeader__move"
data-test-subj="docTableRemoveHeader-first"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-first-tt"
aria-label="Move first column to the right"
class="fa fa-angle-double-right kbnDocTableHeader__move"
data-test-subj="docTableMoveRightHeader-first"
/>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-middle"
>
middle
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-middle-tt"
aria-label="Remove middle column"
class="fa fa-remove kbnDocTableHeader__move"
data-test-subj="docTableRemoveHeader-middle"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-middle-tt"
aria-label="Move middle column to the left"
class="fa fa-angle-double-left kbnDocTableHeader__move"
data-test-subj="docTableMoveLeftHeader-middle"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-middle-tt"
aria-label="Move middle column to the right"
class="fa fa-angle-double-right kbnDocTableHeader__move"
data-test-subj="docTableMoveRightHeader-middle"
/>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
data-test-subj="docTableHeader-last"
>
last
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-last-tt"
aria-label="Remove last column"
class="fa fa-remove kbnDocTableHeader__move"
data-test-subj="docTableRemoveHeader-last"
/>
</span>
<span
class="euiToolTipAnchor"
>
<button
aria-describedby="docTableHeader-last-tt"
aria-label="Move last column to the left"
class="fa fa-angle-double-left kbnDocTableHeader__move"
data-test-subj="docTableMoveLeftHeader-last"
/>
</span>
</span>
</th>
</tr>
`;

View file

@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatternEnhanced } from 'ui/index_patterns/_index_pattern';
// @ts-ignore
import { shortenDottedString } from '../../../../../common/utils/shorten_dotted_string';
export type SortOrder = [string, 'asc' | 'desc'];
export interface ColumnProps {
name: string;
displayName: string;
isSortable: boolean;
isRemoveable: boolean;
colLeftIdx: number;
colRightIdx: number;
}
/**
* Returns properties necessary to display the time column
* If it's an IndexPattern with timefield, the time column is
* prepended, not moveable and removeable
* @param timeFieldName
*/
export function getTimeColumn(timeFieldName: string): ColumnProps {
return {
name: timeFieldName,
displayName: 'Time',
isSortable: true,
isRemoveable: false,
colLeftIdx: -1,
colRightIdx: -1,
};
}
/**
* A given array of column names returns an array of properties
* necessary to display the columns. If the given indexPattern
* has a timefield, a time column is prepended
* @param columns
* @param indexPattern
* @param hideTimeField
* @param isShortDots
*/
export function getDisplayedColumns(
columns: string[],
indexPattern: IndexPatternEnhanced,
hideTimeField: boolean,
isShortDots: boolean
) {
if (!Array.isArray(columns) || typeof indexPattern !== 'object' || !indexPattern.getFieldByName) {
return [];
}
const columnProps = columns.map((column, idx) => {
const field = indexPattern.getFieldByName(column);
return {
name: column,
displayName: isShortDots ? shortenDottedString(column) : column,
isSortable: field && field.sortable ? true : false,
isRemoveable: column !== '_source' || columns.length > 1,
colLeftIdx: idx - 1 < 0 ? -1 : idx - 1,
colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1,
};
});
return !hideTimeField && indexPattern.timeFieldName
? [getTimeColumn(indexPattern.timeFieldName), ...columnProps]
: columnProps;
}

View file

@ -0,0 +1,203 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { TableHeader } from './table_header';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { SortOrder } from './helpers';
import { IndexPatternEnhanced, StaticIndexPatternField } from 'ui/index_patterns/_index_pattern';
function getMockIndexPattern() {
return {
id: 'test',
title: 'Test',
timeFieldName: 'time',
fields: [],
isTimeNanosBased: () => false,
getFieldByName: name => {
if (name === 'test1') {
return {
name,
type: 'string',
aggregatable: false,
searchable: true,
sortable: true,
} as StaticIndexPatternField;
} else {
return {
name,
type: 'string',
aggregatable: false,
searchable: true,
sortable: false,
} as StaticIndexPatternField;
}
},
} as IndexPatternEnhanced;
}
function getMockProps(props = {}) {
const defaultProps = {
indexPattern: getMockIndexPattern(),
hideTimeColumn: false,
columns: ['first', 'middle', 'last'],
sortOrder: ['time', 'asc'] as SortOrder,
isShortDots: true,
onRemoveColumn: jest.fn(),
onChangeSortOrder: jest.fn(),
onMoveColumn: jest.fn(),
onPageNext: jest.fn(),
onPagePrevious: jest.fn(),
};
return Object.assign({}, defaultProps, props);
}
describe('TableHeader with time column', () => {
const props = getMockProps();
const wrapper = mountWithIntl(
<table>
<thead>
<TableHeader {...props} />
</thead>
</table>
);
test('renders correctly', () => {
const docTableHeader = findTestSubject(wrapper, 'docTableHeader');
expect(docTableHeader.getDOMNode()).toMatchSnapshot();
});
test('time column is sortable with button, cycling sort direction', () => {
findTestSubject(wrapper, 'docTableHeaderFieldSort_time').simulate('click');
expect(props.onChangeSortOrder).toHaveBeenCalledWith('time', 'desc');
});
test('time column is not removeable, no button displayed', () => {
const removeButton = findTestSubject(wrapper, 'docTableRemoveHeader-time');
expect(removeButton.length).toBe(0);
});
test('time column is not moveable, no button displayed', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-time');
expect(moveButtonLeft.length).toBe(0);
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-time');
expect(moveButtonRight.length).toBe(0);
});
test('first column is removeable', () => {
const removeButton = findTestSubject(wrapper, 'docTableRemoveHeader-first');
expect(removeButton.length).toBe(1);
removeButton.simulate('click');
expect(props.onRemoveColumn).toHaveBeenCalledWith('first');
});
test('first column is not moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-first');
expect(moveButtonLeft.length).toBe(0);
});
test('first column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-first');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('first', 1);
});
test('middle column is moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-middle');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 0);
});
test('middle column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-middle');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 2);
});
test('last column moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-last');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('last', 1);
});
});
describe('TableHeader without time column', () => {
const props = getMockProps({ hideTimeColumn: true });
const wrapper = mountWithIntl(
<table>
<thead>
<TableHeader {...props} />
</thead>
</table>
);
test('renders correctly', () => {
const docTableHeader = findTestSubject(wrapper, 'docTableHeader');
expect(docTableHeader.getDOMNode()).toMatchSnapshot();
});
test('first column is removeable', () => {
const removeButton = findTestSubject(wrapper, 'docTableRemoveHeader-first');
expect(removeButton.length).toBe(1);
removeButton.simulate('click');
expect(props.onRemoveColumn).toHaveBeenCalledWith('first');
});
test('first column is not moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-first');
expect(moveButtonLeft.length).toBe(0);
});
test('first column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-first');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('first', 1);
});
test('middle column is moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-middle');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 0);
});
test('middle column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-middle');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 2);
});
test('last column moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-last');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('last', 1);
});
});

View file

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { IndexPatternEnhanced } from 'ui/index_patterns/_index_pattern';
// @ts-ignore
import { shortenDottedString } from '../../../../../common/utils/shorten_dotted_string';
import { TableHeaderColumn } from './table_header_column';
import { SortOrder, getDisplayedColumns } from './helpers';
interface Props {
columns: string[];
hideTimeColumn: boolean;
indexPattern: IndexPatternEnhanced;
isShortDots: boolean;
onChangeSortOrder: (name: string, direction: 'asc' | 'desc') => void;
onMoveColumn: (name: string, index: number) => void;
onRemoveColumn: (name: string) => void;
sortOrder: SortOrder;
}
export function TableHeader({
columns,
hideTimeColumn,
indexPattern,
isShortDots,
onChangeSortOrder,
onMoveColumn,
onRemoveColumn,
sortOrder,
}: Props) {
const displayedColumns = getDisplayedColumns(columns, indexPattern, hideTimeColumn, isShortDots);
const [currColumnName, currDirection = 'asc'] = sortOrder;
return (
<tr data-test-subj="docTableHeader" className="kbnDocTableHeader">
<th style={{ width: '24px' }}></th>
{displayedColumns.map(col => {
return (
<TableHeaderColumn
key={col.name}
{...col}
sortDirection={col.name === currColumnName ? currDirection : ''}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
onChangeSortOrder={onChangeSortOrder}
/>
);
})}
</tr>
);
}

View file

@ -0,0 +1,154 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
// @ts-ignore
import { shortenDottedString } from '../../../../../common/utils/shorten_dotted_string';
interface Props {
colLeftIdx: number; // idx of the column to the left, -1 if moving is not possible
colRightIdx: number; // idx of the column to the right, -1 if moving is not possible
displayName: string;
isRemoveable: boolean;
isSortable: boolean;
name: string;
onChangeSortOrder?: (name: string, direction: 'asc' | 'desc') => void;
onMoveColumn?: (name: string, idx: number) => void;
onRemoveColumn?: (name: string) => void;
sortDirection: 'asc' | 'desc' | ''; // asc|desc -> field is sorted in this direction, else ''
}
export function TableHeaderColumn({
colLeftIdx,
colRightIdx,
displayName,
isRemoveable,
isSortable,
name,
onChangeSortOrder,
onMoveColumn,
onRemoveColumn,
sortDirection,
}: Props) {
const btnSortIcon = sortDirection === 'desc' ? 'fa fa-sort-down' : 'fa fa-sort-up';
const btnSortClassName =
sortDirection !== '' ? btnSortIcon : `kbnDocTableHeader__sortChange ${btnSortIcon}`;
// action buttons displayed on the right side of the column name
const buttons = [
// Sort Button
{
active: isSortable,
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 },
}),
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);
}
},
testSubject: `docTableHeaderFieldSort_${name}`,
tooltip: i18n.translate('kbn.docTable.tableHeader.sortByColumnTooltip', {
defaultMessage: 'Sort by {columnName}',
values: { columnName: name },
}),
},
// Remove Button
{
active: isRemoveable,
ariaLabel: i18n.translate('kbn.docTable.tableHeader.removeColumnButtonAriaLabel', {
defaultMessage: 'Remove {columnName} column',
values: { columnName: name },
}),
className: 'fa fa-remove kbnDocTableHeader__move',
onClick: () => onRemoveColumn && onRemoveColumn(name),
testSubject: `docTableRemoveHeader-${name}`,
tooltip: i18n.translate('kbn.docTable.tableHeader.removeColumnButtonTooltip', {
defaultMessage: 'Remove Column',
}),
},
// Move Left Button
{
active: colLeftIdx >= 0,
ariaLabel: i18n.translate('kbn.docTable.tableHeader.moveColumnLeftButtonAriaLabel', {
defaultMessage: 'Move {columnName} column to the left',
values: { columnName: name },
}),
className: 'fa fa-angle-double-left kbnDocTableHeader__move',
onClick: () => onMoveColumn && onMoveColumn(name, colLeftIdx),
testSubject: `docTableMoveLeftHeader-${name}`,
tooltip: i18n.translate('kbn.docTable.tableHeader.moveColumnLeftButtonTooltip', {
defaultMessage: 'Move column to the left',
}),
},
// Move Right Button
{
active: colRightIdx >= 0,
ariaLabel: i18n.translate('kbn.docTable.tableHeader.moveColumnRightButtonAriaLabel', {
defaultMessage: 'Move {columnName} column to the right',
values: { columnName: name },
}),
className: 'fa fa-angle-double-right kbnDocTableHeader__move',
onClick: () => onMoveColumn && onMoveColumn(name, colRightIdx),
testSubject: `docTableMoveRightHeader-${name}`,
tooltip: i18n.translate('kbn.docTable.tableHeader.moveColumnRightButtonTooltip', {
defaultMessage: 'Move column to the right',
}),
},
];
return (
<th data-test-subj="docTableHeaderField">
<span data-test-subj={`docTableHeader-${name}`}>
{displayName}
{buttons
.filter(button => button.active)
.map((button, idx) => (
<EuiToolTip
id={`docTableHeader-${name}-tt`}
content={button.tooltip}
key={`button-${idx}`}
>
<button
aria-label={button.ariaLabel}
className={button.className}
data-test-subj={button.testSubject}
onClick={button.onClick}
/>
</EuiToolTip>
))}
</span>
</th>
);
}

View file

@ -34,6 +34,7 @@ export interface IndexPattern {
// that currently depend on an interface without methods to succeed
export interface IndexPatternEnhanced extends IndexPattern {
isTimeNanosBased: () => boolean;
getFieldByName: (name: string) => StaticIndexPatternField | undefined;
}
export interface IndexPatternGetProvider {
@ -45,6 +46,7 @@ export interface StaticIndexPatternField {
type: string;
aggregatable: boolean;
searchable: boolean;
sortable?: boolean;
}
export interface StaticIndexPattern {

View file

@ -328,6 +328,11 @@ export class IndexPattern {
return this.fields.byName[this.timeFieldName];
}
getFieldByName(name) {
if (!this.fields || !this.fields.byName) return;
return this.fields.byName[name];
}
isWildcard() {
return _.includes(this.title, '*');
}

View file

@ -36,6 +36,7 @@ export default function () {
this.isTimeBased = () => Boolean(this.timeFieldName);
this.getNonScriptedFields = sinon.spy(IndexPattern.prototype.getNonScriptedFields);
this.getScriptedFields = sinon.spy(IndexPattern.prototype.getScriptedFields);
this.getFieldByName = sinon.spy(IndexPattern.prototype.getFieldByName);
this.getSourceFiltering = sinon.stub();
this.metaFields = ['_id', '_type', '_source'];
this.fieldFormatMap = {};

View file

@ -1599,8 +1599,6 @@
"kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel": "{columnName} を昇順に並べ替える",
"kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel": "{columnName} を降順に並べ替える",
"kbn.docTable.tableHeader.sortByColumnTooltip": "{columnName} で並べ替えます",
"kbn.docTable.tableHeader.sortByTimeTooltip": "時間で並べ替えます",
"kbn.docTable.tableHeader.timeHeaderCellTitle": "時間",
"kbn.docTable.tableRow.detailHeading": "拡張ドキュメント",
"kbn.docTable.tableRow.filterForValueButtonAriaLabel": "値でフィルタリング",
"kbn.docTable.tableRow.filterForValueButtonTooltip": "値でフィルタリング",

View file

@ -1600,8 +1600,6 @@
"kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel": "升序排序 {columnName}",
"kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel": "降序排序 {columnName}",
"kbn.docTable.tableHeader.sortByColumnTooltip": "按“{columnName}”排序",
"kbn.docTable.tableHeader.sortByTimeTooltip": "按时间排序",
"kbn.docTable.tableHeader.timeHeaderCellTitle": "时间",
"kbn.docTable.tableRow.detailHeading": "已展开文档",
"kbn.docTable.tableRow.filterForValueButtonAriaLabel": "筛留值",
"kbn.docTable.tableRow.filterForValueButtonTooltip": "筛留值",