mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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 commit144a7cd77c
. * Use feature flag for legacy vis * Add footer row * Remove lock files * Revert "Remove lock files" This reverts commit5c5acd79f4
. * 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:
parent
62623cdab9
commit
845f716271
68 changed files with 2665 additions and 394 deletions
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [allowsNumericalAggregations](./kibana-plugin-plugins-data-public.fieldformat.allowsnumericalaggregations.md)
|
||||
|
||||
## FieldFormat.allowsNumericalAggregations property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
allowsNumericalAggregations?: boolean;
|
||||
```
|
|
@ -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 | 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 | string[]</code> | {<!-- -->string<!-- -->} - Field Format Type |
|
||||
| [getConfig](./kibana-plugin-plugins-data-public.fieldformat.getconfig.md) | | <code>FieldFormatsGetConfigFn | undefined</code> | |
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type IFieldFormat = PublicMethodsOf<FieldFormat>;
|
||||
export declare type IFieldFormat = FieldFormat;
|
||||
```
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
//
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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>;
|
||||
|
|
|
@ -11,8 +11,10 @@
|
|||
],
|
||||
"requiredBundles": [
|
||||
"kibanaUtils",
|
||||
"kibanaReact",
|
||||
"share",
|
||||
"charts",
|
||||
"visDefaultEditor"
|
||||
]
|
||||
],
|
||||
"optionalPlugins": ["usageCollection"]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
20
src/plugins/vis_type_table/public/components/index.ts
Normal file
20
src/plugins/vis_type_table/public/components/index.ts
Normal 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';
|
164
src/plugins/vis_type_table/public/components/table_vis_basic.tsx
Normal file
164
src/plugins/vis_type_table/public/components/table_vis_basic.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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 };
|
44
src/plugins/vis_type_table/public/legacy/__snapshots__/table_vis_legacy_fn.test.ts.snap
generated
Normal file
44
src/plugins/vis_type_table/public/legacy/__snapshots__/table_vis_legacy_fn.test.ts.snap
generated
Normal 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",
|
||||
},
|
||||
}
|
||||
`;
|
20
src/plugins/vis_type_table/public/legacy/index.ts
Normal file
20
src/plugins/vis_type_table/public/legacy/index.ts
Normal 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';
|
|
@ -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);
|
||||
};
|
|
@ -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' },
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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) {
|
||||
|
|
36
src/plugins/vis_type_table/public/register_vis.ts
Normal file
36
src/plugins/vis_type_table/public/register_vis.ts
Normal 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);
|
||||
};
|
59
src/plugins/vis_type_table/public/table_vis_renderer.tsx
Normal file
59
src/plugins/vis_type_table/public/table_vis_renderer.tsx
Normal 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
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
75
src/plugins/vis_type_table/public/utils/export_as_csv.ts
Normal file
75
src/plugins/vis_type_table/public/utils/export_as_csv.ts
Normal 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`);
|
||||
};
|
21
src/plugins/vis_type_table/public/utils/index.ts
Normal file
21
src/plugins/vis_type_table/public/utils/index.ts
Normal 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';
|
22
src/plugins/vis_type_table/public/utils/use/index.ts
Normal file
22
src/plugins/vis_type_table/public/utils/use/index.ts
Normal 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';
|
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
};
|
139
src/plugins/vis_type_table/public/utils/use/use_ui_state.ts
Normal file
139
src/plugins/vis_type_table/public/utils/use/use_ui_state.ts
Normal 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 };
|
||||
};
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
|
|
@ -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']]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
331
test/functional/apps/visualize/legacy/_data_table.ts
Normal file
331
test/functional/apps/visualize/legacy/_data_table.ts
Normal 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'],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
48
test/functional/apps/visualize/legacy/index.ts
Normal file
48
test/functional/apps/visualize/legacy/index.ts
Normal 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'));
|
||||
});
|
||||
});
|
||||
}
|
39
test/functional/config.legacy.ts
Normal file
39
test/functional/config.legacy.ts
Normal 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',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
96
test/functional/page_objects/legacy/data_table_vis.ts
Normal file
96
test/functional/page_objects/legacy/data_table_vis.ts
Normal 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(/ /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();
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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(/ /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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(/ /g, '')
|
||||
.trim()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Table();
|
||||
}
|
|
@ -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": "パーセンテージ列",
|
||||
|
|
|
@ -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": "百分比列",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue