[Data table] Reactify & EUIficate the visualization (#70801)

* Use data grid for table vis

* Create basic table template

* Add table_vis_split component

* Apply cell filtering

* Add aria-label attributes

* Use field formatters for values

* Add no results component

* Remove legacy dependencies

* Add usePagination

* Create usePagination util

* Use percentage column and total row

* Use csv export button

* Update labels

* Fix merge conflicts

* Use split table

* Fix functional tests

* Fix dashboard tests

* Update data table functional tests

* Fix missed test

* Introduce showToolbar option

* Remove useless package

* Fix merge conflicts

* Return back kibanaUtils required bundle

* Revert "Remove useless package"

This reverts commit 144a7cd77c.

* Use feature flag for legacy vis

* Add footer row

* Remove lock files

* Revert "Remove lock files"

This reverts commit 5c5acd79f4.

* Minor fixes

* Use common no result message

* Fix broken tests

* Use ui state sorting

* Fix error

* Fix merge conflicts

* Add legacy functional tests

* Pull pagination footer up to keep with table

and fix column split growing continuously in dashboard

* Comments fixes

* Use cell actions for filtering

* Fix translations

* Fix comments

* Reduce legacy tests amount

* Update sorting

* Update split column layout

* Add telemetry for legacy vis

* Apply latest changes for split table

* Fix eslint errors

* Use aria labels with values

* Use aria label for export btn

* Fix functional test

* Update translations

* Cleanup

* Truncate cells content

* Enhance types in table_vis_response_handler.ts

* Persist columns width on change

* Fix sorting history

* Add a migration script for toolbar

* Export sorted table

* Use reportUiCounter instead of reportUiStats

* Fix integration tests

* Fix typos

* Adjust FieldFormat type

* Hide the density selector

* Update docs

* Fix pagination

* Restrict hiding the toolbar

* Fix column index on filter

* Add closePopover action

Co-authored-by: cchaos <caroline.horn@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Daniil 2020-12-15 14:44:20 +03:00 committed by GitHub
parent 62623cdab9
commit 845f716271
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 2665 additions and 394 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) &gt; [allowsNumericalAggregations](./kibana-plugin-plugins-data-public.fieldformat.allowsnumericalaggregations.md)
## FieldFormat.allowsNumericalAggregations property
<b>Signature:</b>
```typescript
allowsNumericalAggregations?: boolean;
```

View file

@ -21,6 +21,7 @@ export declare abstract class FieldFormat
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [\_params](./kibana-plugin-plugins-data-public.fieldformat._params.md) | | <code>any</code> | |
| [allowsNumericalAggregations](./kibana-plugin-plugins-data-public.fieldformat.allowsnumericalaggregations.md) | | <code>boolean</code> | |
| [convertObject](./kibana-plugin-plugins-data-public.fieldformat.convertobject.md) | | <code>FieldFormatConvert &#124; undefined</code> | {<!-- -->FieldFormatConvert<!-- -->} have to remove the private because of https://github.com/Microsoft/TypeScript/issues/17293 |
| [fieldType](./kibana-plugin-plugins-data-public.fieldformat.fieldtype.md) | <code>static</code> | <code>string &#124; string[]</code> | {<!-- -->string<!-- -->} - Field Format Type |
| [getConfig](./kibana-plugin-plugins-data-public.fieldformat.getconfig.md) | | <code>FieldFormatsGetConfigFn &#124; undefined</code> | |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
export declare type IFieldFormat = PublicMethodsOf<FieldFormat>;
export declare type IFieldFormat = FieldFormat;
```

View file

@ -24,6 +24,7 @@ const alwaysImportedTests = [
require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'),
require.resolve('../test/new_visualize_flow/config.ts'),
require.resolve('../test/security_functional/config.ts'),
require.resolve('../test/functional/config.legacy.ts'),
];
// eslint-disable-next-line no-restricted-syntax
const onlyNotInCoverageTests = [

View file

@ -83,6 +83,7 @@ export abstract class FieldFormat {
* @private
*/
public type: any = this.constructor;
public allowsNumericalAggregations?: boolean;
protected readonly _params: any;
protected getConfig: FieldFormatsGetConfigFn | undefined;

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PublicMethodsOf } from '@kbn/utility-types';
import { GetConfigFn } from '../types';
import { FieldFormat } from './field_format';
import { FieldFormatsRegistry } from './field_formats_registry';
@ -77,7 +76,7 @@ export interface FieldFormatConfig {
export type FieldFormatsGetConfigFn = GetConfigFn;
export type IFieldFormat = PublicMethodsOf<FieldFormat>;
export type IFieldFormat = FieldFormat;
/**
* @string id type is needed for creating custom converters.

View file

@ -850,6 +850,8 @@ export const extractSearchSourceReferences: (state: SearchSourceFields) => [Sear
export abstract class FieldFormat {
// Warning: (ae-forgotten-export) The symbol "IFieldFormatMetaParams" needs to be exported by the entry point index.d.ts
constructor(_params?: IFieldFormatMetaParams, getConfig?: FieldFormatsGetConfigFn);
// (undocumented)
allowsNumericalAggregations?: boolean;
// Warning: (ae-forgotten-export) The symbol "HtmlContextTypeOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "TextContextTypeOptions" needs to be exported by the entry point index.d.ts
convert(value: any, contentType?: FieldFormatsContentType, options?: HtmlContextTypeOptions | TextContextTypeOptions): string;
@ -1091,7 +1093,7 @@ export type IEsSearchResponse<Source = any> = IKibanaSearchResponse<SearchRespon
// Warning: (ae-missing-release-tag) "IFieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type IFieldFormat = PublicMethodsOf<FieldFormat>;
export type IFieldFormat = FieldFormat;
// Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//

View file

@ -1 +1,8 @@
Contains the data table visualization, that allows presenting data in a simple table format.
Contains the data table visualization, that allows presenting data in a simple table format.
By default a new version of visualization will be used. To use the previous version of visualization the config must have the `vis_type_table.legacyVisEnabled: true` setting
configured in `kibana.dev.yml` or `kibana.yml`, as shown in the example below:
```yaml
vis_type_table.legacyVisEnabled: true
```

View file

@ -21,6 +21,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
legacyVisEnabled: schema.boolean({ defaultValue: false }),
});
export type ConfigSchema = TypeOf<typeof configSchema>;

View file

@ -11,8 +11,10 @@
],
"requiredBundles": [
"kibanaUtils",
"kibanaReact",
"share",
"charts",
"visDefaultEditor"
]
],
"optionalPlugins": ["usageCollection"]
}

View file

@ -35,7 +35,7 @@ Object {
Object {
"arguments": Object {
"visConfig": Array [
"{\\"perPage\\":20,\\"percentageCol\\":\\"Count\\",\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"showTotal\\":true,\\"sort\\":{\\"columnIndex\\":null,\\"direction\\":null},\\"totalFunc\\":\\"sum\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{},\\"label\\":\\"Count\\",\\"aggType\\":\\"count\\"}],\\"buckets\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"YYYY-MM-DD HH:mm\\"}},\\"params\\":{},\\"label\\":\\"order_date per 3 hours\\",\\"aggType\\":\\"date_histogram\\"}]}}",
"{\\"perPage\\":20,\\"percentageCol\\":\\"Count\\",\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"showTotal\\":true,\\"showToolbar\\":false,\\"totalFunc\\":\\"sum\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{},\\"label\\":\\"Count\\",\\"aggType\\":\\"count\\"}],\\"buckets\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"YYYY-MM-DD HH:mm\\"}},\\"params\\":{},\\"label\\":\\"order_date per 3 hours\\",\\"aggType\\":\\"date_histogram\\"}]}}",
],
},
"function": "kibana_table",

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { TableOptions } from './table_vis_options_lazy';

View file

@ -0,0 +1,164 @@
/*
* 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, { memo, useCallback, useMemo } from 'react';
import { EuiDataGrid, EuiDataGridProps, EuiDataGridSorting, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { orderBy } from 'lodash';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { createTableVisCell } from './table_vis_cell';
import { Table } from '../table_vis_response_handler';
import { TableVisConfig, TableVisUseUiStateProps } from '../types';
import { useFormattedColumnsAndRows, usePagination } from '../utils';
import { TableVisControls } from './table_vis_controls';
import { createGridColumns } from './table_vis_columns';
interface TableVisBasicProps {
fireEvent: IInterpreterRenderHandlers['event'];
table: Table;
visConfig: TableVisConfig;
title?: string;
uiStateProps: TableVisUseUiStateProps;
}
export const TableVisBasic = memo(
({
fireEvent,
table,
visConfig,
title,
uiStateProps: { columnsWidth, sort, setColumnsWidth, setSort },
}: TableVisBasicProps) => {
const { columns, rows } = useFormattedColumnsAndRows(table, visConfig);
// custom sorting is in place until the EuiDataGrid sorting gets rid of flaws -> https://github.com/elastic/eui/issues/4108
const sortedRows = useMemo(
() =>
sort.columnIndex !== null && sort.direction
? orderBy(rows, columns[sort.columnIndex]?.id, sort.direction)
: rows,
[columns, rows, sort]
);
// renderCellValue is a component which renders a cell based on column and row indexes
const renderCellValue = useMemo(() => createTableVisCell(columns, sortedRows), [
columns,
sortedRows,
]);
// Columns config
const gridColumns = createGridColumns(table, columns, columnsWidth, sortedRows, fireEvent);
// Pagination config
const pagination = usePagination(visConfig, rows.length);
// Sorting config
const sortingColumns = useMemo(
() =>
sort.columnIndex !== null && sort.direction
? [{ id: columns[sort.columnIndex]?.id, direction: sort.direction }]
: [],
[columns, sort]
);
const onSort = useCallback(
(sortingCols: EuiDataGridSorting['columns'] | []) => {
// data table vis sorting now only handles one column sorting
// if data grid provides more columns to sort, pick only the next column to sort
const newSortValue = sortingCols.length <= 1 ? sortingCols[0] : sortingCols[1];
setSort(
newSortValue && {
columnIndex: columns.findIndex((c) => c.id === newSortValue.id),
direction: newSortValue.direction,
}
);
},
[columns, setSort]
);
const dataGridAriaLabel =
title ||
visConfig.title ||
i18n.translate('visTypeTable.defaultAriaLabel', {
defaultMessage: 'Data table visualization',
});
const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback(
({ columnId, width }) => {
const colIndex = columns.findIndex((c) => c.id === columnId);
setColumnsWidth({
colIndex,
width,
});
},
[columns, setColumnsWidth]
);
return (
<>
{title && (
<EuiTitle size="xs">
<h3>{title}</h3>
</EuiTitle>
)}
<EuiDataGrid
aria-label={dataGridAriaLabel}
columns={gridColumns}
gridStyle={{
border: 'horizontal',
header: 'underline',
}}
rowCount={rows.length}
columnVisibility={{
visibleColumns: columns.map(({ id }) => id),
setVisibleColumns: () => {},
}}
toolbarVisibility={
visConfig.showToolbar && {
showColumnSelector: false,
showFullScreenSelector: false,
showSortSelector: false,
showStyleSelector: false,
additionalControls: (
<TableVisControls
dataGridAriaLabel={dataGridAriaLabel}
cols={columns}
// csv exports sorted table
rows={sortedRows}
table={table}
filename={visConfig.title}
/>
),
}
}
renderCellValue={renderCellValue}
renderFooterCellValue={
visConfig.showTotal
? // @ts-expect-error
({ colIndex }) => columns[colIndex].formattedTotal || null
: undefined
}
pagination={pagination}
sorting={{ columns: sortingColumns, onSort }}
onColumnResize={onColumnResize}
minSizeForControls={1}
/>
</>
);
}
);

View file

@ -0,0 +1,50 @@
/*
* 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 { EuiDataGridCellValueElementProps } from '@elastic/eui';
import { Table } from '../table_vis_response_handler';
import { FormattedColumn } from '../types';
export const createTableVisCell = (formattedColumns: FormattedColumn[], rows: Table['rows']) => ({
// @ts-expect-error
colIndex,
rowIndex,
columnId,
}: EuiDataGridCellValueElementProps) => {
const rowValue = rows[rowIndex][columnId];
const column = formattedColumns[colIndex];
const content = column.formatter.convert(rowValue, 'html');
const cellContent = (
<div
/*
* Justification for dangerouslySetInnerHTML:
* The Data table visualization can "enrich" cell contents by applying a field formatter,
* which we want to do if possible.
*/
dangerouslySetInnerHTML={{ __html: content }} // eslint-disable-line react/no-danger
data-test-subj="tbvChartCellContent"
className="tbvChartCellContent"
/>
);
return cellContent;
};

View file

@ -0,0 +1,185 @@
/*
* 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 { EuiDataGridColumnCellActionProps, EuiDataGridColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { Table } from '../table_vis_response_handler';
import { FormattedColumn, TableVisUiState } from '../types';
interface FilterCellData {
/**
* Row index
*/
row: number;
/**
* Column index
*/
column: number;
value: unknown;
}
export const createGridColumns = (
table: Table,
columns: FormattedColumn[],
columnsWidth: TableVisUiState['colWidth'],
rows: Table['rows'],
fireEvent: IInterpreterRenderHandlers['event']
) => {
const onFilterClick = (data: FilterCellData, negate: boolean) => {
/**
* Visible column index and the actual one from the source table could be different.
* e.x. a column could be filtered out if it's not a dimension -
* see formattedColumns in use_formatted_columns.ts file,
* or an extra percantage column could be added, which doesn't exist in the raw table
*/
const rawTableActualColumnIndex = table.columns.findIndex(
(c) => c.id === columns[data.column].id
);
fireEvent({
name: 'filterBucket',
data: {
data: [
{
table: {
...table,
rows,
},
...data,
column: rawTableActualColumnIndex,
},
],
negate,
},
});
};
return columns.map(
(col, colIndex): EuiDataGridColumn => {
const cellActions = col.filterable
? [
({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => {
const rowValue = rows[rowIndex][columnId];
const contentsIsDefined = rowValue !== null && rowValue !== undefined;
const cellContent = col.formatter.convert(rowValue);
const filterForText = i18n.translate(
'visTypeTable.tableCellFilter.filterForValueText',
{
defaultMessage: 'Filter for value',
}
);
const filterForAriaLabel = i18n.translate(
'visTypeTable.tableCellFilter.filterForValueAriaLabel',
{
defaultMessage: 'Filter for value: {cellContent}',
values: {
cellContent,
},
}
);
return (
contentsIsDefined && (
<Component
aria-label={filterForAriaLabel}
data-test-subj="tbvChartCell__filterForCellValue"
onClick={() => {
onFilterClick({ row: rowIndex, column: colIndex, value: rowValue }, false);
closePopover();
}}
iconType="plusInCircle"
>
{filterForText}
</Component>
)
);
},
({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => {
const rowValue = rows[rowIndex][columnId];
const contentsIsDefined = rowValue !== null && rowValue !== undefined;
const cellContent = col.formatter.convert(rowValue);
const filterOutText = i18n.translate(
'visTypeTable.tableCellFilter.filterOutValueText',
{
defaultMessage: 'Filter out value',
}
);
const filterOutAriaLabel = i18n.translate(
'visTypeTable.tableCellFilter.filterOutValueAriaLabel',
{
defaultMessage: 'Filter out value: {cellContent}',
values: {
cellContent,
},
}
);
return (
contentsIsDefined && (
<Component
aria-label={filterOutAriaLabel}
onClick={() => {
onFilterClick({ row: rowIndex, column: colIndex, value: rowValue }, true);
closePopover();
}}
iconType="minusInCircle"
>
{filterOutText}
</Component>
)
);
},
]
: undefined;
const initialWidth = columnsWidth.find((c) => c.colIndex === colIndex);
const column: EuiDataGridColumn = {
id: col.id,
display: col.title,
displayAsText: col.title,
actions: {
showHide: false,
showMoveLeft: false,
showMoveRight: false,
showSortAsc: {
label: i18n.translate('visTypeTable.sort.ascLabel', {
defaultMessage: 'Sort asc',
}),
},
showSortDesc: {
label: i18n.translate('visTypeTable.sort.descLabel', {
defaultMessage: 'Sort desc',
}),
},
},
cellActions,
};
if (initialWidth) {
column.initialWidth = initialWidth.width;
}
return column;
}
);
};

View file

@ -0,0 +1,102 @@
/*
* 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, { memo, useState, useCallback } from 'react';
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { DatatableRow } from 'src/plugins/expressions';
import { CoreStart } from 'kibana/public';
import { useKibana } from '../../../kibana_react/public';
import { FormattedColumn } from '../types';
import { Table } from '../table_vis_response_handler';
import { exportAsCsv } from '../utils';
interface TableVisControlsProps {
dataGridAriaLabel: string;
filename?: string;
cols: FormattedColumn[];
rows: DatatableRow[];
table: Table;
}
export const TableVisControls = memo(({ dataGridAriaLabel, ...props }: TableVisControlsProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = useCallback(() => setIsPopoverOpen((state) => !state), []);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const {
services: { uiSettings },
} = useKibana<CoreStart>();
const onClickExport = useCallback(
(formatted: boolean) =>
exportAsCsv(formatted, {
...props,
uiSettings,
}),
[props, uiSettings]
);
const exportBtnAriaLabel = i18n.translate('visTypeTable.vis.controls.exportButtonAriaLabel', {
defaultMessage: 'Export {dataGridAriaLabel} as CSV',
values: {
dataGridAriaLabel,
},
});
const button = (
<EuiButtonEmpty
aria-label={exportBtnAriaLabel}
size="xs"
iconType="exportAction"
color="text"
className="euiDataGrid__controlBtn"
onClick={togglePopover}
>
<FormattedMessage id="visTypeTable.vis.controls.exportButtonLabel" defaultMessage="Export" />
</EuiButtonEmpty>
);
const items = [
<EuiContextMenuItem key="rawCsv" onClick={() => onClickExport(false)}>
<FormattedMessage id="visTypeTable.vis.controls.rawCSVButtonLabel" defaultMessage="Raw" />
</EuiContextMenuItem>,
<EuiContextMenuItem key="csv" onClick={() => onClickExport(true)}>
<FormattedMessage
id="visTypeTable.vis.controls.formattedCSVButtonLabel"
defaultMessage="Formatted"
/>
</EuiContextMenuItem>,
];
return (
<EuiPopover
id="dataTableExportData"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
repositionOnScroll
>
<EuiContextMenuPanel className="eui-textNoWrap" items={items} />
</EuiPopover>
);
});

View file

@ -114,6 +114,15 @@ function TableOptions({
data-test-subj="showPartialRows"
/>
<SwitchOption
label={i18n.translate('visTypeTable.params.showToolbarLabel', {
defaultMessage: 'Show toolbar',
})}
paramName="showToolbar"
value={stateParams.showToolbar}
setValue={setValue}
/>
<SwitchOption
label={i18n.translate('visTypeTable.params.showTotalLabel', {
defaultMessage: 'Show total',

View file

@ -0,0 +1,52 @@
/*
* 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, { memo } from 'react';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { TableGroup } from '../table_vis_response_handler';
import { TableVisConfig, TableVisUseUiStateProps } from '../types';
import { TableVisBasic } from './table_vis_basic';
interface TableVisSplitProps {
fireEvent: IInterpreterRenderHandlers['event'];
tables: TableGroup[];
visConfig: TableVisConfig;
uiStateProps: TableVisUseUiStateProps;
}
export const TableVisSplit = memo(
({ fireEvent, tables, visConfig, uiStateProps }: TableVisSplitProps) => {
return (
<>
{tables.map(({ tables: dataTable, key, title }) => (
<div key={key} className="tbvChart__split">
<TableVisBasic
fireEvent={fireEvent}
table={dataTable[0]}
visConfig={visConfig}
title={title}
uiStateProps={uiStateProps}
/>
</div>
))}
</>
);
}
);

View file

@ -0,0 +1,33 @@
// Prefix all styles with "tbv" to avoid conflicts.
// Examples
// tbvChart
// tbvChart__legend
// tbvChart__legend--small
// tbvChart__legend-isLoading
.tbvChart {
display: flex;
flex-direction: column;
flex: 1 0 0;
overflow: auto;
@include euiScrollBar;
}
.tbvChart__split {
padding: $euiSizeS;
margin-bottom: $euiSizeL;
> h3 {
text-align: center;
}
}
.tbvChart__splitColumns {
flex-direction: row;
align-items: flex-start;
}
.tbvChartCellContent {
@include euiTextTruncate;
}

View file

@ -0,0 +1,86 @@
/*
* 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 './table_visualization.scss';
import React, { useEffect } from 'react';
import classNames from 'classnames';
import { CoreStart } from 'kibana/public';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { KibanaContextProvider } from '../../../kibana_react/public';
import { TableVisConfig } from '../types';
import { TableContext } from '../table_vis_response_handler';
import { TableVisBasic } from './table_vis_basic';
import { TableVisSplit } from './table_vis_split';
import { useUiState } from '../utils';
interface TableVisualizationComponentProps {
core: CoreStart;
handlers: IInterpreterRenderHandlers;
visData: TableContext;
visConfig: TableVisConfig;
}
const TableVisualizationComponent = ({
core,
handlers,
visData: { direction, table, tables },
visConfig,
}: TableVisualizationComponentProps) => {
useEffect(() => {
handlers.done();
}, [handlers]);
const uiStateProps = useUiState(handlers.uiState);
const className = classNames('tbvChart', {
// eslint-disable-next-line @typescript-eslint/naming-convention
tbvChart__splitColumns: direction === 'column',
});
return (
<core.i18n.Context>
<KibanaContextProvider services={core}>
<div className={className} data-test-subj="tbvChart">
{table ? (
<div className="tbvChart__split">
<TableVisBasic
fireEvent={handlers.event}
table={table}
visConfig={visConfig}
uiStateProps={uiStateProps}
/>
</div>
) : (
<TableVisSplit
fireEvent={handlers.event}
tables={tables}
visConfig={visConfig}
uiStateProps={uiStateProps}
/>
)}
</div>
</KibanaContextProvider>
</core.i18n.Context>
);
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TableVisualizationComponent as default };

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#table returns an object with the correct structure 1`] = `
Object {
"as": "table_vis",
"type": "render",
"value": Object {
"visConfig": Object {
"dimensions": Object {
"buckets": Array [],
"metrics": Array [
Object {
"accessor": 0,
"aggType": "count",
"format": Object {
"id": "number",
},
"params": Object {},
},
],
},
"perPage": 10,
"showMetricsAtAllLevels": false,
"showPartialRows": false,
"showTotal": false,
"sort": Object {
"columnIndex": null,
"direction": null,
},
"title": "My Chart title",
"totalFunc": "sum",
},
"visData": Object {
"tables": Array [
Object {
"columns": Array [],
"rows": Array [],
},
],
},
"visType": "table",
},
}
`;

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { registerLegacyVis } from './register_legacy_vis';

View file

@ -0,0 +1,37 @@
/*
* 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 { METRIC_TYPE } from '@kbn/analytics';
import { PluginInitializerContext, CoreSetup } from 'kibana/public';
import { TablePluginSetupDependencies, TablePluginStartDependencies } from '../plugin';
import { createTableVisLegacyFn } from './table_vis_legacy_fn';
import { getTableVisLegacyRenderer } from './table_vis_legacy_renderer';
import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type';
export const registerLegacyVis = (
core: CoreSetup<TablePluginStartDependencies>,
{ expressions, visualizations, usageCollection }: TablePluginSetupDependencies,
context: PluginInitializerContext
) => {
usageCollection?.reportUiCounter('vis_type_table', METRIC_TYPE.LOADED, 'legacyVisEnabled');
expressions.registerFunction(createTableVisLegacyFn);
expressions.registerRenderer(getTableVisLegacyRenderer(core, context));
visualizations.createBaseVisualization(tableVisLegacyTypeDefinition);
};

View file

@ -24,10 +24,10 @@ import $ from 'jquery';
import { getAngularModule } from './get_inner_angular';
import { initTableVisLegacyModule } from './table_vis_legacy_module';
import { tableVisTypeDefinition } from '../table_vis_type';
import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type';
import { Vis } from '../../../visualizations/public';
import { stubFields } from '../../../data/public/stubs';
import { tableVisResponseHandler } from '../table_vis_response_handler';
import { tableVisLegacyResponseHandler } from './table_vis_legacy_response_handler';
import { coreMock } from '../../../../core/public/mocks';
import { IAggConfig, search } from '../../../data/public';
import { getStubIndexPattern } from '../../../data/public/test_utils';
@ -94,7 +94,7 @@ describe('Table Vis - Controller', () => {
angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => {
$rootScope = _$rootScope_;
$compile = _$compile_;
tableAggResponse = tableVisResponseHandler;
tableAggResponse = tableVisLegacyResponseHandler;
})
);
@ -110,8 +110,8 @@ describe('Table Vis - Controller', () => {
function getRangeVis(params?: object) {
return ({
type: tableVisTypeDefinition,
params: Object.assign({}, tableVisTypeDefinition.visConfig?.defaults, params),
type: tableVisLegacyTypeDefinition,
params: Object.assign({}, tableVisLegacyTypeDefinition.visConfig?.defaults, params),
data: {
aggs: createAggConfigs(stubIndexPattern, [
{ type: 'count', schema: 'metric' },

View file

@ -0,0 +1,78 @@
/*
* 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 { createTableVisLegacyFn } from './table_vis_legacy_fn';
import { tableVisLegacyResponseHandler } from './table_vis_legacy_response_handler';
import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils';
jest.mock('./table_vis_legacy_response_handler', () => ({
tableVisLegacyResponseHandler: jest.fn().mockReturnValue({
tables: [{ columns: [], rows: [] }],
}),
}));
describe('interpreter/functions#table', () => {
const fn = functionWrapper(createTableVisLegacyFn());
const context = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
const visConfig = {
title: 'My Chart title',
perPage: 10,
showPartialRows: false,
showMetricsAtAllLevels: false,
sort: {
columnIndex: null,
direction: null,
},
showTotal: false,
totalFunc: 'sum',
dimensions: {
metrics: [
{
accessor: 0,
format: {
id: 'number',
},
params: {},
aggType: 'count',
},
],
buckets: [],
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
const actual = await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined);
expect(actual).toMatchSnapshot();
});
it('calls response handler with correct values', async () => {
await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined);
expect(tableVisLegacyResponseHandler).toHaveBeenCalledTimes(1);
expect(tableVisLegacyResponseHandler).toHaveBeenCalledWith(context, visConfig.dimensions);
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public';
import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler';
import { TableVisConfig } from '../types';
export type Input = Datatable;
interface Arguments {
visConfig: string | null;
}
export interface TableVisRenderValue {
visData: TableContext;
visType: 'table';
visConfig: TableVisConfig;
}
export type TableExpressionFunctionDefinition = ExpressionFunctionDefinition<
'kibana_table',
Input,
Arguments,
Render<TableVisRenderValue>
>;
export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => ({
name: 'kibana_table',
type: 'render',
inputTypes: ['datatable'],
help: i18n.translate('visTypeTable.function.help', {
defaultMessage: 'Table visualization',
}),
args: {
visConfig: {
types: ['string', 'null'],
default: '"{}"',
help: '',
},
},
fn(input, args) {
const visConfig = args.visConfig && JSON.parse(args.visConfig);
const convertedData = tableVisLegacyResponseHandler(input, visConfig.dimensions);
return {
type: 'render',
as: 'table_vis',
value: {
visData: convertedData,
visType: 'table',
visConfig,
},
};
},
});

View file

@ -0,0 +1,99 @@
/*
* 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 { Required } from '@kbn/utility-types';
import { getFormatService } from '../services';
import { Dimensions } from '../types';
import { Input } from './table_vis_legacy_fn';
export interface TableContext {
tables: Array<TableGroup | Table>;
direction?: 'row' | 'column';
}
export interface TableGroup {
$parent: TableContext;
table: Input;
tables: Table[];
title: string;
name: string;
key: any;
column: number;
row: number;
}
export interface Table {
$parent?: TableGroup;
columns: Input['columns'];
rows: Input['rows'];
}
export function tableVisLegacyResponseHandler(table: Input, dimensions: Dimensions): TableContext {
const converted: TableContext = {
tables: [],
};
const split = dimensions.splitColumn || dimensions.splitRow;
if (split) {
converted.direction = dimensions.splitRow ? 'row' : 'column';
const splitColumnIndex = split[0].accessor;
const splitColumnFormatter = getFormatService().deserialize(split[0].format);
const splitColumn = table.columns[splitColumnIndex];
const splitMap: Record<string, number> = {};
let splitIndex = 0;
table.rows.forEach((row, rowIndex) => {
const splitValue = row[splitColumn.id];
if (!splitMap.hasOwnProperty(splitValue)) {
splitMap[splitValue] = splitIndex++;
const tableGroup: Required<TableGroup, 'tables'> = {
$parent: converted,
title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`,
name: splitColumn.name,
key: splitValue,
column: splitColumnIndex,
row: rowIndex,
table,
tables: [],
};
tableGroup.tables.push({
$parent: tableGroup,
columns: table.columns,
rows: [],
});
converted.tables.push(tableGroup);
}
const tableIndex = splitMap[splitValue];
(converted.tables[tableIndex] as TableGroup).tables[0].rows.push(row);
});
} else {
converted.tables.push({
columns: table.columns,
rows: table.rows,
});
}
return converted;
}

View file

@ -0,0 +1,95 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { AggGroupNames } from '../../../data/public';
import { Schemas } from '../../../vis_default_editor/public';
import { BaseVisTypeOptions } from '../../../visualizations/public';
import { TableOptions } from '../components/table_vis_options_lazy';
import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public';
import { toExpressionAst } from '../to_ast';
import { TableVisParams } from '../types';
export const tableVisLegacyTypeDefinition: BaseVisTypeOptions<TableVisParams> = {
name: 'table',
title: i18n.translate('visTypeTable.tableVisTitle', {
defaultMessage: 'Data table',
}),
icon: 'visTable',
description: i18n.translate('visTypeTable.tableVisDescription', {
defaultMessage: 'Display data in rows and columns.',
}),
getSupportedTriggers: () => {
return [VIS_EVENT_TO_TRIGGER.filter];
},
visConfig: {
defaults: {
perPage: 10,
showPartialRows: false,
showMetricsAtAllLevels: false,
sort: {
columnIndex: null,
direction: null,
},
showTotal: false,
totalFunc: 'sum',
percentageCol: '',
},
},
editorConfig: {
optionsTemplate: TableOptions,
schemas: new Schemas([
{
group: AggGroupNames.Metrics,
name: 'metric',
title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', {
defaultMessage: 'Metric',
}),
aggFilter: ['!geo_centroid', '!geo_bounds'],
aggSettings: {
top_hits: {
allowStrings: true,
},
},
min: 1,
defaults: [{ type: 'count', schema: 'metric' }],
},
{
group: AggGroupNames.Buckets,
name: 'bucket',
title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', {
defaultMessage: 'Split rows',
}),
aggFilter: ['!filter'],
},
{
group: AggGroupNames.Buckets,
name: 'split',
title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', {
defaultMessage: 'Split table',
}),
min: 0,
max: 1,
aggFilter: ['!filter'],
},
]),
},
toExpressionAst,
hierarchicalData: (vis) => vis.params.showPartialRows || vis.params.showMetricsAtAllLevels,
};

View file

@ -19,18 +19,21 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public';
import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
import { VisualizationsSetup } from '../../visualizations/public';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { createTableVisFn } from './table_vis_fn';
import { tableVisTypeDefinition } from './table_vis_type';
import { DataPublicPluginStart } from '../../data/public';
import { setFormatService } from './services';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { getTableVisLegacyRenderer } from './legacy/table_vis_legacy_renderer';
interface ClientConfigType {
legacyVisEnabled: boolean;
}
/** @internal */
export interface TablePluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
usageCollection?: UsageCollectionSetup;
}
/** @internal */
@ -43,8 +46,7 @@ export interface TablePluginStartDependencies {
export class TableVisPlugin
implements
Plugin<Promise<void>, void, TablePluginSetupDependencies, TablePluginStartDependencies> {
initializerContext: PluginInitializerContext;
createBaseVisualization: any;
initializerContext: PluginInitializerContext<ClientConfigType>;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
@ -52,11 +54,17 @@ export class TableVisPlugin
public async setup(
core: CoreSetup<TablePluginStartDependencies>,
{ expressions, visualizations }: TablePluginSetupDependencies
deps: TablePluginSetupDependencies
) {
expressions.registerFunction(createTableVisFn);
expressions.registerRenderer(getTableVisLegacyRenderer(core, this.initializerContext));
visualizations.createBaseVisualization(tableVisTypeDefinition);
const { legacyVisEnabled } = this.initializerContext.config.get();
if (legacyVisEnabled) {
const { registerLegacyVis } = await import('./legacy');
registerLegacyVis(core, deps, this.initializerContext);
} else {
const { registerTableVis } = await import('./register_vis');
registerTableVis(core, deps, this.initializerContext);
}
}
public start(core: CoreStart, { data }: TablePluginStartDependencies) {

View file

@ -0,0 +1,36 @@
/*
* 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 { PluginInitializerContext, CoreSetup } from 'kibana/public';
import { TablePluginSetupDependencies, TablePluginStartDependencies } from './plugin';
import { createTableVisFn } from './table_vis_fn';
import { getTableVisRenderer } from './table_vis_renderer';
import { tableVisTypeDefinition } from './table_vis_type';
export const registerTableVis = async (
core: CoreSetup<TablePluginStartDependencies>,
{ expressions, visualizations }: TablePluginSetupDependencies,
context: PluginInitializerContext
) => {
const [coreStart] = await core.getStartServices();
expressions.registerFunction(createTableVisFn);
expressions.registerRenderer(getTableVisRenderer(coreStart));
visualizations.createBaseVisualization(tableVisTypeDefinition);
};

View file

@ -0,0 +1,59 @@
/*
* 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, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { CoreStart } from 'kibana/public';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
import { TableVisRenderValue } from './table_vis_fn';
const TableVisualizationComponent = lazy(() => import('./components/table_visualization'));
export const getTableVisRenderer: (
core: CoreStart
) => ExpressionRenderDefinition<TableVisRenderValue> = (core) => ({
name: 'table_vis',
reuseDomNode: true,
render: async (domNode, { visData, visConfig }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
const showNoResult =
visData.table?.rows.length === 0 || (!visData.table && visData.tables.length === 0);
render(
<VisualizationContainer
data-test-subj="tbvChartContainer"
handlers={handlers}
showNoResult={showNoResult}
>
<TableVisualizationComponent
core={core}
handlers={handlers}
visData={visData}
visConfig={visConfig}
/>
</VisualizationContainer>,
domNode
);
},
});

View file

@ -21,78 +21,81 @@ import { Required } from '@kbn/utility-types';
import { getFormatService } from './services';
import { Input } from './table_vis_fn';
import { Dimensions } from './types';
export interface TableContext {
tables: Array<TableGroup | Table>;
table?: Table;
tables: TableGroup[];
direction?: 'row' | 'column';
}
export interface TableGroup {
$parent: TableContext;
table: Input;
tables: Table[];
title: string;
name: string;
key: any;
key: string | number;
column: number;
row: number;
}
export interface Table {
$parent?: TableGroup;
columns: Input['columns'];
rows: Input['rows'];
}
export function tableVisResponseHandler(table: Input, dimensions: any): TableContext {
const converted: TableContext = {
tables: [],
};
export function tableVisResponseHandler(input: Input, dimensions: Dimensions): TableContext {
let table: Table | undefined;
let tables: TableGroup[] = [];
let direction: TableContext['direction'];
const split = dimensions.splitColumn || dimensions.splitRow;
if (split) {
converted.direction = dimensions.splitRow ? 'row' : 'column';
tables = [];
direction = dimensions.splitRow ? 'row' : 'column';
const splitColumnIndex = split[0].accessor;
const splitColumnFormatter = getFormatService().deserialize(split[0].format);
const splitColumn = table.columns[splitColumnIndex];
const splitMap = {};
const splitColumn = input.columns[splitColumnIndex];
const splitMap: { [key: string]: number } = {};
let splitIndex = 0;
table.rows.forEach((row, rowIndex) => {
const splitValue: any = row[splitColumn.id];
input.rows.forEach((row, rowIndex) => {
const splitValue: string | number = row[splitColumn.id];
if (!splitMap.hasOwnProperty(splitValue as any)) {
(splitMap as any)[splitValue] = splitIndex++;
if (!splitMap.hasOwnProperty(splitValue)) {
splitMap[splitValue] = splitIndex++;
const tableGroup: Required<TableGroup, 'tables'> = {
$parent: converted,
title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`,
name: splitColumn.name,
key: splitValue,
column: splitColumnIndex,
row: rowIndex,
table,
table: input,
tables: [],
};
tableGroup.tables.push({
$parent: tableGroup,
columns: table.columns,
columns: input.columns,
rows: [],
});
converted.tables.push(tableGroup);
tables.push(tableGroup);
}
const tableIndex = (splitMap as any)[splitValue];
(converted.tables[tableIndex] as any).tables[0].rows.push(row);
const tableIndex = splitMap[splitValue];
tables[tableIndex].tables[0].rows.push(row);
});
} else {
converted.tables.push({
columns: table.columns,
rows: table.rows,
});
table = {
columns: input.columns,
rows: input.rows,
};
}
return converted;
return {
direction,
table,
tables,
};
}

View file

@ -43,11 +43,8 @@ export const tableVisTypeDefinition: BaseVisTypeOptions<TableVisParams> = {
perPage: 10,
showPartialRows: false,
showMetricsAtAllLevels: false,
sort: {
columnIndex: null,
direction: null,
},
showTotal: false,
showToolbar: false,
totalFunc: 'sum',
percentageCol: '',
},
@ -91,7 +88,5 @@ export const tableVisTypeDefinition: BaseVisTypeOptions<TableVisParams> = {
]),
},
toExpressionAst,
hierarchicalData: (vis) => {
return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels);
},
hierarchicalData: (vis) => vis.params.showPartialRows || vis.params.showMetricsAtAllLevels,
};

View file

@ -70,7 +70,7 @@ describe('table vis toExpressionAst function', () => {
showMetricsAtAllLevels: true,
showPartialRows: true,
showTotal: true,
sort: { columnIndex: null, direction: null },
showToolbar: false,
totalFunc: AggTypes.SUM,
};
const actual = toExpressionAst(vis, {} as any);

View file

@ -30,14 +30,15 @@ const buildTableVisConfig = (
schemas: ReturnType<typeof getVisSchemas>,
visParams: TableVisParams
) => {
const visConfig = {} as any;
const metrics = schemas.metric;
const buckets = schemas.bucket || [];
visConfig.dimensions = {
metrics,
buckets,
splitRow: schemas.split_row,
splitColumn: schemas.split_column,
const visConfig = {
dimensions: {
metrics,
buckets,
splitRow: schemas.split_row,
splitColumn: schemas.split_column,
},
};
if (visParams.showPartialRows && !visParams.showMetricsAtAllLevels) {

View file

@ -17,7 +17,8 @@
* under the License.
*/
import { SchemaConfig } from '../../visualizations/public';
import { IFieldFormat } from 'src/plugins/data/public';
import { SchemaConfig } from 'src/plugins/visualizations/public';
export enum AggTypes {
SUM = 'sum',
@ -30,16 +31,35 @@ export enum AggTypes {
export interface Dimensions {
buckets: SchemaConfig[];
metrics: SchemaConfig[];
splitColumn?: SchemaConfig[];
splitRow?: SchemaConfig[];
}
export interface ColumnWidthData {
colIndex: number;
width: number;
}
export interface TableVisUiState {
sort: {
columnIndex: number | null;
direction: 'asc' | 'desc' | null;
};
colWidth: ColumnWidthData[];
}
export interface TableVisUseUiStateProps {
columnsWidth: TableVisUiState['colWidth'];
sort: TableVisUiState['sort'];
setSort: (s?: TableVisUiState['sort']) => void;
setColumnsWidth: (column: ColumnWidthData) => void;
}
export interface TableVisParams {
perPage: number | '';
showPartialRows: boolean;
showMetricsAtAllLevels: boolean;
sort: {
columnIndex: number | null;
direction: string | null;
};
showToolbar: boolean;
showTotal: boolean;
totalFunc: AggTypes;
percentageCol: string;
@ -49,3 +69,13 @@ export interface TableVisConfig extends TableVisParams {
title: string;
dimensions: Dimensions;
}
export interface FormattedColumn {
id: string;
title: string;
formatter: IFieldFormat;
formattedTotal?: string | number;
filterable: boolean;
sumTotal?: number;
total?: number;
}

View file

@ -0,0 +1,64 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { DatatableRow } from 'src/plugins/expressions';
import { getFormatService } from '../services';
import { FormattedColumn } from '../types';
import { Table } from '../table_vis_response_handler';
function insertColumn(arr: FormattedColumn[], index: number, col: FormattedColumn) {
const newArray = [...arr];
newArray.splice(index + 1, 0, col);
return newArray;
}
/**
* @param columns - the formatted columns that will be displayed
* @param title - the title of the column to add to
* @param rows - the row data for the columns
* @param insertAtIndex - the index to insert the percentage column at
* @returns cols and rows for the table to render now included percentage column(s)
*/
export function addPercentageColumn(
columns: FormattedColumn[],
title: string,
rows: Table['rows'],
insertAtIndex: number
) {
const { id, sumTotal } = columns[insertAtIndex];
const newId = `${id}-percents`;
const formatter = getFormatService().deserialize({ id: 'percent' });
const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', {
defaultMessage: '{title} percentages',
values: { title },
});
const newCols = insertColumn(columns, insertAtIndex, {
title: i18nTitle,
id: newId,
formatter,
filterable: false,
});
const newRows = rows.map<DatatableRow>((row) => ({
[newId]: (row[id] as number) / (sumTotal as number),
...row,
}));
return { cols: newCols, rows: newRows };
}

View file

@ -0,0 +1,75 @@
/*
* 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 { isObject } from 'lodash';
// @ts-ignore
import { saveAs } from '@elastic/filesaver';
import { CoreStart } from 'kibana/public';
import { DatatableRow } from 'src/plugins/expressions';
import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public';
import { FormattedColumn } from '../types';
import { Table } from '../table_vis_response_handler';
const nonAlphaNumRE = /[^a-zA-Z0-9]/;
const allDoubleQuoteRE = /"/g;
interface ToCsvData {
filename?: string;
cols: FormattedColumn[];
rows: DatatableRow[];
table: Table;
uiSettings: CoreStart['uiSettings'];
}
const toCsv = (formatted: boolean, { cols, rows, table, uiSettings }: ToCsvData) => {
const separator = uiSettings.get(CSV_SEPARATOR_SETTING);
const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING);
function escape(val: unknown) {
if (!formatted && isObject(val)) val = val.valueOf();
val = String(val);
if (quoteValues && nonAlphaNumRE.test(val as string)) {
val = '"' + (val as string).replace(allDoubleQuoteRE, '""') + '"';
}
return val as string;
}
const csvRows: string[][] = [];
for (const row of rows) {
const rowArray: string[] = [];
for (const col of cols) {
const value = row[col.id];
const formattedValue = formatted ? escape(col.formatter.convert(value)) : escape(value);
rowArray.push(formattedValue);
}
csvRows.push(rowArray);
}
// add headers to the rows
csvRows.unshift(cols.map(({ title }) => escape(title)));
return csvRows.map((row) => row.join(separator) + '\r\n').join('');
};
export const exportAsCsv = (formatted: boolean, data: ToCsvData) => {
const csv = new Blob([toCsv(formatted, data)], { type: 'text/plain;charset=utf-8' });
saveAs(csv, `${data.filename || 'unsaved'}.csv`);
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export * from './use';
export * from './export_as_csv';

View file

@ -0,0 +1,22 @@
/*
* 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.
*/
export * from './use_formatted_columns';
export * from './use_pagination';
export * from './use_ui_state';

View file

@ -0,0 +1,125 @@
/*
* 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 { useMemo } from 'react';
import { chain, findIndex } from 'lodash';
import { Table } from '../../table_vis_response_handler';
import { FormattedColumn, TableVisConfig, AggTypes } from '../../types';
import { getFormatService } from '../../services';
import { addPercentageColumn } from '../add_percentage_column';
export const useFormattedColumnsAndRows = (table: Table, visConfig: TableVisConfig) => {
const { formattedColumns: columns, formattedRows: rows } = useMemo(() => {
const { buckets, metrics } = visConfig.dimensions;
let formattedRows = table.rows;
let formattedColumns = table.columns
.map((col, i) => {
const isBucket = buckets.find(({ accessor }) => accessor === i);
const dimension = isBucket || metrics.find(({ accessor }) => accessor === i);
if (!dimension) return undefined;
const formatter = getFormatService().deserialize(dimension.format);
const formattedColumn: FormattedColumn = {
id: col.id,
title: col.name,
formatter,
filterable: !!isBucket,
};
const isDate = dimension.format.id === 'date' || dimension.format.params?.id === 'date';
const allowsNumericalAggregations = formatter.allowsNumericalAggregations;
if (allowsNumericalAggregations || isDate || visConfig.totalFunc === AggTypes.COUNT) {
const sumOfColumnValues = table.rows.reduce((prev, curr) => {
// some metrics return undefined for some of the values
// derivative is an example of this as it returns undefined in the first row
if (curr[col.id] === undefined) return prev;
return prev + (curr[col.id] as number);
}, 0);
formattedColumn.sumTotal = sumOfColumnValues;
switch (visConfig.totalFunc) {
case AggTypes.SUM: {
if (!isDate) {
formattedColumn.formattedTotal = formatter.convert(sumOfColumnValues);
formattedColumn.total = sumOfColumnValues;
}
break;
}
case AggTypes.AVG: {
if (!isDate) {
const total = sumOfColumnValues / table.rows.length;
formattedColumn.formattedTotal = formatter.convert(total);
formattedColumn.total = total;
}
break;
}
case AggTypes.MIN: {
const total = chain(table.rows).map(col.id).min().value() as number;
formattedColumn.formattedTotal = formatter.convert(total);
formattedColumn.total = total;
break;
}
case AggTypes.MAX: {
const total = chain(table.rows).map(col.id).max().value() as number;
formattedColumn.formattedTotal = formatter.convert(total);
formattedColumn.total = total;
break;
}
case AggTypes.COUNT: {
const total = table.rows.length;
formattedColumn.formattedTotal = total;
formattedColumn.total = total;
break;
}
default:
break;
}
}
return formattedColumn;
})
.filter((column): column is FormattedColumn => !!column);
if (visConfig.percentageCol) {
const insertAtIndex = findIndex(formattedColumns, { title: visConfig.percentageCol });
// column to show percentage for was removed
if (insertAtIndex < 0) return { formattedColumns, formattedRows };
const { cols, rows: rowsWithPercentage } = addPercentageColumn(
formattedColumns,
visConfig.percentageCol,
table.rows,
insertAtIndex
);
formattedRows = rowsWithPercentage;
formattedColumns = cols;
}
return { formattedColumns, formattedRows };
}, [table, visConfig.dimensions, visConfig.percentageCol, visConfig.totalFunc]);
return { columns, rows };
};

View file

@ -0,0 +1,64 @@
/*
* 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 { useCallback, useEffect, useMemo, useState } from 'react';
import { TableVisParams } from '../../types';
export const usePagination = (visParams: TableVisParams, rowCount: number) => {
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: visParams.perPage || 0,
});
const onChangeItemsPerPage = useCallback(
(pageSize: number) => setPagination((pag) => ({ ...pag, pageSize, pageIndex: 0 })),
[]
);
const onChangePage = useCallback(
(pageIndex: number) => setPagination((pag) => ({ ...pag, pageIndex })),
[]
);
useEffect(() => {
const pageSize = visParams.perPage || 0;
const lastPageIndex = Math.ceil(rowCount / pageSize) - 1;
/**
* When the underlying data changes, there might be a case when actual pagination page
* doesn't exist anymore - if the number of rows has decreased.
* Set the last page as an actual.
*/
setPagination((pag) => ({
pageIndex: pag.pageIndex > lastPageIndex ? lastPageIndex : pag.pageIndex,
pageSize,
}));
}, [visParams.perPage, rowCount]);
const paginationMemoized = useMemo(
() =>
pagination.pageSize
? {
...pagination,
onChangeItemsPerPage,
onChangePage,
}
: undefined,
[onChangeItemsPerPage, onChangePage, pagination]
);
return paginationMemoized;
};

View file

@ -0,0 +1,139 @@
/*
* 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 { debounce, isEqual } from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { ColumnWidthData, TableVisUiState, TableVisUseUiStateProps } from '../../types';
const defaultSort = {
columnIndex: null,
direction: null,
};
export const useUiState = (
uiState: IInterpreterRenderHandlers['uiState']
): TableVisUseUiStateProps => {
const [sort, setSortState] = useState<TableVisUiState['sort']>(
uiState?.get('vis.params.sort') || defaultSort
);
const [columnsWidth, setColumnsWidthState] = useState<TableVisUiState['colWidth']>(
uiState?.get('vis.params.colWidth') || []
);
const uiStateValues = useRef<{
columnsWidth: ColumnWidthData[];
sort: TableVisUiState['sort'];
/**
* Property to filter out the changes, which were done internally via local state.
*/
pendingUpdate: boolean;
}>({
columnsWidth: uiState?.get('vis.params.colWidth'),
sort: uiState?.get('vis.params.sort'),
pendingUpdate: false,
});
const setSort = useCallback(
(s: TableVisUiState['sort'] = defaultSort) => {
setSortState(s || defaultSort);
uiStateValues.current.sort = s;
uiStateValues.current.pendingUpdate = true;
/**
* Since the visualize app state is listening for uiState changes,
* it synchronously re-renders an editor frame.
* Setting new uiState values in the new event loop task,
* helps to update the visualization frame firstly and not to block the rendering flow
*/
setTimeout(() => {
uiState?.set('vis.params.sort', s);
uiStateValues.current.pendingUpdate = false;
});
},
[uiState]
);
const setColumnsWidth = useCallback(
(col: ColumnWidthData) => {
setColumnsWidthState((prevState) => {
const updated = [...prevState];
const idx = prevState.findIndex((c) => c.colIndex === col.colIndex);
if (idx >= 0) {
updated[idx] = col;
} else {
updated.push(col);
}
uiStateValues.current.columnsWidth = updated;
uiStateValues.current.pendingUpdate = true;
/**
* Since the visualize app state is listening for uiState changes,
* it synchronously re-renders an editor frame.
* Setting new uiState values in the new event loop task,
* helps to update the visualization frame firstly and not to block the rendering flow
*/
setTimeout(() => {
uiState?.set('vis.params.colWidth', updated);
uiStateValues.current.pendingUpdate = false;
});
return updated;
});
},
[uiState]
);
useEffect(() => {
/**
* Debounce is in place since there are couple of synchronous updates of the uiState,
* which are also handled synchronously.
*/
const updateOnChange = debounce(() => {
// skip uiState updates if there are pending internal state updates
if (uiStateValues.current.pendingUpdate) {
return;
}
const { vis } = uiState?.getChanges();
if (!isEqual(vis?.params.colWidth, uiStateValues.current.columnsWidth)) {
uiStateValues.current.columnsWidth = vis?.params.colWidth;
setColumnsWidthState(vis?.params.colWidth || []);
}
if (!isEqual(vis?.params.sort, uiStateValues.current.sort)) {
uiStateValues.current.sort = vis?.params.sort;
setSortState(vis?.params.sort || defaultSort);
}
});
uiState?.on('change', updateOnChange);
return () => {
uiState?.off('change', updateOnChange);
};
}, [uiState]);
return { columnsWidth, sort, setColumnsWidth, setSort };
};

View file

@ -22,6 +22,9 @@ import { PluginConfigDescriptor } from 'kibana/server';
import { configSchema, ConfigSchema } from '../config';
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {
legacyVisEnabled: true,
},
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'),

View file

@ -1654,4 +1654,26 @@ describe('migration visualization', () => {
expect(attributes).toEqual(oldAttributes);
});
});
describe('7.11.0 Data table vis - enable toolbar', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.11.0'](
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
const testDoc = {
attributes: {
title: 'My data table vis',
description: 'Data table vis for test.',
visState: `{"type":"table","params": {"perPage": 10,"showPartialRows": false,"showTotal": false,"totalFunc": "sum"}}`,
},
};
it('should enable toolbar in visState.params', () => {
const migratedDataTableVisDoc = migrate(testDoc);
const visState = JSON.parse(migratedDataTableVisDoc.attributes.visState);
expect(visState.params.showToolbar).toEqual(true);
});
});
});

View file

@ -757,6 +757,35 @@ const removeTSVBSearchSource: SavedObjectMigrationFn<any, any> = (doc) => {
return doc;
};
// [Data table visualization] Enable toolbar by default
const enableDataTableVisToolbar: SavedObjectMigrationFn<any, any> = (doc) => {
let visState;
try {
visState = JSON.parse(doc.attributes.visState);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
if (visState?.type === 'table') {
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify({
...visState,
params: {
...visState.params,
showToolbar: true,
},
}),
},
};
}
return doc;
};
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@ -790,4 +819,5 @@ export const visualizationSavedObjectTypeMigrations = {
'7.8.0': flow(migrateTsvbDefaultColorPalettes),
'7.9.3': flow(migrateMatchAllQuery),
'7.10.0': flow(migrateFilterRatioQuery, removeTSVBSearchSource),
'7.11.0': flow(enableDataTableVisToolbar),
};

View file

@ -195,7 +195,7 @@ export default function ({ getService }) {
},
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
migrationVersion: {
visualization: '7.10.0',
visualization: '7.11.0',
},
namespaces: ['foo-ns'],
references: [

View file

@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }) {
});
it('data tables are filtered', async () => {
await dashboardExpect.dataTableRowCount(0);
await dashboardExpect.dataTableNoResult();
});
it('goal and guages are filtered', async () => {
@ -145,7 +145,7 @@ export default function ({ getService, getPageObjects }) {
});
it('data tables are filtered', async () => {
await dashboardExpect.dataTableRowCount(0);
await dashboardExpect.dataTableNoResult();
});
it('goal and guages are filtered', async () => {

View file

@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }) {
const expectNoDataRenders = async () => {
await pieChart.expectPieSliceCount(0);
await dashboardExpect.seriesElementCount(0);
await dashboardExpect.dataTableRowCount(0);
await dashboardExpect.dataTableNoResult();
await dashboardExpect.savedSearchRowCount(0);
await dashboardExpect.inputControlItemCount(5);
await dashboardExpect.metricValuesExist(['0']);

View file

@ -18,8 +18,9 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }) {
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const inspector = getService('inspector');
const testSubjects = getService('testSubjects');
@ -97,12 +98,10 @@ export default function ({ getService, getPageObjects }) {
it('should show percentage columns', async () => {
async function expectValidTableData() {
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql([
'≥ 0B and < 1,000B',
'1,351 64.703%',
'≥ 1,000B and < 1.953KB',
'737 35.297%',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['≥ 0B and < 1,000B', '1,351', '64.703%'],
['≥ 1,000B and < 1.953KB', '737', '35.297%'],
]);
}
@ -142,12 +141,10 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.clickGo();
await PageObjects.visEditor.clickOptionsTab();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql([
'≥ 0B and < 1,000B',
'344.094B',
'≥ 1,000B and < 1.953KB',
'1.697KB',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['≥ 0B and < 1,000B', '344.094B'],
['≥ 1,000B and < 1.953KB', '1.697KB'],
]);
});
@ -158,12 +155,11 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Metric', 'metrics');
await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics');
await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets');
await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets');
await PageObjects.visEditor.selectAggregation('Terms', 'metrics', true);
await PageObjects.visEditor.selectField('geo.src', 'metrics', true);
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']);
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([['14,004', '1,412.6']]);
});
it('should show correct data for a data table with date histogram', async () => {
@ -176,36 +172,11 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.selectField('@timestamp');
await PageObjects.visEditor.setInterval('Day');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql([
'2015-09-20',
'4,757',
'2015-09-21',
'4,614',
'2015-09-22',
'4,633',
]);
});
it('should show correct data for a data table with date histogram', async () => {
await PageObjects.visualize.navigateToNewAggBasedVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Date Histogram');
await PageObjects.visEditor.selectField('@timestamp');
await PageObjects.visEditor.setInterval('Day');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql([
'2015-09-20',
'4,757',
'2015-09-21',
'4,614',
'2015-09-22',
'4,633',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['2015-09-20', '4,757'],
['2015-09-21', '4,614'],
['2015-09-22', '4,633'],
]);
});
@ -219,14 +190,11 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.selectField('UTC time');
await PageObjects.visEditor.setInterval('Day');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql([
'2015-09-20',
'4,757',
'2015-09-21',
'4,614',
'2015-09-22',
'4,633',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['2015-09-20', '4,757'],
['2015-09-21', '4,614'],
['2015-09-22', '4,633'],
]);
const header = await PageObjects.visChart.getTableVisHeader();
expect(header).to.contain('UTC time');
@ -235,15 +203,15 @@ export default function ({ getService, getPageObjects }) {
it('should correctly filter for applied time filter on the main timefield', async () => {
await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21');
await PageObjects.visChart.waitForVisualizationRenderingStabilized();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']);
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([['2015-09-20', '4,757']]);
});
it('should correctly filter for pinned filters', async () => {
await filterBar.toggleFilterPinned('@timestamp');
await PageObjects.visChart.waitForVisualizationRenderingStabilized();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']);
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([['2015-09-20', '4,757']]);
});
it('should show correct data for a data table with top hits', async () => {
@ -255,7 +223,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.selectAggregation('Top Hit', 'metrics');
await PageObjects.visEditor.selectField('agent.raw', 'metrics');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
const data = await PageObjects.visChart.getTableVisContent();
log.debug(data);
expect(data.length).to.be.greaterThan(0);
});
@ -269,12 +237,10 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.selectAggregation('Range');
await PageObjects.visEditor.selectField('bytes');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql([
'≥ 0B and < 1,000B',
'1,351',
'≥ 1,000B and < 1.953KB',
'737',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['≥ 0B and < 1,000B', '1,351'],
['≥ 1,000B and < 1.953KB', '737'],
]);
});

View file

@ -18,8 +18,9 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }) {
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const inspector = getService('inspector');
const retry = getService('retry');
@ -104,12 +105,11 @@ export default function ({ getService, getPageObjects }) {
);
await PageObjects.visEditor.clickBucket('Metric', 'metrics');
await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics');
await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets');
await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets');
await PageObjects.visEditor.selectAggregation('Terms', 'metrics', true);
await PageObjects.visEditor.selectField('geo.src', 'metrics', true);
await PageObjects.visEditor.clickGo();
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']);
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([['14,004', '1,412.6']]);
});
describe('data table with date histogram', async () => {
@ -127,15 +127,11 @@ export default function ({ getService, getPageObjects }) {
});
it('should show correct data', async () => {
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql([
'2015-09-20',
'4,757',
'2015-09-21',
'4,614',
'2015-09-22',
'4,633',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['2015-09-20', '4,757'],
['2015-09-21', '4,614'],
['2015-09-22', '4,633'],
]);
});
@ -143,16 +139,16 @@ export default function ({ getService, getPageObjects }) {
await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']);
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([['2015-09-20', '4,757']]);
});
it('should correctly filter for pinned filters', async () => {
await filterBar.toggleFilterPinned('@timestamp');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
const data = await PageObjects.visChart.getTableVisData();
expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']);
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([['2015-09-20', '4,757']]);
});
});
});

View file

@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardAddPanel.addVisualization(vizName1);
// hover and click on cell to filter
await PageObjects.visChart.filterOnTableCell('1', '2');
await PageObjects.visChart.filterOnTableCell(1, 2);
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();

View file

@ -18,10 +18,10 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }) {
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const filterBar = getService('filterBar');
const log = getService('log');
const renderable = getService('renderable');
const embedding = getService('embedding');
const PageObjects = getPageObjects([
@ -54,39 +54,18 @@ export default function ({ getService, getPageObjects }) {
await embedding.openInEmbeddedMode();
await renderable.waitForRender();
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql([
'2015-09-20 00:00',
'0B',
'5',
'2015-09-20 00:00',
'1.953KB',
'5',
'2015-09-20 00:00',
'3.906KB',
'9',
'2015-09-20 00:00',
'5.859KB',
'4',
'2015-09-20 00:00',
'7.813KB',
'14',
'2015-09-20 03:00',
'0B',
'32',
'2015-09-20 03:00',
'1.953KB',
'33',
'2015-09-20 03:00',
'3.906KB',
'45',
'2015-09-20 03:00',
'5.859KB',
'31',
'2015-09-20 03:00',
'7.813KB',
'48',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['2015-09-20 00:00', '0B', '5'],
['2015-09-20 00:00', '1.953KB', '5'],
['2015-09-20 00:00', '3.906KB', '9'],
['2015-09-20 00:00', '5.859KB', '4'],
['2015-09-20 00:00', '7.813KB', '14'],
['2015-09-20 03:00', '0B', '32'],
['2015-09-20 03:00', '1.953KB', '33'],
['2015-09-20 03:00', '3.906KB', '45'],
['2015-09-20 03:00', '5.859KB', '31'],
['2015-09-20 03:00', '7.813KB', '48'],
]);
});
@ -95,39 +74,18 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql([
'2015-09-21 00:00',
'0B',
'7',
'2015-09-21 00:00',
'1.953KB',
'9',
'2015-09-21 00:00',
'3.906KB',
'9',
'2015-09-21 00:00',
'5.859KB',
'6',
'2015-09-21 00:00',
'7.813KB',
'10',
'2015-09-21 00:00',
'11.719KB',
'1',
'2015-09-21 03:00',
'0B',
'28',
'2015-09-21 03:00',
'1.953KB',
'39',
'2015-09-21 03:00',
'3.906KB',
'36',
'2015-09-21 03:00',
'5.859KB',
'43',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['2015-09-21 00:00', '0B', '7'],
['2015-09-21 00:00', '1.953KB', '9'],
['2015-09-21 00:00', '3.906KB', '9'],
['2015-09-21 00:00', '5.859KB', '6'],
['2015-09-21 00:00', '7.813KB', '10'],
['2015-09-21 00:00', '11.719KB', '1'],
['2015-09-21 03:00', '0B', '28'],
['2015-09-21 03:00', '1.953KB', '39'],
['2015-09-21 03:00', '3.906KB', '36'],
['2015-09-21 03:00', '5.859KB', '43'],
]);
});
@ -136,39 +94,18 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
const data = await PageObjects.visChart.getTableVisData();
log.debug(data.split('\n'));
expect(data.trim().split('\n')).to.be.eql([
'03:00',
'0B',
'1',
'03:00',
'1.953KB',
'1',
'03:00',
'3.906KB',
'1',
'03:00',
'5.859KB',
'2',
'03:10',
'0B',
'1',
'03:10',
'5.859KB',
'1',
'03:10',
'7.813KB',
'1',
'03:15',
'0B',
'1',
'03:15',
'1.953KB',
'1',
'03:20',
'1.953KB',
'1',
const data = await PageObjects.visChart.getTableVisContent();
expect(data).to.be.eql([
['03:00', '0B', '1'],
['03:00', '1.953KB', '1'],
['03:00', '3.906KB', '1'],
['03:00', '5.859KB', '2'],
['03:10', '0B', '1'],
['03:10', '5.859KB', '1'],
['03:10', '7.813KB', '1'],
['03:15', '0B', '1'],
['03:15', '1.953KB', '1'],
['03:20', '1.953KB', '1'],
]);
});
});

View file

@ -18,10 +18,12 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }) {
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const PageObjects = getPageObjects([
'common',
'visualize',
@ -48,33 +50,32 @@ export default function ({ getService, getPageObjects }) {
describe('interval parameter uses autoBounds', function () {
it('should use provided value when number of generated buckets is less than histogram:maxBars', async function () {
const providedInterval = 2400000000;
const providedInterval = '2400000000';
log.debug(`Interval = ${providedInterval}`);
await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' });
await PageObjects.visEditor.clickGo();
await retry.try(async () => {
const data = await PageObjects.visChart.getTableVisData();
const dataArray = data.replace(/,/g, '').split('\n');
expect(dataArray.length).to.eql(20);
const bucketStart = parseInt(dataArray[0], 10);
const bucketEnd = parseInt(dataArray[2], 10);
const data = await PageObjects.visChart.getTableVisContent();
expect(data.length).to.eql(10);
const bucketStart = parseInt((data[0][0] as string).replace(/,/g, ''), 10);
const bucketEnd = parseInt((data[1][0] as string).replace(/,/g, ''), 10);
const actualInterval = bucketEnd - bucketStart;
expect(actualInterval).to.eql(providedInterval);
});
});
it('should scale value to round number when number of generated buckets is greater than histogram:maxBars', async function () {
const providedInterval = 100;
const providedInterval = '100';
log.debug(`Interval = ${providedInterval}`);
await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' });
await PageObjects.visEditor.clickGo();
await PageObjects.common.sleep(1000); //fix this
await PageObjects.common.sleep(1000); // fix this
await retry.try(async () => {
const data = await PageObjects.visChart.getTableVisData();
const dataArray = data.replace(/,/g, '').split('\n');
expect(dataArray.length).to.eql(20);
const bucketStart = parseInt(dataArray[0], 10);
const bucketEnd = parseInt(dataArray[2], 10);
const data = await PageObjects.visChart.getTableVisContent();
expect(data.length).to.eql(10);
const bucketStart = parseInt((data[0][0] as string).replace(/,/g, ''), 10);
const bucketEnd = parseInt((data[1][0] as string).replace(/,/g, ''), 10);
const actualInterval = bucketEnd - bucketStart;
expect(actualInterval).to.eql(1200000000);
});

View file

@ -52,8 +52,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.clickSavedSearch(savedSearchName);
await PageObjects.timePicker.setDefaultAbsoluteRange();
await retry.waitFor('wait for count to equal 9,109', async () => {
const data = await PageObjects.visChart.getTableVisData();
return data.trim() === '9,109';
const data = await PageObjects.visChart.getTableVisContent();
return data[0][0] === '9,109';
});
});
@ -81,8 +81,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'Sep 21, 2015 @ 10:00:00.000'
);
await retry.waitFor('wait for count to equal 3,950', async () => {
const data = await PageObjects.visChart.getTableVisData();
return data.trim() === '3,950';
const data = await PageObjects.visChart.getTableVisContent();
return data[0][0] === '3,950';
});
});
@ -90,16 +90,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await filterBar.addFilter('bytes', 'is between', '100', '3000');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('wait for count to equal 707', async () => {
const data = await PageObjects.visChart.getTableVisData();
return data.trim() === '707';
const data = await PageObjects.visChart.getTableVisContent();
return data[0][0] === '707';
});
});
it('should allow unlinking from a linked search', async () => {
await PageObjects.visualize.clickUnlinkSavedSearch();
await retry.waitFor('wait for count to equal 707', async () => {
const data = await PageObjects.visChart.getTableVisData();
return data.trim() === '707';
const data = await PageObjects.visChart.getTableVisContent();
return data[0][0] === '707';
});
// The filter on the saved search should now be in the editor
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
@ -109,8 +109,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await filterBar.toggleFilterEnabled('extension.raw');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('wait for count to equal 1,293', async () => {
const unfilteredData = await PageObjects.visChart.getTableVisData();
return unfilteredData.trim() === '1,293';
const unfilteredData = await PageObjects.visChart.getTableVisContent();
return unfilteredData[0][0] === '1,293';
});
});
@ -118,8 +118,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.saveVisualizationExpectSuccess('Unlinked before saved');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('wait for count to equal 1,293', async () => {
const data = await PageObjects.visChart.getTableVisData();
return data.trim() === '1,293';
const data = await PageObjects.visChart.getTableVisContent();
return data[0][0] === '1,293';
});
});
});

View file

@ -0,0 +1,331 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects([
'visualize',
'timePicker',
'visEditor',
'visChart',
'legacyDataTableVis',
]);
describe('legacy data table visualization', function indexPatternCreation() {
before(async function () {
log.debug('navigateToApp visualize');
await PageObjects.visualize.navigateToNewAggBasedVisualization();
log.debug('clickDataTable');
await PageObjects.visualize.clickDataTable();
log.debug('clickNewSearch');
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
log.debug('Bucket = Split rows');
await PageObjects.visEditor.clickBucket('Split rows');
log.debug('Aggregation = Histogram');
await PageObjects.visEditor.selectAggregation('Histogram');
log.debug('Field = bytes');
await PageObjects.visEditor.selectField('bytes');
log.debug('Interval = 2000');
await PageObjects.visEditor.setInterval('2000', { type: 'numeric' });
await PageObjects.visEditor.clickGo();
});
it('should show percentage columns', async () => {
async function expectValidTableData() {
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['≥ 0B and < 1,000B', '1,351', '64.703%'],
['≥ 1,000B and < 1.953KB', '737', '35.297%'],
]);
}
// load a plain table
await PageObjects.visualize.navigateToNewAggBasedVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Range');
await PageObjects.visEditor.selectField('bytes');
await PageObjects.visEditor.clickGo();
await PageObjects.visEditor.clickOptionsTab();
await PageObjects.visEditor.setSelectByOptionText(
'datatableVisualizationPercentageCol',
'Count'
);
await PageObjects.visEditor.clickGo();
await expectValidTableData();
// check that it works after selecting a column that's deleted
await PageObjects.visEditor.clickDataTab();
await PageObjects.visEditor.clickBucket('Metric', 'metrics');
await PageObjects.visEditor.selectAggregation('Average', 'metrics');
await PageObjects.visEditor.selectField('bytes', 'metrics');
await PageObjects.visEditor.removeDimension(1);
await PageObjects.visEditor.clickGo();
await PageObjects.visEditor.clickOptionsTab();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['≥ 0B and < 1,000B', '344.094B'],
['≥ 1,000B and < 1.953KB', '1.697KB'],
]);
});
it('should show correct data for a data table with date histogram', async () => {
await PageObjects.visualize.navigateToNewAggBasedVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Date Histogram');
await PageObjects.visEditor.selectField('@timestamp');
await PageObjects.visEditor.setInterval('Day');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['2015-09-20', '4,757'],
['2015-09-21', '4,614'],
['2015-09-22', '4,633'],
]);
});
describe('otherBucket', () => {
before(async () => {
await PageObjects.visualize.navigateToNewAggBasedVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Terms');
await PageObjects.visEditor.selectField('extension.raw');
await PageObjects.visEditor.setSize(2);
await PageObjects.visEditor.clickGo();
await PageObjects.visEditor.toggleOtherBucket();
await PageObjects.visEditor.toggleMissingBucket();
await PageObjects.visEditor.clickGo();
});
it('should show correct data', async () => {
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['jpg', '9,109'],
['css', '2,159'],
['Other', '2,736'],
]);
});
it('should apply correct filter', async () => {
await PageObjects.legacyDataTableVis.filterOnTableCell(1, 3);
await PageObjects.visChart.waitForVisualizationRenderingStabilized();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['png', '1,373'],
['gif', '918'],
['Other', '445'],
]);
});
});
describe('metricsOnAllLevels', () => {
before(async () => {
await PageObjects.visualize.navigateToNewAggBasedVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Terms');
await PageObjects.visEditor.selectField('extension.raw');
await PageObjects.visEditor.setSize(2);
await PageObjects.visEditor.toggleOpenEditor(2, 'false');
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Terms');
await PageObjects.visEditor.selectField('geo.dest');
await PageObjects.visEditor.toggleOpenEditor(3, 'false');
await PageObjects.visEditor.clickGo();
});
it('should show correct data without showMetricsAtAllLevels', async () => {
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['jpg', 'CN', '1,718'],
['jpg', 'IN', '1,511'],
['jpg', 'US', '770'],
['jpg', 'ID', '314'],
['jpg', 'PK', '244'],
['css', 'CN', '422'],
['css', 'IN', '346'],
['css', 'US', '189'],
['css', 'ID', '68'],
['css', 'BR', '58'],
]);
});
it('should show correct data without showMetricsAtAllLevels even if showPartialRows is selected', async () => {
await PageObjects.visEditor.clickOptionsTab();
await testSubjects.setCheckbox('showPartialRows', 'check');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['jpg', 'CN', '1,718'],
['jpg', 'IN', '1,511'],
['jpg', 'US', '770'],
['jpg', 'ID', '314'],
['jpg', 'PK', '244'],
['css', 'CN', '422'],
['css', 'IN', '346'],
['css', 'US', '189'],
['css', 'ID', '68'],
['css', 'BR', '58'],
]);
});
it('should show metrics on each level', async () => {
await PageObjects.visEditor.clickOptionsTab();
await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['jpg', '9,109', 'CN', '1,718'],
['jpg', '9,109', 'IN', '1,511'],
['jpg', '9,109', 'US', '770'],
['jpg', '9,109', 'ID', '314'],
['jpg', '9,109', 'PK', '244'],
['css', '2,159', 'CN', '422'],
['css', '2,159', 'IN', '346'],
['css', '2,159', 'US', '189'],
['css', '2,159', 'ID', '68'],
['css', '2,159', 'BR', '58'],
]);
});
it('should show metrics other than count on each level', async () => {
await PageObjects.visEditor.clickDataTab();
await PageObjects.visEditor.clickBucket('Metric', 'metrics');
await PageObjects.visEditor.selectAggregation('Average', 'metrics');
await PageObjects.visEditor.selectField('bytes', 'metrics');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
['jpg', '9,109', '5.469KB', 'CN', '1,718', '5.477KB'],
['jpg', '9,109', '5.469KB', 'IN', '1,511', '5.456KB'],
['jpg', '9,109', '5.469KB', 'US', '770', '5.371KB'],
['jpg', '9,109', '5.469KB', 'ID', '314', '5.424KB'],
['jpg', '9,109', '5.469KB', 'PK', '244', '5.41KB'],
['css', '2,159', '5.566KB', 'CN', '422', '5.712KB'],
['css', '2,159', '5.566KB', 'IN', '346', '5.754KB'],
['css', '2,159', '5.566KB', 'US', '189', '5.333KB'],
['css', '2,159', '5.566KB', 'ID', '68', '4.82KB'],
['css', '2,159', '5.566KB', 'BR', '58', '5.915KB'],
]);
});
});
describe('split tables', () => {
before(async () => {
await PageObjects.visualize.navigateToNewAggBasedVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.visEditor.clickBucket('Split table');
await PageObjects.visEditor.selectAggregation('Terms');
await PageObjects.visEditor.selectField('extension.raw');
await PageObjects.visEditor.setSize(2);
await PageObjects.visEditor.toggleOpenEditor(2, 'false');
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Terms');
await PageObjects.visEditor.selectField('geo.dest');
await PageObjects.visEditor.setSize(3, 3);
await PageObjects.visEditor.toggleOpenEditor(3, 'false');
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Terms');
await PageObjects.visEditor.selectField('geo.src');
await PageObjects.visEditor.setSize(3, 4);
await PageObjects.visEditor.toggleOpenEditor(4, 'false');
await PageObjects.visEditor.clickGo();
});
it('should have a splitted table', async () => {
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
[
['CN', 'CN', '330'],
['CN', 'IN', '274'],
['CN', 'US', '140'],
['IN', 'CN', '286'],
['IN', 'IN', '281'],
['IN', 'US', '133'],
['US', 'CN', '135'],
['US', 'IN', '134'],
['US', 'US', '52'],
],
[
['CN', 'CN', '90'],
['CN', 'IN', '84'],
['CN', 'US', '27'],
['IN', 'CN', '69'],
['IN', 'IN', '58'],
['IN', 'US', '34'],
['US', 'IN', '36'],
['US', 'CN', '29'],
['US', 'US', '13'],
],
]);
});
it('should show metrics for split bucket when using showMetricsAtAllLevels', async () => {
await PageObjects.visEditor.clickOptionsTab();
await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check');
await PageObjects.visEditor.clickGo();
const data = await PageObjects.legacyDataTableVis.getTableVisContent();
expect(data).to.be.eql([
[
['CN', '1,718', 'CN', '330'],
['CN', '1,718', 'IN', '274'],
['CN', '1,718', 'US', '140'],
['IN', '1,511', 'CN', '286'],
['IN', '1,511', 'IN', '281'],
['IN', '1,511', 'US', '133'],
['US', '770', 'CN', '135'],
['US', '770', 'IN', '134'],
['US', '770', 'US', '52'],
],
[
['CN', '422', 'CN', '90'],
['CN', '422', 'IN', '84'],
['CN', '422', 'US', '27'],
['IN', '346', 'CN', '69'],
['IN', '346', 'IN', '58'],
['IN', '346', 'US', '34'],
['US', '189', 'IN', '36'],
['US', '189', 'CN', '29'],
['US', '189', 'US', '13'],
],
]);
});
});
});
}

View file

@ -0,0 +1,48 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context.d';
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const browser = getService('browser');
const log = getService('log');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
describe('visualize with legacy visualizations', () => {
before(async () => {
log.debug('Starting visualize legacy before method');
await browser.setWindowSize(1280, 800);
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.loadIfNeeded('long_window_logstash');
await esArchiver.load('visualize');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
[UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b',
});
});
describe('legacy data table visualization', function () {
this.tags('ciGroup9');
loadTestFile(require.resolve('./_data_table'));
});
});
}

View file

@ -0,0 +1,39 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr';
// eslint-disable-next-line import/no-default-export
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const defaultConfig = await readConfigFile(require.resolve('./config'));
return {
...defaultConfig.getAll(),
testFiles: [require.resolve('./apps/visualize/legacy')],
kbnTestServer: {
...defaultConfig.get('kbnTestServer'),
serverArgs: [
...defaultConfig.get('kbnTestServer.serverArgs'),
'--vis_type_table.legacyVisEnabled=true',
],
},
};
}

View file

@ -39,6 +39,7 @@ import { TileMapPageProvider } from './tile_map_page';
import { TagCloudPageProvider } from './tag_cloud_page';
import { VegaChartPageProvider } from './vega_chart_page';
import { SavedObjectsPageProvider } from './management/saved_objects_page';
import { LegacyDataTableVisProvider } from './legacy/data_table_vis';
export const pageObjects = {
common: CommonPageProvider,
@ -52,6 +53,7 @@ export const pageObjects = {
newsfeed: NewsfeedPageProvider,
settings: SettingsPageProvider,
share: SharePageProvider,
legacyDataTableVis: LegacyDataTableVisProvider,
login: LoginPageProvider,
timelion: TimelionPageProvider,
timePicker: TimePickerProvider,

View file

@ -0,0 +1,96 @@
/*
* 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 { FtrProviderContext } from 'test/functional/ftr_provider_context';
import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper';
export function LegacyDataTableVisProvider({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
class LegacyDataTableVis {
/**
* Converts the table data into nested array
* [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ]
* @param element table
*/
private async getDataFromElement(element: WebElementWrapper): Promise<string[][]> {
const $ = await element.parseDomContent();
return $('tr')
.toArray()
.map((row) =>
$(row)
.find('td')
.toArray()
.map((cell) =>
$(cell)
.text()
.replace(/&nbsp;/g, '')
.trim()
)
);
}
public async getTableVisContent({ stripEmptyRows = true } = {}) {
return await retry.try(async () => {
const container = await testSubjects.find('tableVis');
const allTables = await testSubjects.findAllDescendant('paginated-table-body', container);
if (allTables.length === 0) {
return [];
}
const allData = await Promise.all(
allTables.map(async (t) => {
let data = await this.getDataFromElement(t);
if (stripEmptyRows) {
data = data.filter(
(row) => row.length > 0 && row.some((cell) => cell.trim().length > 0)
);
}
return data;
})
);
if (allTables.length === 1) {
// If there was only one table we return only the data for that table
// This prevents an unnecessary array around that single table, which
// is the case we have in most tests.
return allData[0];
}
return allData;
});
}
public async filterOnTableCell(column: number, row: number) {
await retry.try(async () => {
const tableVis = await testSubjects.find('tableVis');
const cell = await tableVis.findByCssSelector(
`tbody tr:nth-child(${row}) td:nth-child(${column})`
);
await cell.moveMouseTo();
const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell);
await filterBtn.click();
});
}
}
return new LegacyDataTableVis();
}

View file

@ -25,7 +25,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
const find = getService('find');
const log = getService('log');
const retry = getService('retry');
const table = getService('table');
const dataGrid = getService('dataGrid');
const defaultFindTimeout = config.get('timeouts.find');
const { common } = getPageObjects(['common']);
@ -283,18 +283,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
});
}
public async filterOnTableCell(column: string, row: string) {
await retry.try(async () => {
const tableVis = await testSubjects.find('tableVis');
const cell = await tableVis.findByCssSelector(
`tbody tr:nth-child(${row}) td:nth-child(${column})`
);
await cell.moveMouseTo();
const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell);
await filterBtn.click();
});
}
public async getMarkdownText() {
const markdownContainer = await testSubjects.find('markdownBody');
return markdownContainer.getVisibleText();
@ -306,44 +294,33 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
return element.getVisibleText();
}
public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) {
const tableVis = await testSubjects.find('tableVis');
const $ = await tableVis.parseDomContent();
const headers = $('span[ng-bind="::col.title"]')
.toArray()
.map((header: any) => $(header).text());
const fieldColumnIndex = headers.indexOf(fieldName);
return await find.byCssSelector(
`[data-test-subj="paginated-table-body"] tr:nth-of-type(${rowIndex}) td:nth-of-type(${
fieldColumnIndex + 1
}) a`
);
}
// Table visualization
/**
* If you are writing new tests, you should rather look into getTableVisContent method instead.
* @deprecated Use getTableVisContent instead.
*/
public async getTableVisData() {
return await testSubjects.getVisibleText('paginated-table-body');
public async getTableVisNoResult() {
return await testSubjects.find('tbvChartContainer>visNoResult');
}
/**
* This function returns the text displayed in the Table Vis header
*/
public async getTableVisHeader() {
return await testSubjects.getVisibleText('paginated-table-header');
return await testSubjects.getVisibleText('dataGridHeader');
}
public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) {
const headers = await dataGrid.getHeaders();
const fieldColumnIndex = headers.indexOf(fieldName);
const cell = await dataGrid.getCellElement(rowIndex, fieldColumnIndex + 1);
return await cell.findByTagName('a');
}
/**
* This function is the newer function to retrieve data from within a table visualization.
* It uses a better return format, than the old getTableVisData, by properly splitting
* cell values into arrays. Please use this function for newer tests.
* Function to retrieve data from within a table visualization.
*/
public async getTableVisContent({ stripEmptyRows = true } = {}) {
return await retry.try(async () => {
const container = await testSubjects.find('tableVis');
const allTables = await testSubjects.findAllDescendant('paginated-table-body', container);
const container = await testSubjects.find('tbvChart');
const allTables = await testSubjects.findAllDescendant('dataGridWrapper', container);
if (allTables.length === 0) {
return [];
@ -351,7 +328,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
const allData = await Promise.all(
allTables.map(async (t) => {
let data = await table.getDataFromElement(t);
let data = await dataGrid.getDataFromElement(t, 'tbvChartCellContent');
if (stripEmptyRows) {
data = data.filter(
(row) => row.length > 0 && row.some((cell) => cell.trim().length > 0)
@ -372,6 +349,18 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
});
}
public async filterOnTableCell(column: number, row: number) {
await retry.try(async () => {
const cell = await dataGrid.getCellElement(row, column);
await cell.moveMouseTo();
const filterBtn = await testSubjects.findDescendant(
'tbvChartCell__filterForCellValue',
cell
);
await filterBtn.click();
});
}
public async getMetric() {
const elements = await find.allByCssSelector(
'[data-test-subj="visualizationLoader"] .mtrVis__container'

View file

@ -388,7 +388,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP
}
}
public async setSize(newValue: string, aggId: string) {
public async setSize(newValue: number, aggId?: number) {
const dataTestSubj = aggId
? `visEditorAggAccordion${aggId} > sizeParamEditor`
: 'sizeParamEditor';

View file

@ -27,7 +27,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi
const testSubjects = getService('testSubjects');
const find = getService('find');
const filterBar = getService('filterBar');
const PageObjects = getPageObjects(['dashboard', 'visualize']);
const PageObjects = getPageObjects(['dashboard', 'visualize', 'visChart']);
const findTimeout = 2500;
return new (class DashboardExpect {
@ -233,14 +233,18 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi
async dataTableRowCount(expectedCount: number) {
log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`);
await retry.try(async () => {
const dataTableRows = await find.allByCssSelector(
'[data-test-subj="paginated-table-body"] [data-cell-content]',
findTimeout
);
const dataTableRows = await PageObjects.visChart.getTableVisContent();
expect(dataTableRows.length).to.be(expectedCount);
});
}
async dataTableNoResult(expectedCount: number) {
log.debug(`DashboardExpect.dataTableNoResult`);
await retry.try(async () => {
await PageObjects.visChart.getTableVisNoResult();
});
}
async seriesElementCount(expectedCount: number) {
log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`);
await retry.try(async () => {

View file

@ -18,6 +18,7 @@
*/
import { FtrProviderContext } from '../ftr_provider_context';
import { WebElementWrapper } from './lib/web_element_wrapper';
interface TabbedGridData {
columns: string[];
@ -26,6 +27,7 @@ interface TabbedGridData {
export function DataGridProvider({ getService }: FtrProviderContext) {
const find = getService('find');
const testSubjects = getService('testSubjects');
class DataGrid {
async getDataGridTableData(): Promise<TabbedGridData> {
@ -49,6 +51,58 @@ export function DataGridProvider({ getService }: FtrProviderContext) {
rows,
};
}
/**
* Converts the data grid data into nested array
* [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ]
* @param element table
*/
public async getDataFromElement(
element: WebElementWrapper,
cellDataTestSubj: string
): Promise<string[][]> {
const $ = await element.parseDomContent();
return $('[data-test-subj="dataGridRow"]')
.toArray()
.map((row) =>
$(row)
.findTestSubjects('dataGridRowCell')
.toArray()
.map((cell) =>
$(cell)
.findTestSubject(cellDataTestSubj)
.text()
.replace(/&nbsp;/g, '')
.trim()
)
);
}
/**
* Returns an array of data grid headers names
*/
public async getHeaders() {
const header = await testSubjects.find('dataGridWrapper > dataGridHeader');
const $ = await header.parseDomContent();
return $('.euiDataGridHeaderCell__content')
.toArray()
.map((cell) => $(cell).text());
}
/**
* Returns a grid cell element by row & column indexes.
* The row offset equals 1 since the first row of data grid is the header row.
* @param rowIndex data row index starting from 1 (1 means 1st row)
* @param columnIndex column index starting from 1 (1 means 1st column)
*/
public async getCellElement(rowIndex: number, columnIndex: number) {
return await find.byCssSelector(
`[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRow"]:nth-of-type(${
rowIndex + 1
})
[data-test-subj="dataGridRowCell"]:nth-of-type(${columnIndex})`
);
}
}
return new DataGrid();

View file

@ -46,7 +46,6 @@ import { ManagementMenuProvider } from './management';
import { QueryBarProvider } from './query_bar';
import { RemoteProvider } from './remote';
import { RenderableProvider } from './renderable';
import { TableProvider } from './table';
import { ToastsProvider } from './toasts';
import { DataGridProvider } from './data_grid';
import {
@ -82,7 +81,6 @@ export const services = {
dataGrid: DataGridProvider,
embedding: EmbeddingProvider,
renderable: RenderableProvider,
table: TableProvider,
browser: BrowserProvider,
pieChart: PieChartProvider,
inspector: InspectorProvider,

View file

@ -1,62 +0,0 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
import { WebElementWrapper } from './lib/web_element_wrapper';
export function TableProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
class Table {
/**
* Finds table and returns data in the nested array format
* [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ]
* @param dataTestSubj data-test-subj selector
*/
public async getDataFromTestSubj(dataTestSubj: string): Promise<string[][]> {
const table = await testSubjects.find(dataTestSubj);
return await this.getDataFromElement(table);
}
/**
* Converts the table data into nested array
* [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ]
* @param element table
*/
public async getDataFromElement(element: WebElementWrapper): Promise<string[][]> {
const $ = await element.parseDomContent();
return $('tr')
.toArray()
.map((row) =>
$(row)
.find('td')
.toArray()
.map((cell) =>
$(cell)
.text()
.replace(/&nbsp;/g, '')
.trim()
)
);
}
}
return new Table();
}

View file

@ -3693,6 +3693,8 @@
"visTypeTable.aggTable.rawLabel": "生",
"visTypeTable.directives.tableCellFilter.filterForValueTooltip": "値でフィルタリング",
"visTypeTable.directives.tableCellFilter.filterOutValueTooltip": "値を除外",
"visTypeTable.tableCellFilter.filterForValueText": "値でフィルタリング",
"visTypeTable.tableCellFilter.filterOutValueText": "値を除外",
"visTypeTable.function.help": "表ビジュアライゼーション",
"visTypeTable.params.defaultPercentageCol": "非表示",
"visTypeTable.params.PercentageColLabel": "パーセンテージ列",

View file

@ -3694,6 +3694,8 @@
"visTypeTable.aggTable.rawLabel": "原始",
"visTypeTable.directives.tableCellFilter.filterForValueTooltip": "筛留值",
"visTypeTable.directives.tableCellFilter.filterOutValueTooltip": "筛除值",
"visTypeTable.tableCellFilter.filterForValueText": "筛留值",
"visTypeTable.tableCellFilter.filterOutValueText": "筛除值",
"visTypeTable.function.help": "表可视化",
"visTypeTable.params.defaultPercentageCol": "不显示",
"visTypeTable.params.PercentageColLabel": "百分比列",