inspector table adapter cleanup (#84232) (#86260)

This commit is contained in:
Peter Pisljar 2020-12-17 14:45:55 +01:00 committed by GitHub
parent dc09ad35e8
commit 385f72792e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 719 additions and 890 deletions

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { bfetch, expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup;
```
## Parameters
@ -15,7 +15,7 @@ setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { bfetch, e
| Parameter | Type | Description |
| --- | --- | --- |
| core | <code>CoreSetup&lt;DataStartDependencies, DataPublicPluginStart&gt;</code> | |
| { bfetch, expressions, uiActions, usageCollection } | <code>DataSetupDependencies</code> | |
| { bfetch, expressions, uiActions, usageCollection, inspector } | <code>DataSetupDependencies</code> | |
<b>Returns:</b>

View file

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

View file

@ -16,6 +16,5 @@ export interface Adapters
| Property | Type | Description |
| --- | --- | --- |
| [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) | <code>DataAdapter</code> | |
| [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) | <code>RequestAdapter</code> | |

View file

@ -20,6 +20,7 @@
| [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | <code>ExpressionsService</code> class is used for multiple purposes:<!-- -->1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. <code>ExpressionsService</code> defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. <code>ExpressionsService</code> creates a bound version of all exported contract functions. 5. Functions are bound the way there are:<!-- -->\`\`\`<!-- -->ts registerFunction = (...args: Parameters<!-- -->&lt;<!-- -->Executor\['registerFunction'\]<!-- -->&gt; ): ReturnType<!-- -->&lt;<!-- -->Executor\['registerFunction'\]<!-- -->&gt; =<!-- -->&gt; this.executor.registerFunction(...args); \`\`\`<!-- -->so that JSDoc appears in developers IDE when they use those <code>plugins.expressions.registerFunction(</code>. |
| [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | |
| [FunctionsRegistry](./kibana-plugin-plugins-expressions-public.functionsregistry.md) | |
| [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) | |
| [TypesRegistry](./kibana-plugin-plugins-expressions-public.typesregistry.md) | |
## Enumerations

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) &gt; [logDatatable](./kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md)
## TablesAdapter.logDatatable() method
<b>Signature:</b>
```typescript
logDatatable(name: string, datatable: Datatable): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| name | <code>string</code> | |
| datatable | <code>Datatable</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md)
## TablesAdapter class
<b>Signature:</b>
```typescript
export declare class TablesAdapter extends EventEmitter
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [tables](./kibana-plugin-plugins-expressions-public.tablesadapter.tables.md) | | <code>{</code><br/><code> [key: string]: Datatable;</code><br/><code> }</code> | |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [logDatatable(name, datatable)](./kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md) | | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) &gt; [tables](./kibana-plugin-plugins-expressions-public.tablesadapter.tables.md)
## TablesAdapter.tables property
<b>Signature:</b>
```typescript
get tables(): {
[key: string]: Datatable;
};
```

View file

@ -65,7 +65,7 @@ export class ExportCSVAction implements ActionByType<typeof ACTION_EXPORT_CSV> {
}
private hasDatatableContent = (adapters: Adapters | undefined) => {
return Object.keys(adapters?.tables || {}).length > 0;
return Object.keys(adapters?.tables || {}).length > 0 && adapters!.tables.allowCsvExport;
};
private getFormatter = (): FormatFactory | undefined => {
@ -76,7 +76,7 @@ export class ExportCSVAction implements ActionByType<typeof ACTION_EXPORT_CSV> {
private getDataTableContent = (adapters: Adapters | undefined) => {
if (this.hasDatatableContent(adapters)) {
return adapters?.tables;
return adapters?.tables.tables;
}
return;
};

View file

@ -203,11 +203,12 @@ describe('Aggs service', () => {
describe('start()', () => {
test('exposes proper contract', () => {
const start = service.start(startDeps);
expect(Object.keys(start).length).toBe(4);
expect(Object.keys(start).length).toBe(5);
expect(start).toHaveProperty('calculateAutoTimeExpression');
expect(start).toHaveProperty('getDateMetaByDatatableColumn');
expect(start).toHaveProperty('createAggConfigs');
expect(start).toHaveProperty('types');
expect(start).toHaveProperty('datatableUtilities');
});
test('types registry returns uninitialized type providers', () => {

View file

@ -18,7 +18,7 @@
*/
import { ExpressionsServiceSetup } from 'src/plugins/expressions/common';
import { IndexPattern, UI_SETTINGS } from '../../../common';
import { CreateAggConfigParams, IndexPattern, UI_SETTINGS } from '../../../common';
import { GetConfigFn } from '../../types';
import {
AggConfigs,
@ -29,6 +29,7 @@ import {
} from './';
import { AggsCommonSetup, AggsCommonStart } from './types';
import { getDateMetaByDatatableColumn } from './utils/time_column_meta';
import { getDatatableColumnUtilities } from './utils/datatable_column_meta';
/** @internal */
export const aggsRequiredUiSettings = [
@ -88,6 +89,15 @@ export class AggsCommonService {
const aggTypesStart = this.aggTypesRegistry.start();
const calculateAutoTimeExpression = getCalculateAutoTimeExpression(getConfig);
const createAggConfigs = (
indexPattern: IndexPattern,
configStates?: CreateAggConfigParams[]
) => {
return new AggConfigs(indexPattern, configStates, {
typesRegistry: aggTypesStart,
});
};
return {
calculateAutoTimeExpression,
getDateMetaByDatatableColumn: getDateMetaByDatatableColumn({
@ -96,11 +106,12 @@ export class AggsCommonService {
getConfig,
isDefaultTimezone,
}),
createAggConfigs: (indexPattern, configStates = [], schemas) => {
return new AggConfigs(indexPattern, configStates, {
typesRegistry: aggTypesStart,
});
},
datatableUtilities: getDatatableColumnUtilities({
getIndexPattern,
createAggConfigs,
aggTypesStart,
}),
createAggConfigs,
types: aggTypesStart,
};
}

View file

@ -94,6 +94,7 @@ import {
CreateAggConfigParams,
getCalculateAutoTimeExpression,
METRIC_TYPES,
AggConfig,
} from './';
export { IAggConfig, AggConfigSerialized } from './agg_config';
@ -127,10 +128,14 @@ export interface AggsCommonStart {
getDateMetaByDatatableColumn: (
column: DatatableColumn
) => Promise<undefined | { timeZone: string; timeRange?: TimeRange; interval: string }>;
datatableUtilities: {
getIndexPattern: (column: DatatableColumn) => Promise<IndexPattern | undefined>;
getAggConfig: (column: DatatableColumn) => Promise<AggConfig | undefined>;
isFilterable: (column: DatatableColumn) => boolean;
};
createAggConfigs: (
indexPattern: IndexPattern,
configStates?: CreateAggConfigParams[],
schemas?: Record<string, any>
configStates?: CreateAggConfigParams[]
) => InstanceType<typeof AggConfigs>;
types: ReturnType<AggTypesRegistry['start']>;
}

View file

@ -0,0 +1,68 @@
/*
* 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 { DatatableColumn } from 'src/plugins/expressions/common';
import { IndexPattern } from '../../../index_patterns';
import { AggConfigs, CreateAggConfigParams } from '../agg_configs';
import { AggTypesRegistryStart } from '../agg_types_registry';
import { IAggType } from '../agg_type';
export interface MetaByColumnDeps {
getIndexPattern: (id: string) => Promise<IndexPattern>;
createAggConfigs: (
indexPattern: IndexPattern,
configStates?: CreateAggConfigParams[]
) => InstanceType<typeof AggConfigs>;
aggTypesStart: AggTypesRegistryStart;
}
export const getDatatableColumnUtilities = (deps: MetaByColumnDeps) => {
const { getIndexPattern, createAggConfigs, aggTypesStart } = deps;
const getIndexPatternFromDatatableColumn = async (column: DatatableColumn) => {
if (!column.meta.index) return;
return await getIndexPattern(column.meta.index);
};
const getAggConfigFromDatatableColumn = async (column: DatatableColumn) => {
const indexPattern = await getIndexPatternFromDatatableColumn(column);
if (!indexPattern) return;
const aggConfigs = await createAggConfigs(indexPattern, [column.meta.sourceParams as any]);
return aggConfigs.aggs[0];
};
const isFilterableAggDatatableColumn = (column: DatatableColumn) => {
if (column.meta.source !== 'esaggs') {
return false;
}
const aggType = (aggTypesStart.get(column.meta.sourceParams?.type as string) as any)(
{}
) as IAggType;
return Boolean(aggType.createFilter);
};
return {
getIndexPattern: getIndexPatternFromDatatableColumn,
getAggConfig: getAggConfigFromDatatableColumn,
isFilterable: isFilterableAggDatatableColumn,
};
};

View file

@ -1,120 +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 { set } from '@elastic/safer-lodash-set';
import {
FormattedData,
TabularData,
TabularDataValue,
} from '../../../../../../plugins/inspector/common';
import { Filter } from '../../../es_query';
import { FormatFactory } from '../../../field_formats/utils';
import { TabbedTable } from '../../tabify';
import { createFilter } from './create_filter';
/**
* Type borrowed from the client-side FilterManager['addFilters'].
*
* We need to use a custom type to make this isomorphic since FilterManager
* doesn't exist on the server.
*
* @internal
*/
export type AddFilters = (filters: Filter[] | Filter, pinFilterStatus?: boolean) => void;
/**
* This function builds tabular data from the response and attaches it to the
* inspector. It will only be called when the data view in the inspector is opened.
*
* @internal
*/
export async function buildTabularInspectorData(
table: TabbedTable,
{
addFilters,
deserializeFieldFormat,
}: {
addFilters?: AddFilters;
deserializeFieldFormat: FormatFactory;
}
): Promise<TabularData> {
const aggConfigs = table.columns.map((column) => column.aggConfig);
const rows = table.rows.map((row) => {
return table.columns.reduce<Record<string, FormattedData>>((prev, cur, colIndex) => {
const value = row[cur.id];
let format = cur.aggConfig.toSerializedFieldFormat();
if (Object.keys(format).length < 1) {
// If no format exists, fall back to string as a default
format = { id: 'string' };
}
const fieldFormatter = deserializeFieldFormat(format);
prev[`col-${colIndex}-${cur.aggConfig.id}`] = new FormattedData(
value,
fieldFormatter.convert(value)
);
return prev;
}, {});
});
const columns = table.columns.map((col, colIndex) => {
const field = col.aggConfig.getField();
const isCellContentFilterable = col.aggConfig.isFilterable() && (!field || field.filterable);
return {
name: col.name,
field: `col-${colIndex}-${col.aggConfig.id}`,
filter:
addFilters &&
isCellContentFilterable &&
((value: TabularDataValue) => {
const rowIndex = rows.findIndex(
(row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
);
const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
if (filter) {
addFilters(filter);
}
}),
filterOut:
addFilters &&
isCellContentFilterable &&
((value: TabularDataValue) => {
const rowIndex = rows.findIndex(
(row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
);
const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
if (filter) {
const notOther = value.raw !== '__other__';
const notMissing = value.raw !== '__missing__';
if (Array.isArray(filter)) {
filter.forEach((f) => set(f, 'meta.negate', notOther && notMissing));
} else {
set(filter, 'meta.negate', notOther && notMissing);
}
addFilters(filter);
}
}),
};
});
return { columns, rows };
}

View file

@ -34,7 +34,6 @@ import { AggsStart, AggExpressionType } from '../../aggs';
import { ISearchStartSearchSource } from '../../search_source';
import { KibanaContext } from '../kibana_context_type';
import { AddFilters } from './build_tabular_inspector_data';
import { handleRequest, RequestHandlerParams } from './request_handler';
const name = 'esaggs';
@ -59,7 +58,6 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<
/** @internal */
export interface EsaggsStartDependencies {
addFilters?: AddFilters;
aggs: AggsStart;
deserializeFieldFormat: FormatFactory;
indexPatterns: IndexPatternsContract;

View file

@ -39,7 +39,6 @@ describe('esaggs expression function - public', () => {
jest.clearAllMocks();
mockParams = {
abortSignal: (jest.fn() as unknown) as jest.Mocked<AbortSignal>,
addFilters: jest.fn(),
aggs: ({
aggs: [{ type: { name: 'terms', postFlightRequest: jest.fn().mockResolvedValue({}) } }],
setTimeRange: jest.fn(),

View file

@ -36,13 +36,9 @@ import { ISearchStartSearchSource } from '../../search_source';
import { tabifyAggResponse } from '../../tabify';
import { getRequestInspectorStats, getResponseInspectorStats } from '../utils';
import type { AddFilters } from './build_tabular_inspector_data';
import { buildTabularInspectorData } from './build_tabular_inspector_data';
/** @internal */
export interface RequestHandlerParams {
abortSignal?: AbortSignal;
addFilters?: AddFilters;
aggs: IAggConfigs;
deserializeFieldFormat: FormatFactory;
filters?: Filter[];
@ -59,7 +55,6 @@ export interface RequestHandlerParams {
export const handleRequest = async ({
abortSignal,
addFilters,
aggs,
deserializeFieldFormat,
filters,
@ -199,16 +194,5 @@ export const handleRequest = async ({
const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams);
if (inspectorAdapters.data) {
inspectorAdapters.data.setTabularLoader(
() =>
buildTabularInspectorData(tabifiedResponse, {
addFilters,
deserializeFieldFormat,
}),
{ returnsFormattedValues: true }
);
}
return tabifiedResponse;
};

View file

@ -7,7 +7,8 @@
"bfetch",
"expressions",
"uiActions",
"share"
"share",
"inspector"
],
"optionalPlugins": ["usageCollection"],
"extraPublicDirs": ["common"],

View file

@ -1 +1,2 @@
@import './ui/index';
@import './utils/table_inspector_view/index';

View file

@ -71,6 +71,7 @@ import {
import { SavedObjectsClientPublicToCommon } from './index_patterns';
import { getIndexPatternLoad } from './index_patterns/expressions';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { getTableViewDescription } from './utils/table_inspector_view';
declare module '../../ui_actions/public' {
export interface ActionContextMapping {
@ -105,7 +106,7 @@ export class DataPublicPlugin
public setup(
core: CoreSetup<DataStartDependencies, DataPublicPluginStart>,
{ bfetch, expressions, uiActions, usageCollection }: DataSetupDependencies
{ bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies
): DataPublicPluginSetup {
const startServices = createStartServicesGetter(core.getStartServices);
@ -142,6 +143,15 @@ export class DataPublicPlugin
expressions,
});
inspector.registerView(
getTableViewDescription(() => ({
uiActions: startServices().plugins.uiActions,
uiSettings: startServices().core.uiSettings,
fieldFormats: startServices().self.fieldFormats,
isFilterable: startServices().self.search.aggs.datatableUtilities.isFilterable,
}))
);
return {
autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }),
search: searchService,

View file

@ -19,6 +19,7 @@ import { CoreSetup } from 'src/core/public';
import { CoreSetup as CoreSetup_2 } from 'kibana/public';
import { CoreStart } from 'kibana/public';
import { CoreStart as CoreStart_2 } from 'src/core/public';
import * as CSS from 'csstype';
import { Datatable as Datatable_2 } from 'src/plugins/expressions';
import { Datatable as Datatable_3 } from 'src/plugins/expressions/common';
import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions';
@ -66,11 +67,12 @@ import { Plugin as Plugin_2 } from 'src/core/public';
import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public';
import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public';
import { PopoverAnchorPosition } from '@elastic/eui';
import * as PropTypes from 'prop-types';
import { PublicContract } from '@kbn/utility-types';
import { PublicMethodsOf } from '@kbn/utility-types';
import { PublicUiSettingsParams } from 'src/core/server/types';
import React from 'react';
import * as React_2 from 'react';
import * as React_3 from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Reporter } from '@kbn/analytics';
import { RequestAdapter } from 'src/plugins/inspector/common';
@ -1896,7 +1898,7 @@ export class Plugin implements Plugin_2<DataPublicPluginSetup, DataPublicPluginS
// Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts
constructor(initializerContext: PluginInitializerContext_2<ConfigSchema>);
// (undocumented)
setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { bfetch, expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup;
// (undocumented)
start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart;
// (undocumented)
@ -2568,7 +2570,7 @@ export const UI_SETTINGS: {
// src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:128:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/search/aggs/types.ts:145:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/search/aggs/types.ts:150:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/search/search_source/search_source.ts:197:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts

View file

@ -88,11 +88,12 @@ describe('AggsService - public', () => {
describe('start()', () => {
test('exposes proper contract', () => {
const start = service.start(startDeps);
expect(Object.keys(start).length).toBe(4);
expect(Object.keys(start).length).toBe(5);
expect(start).toHaveProperty('calculateAutoTimeExpression');
expect(start).toHaveProperty('getDateMetaByDatatableColumn');
expect(start).toHaveProperty('createAggConfigs');
expect(start).toHaveProperty('types');
expect(start).toHaveProperty('datatableUtilities');
});
test('types registry returns initialized agg types', () => {

View file

@ -102,6 +102,7 @@ export class AggsService {
const {
calculateAutoTimeExpression,
getDateMetaByDatatableColumn,
datatableUtilities,
types,
} = this.aggsCommonService.start({
getConfig: this.getConfig!,
@ -148,7 +149,8 @@ export class AggsService {
return {
calculateAutoTimeExpression,
getDateMetaByDatatableColumn,
createAggConfigs: (indexPattern, configStates = [], schemas) => {
datatableUtilities,
createAggConfigs: (indexPattern, configStates = []) => {
return new AggConfigs(indexPattern, configStates, { typesRegistry });
},
types: typesRegistry,

View file

@ -68,6 +68,11 @@ export const searchAggsSetupMock = (): AggsSetup => ({
export const searchAggsStartMock = (): AggsStart => ({
calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig),
getDateMetaByDatatableColumn: jest.fn(),
datatableUtilities: {
isFilterable: jest.fn(),
getAggConfig: jest.fn(),
getIndexPattern: jest.fn(),
},
createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => {
return new AggConfigs(indexPattern, configStates, {
typesRegistry: mockAggTypesRegistry(),

View file

@ -72,7 +72,6 @@ describe('esaggs expression function - public', () => {
types: {},
};
startDependencies = {
addFilters: jest.fn(),
aggs: ({
createAggConfigs: jest.fn().mockReturnValue({ foo: 'bar' }),
} as unknown) as jest.Mocked<AggsStart>,
@ -113,7 +112,6 @@ describe('esaggs expression function - public', () => {
expect(handleEsaggsRequest).toHaveBeenCalledWith(null, args, {
abortSignal: mockHandlers.abortSignal,
addFilters: startDependencies.addFilters,
aggs: { foo: 'bar' },
deserializeFieldFormat: startDependencies.deserializeFieldFormat,
filters: undefined,

View file

@ -49,7 +49,6 @@ export function getFunctionDefinition({
...getEsaggsMeta(),
async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) {
const {
addFilters,
aggs,
deserializeFieldFormat,
indexPatterns,
@ -64,7 +63,6 @@ export function getFunctionDefinition({
return await handleEsaggsRequest(input, args, {
abortSignal: (abortSignal as unknown) as AbortSignal,
addFilters,
aggs: aggConfigs,
deserializeFieldFormat,
filters: get(input, 'filters', undefined),
@ -104,9 +102,8 @@ export function getEsaggs({
return getFunctionDefinition({
getStartDependencies: async () => {
const [, , self] = await getStartServices();
const { fieldFormats, indexPatterns, query, search } = self;
const { fieldFormats, indexPatterns, search } = self;
return {
addFilters: query.filterManager.addFilters.bind(query.filterManager),
aggs: search.aggs,
deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats),
indexPatterns,

View file

@ -31,6 +31,7 @@ import { QuerySetup, QueryStart } from './query';
import { IndexPatternsContract } from './index_patterns';
import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { Setup as InspectorSetup } from '../../inspector/public';
export interface DataPublicPluginEnhancements {
search: SearchEnhancements;
@ -40,6 +41,7 @@ export interface DataSetupDependencies {
bfetch: BfetchPublicSetup;
expressions: ExpressionsSetup;
uiActions: UiActionsSetup;
inspector: InspectorSetup;
usageCollection?: UsageCollectionSetup;
}

View file

@ -1,17 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Inspector Data View component should render empty state 1`] = `
<EnhancedType
<Component
adapters={
Object {
"data": DataAdapter {
"tables": TablesAdapter {
"_events": Object {
"change": [Function],
},
"_eventsCount": 1,
"_maxListeners": undefined,
"tabular": [Function],
"tabularOptions": Object {},
"_tables": Object {
"[object Object]": undefined,
},
Symbol(kCapture): false,
},
}
@ -123,138 +124,24 @@ exports[`Inspector Data View component should render empty state 1`] = `
<DataViewComponent
adapters={
Object {
"data": DataAdapter {
"tables": TablesAdapter {
"_events": Object {
"change": [Function],
},
"_eventsCount": 1,
"_maxListeners": undefined,
"tabular": [Function],
"tabularOptions": Object {},
"_tables": Object {
"[object Object]": undefined,
},
Symbol(kCapture): false,
},
}
}
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
kibana={
Object {
"notifications": Object {
"toasts": Object {
"danger": [Function],
"show": [Function],
"success": [Function],
"warning": [Function],
},
},
"overlays": Object {
"openFlyout": [Function],
"openModal": [Function],
},
"services": Object {},
}
}
fieldFormats={Object {}}
isFilterable={[MockFunction]}
title="Test Data"
uiActions={Object {}}
uiSettings={Object {}}
>
<EuiEmptyPrompt
body={
@ -262,7 +149,7 @@ exports[`Inspector Data View component should render empty state 1`] = `
<p>
<FormattedMessage
defaultMessage="The element did not provide any data."
id="inspector.data.noDataAvailableDescription"
id="data.inspector.table.noDataAvailableDescription"
values={Object {}}
/>
</p>
@ -272,7 +159,7 @@ exports[`Inspector Data View component should render empty state 1`] = `
<h2>
<FormattedMessage
defaultMessage="No data available"
id="inspector.data.noDataAvailableTitle"
id="data.inspector.table.noDataAvailableTitle"
values={Object {}}
/>
</h2>
@ -295,7 +182,7 @@ exports[`Inspector Data View component should render empty state 1`] = `
>
<FormattedMessage
defaultMessage="No data available"
id="inspector.data.noDataAvailableTitle"
id="data.inspector.table.noDataAvailableTitle"
values={Object {}}
>
No data available
@ -316,7 +203,7 @@ exports[`Inspector Data View component should render empty state 1`] = `
<p>
<FormattedMessage
defaultMessage="The element did not provide any data."
id="inspector.data.noDataAvailableDescription"
id="data.inspector.table.noDataAvailableDescription"
values={Object {}}
>
The element did not provide any data.
@ -329,7 +216,7 @@ exports[`Inspector Data View component should render empty state 1`] = `
</div>
</EuiEmptyPrompt>
</DataViewComponent>
</EnhancedType>
</Component>
`;
exports[`Inspector Data View component should render loading state 1`] = `
@ -442,6 +329,20 @@ exports[`Inspector Data View component should render loading state 1`] = `
}
}
>
<Component
adapters={
Object {
"tables": TablesAdapter {
"_events": Object {},
"_eventsCount": 0,
"_maxListeners": undefined,
"_tables": Object {},
Symbol(kCapture): false,
},
}
}
title="Test Data"
/>
<div>
loading
</div>

View file

@ -35,8 +35,10 @@ import { i18n } from '@kbn/i18n';
import { DataDownloadOptions } from './download_options';
import { DataViewRow, DataViewColumn } from '../types';
import { TabularData } from '../../../../common/adapters/data/types';
import { IUiSettingsClient } from '../../../../../../core/public';
import { Datatable, DatatableColumn } from '../../../../../expressions/public';
import { FieldFormatsStart } from '../../../field_formats';
import { UiActionsStart } from '../../../../../ui_actions/public';
interface DataTableFormatState {
columns: DataViewColumn[];
@ -44,10 +46,21 @@ interface DataTableFormatState {
}
interface DataTableFormatProps {
data: TabularData;
data: Datatable;
exportTitle: string;
uiSettings: IUiSettingsClient;
isFormatted?: boolean;
fieldFormats: FieldFormatsStart;
uiActions: UiActionsStart;
isFilterable: (column: DatatableColumn) => boolean;
}
interface RenderCellArguments {
table: Datatable;
columnIndex: number;
rowIndex: number;
formattedValue: string;
uiActions: UiActionsStart;
isFilterable: boolean;
}
export class DataTableFormat extends Component<DataTableFormatProps, DataTableFormatState> {
@ -55,25 +68,35 @@ export class DataTableFormat extends Component<DataTableFormatProps, DataTableFo
data: PropTypes.object.isRequired,
exportTitle: PropTypes.string.isRequired,
uiSettings: PropTypes.object.isRequired,
isFormatted: PropTypes.bool,
fieldFormats: PropTypes.object.isRequired,
uiActions: PropTypes.object.isRequired,
isFilterable: PropTypes.func.isRequired,
};
csvSeparator = this.props.uiSettings.get('csv:separator', ',');
quoteValues = this.props.uiSettings.get('csv:quoteValues', true);
state = {} as DataTableFormatState;
static renderCell(dataColumn: any, value: any, isFormatted: boolean = false) {
static renderCell({
table,
columnIndex,
rowIndex,
formattedValue,
uiActions,
isFilterable,
}: RenderCellArguments) {
const column = table.columns[columnIndex];
return (
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>{isFormatted ? value.formatted : value}</EuiFlexItem>
<EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} gutterSize="none" alignItems="center">
{dataColumn.filter && (
{isFilterable && (
<EuiToolTip
position="bottom"
content={
<FormattedMessage
id="inspector.data.filterForValueButtonTooltip"
id="data.inspector.table.filterForValueButtonTooltip"
defaultMessage="Filter for value"
/>
}
@ -81,23 +104,29 @@ export class DataTableFormat extends Component<DataTableFormatProps, DataTableFo
<EuiButtonIcon
iconType="plusInCircle"
color="text"
aria-label={i18n.translate('inspector.data.filterForValueButtonAriaLabel', {
aria-label={i18n.translate('data.inspector.table.filterForValueButtonAriaLabel', {
defaultMessage: 'Filter for value',
})}
data-test-subj="filterForInspectorCellValue"
className="insDataTableFormat__filter"
onClick={() => dataColumn.filter(value)}
onClick={() => {
const value = table.rows[rowIndex][column.id];
const eventData = { table, column: columnIndex, row: rowIndex, value };
uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER', {
data: { data: [eventData] },
});
}}
/>
</EuiToolTip>
)}
{dataColumn.filterOut && (
{isFilterable && (
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<FormattedMessage
id="inspector.data.filterOutValueButtonTooltip"
id="data.inspector.table.filterOutValueButtonTooltip"
defaultMessage="Filter out value"
/>
}
@ -105,12 +134,21 @@ export class DataTableFormat extends Component<DataTableFormatProps, DataTableFo
<EuiButtonIcon
iconType="minusInCircle"
color="text"
aria-label={i18n.translate('inspector.data.filterOutValueButtonAriaLabel', {
defaultMessage: 'Filter out value',
})}
aria-label={i18n.translate(
'data.inspector.table.filterOutValueButtonAriaLabel',
{
defaultMessage: 'Filter out value',
}
)}
data-test-subj="filterOutInspectorCellValue"
className="insDataTableFormat__filter"
onClick={() => dataColumn.filterOut(value)}
onClick={() => {
const value = table.rows[rowIndex][column.id];
const eventData = { table, column: columnIndex, row: rowIndex, value };
uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER', {
data: { data: [eventData], negate: true },
});
}}
/>
</EuiToolTip>
</EuiFlexItem>
@ -121,7 +159,12 @@ export class DataTableFormat extends Component<DataTableFormatProps, DataTableFo
);
}
static getDerivedStateFromProps({ data, isFormatted }: DataTableFormatProps) {
static getDerivedStateFromProps({
data,
uiActions,
fieldFormats,
isFilterable,
}: DataTableFormatProps) {
if (!data) {
return {
columns: null,
@ -129,12 +172,30 @@ export class DataTableFormat extends Component<DataTableFormatProps, DataTableFo
};
}
const columns = data.columns.map((dataColumn: any) => ({
name: dataColumn.name,
field: dataColumn.field,
sortable: isFormatted ? (row: DataViewRow) => row[dataColumn.field].raw : true,
render: (value: any) => DataTableFormat.renderCell(dataColumn, value, isFormatted),
}));
const columns = data.columns.map((dataColumn: any, index: number) => {
const formatParams = { id: 'string', ...dataColumn.meta.params };
const fieldFormatter = fieldFormats.deserialize(formatParams);
const filterable = isFilterable(dataColumn);
return {
originalColumn: () => dataColumn,
name: dataColumn.name,
field: dataColumn.id,
sortable: true,
render: (value: any) => {
const formattedValue = fieldFormatter.convert(value);
const rowIndex = data.rows.findIndex((row) => row[dataColumn.id] === value) || 0;
return DataTableFormat.renderCell({
table: data,
columnIndex: index,
rowIndex,
formattedValue,
uiActions,
isFilterable: filterable,
});
},
};
});
return { columns, rows: data.rows };
}
@ -152,12 +213,12 @@ export class DataTableFormat extends Component<DataTableFormatProps, DataTableFo
<EuiFlexItem grow={true} />
<EuiFlexItem grow={false}>
<DataDownloadOptions
isFormatted={this.props.isFormatted}
title={this.props.exportTitle}
csvSeparator={this.csvSeparator}
quoteValues={this.quoteValues}
columns={columns}
rows={rows}
fieldFormats={this.props.fieldFormats}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -18,11 +18,11 @@
*/
import React, { Suspense } from 'react';
import { getDataViewDescription } from '../index';
import { DataAdapter } from '../../../../common/adapters/data';
import { getTableViewDescription } from '../index';
import { mountWithIntl } from '@kbn/test/jest';
import { TablesAdapter } from '../../../../../expressions/common';
jest.mock('../lib/export_csv', () => ({
jest.mock('./export_csv', () => ({
exportAsCsv: jest.fn(),
}));
@ -30,13 +30,18 @@ describe('Inspector Data View', () => {
let DataView: any;
beforeEach(() => {
DataView = getDataViewDescription();
DataView = getTableViewDescription(() => ({
uiActions: {} as any,
uiSettings: {} as any,
fieldFormats: {} as any,
isFilterable: jest.fn(),
}));
});
it('should only show if data adapter is present', () => {
const adapter = new DataAdapter();
const adapter = new TablesAdapter();
expect(DataView.shouldShow({ data: adapter })).toBe(true);
expect(DataView.shouldShow({ tables: adapter })).toBe(true);
expect(DataView.shouldShow({})).toBe(false);
});
@ -44,7 +49,7 @@ describe('Inspector Data View', () => {
let adapters: any;
beforeEach(() => {
adapters = { data: new DataAdapter() };
adapters = { tables: new TablesAdapter() };
});
it('should render loading state', () => {
@ -60,9 +65,7 @@ describe('Inspector Data View', () => {
it('should render empty state', async () => {
const component = mountWithIntl(<DataView.component title="Test Data" adapters={adapters} />); // eslint-disable-line react/jsx-pascal-case
const tabularLoader = Promise.resolve(null);
adapters.data.setTabularLoader(() => tabularLoader);
await tabularLoader;
adapters.tables.logDatatable({ columns: [{ id: '1' }], rows: [{ '1': 123 }] });
// After the loader has resolved we'll still need one update, to "flush" the state changes
component.update();
expect(component).toMatchSnapshot();

View file

@ -0,0 +1,137 @@
/*
* 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, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { DataTableFormat } from './data_table';
import { IUiSettingsClient } from '../../../../../../core/public';
import { InspectorViewProps, Adapters } from '../../../../../inspector/public';
import { UiActionsStart } from '../../../../../ui_actions/public';
import { FieldFormatsStart } from '../../../field_formats';
import { TablesAdapter, Datatable, DatatableColumn } from '../../../../../expressions/public';
interface DataViewComponentState {
datatable: Datatable;
adapters: Adapters;
}
interface DataViewComponentProps extends InspectorViewProps {
uiSettings: IUiSettingsClient;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
isFilterable: (column: DatatableColumn) => boolean;
}
class DataViewComponent extends Component<DataViewComponentProps, DataViewComponentState> {
static propTypes = {
adapters: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
uiSettings: PropTypes.object,
uiActions: PropTypes.object.isRequired,
fieldFormats: PropTypes.object.isRequired,
isFilterable: PropTypes.func.isRequired,
};
state = {} as DataViewComponentState;
static getDerivedStateFromProps(
nextProps: Readonly<DataViewComponentProps>,
state: DataViewComponentState
) {
if (state && nextProps.adapters === state.adapters) {
return null;
}
const { tables } = nextProps.adapters.tables;
const keys = Object.keys(tables);
const datatable = keys.length ? tables[keys[0]] : undefined;
return {
adapters: nextProps.adapters,
datatable,
};
}
onUpdateData = (tables: TablesAdapter['tables']) => {
const keys = Object.keys(tables);
const datatable = keys.length ? tables[keys[0]] : undefined;
if (datatable) {
this.setState({
datatable,
});
}
};
componentDidMount() {
this.props.adapters.tables!.on('change', this.onUpdateData);
}
componentWillUnmount() {
this.props.adapters.tables!.removeListener('change', this.onUpdateData);
}
static renderNoData() {
return (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="data.inspector.table.noDataAvailableTitle"
defaultMessage="No data available"
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="data.inspector.table.noDataAvailableDescription"
defaultMessage="The element did not provide any data."
/>
</p>
</React.Fragment>
}
/>
);
}
render() {
if (!this.state.datatable) {
return DataViewComponent.renderNoData();
}
return (
<DataTableFormat
data={this.state.datatable}
exportTitle={this.props.title}
uiSettings={this.props.uiSettings}
fieldFormats={this.props.fieldFormats}
uiActions={this.props.uiActions}
isFilterable={this.props.isFilterable}
/>
);
}
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export default DataViewComponent;

View file

@ -0,0 +1,47 @@
/*
* 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 { IUiSettingsClient } from 'kibana/public';
import { UiActionsStart } from '../../../../../ui_actions/public';
import { FieldFormatsStart } from '../../../field_formats';
import { DatatableColumn } from '../../../../../expressions/common/expression_types/specs';
const DataViewComponent = lazy(() => import('./data_view'));
export const getDataViewComponentWrapper = (
getStartServices: () => {
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
uiSettings: IUiSettingsClient;
isFilterable: (column: DatatableColumn) => boolean;
}
) => {
return (props: any) => {
return (
<DataViewComponent
adapters={props.adapters}
title={props.title}
uiSettings={getStartServices().uiSettings}
fieldFormats={getStartServices().fieldFormats}
uiActions={getStartServices().uiActions}
isFilterable={getStartServices().isFilterable}
/>
);
};
};

View file

@ -24,8 +24,8 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { DataViewColumn, DataViewRow } from '../types';
import { exportAsCsv } from '../lib/export_csv';
import { exportAsCsv } from './export_csv';
import { FieldFormatsStart } from '../../../field_formats';
interface DataDownloadOptionsState {
isPopoverOpen: boolean;
@ -38,6 +38,7 @@ interface DataDownloadOptionsProps {
csvSeparator: string;
quoteValues: boolean;
isFormatted?: boolean;
fieldFormats: FieldFormatsStart;
}
class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownloadOptionsState> {
@ -45,9 +46,9 @@ class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownlo
title: PropTypes.string.isRequired,
csvSeparator: PropTypes.string.isRequired,
quoteValues: PropTypes.bool.isRequired,
isFormatted: PropTypes.bool,
columns: PropTypes.array,
rows: PropTypes.array,
fieldFormats: PropTypes.object.isRequired,
};
state = {
@ -66,10 +67,10 @@ class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownlo
});
};
exportCsv = (customParams: any = {}) => {
exportCsv = (isFormatted: boolean = true) => {
let filename = this.props.title;
if (!filename || filename.length === 0) {
filename = i18n.translate('inspector.data.downloadOptionsUnsavedFilename', {
filename = i18n.translate('data.inspector.table.downloadOptionsUnsavedFilename', {
defaultMessage: 'unsaved',
});
}
@ -79,38 +80,24 @@ class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownlo
rows: this.props.rows,
csvSeparator: this.props.csvSeparator,
quoteValues: this.props.quoteValues,
...customParams,
isFormatted,
fieldFormats: this.props.fieldFormats,
});
};
exportFormattedCsv = () => {
this.exportCsv({
valueFormatter: (item: any) => item.formatted,
});
this.exportCsv(true);
};
exportFormattedAsRawCsv = () => {
this.exportCsv({
valueFormatter: (item: any) => item.raw,
});
this.exportCsv(false);
};
renderUnformattedDownload() {
return (
<EuiButton size="s" onClick={this.exportCsv}>
<FormattedMessage
id="inspector.data.downloadCSVButtonLabel"
defaultMessage="Download CSV"
/>
</EuiButton>
);
}
renderFormattedDownloads() {
const button = (
<EuiButton iconType="arrowDown" iconSide="right" size="s" onClick={this.onTogglePopover}>
<FormattedMessage
id="inspector.data.downloadCSVToggleButtonLabel"
id="data.inspector.table.downloadCSVToggleButtonLabel"
defaultMessage="Download CSV"
/>
</EuiButton>
@ -121,14 +108,14 @@ class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownlo
onClick={this.exportFormattedCsv}
toolTipContent={
<FormattedMessage
id="inspector.data.formattedCSVButtonTooltip"
id="data.inspector.table.formattedCSVButtonTooltip"
defaultMessage="Download the data in table format"
/>
}
toolTipPosition="left"
>
<FormattedMessage
id="inspector.data.formattedCSVButtonLabel"
id="data.inspector.table.formattedCSVButtonLabel"
defaultMessage="Formatted CSV"
/>
</EuiContextMenuItem>,
@ -137,13 +124,13 @@ class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownlo
onClick={this.exportFormattedAsRawCsv}
toolTipContent={
<FormattedMessage
id="inspector.data.rawCSVButtonTooltip"
id="data.inspector.table.rawCSVButtonTooltip"
defaultMessage="Download the data as provided, for example, dates as timestamps"
/>
}
toolTipPosition="left"
>
<FormattedMessage id="inspector.data.rawCSVButtonLabel" defaultMessage="Raw CSV" />
<FormattedMessage id="data.inspector.table.rawCSVButtonLabel" defaultMessage="Raw CSV" />
</EuiContextMenuItem>,
];
@ -162,9 +149,7 @@ class DataDownloadOptions extends Component<DataDownloadOptionsProps, DataDownlo
}
render() {
return this.props.isFormatted
? this.renderFormattedDownloads()
: this.renderUnformattedDownload();
return this.renderFormattedDownloads();
}
}

View file

@ -22,6 +22,7 @@ import { isObject } from 'lodash';
import { saveAs } from '@elastic/filesaver';
import { DataViewColumn, DataViewRow } from '../types';
import { FieldFormatsStart } from '../../../field_formats/field_formats_service';
const LINE_FEED_CHARACTER = '\r\n';
const nonAlphaNumRE = /[^a-zA-Z0-9]/;
@ -46,17 +47,24 @@ function buildCsv(
rows: DataViewRow[],
csvSeparator: string,
quoteValues: boolean,
valueFormatter?: Function
isFormatted: boolean,
fieldFormats: FieldFormatsStart
) {
// Build the header row by its names
const header = columns.map((col) => escape(col.name, quoteValues));
const formatters = columns.map((column) => {
return fieldFormats.deserialize(column.originalColumn().meta.params);
});
// Convert the array of row objects to an array of row arrays
const orderedFieldNames = columns.map((col) => col.field);
const csvRows = rows.map((row) => {
return orderedFieldNames.map((field) =>
escape(valueFormatter ? valueFormatter(row[field]) : row[field], quoteValues)
);
return columns.map((column, i) => {
return escape(
isFormatted ? formatters[i].convert(row[column.field]) : row[column.field],
quoteValues
);
});
});
return (
@ -69,14 +77,18 @@ export function exportAsCsv({
filename,
columns,
rows,
valueFormatter,
isFormatted,
csvSeparator,
quoteValues,
fieldFormats,
}: any) {
const type = 'text/plain;charset=utf-8';
const csv = new Blob([buildCsv(columns, rows, csvSeparator, quoteValues, valueFormatter)], {
type,
});
const csv = new Blob(
[buildCsv(columns, rows, csvSeparator, quoteValues, isFormatted, fieldFormats)],
{
type,
}
);
saveAs(csv, filename);
}

View file

@ -16,24 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
import { Adapters, InspectorViewDescription } from '../../../../inspector/public';
import { getDataViewComponentWrapper } from './components/data_view_wrapper';
import { UiActionsStart } from '../../../../ui_actions/public';
import { FieldFormatsStart } from '../../field_formats';
import { DatatableColumn } from '../../../../expressions/common/expression_types/specs';
import { InspectorViewDescription } from '../../types';
import { Adapters } from '../../../common';
const DataViewComponent = lazy(() => import('./components/data_view'));
export const getDataViewDescription = (): InspectorViewDescription => ({
title: i18n.translate('inspector.data.dataTitle', {
export const getTableViewDescription = (
getStartServices: () => {
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
isFilterable: (column: DatatableColumn) => boolean;
uiSettings: IUiSettingsClient;
}
): InspectorViewDescription => ({
title: i18n.translate('data.inspector.table.dataTitle', {
defaultMessage: 'Data',
}),
order: 10,
help: i18n.translate('inspector.data.dataDescriptionTooltip', {
help: i18n.translate('data.inspector.table..dataDescriptionTooltip', {
defaultMessage: 'View the data behind the visualization',
}),
shouldShow(adapters: Adapters) {
return Boolean(adapters.data);
return Boolean(adapters.tables);
},
component: DataViewComponent,
component: getDataViewComponentWrapper(getStartServices),
});

View file

@ -17,15 +17,20 @@
* under the License.
*/
import { TabularDataRow } from '../../../common/adapters';
import { Datatable, DatatableColumn, DatatableRow } from '../../../../expressions/common';
type DataViewColumnRender = (value: string, _item: TabularDataRow) => string;
type DataViewColumnRender = (value: string, _item: DatatableRow) => string;
export interface DataViewColumn {
originalColumn: () => DatatableColumn;
name: string;
field: string;
sortable: (item: TabularDataRow) => string | number;
sortable: (item: DatatableRow) => string | number;
render: DataViewColumnRender;
}
export type DataViewRow = TabularDataRow;
export type DataViewRow = DatatableRow;
export interface TableInspectorAdapter {
[key: string]: Datatable;
}

View file

@ -86,6 +86,7 @@ export class AggsService {
const {
calculateAutoTimeExpression,
getDateMetaByDatatableColumn,
datatableUtilities,
types,
} = this.aggsCommonService.start({
getConfig,
@ -130,7 +131,8 @@ export class AggsService {
return {
calculateAutoTimeExpression,
getDateMetaByDatatableColumn,
createAggConfigs: (indexPattern, configStates = [], schemas) => {
datatableUtilities,
createAggConfigs: (indexPattern, configStates = []) => {
return new AggConfigs(indexPattern, configStates, { typesRegistry });
},
types: typesRegistry,

View file

@ -70,6 +70,11 @@ export const searchAggsSetupMock = (): AggsSetup => ({
const commonStartMock = (): AggsCommonStart => ({
calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig),
getDateMetaByDatatableColumn: jest.fn(),
datatableUtilities: {
getIndexPattern: jest.fn(),
getAggConfig: jest.fn(),
isFilterable: jest.fn(),
},
createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => {
return new AggConfigs(indexPattern, configStates, {
typesRegistry: mockAggTypesRegistry(),

View file

@ -23,18 +23,21 @@ export class ContactCardExportableEmbeddable extends ContactCardEmbeddable {
public getInspectorAdapters = () => {
return {
tables: {
layer1: {
type: 'datatable',
columns: [
{ id: 'firstName', name: 'First Name' },
{ id: 'originalLastName', name: 'Last Name' },
],
rows: [
{
firstName: this.getInput().firstName,
orignialLastName: this.getInput().lastName,
},
],
allowCsvExport: true,
tables: {
layer1: {
type: 'datatable',
columns: [
{ id: 'firstName', name: 'First Name' },
{ id: 'originalLastName', name: 'Last Name' },
],
rows: [
{
firstName: this.getInput().firstName,
orignialLastName: this.getInput().lastName,
},
],
},
},
},
};

View file

@ -109,10 +109,6 @@ export const ACTION_EDIT_PANEL = "editPanel";
export interface Adapters {
// (undocumented)
[key: string]: any;
// Warning: (ae-forgotten-export) The symbol "DataAdapter" needs to be exported by the entry point index.d.ts
//
// (undocumented)
data?: DataAdapter;
// Warning: (ae-forgotten-export) The symbol "RequestAdapter" needs to be exported by the entry point index.d.ts
//
// (undocumented)

View file

@ -220,10 +220,10 @@ describe('Execution', () => {
});
describe('inspector adapters', () => {
test('by default, "data" and "requests" inspector adapters are available', async () => {
test('by default, "tables" and "requests" inspector adapters are available', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
expect(result).toMatchObject({
data: expect.any(Object),
tables: expect.any(Object),
requests: expect.any(Object),
});
});

View file

@ -23,7 +23,7 @@ import { Executor } from '../executor';
import { createExecutionContainer, ExecutionContainer } from './container';
import { createError } from '../util';
import { abortSignalToPromise, Defer, now } from '../../../kibana_utils/common';
import { RequestAdapter, DataAdapter, Adapters } from '../../../inspector/common';
import { RequestAdapter, Adapters } from '../../../inspector/common';
import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error';
import {
ExpressionAstExpression,
@ -34,11 +34,12 @@ import {
ExpressionAstNode,
} from '../ast';
import { ExecutionContext, DefaultInspectorAdapters } from './types';
import { getType, ExpressionValue } from '../expression_types';
import { getType, ExpressionValue, Datatable } from '../expression_types';
import { ArgumentType, ExpressionFunction } from '../expression_functions';
import { getByAlias } from '../util/get_by_alias';
import { ExecutionContract } from './execution_contract';
import { ExpressionExecutionParams } from '../service';
import { TablesAdapter } from '../util/tables_adapter';
/**
* AbortController is not available in Node until v15, so we
@ -72,7 +73,7 @@ export interface ExecutionParams {
const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({
requests: new RequestAdapter(),
data: new DataAdapter(),
tables: new TablesAdapter(),
});
export class Execution<
@ -166,6 +167,9 @@ export class Execution<
ast,
});
const inspectorAdapters =
execution.params.inspectorAdapters || createDefaultInspectorAdapters();
this.context = {
getSearchContext: () => this.execution.params.searchContext || {},
getSearchSessionId: () => execution.params.searchSessionId,
@ -175,7 +179,10 @@ export class Execution<
variables: execution.params.variables || {},
types: executor.getTypes(),
abortSignal: this.abortController.signal,
inspectorAdapters: execution.params.inspectorAdapters || createDefaultInspectorAdapters(),
inspectorAdapters,
logDatatable: (name: string, datatable: Datatable) => {
inspectorAdapters.tables[name] = datatable;
},
...(execution.params as any).extraContext,
};
}

View file

@ -71,7 +71,7 @@ describe('ExecutionContract', () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
expect(contract.inspect()).toMatchObject({
data: expect.any(Object),
tables: expect.any(Object),
requests: expect.any(Object),
});
});

View file

@ -21,8 +21,9 @@
import type { KibanaRequest } from 'src/core/server';
import { ExpressionType, SerializableState } from '../expression_types';
import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common';
import { Adapters, RequestAdapter } from '../../../inspector/common';
import { SavedObject, SavedObjectAttributes } from '../../../../core/public';
import { TablesAdapter } from '../util/tables_adapter';
/**
* `ExecutionContext` is an object available to all functions during a single execution;
@ -89,5 +90,5 @@ export interface ExecutionContext<
*/
export interface DefaultInspectorAdapters extends Adapters {
requests: RequestAdapter;
data: DataAdapter;
tables: TablesAdapter;
}

View file

@ -19,3 +19,4 @@
export * from './create_error';
export * from './get_by_alias';
export * from './tables_adapter';

View file

@ -17,6 +17,18 @@
* under the License.
*/
export class FormattedData {
constructor(public readonly raw: any, public readonly formatted: any) {}
import { EventEmitter } from 'events';
import { Datatable } from '../expression_types/specs';
export class TablesAdapter extends EventEmitter {
private _tables: { [key: string]: Datatable } = {};
public logDatatable(name: string, datatable: Datatable): void {
this._tables[name] = datatable;
this.emit('change', this.tables);
}
public get tables() {
return this._tables;
}
}

View file

@ -117,4 +117,5 @@ export {
ExpressionsService,
ExpressionsServiceSetup,
ExpressionsServiceStart,
TablesAdapter,
} from '../common';

View file

@ -166,7 +166,7 @@ describe('ExpressionLoader', () => {
it('inspect() returns correct inspector adapters', () => {
const expressionDataHandler = new ExpressionLoader(element, expressionString, {});
expect(expressionDataHandler.inspect()).toHaveProperty('data');
expect(expressionDataHandler.inspect()).toHaveProperty('tables');
expect(expressionDataHandler.inspect()).toHaveProperty('requests');
});
});

View file

@ -1096,6 +1096,18 @@ export interface SerializedFieldFormat<TParams = Record<string, any>> {
// @public (undocumented)
export type Style = ExpressionTypeStyle;
// Warning: (ae-missing-release-tag) "TablesAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class TablesAdapter extends EventEmitter {
// (undocumented)
logDatatable(name: string, datatable: Datatable): void;
// (undocumented)
get tables(): {
[key: string]: Datatable;
};
}
// Warning: (ae-missing-release-tag) "TextAlignment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public

View file

@ -1,40 +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 { EventEmitter } from 'events';
import { TabularCallback, TabularHolder, TabularLoaderOptions } from './types';
export class DataAdapter extends EventEmitter {
private tabular?: TabularCallback;
private tabularOptions?: TabularLoaderOptions;
public setTabularLoader(callback: TabularCallback, options: TabularLoaderOptions = {}): void {
this.tabular = callback;
this.tabularOptions = options;
this.emit('change', 'tabular');
}
public getTabular(): Promise<TabularHolder> {
if (!this.tabular || !this.tabularOptions) {
return Promise.resolve({ data: null, options: {} });
}
const options = this.tabularOptions;
return Promise.resolve(this.tabular()).then((data) => ({ data, options }));
}
}

View file

@ -1,71 +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 { DataAdapter } from './data_adapter';
describe('DataAdapter', () => {
let adapter: DataAdapter;
beforeEach(() => {
adapter = new DataAdapter();
});
describe('getTabular()', () => {
it('should return a null promise when called before initialized', () => {
expect(adapter.getTabular()).resolves.toEqual({
data: null,
options: {},
});
});
it('should call the provided callback and resolve with its value', async () => {
const data = { columns: [], rows: [] };
const spy = jest.fn(() => data);
adapter.setTabularLoader(spy);
expect(spy).not.toBeCalled();
const result = await adapter.getTabular();
expect(spy).toBeCalled();
expect(result.data).toBe(data);
});
it('should pass through options specified via setTabularLoader', async () => {
const data = { columns: [], rows: [] };
adapter.setTabularLoader(() => data, { returnsFormattedValues: true });
const result = await adapter.getTabular();
expect(result.options).toEqual({ returnsFormattedValues: true });
});
it('should return options set when starting loading data', async () => {
const data = { columns: [], rows: [] };
adapter.setTabularLoader(() => data, { returnsFormattedValues: true });
const waitForResult = adapter.getTabular();
adapter.setTabularLoader(() => data, { returnsFormattedValues: false });
const result = await waitForResult;
expect(result.options).toEqual({ returnsFormattedValues: true });
});
});
it('should emit a "tabular" event when a new tabular loader is specified', () => {
const data = { columns: [], rows: [] };
const spy = jest.fn();
adapter.once('change', spy);
adapter.setTabularLoader(() => data);
expect(spy).toBeCalled();
});
});

View file

@ -1,22 +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.
*/
export * from './data_adapter';
export * from './formatted_data';
export * from './types';

View file

@ -1,48 +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.
*/
export interface TabularDataValue {
formatted: string;
raw: unknown;
}
export interface TabularDataColumn {
name: string;
field: string;
filter?: (value: TabularDataValue) => void;
filterOut?: (value: TabularDataValue) => void;
}
export type TabularDataRow = Record<TabularDataColumn['field'], TabularDataValue>;
export interface TabularData {
columns: TabularDataColumn[];
rows: TabularDataRow[];
}
export type TabularCallback = () => TabularData | Promise<TabularData>;
export interface TabularHolder {
data: TabularData | null;
options: TabularLoaderOptions;
}
export interface TabularLoaderOptions {
returnsFormattedValues?: boolean;
}

View file

@ -17,6 +17,5 @@
* under the License.
*/
export * from './data';
export * from './request';
export * from './types';

View file

@ -17,14 +17,12 @@
* under the License.
*/
import type { DataAdapter } from './data';
import type { RequestAdapter } from './request';
/**
* The interface that the adapters used to open an inspector have to fullfill.
*/
export interface Adapters {
data?: DataAdapter;
requests?: RequestAdapter;
[key: string]: any;
}

View file

@ -19,15 +19,9 @@
export {
Adapters,
DataAdapter,
FormattedData,
RequestAdapter,
RequestStatistic,
RequestStatistics,
RequestStatus,
RequestResponder,
TabularData,
TabularDataColumn,
TabularDataRow,
TabularDataValue,
} from './adapters';

View file

@ -26,7 +26,7 @@ import { InspectorOptions, InspectorSession } from './types';
import { InspectorPanel } from './ui/inspector_panel';
import { Adapters } from '../common';
import { getRequestsViewDescription, getDataViewDescription } from './views';
import { getRequestsViewDescription } from './views';
export interface Setup {
registerView: InspectorViewRegistry['register'];
@ -70,7 +70,6 @@ export class InspectorPublicPlugin implements Plugin<Setup, Start> {
public async setup(core: CoreSetup) {
this.views = new InspectorViewRegistry();
this.views.register(getDataViewDescription());
this.views.register(getRequestsViewDescription());
return {

View file

@ -18,19 +18,12 @@
*/
import { inspectorPluginMock } from '../mocks';
import { DataAdapter, RequestAdapter } from '../../common/adapters';
import { RequestAdapter } from '../../common/adapters';
const adapter1 = new DataAdapter();
const adapter2 = new RequestAdapter();
describe('inspector', () => {
describe('isAvailable()', () => {
it('should return false if no view would be available', async () => {
const { doStart } = await inspectorPluginMock.createPlugin();
const start = await doStart();
expect(start.isAvailable({ adapter1 })).toBe(false);
});
it('should return true if views would be available, false otherwise', async () => {
const { setup, doStart } = await inspectorPluginMock.createPlugin();
@ -44,7 +37,6 @@ describe('inspector', () => {
const start = await doStart();
expect(start.isAvailable({ adapter1 })).toBe(true);
expect(start.isAvailable({ adapter2 })).toBe(false);
});
});

View file

@ -1,2 +1 @@
@import './data/index';
@import './requests/index';

View file

@ -1,187 +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 React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingChart,
EuiPanel,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { DataTableFormat } from './data_table';
import { InspectorViewProps } from '../../../types';
import { Adapters } from '../../../../common';
import {
TabularLoaderOptions,
TabularData,
TabularHolder,
} from '../../../../common/adapters/data/types';
import { IUiSettingsClient } from '../../../../../../core/public';
import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public';
interface DataViewComponentState {
tabularData: TabularData | null;
tabularOptions: TabularLoaderOptions;
adapters: Adapters;
tabularPromise: Promise<TabularHolder> | null;
}
interface DataViewComponentProps extends InspectorViewProps {
kibana: KibanaReactContextValue<{ uiSettings: IUiSettingsClient }>;
}
class DataViewComponent extends Component<DataViewComponentProps, DataViewComponentState> {
static propTypes = {
adapters: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
kibana: PropTypes.object,
};
state = {} as DataViewComponentState;
_isMounted = false;
static getDerivedStateFromProps(
nextProps: DataViewComponentProps,
state: DataViewComponentState
) {
if (state && nextProps.adapters === state.adapters) {
return null;
}
return {
adapters: nextProps.adapters,
tabularData: null,
tabularOptions: {},
tabularPromise: nextProps.adapters.data!.getTabular(),
};
}
onUpdateData = (type: string) => {
if (type === 'tabular') {
this.setState({
tabularData: null,
tabularOptions: {},
tabularPromise: this.props.adapters.data!.getTabular(),
});
}
};
async finishLoadingData() {
const { tabularPromise } = this.state;
if (tabularPromise) {
const tabularData: TabularHolder = await tabularPromise;
if (this._isMounted) {
this.setState({
tabularData: tabularData.data,
tabularOptions: tabularData.options,
tabularPromise: null,
});
}
}
}
componentDidMount() {
this._isMounted = true;
this.props.adapters.data!.on('change', this.onUpdateData);
this.finishLoadingData();
}
componentWillUnmount() {
this._isMounted = false;
this.props.adapters.data!.removeListener('change', this.onUpdateData);
}
componentDidUpdate() {
this.finishLoadingData();
}
static renderNoData() {
return (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="inspector.data.noDataAvailableTitle"
defaultMessage="No data available"
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="inspector.data.noDataAvailableDescription"
defaultMessage="The element did not provide any data."
/>
</p>
</React.Fragment>
}
/>
);
}
static renderLoading() {
return (
<EuiFlexGroup justifyContent="center" alignItems="center" style={{ height: '100%' }}>
<EuiFlexItem grow={false}>
<EuiPanel className="eui-textCenter">
<EuiLoadingChart size="m" />
<EuiSpacer size="s" />
<EuiText>
<p>
<FormattedMessage
id="inspector.data.gatheringDataLabel"
defaultMessage="Gathering data"
/>
</p>
</EuiText>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}
render() {
if (this.state.tabularPromise) {
return DataViewComponent.renderLoading();
} else if (!this.state.tabularData) {
return DataViewComponent.renderNoData();
}
return (
<DataTableFormat
data={this.state.tabularData}
isFormatted={this.state.tabularOptions.returnsFormattedValues}
exportTitle={this.props.title}
uiSettings={this.props.kibana.services.uiSettings}
/>
);
}
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export default withKibana(DataViewComponent);

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { getDataViewDescription } from './data';
export { getRequestsViewDescription } from './requests';

View file

@ -34,9 +34,12 @@ export const createRegionMapFn = () => ({
default: '"{}"',
},
},
fn(context, args) {
fn(context, args, handlers) {
const visConfig = JSON.parse(args.visConfig);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: 'visualization',

View file

@ -57,7 +57,11 @@ describe('interpreter/functions#regionmap', () => {
};
it('returns an object with the correct structure', () => {
const actual = fn(context, { visConfig: JSON.stringify(visConfig) });
const actual = fn(
context,
{ visConfig: JSON.stringify(visConfig) },
{ logDatatable: jest.fn() }
);
expect(actual).toMatchSnapshot();
});
});

View file

@ -34,7 +34,7 @@ export const createTileMapFn = () => ({
default: '"{}"',
},
},
fn(context, args) {
fn(context, args, handlers) {
const visConfig = JSON.parse(args.visConfig);
const { geohash, metric, geocentroid } = visConfig.dimensions;
const convertedData = convertToGeoJson(context, {
@ -47,6 +47,9 @@ export const createTileMapFn = () => ({
convertedData.meta.geohash = context.columns[geohash.accessor].meta;
}
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: 'visualization',

View file

@ -80,13 +80,17 @@ describe('interpreter/functions#tilemap', () => {
});
it('returns an object with the correct structure', () => {
const actual = fn(context, { visConfig: JSON.stringify(visConfig) });
const actual = fn(
context,
{ visConfig: JSON.stringify(visConfig) },
{ logDatatable: jest.fn() }
);
expect(actual).toMatchSnapshot();
});
it('calls response handler with correct values', () => {
const { geohash, metric, geocentroid } = visConfig.dimensions;
fn(context, { visConfig: JSON.stringify(visConfig) });
fn(context, { visConfig: JSON.stringify(visConfig) }, { logDatatable: jest.fn() });
expect(convertToGeoJson).toHaveBeenCalledTimes(1);
expect(convertToGeoJson).toHaveBeenCalledWith(context, {
geohash,

View file

@ -160,7 +160,7 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({
}),
},
},
fn(input, args) {
fn(input, args, handlers) {
const dimensions: DimensionsVisParam = {
metrics: args.metric,
};
@ -175,6 +175,9 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({
const fontSize = Number.parseInt(args.font.spec.fontSize || '', 10);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', input);
}
return {
type: 'render',
as: 'metric_vis',

View file

@ -55,10 +55,13 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({
help: '',
},
},
fn(input, args) {
fn(input, args, handlers) {
const visConfig = args.visConfig && JSON.parse(args.visConfig);
const convertedData = tableVisResponseHandler(input, visConfig.dimensions);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', input);
}
return {
type: 'render',
as: 'table_vis',

View file

@ -95,7 +95,7 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
}),
},
},
fn(input, args) {
fn(input, args, handlers) {
const visParams = {
scale: args.scale,
orientation: args.orientation,
@ -109,6 +109,9 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
visParams.bucket = args.bucket;
}
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', input);
}
return {
type: 'render',
as: 'tagloud_vis',

View file

@ -59,10 +59,14 @@ export const createPieVisFn = (): VisTypeVislibPieExpressionFunctionDefinition =
help: 'vislib pie vis config',
},
},
fn(input, args) {
fn(input, args, handlers) {
const visConfig = JSON.parse(args.visConfig) as PieVisParams;
const visData = vislibSlicesResponseHandler(input, visConfig.dimensions);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', input);
}
return {
type: 'render',
as: vislibVisName,

View file

@ -64,11 +64,15 @@ export const createVisTypeVislibVisFn = (): VisTypeVislibExpressionFunctionDefin
help: 'vislib vis config',
},
},
fn(context, args) {
fn(context, args, handlers) {
const visType = args.type;
const visConfig = JSON.parse(args.visConfig) as BasicVislibParams;
const visData = vislibSeriesResponseHandler(context, visConfig.dimensions);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: vislibVisName,

View file

@ -20,8 +20,12 @@
import React from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui';
import { first } from 'rxjs/operators';
import { IInterpreterRenderHandlers, ExpressionValue } from 'src/plugins/expressions';
import { RequestAdapter, DataAdapter } from '../../../../../../../src/plugins/inspector/public';
import {
IInterpreterRenderHandlers,
ExpressionValue,
TablesAdapter,
} from '../../../../../../../src/plugins/expressions/public';
import { RequestAdapter } from '../../../../../../../src/plugins/inspector/public';
import { Adapters, ExpressionRenderHandler } from '../../types';
import { getExpressions } from '../../services';
@ -58,7 +62,7 @@ class Main extends React.Component<{}, State> {
this.setState({ expression });
const adapters: Adapters = {
requests: new RequestAdapter(),
data: new DataAdapter(),
tables: new TablesAdapter(),
};
return getExpressions()
.execute(expression, context || { type: 'null' }, {

View file

@ -290,7 +290,7 @@ describe('workspace_panel', () => {
const onData = expressionRendererMock.mock.calls[0][0].onData$!;
const tableData = { table1: { columns: [], rows: [] } };
onData(undefined, { tables: tableData });
onData(undefined, { tables: { tables: tableData } });
expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_ACTIVE_DATA', tables: tableData });
});

View file

@ -51,9 +51,9 @@ import {
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import { DropIllustration } from '../../../assets/drop_illustration';
import { LensInspectorAdapters } from '../../types';
import { getOriginalRequestErrorMessage } from '../../error_helper';
import { validateDatasourceAndVisualization } from '../state_helpers';
import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common';
export interface WorkspacePanelProps {
activeVisualizationId: string | null;
@ -382,11 +382,11 @@ export const InnerVisualizationWrapper = ({
);
const onData$ = useCallback(
(data: unknown, inspectorAdapters?: LensInspectorAdapters) => {
(data: unknown, inspectorAdapters?: Partial<DefaultInspectorAdapters>) => {
if (inspectorAdapters && inspectorAdapters.tables) {
dispatch({
type: 'UPDATE_ACTIVE_DATA',
tables: inspectorAdapters.tables,
tables: inspectorAdapters.tables.tables,
});
}
},

View file

@ -20,7 +20,7 @@ import { PaletteOutput } from 'src/plugins/charts/public';
import { Subscription } from 'rxjs';
import { toExpression, Ast } from '@kbn/interpreter/common';
import { RenderMode } from 'src/plugins/expressions';
import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions';
import { map, distinctUntilChanged, skip } from 'rxjs/operators';
import isEqual from 'fast-deep-equal';
import {
@ -50,7 +50,6 @@ import { IndexPatternsContract } from '../../../../../../src/plugins/data/public
import { getEditPath, DOC_TYPE } from '../../../common';
import { IBasePath } from '../../../../../../src/core/public';
import { LensAttributeService } from '../../lens_attribute_service';
import { LensInspectorAdapters } from '../types';
export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>;
@ -92,7 +91,7 @@ export class Embeddable
private subscription: Subscription;
private autoRefreshFetchSubscription: Subscription;
private isInitialized = false;
private activeData: LensInspectorAdapters | undefined;
private activeData: Partial<DefaultInspectorAdapters> | undefined;
private externalSearchContext: {
timeRange?: TimeRange;
@ -229,7 +228,7 @@ export class Embeddable
private updateActiveData = (
data: unknown,
inspectorAdapters?: LensInspectorAdapters | undefined
inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined
) => {
this.activeData = inspectorAdapters;
};

View file

@ -14,9 +14,8 @@ import {
ReactExpressionRendererProps,
} from 'src/plugins/expressions/public';
import { ExecutionContextSearch } from 'src/plugins/data/public';
import { RenderMode } from 'src/plugins/expressions';
import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions';
import { getOriginalRequestErrorMessage } from '../error_helper';
import { LensInspectorAdapters } from '../types';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
@ -25,7 +24,10 @@ export interface ExpressionWrapperProps {
searchContext: ExecutionContextSearch;
searchSessionId?: string;
handleEvent: (event: ExpressionRendererEvent) => void;
onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void;
onData$: (
data: unknown,
inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined
) => void;
renderMode?: RenderMode;
hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions'];
}

View file

@ -7,8 +7,12 @@
import moment from 'moment';
import { mergeTables } from './merge_tables';
import { ExpressionValueSearchContext } from 'src/plugins/data/public';
import { Datatable, ExecutionContext } from 'src/plugins/expressions';
import { LensInspectorAdapters } from './types';
import {
Datatable,
ExecutionContext,
DefaultInspectorAdapters,
TablesAdapter,
} from 'src/plugins/expressions';
describe('lens_merge_tables', () => {
const sampleTable1: Datatable = {
@ -50,12 +54,15 @@ describe('lens_merge_tables', () => {
});
it('should store the current tables in the tables inspector', () => {
const adapters: LensInspectorAdapters = { tables: {} };
const adapters: DefaultInspectorAdapters = {
tables: new TablesAdapter(),
requests: {} as never,
};
mergeTables.fn(null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, {
inspectorAdapters: adapters,
} as ExecutionContext<LensInspectorAdapters, ExpressionValueSearchContext>);
expect(adapters.tables!.first).toBe(sampleTable1);
expect(adapters.tables!.second).toBe(sampleTable2);
} as ExecutionContext<DefaultInspectorAdapters, ExpressionValueSearchContext>);
expect(adapters.tables!.tables.first).toBe(sampleTable1);
expect(adapters.tables!.tables.second).toBe(sampleTable2);
});
it('should pass the date range along', () => {

View file

@ -14,7 +14,7 @@ import { ExpressionValueSearchContext, search } from '../../../../../src/plugins
const { toAbsoluteDates } = search.aggs;
import { LensMultiTable } from '../types';
import { LensInspectorAdapters } from './types';
import { Adapters } from '../../../../../src/plugins/inspector/common';
interface MergeTables {
layerIds: string[];
@ -26,7 +26,7 @@ export const mergeTables: ExpressionFunctionDefinition<
ExpressionValueSearchContext | null,
MergeTables,
LensMultiTable,
ExecutionContext<LensInspectorAdapters, ExpressionValueSearchContext>
ExecutionContext<Adapters, ExpressionValueSearchContext>
> = {
name: 'lens_merge_tables',
type: 'lens_multitable',
@ -48,17 +48,14 @@ export const mergeTables: ExpressionFunctionDefinition<
},
inputTypes: ['kibana_context', 'null'],
fn(input, { layerIds, tables }, context) {
if (!context.inspectorAdapters) {
context.inspectorAdapters = {};
}
if (!context.inspectorAdapters.tables) {
context.inspectorAdapters.tables = {};
}
const resultTables: Record<string, Datatable> = {};
tables.forEach((table, index) => {
resultTables[layerIds[index]] = table;
// adapter is always defined at that point because we make sure by the beginning of the function
context.inspectorAdapters.tables![layerIds[index]] = table;
if (context?.inspectorAdapters?.tables) {
context.inspectorAdapters.tables.allowCsvExport = true;
context.inspectorAdapters.tables.logDatatable(layerIds[index], table);
}
});
return {
type: 'lens_multitable',

View file

@ -7,6 +7,3 @@
import { Datatable } from 'src/plugins/expressions';
export type TableInspectorAdapter = Record<string, Datatable>;
export interface LensInspectorAdapters {
tables?: TableInspectorAdapter;
}

View file

@ -2716,22 +2716,6 @@
"inputControl.vis.listControl.selectPlaceholder": "選択してください…",
"inputControl.vis.listControl.selectTextPlaceholder": "選択してください…",
"inspector.closeButton": "インスペクターを閉じる",
"inspector.data.dataDescriptionTooltip": "ビジュアライゼーションの元のデータを表示",
"inspector.data.dataTitle": "データ",
"inspector.data.downloadCSVButtonLabel": "CSV をダウンロード",
"inspector.data.downloadCSVToggleButtonLabel": "CSV をダウンロード",
"inspector.data.downloadOptionsUnsavedFilename": "(未保存)",
"inspector.data.filterForValueButtonAriaLabel": "値でフィルタリング",
"inspector.data.filterForValueButtonTooltip": "値でフィルタリング",
"inspector.data.filterOutValueButtonAriaLabel": "値を除外",
"inspector.data.filterOutValueButtonTooltip": "値を除外",
"inspector.data.formattedCSVButtonLabel": "フォーマット済み CSV",
"inspector.data.formattedCSVButtonTooltip": "データを表形式でダウンロード",
"inspector.data.gatheringDataLabel": "データを収集中",
"inspector.data.noDataAvailableDescription": "エレメントがデータを提供しませんでした。",
"inspector.data.noDataAvailableTitle": "利用可能なデータがありません",
"inspector.data.rawCSVButtonLabel": "CSV",
"inspector.data.rawCSVButtonTooltip": "日付をタイムスタンプとしてなど、提供されたデータをそのままダウンロードします",
"inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です",
"inspector.reqTimestampKey": "リクエストのタイムスタンプ",
"inspector.requests.descriptionRowIconAriaLabel": "説明",

View file

@ -2717,22 +2717,6 @@
"inputControl.vis.listControl.selectPlaceholder": "选择......",
"inputControl.vis.listControl.selectTextPlaceholder": "选择......",
"inspector.closeButton": "关闭检查器",
"inspector.data.dataDescriptionTooltip": "查看可视化后面的数据",
"inspector.data.dataTitle": "数据",
"inspector.data.downloadCSVButtonLabel": "下载 CSV",
"inspector.data.downloadCSVToggleButtonLabel": "下载 CSV",
"inspector.data.downloadOptionsUnsavedFilename": "未保存",
"inspector.data.filterForValueButtonAriaLabel": "筛留值",
"inspector.data.filterForValueButtonTooltip": "筛留值",
"inspector.data.filterOutValueButtonAriaLabel": "筛除值",
"inspector.data.filterOutValueButtonTooltip": "筛除值",
"inspector.data.formattedCSVButtonLabel": "格式化 CSV",
"inspector.data.formattedCSVButtonTooltip": "以表格式下载数据",
"inspector.data.gatheringDataLabel": "正在收集数据",
"inspector.data.noDataAvailableDescription": "该元素未提供任何数据。",
"inspector.data.noDataAvailableTitle": "没有可用数据",
"inspector.data.rawCSVButtonLabel": "原始 CSV",
"inspector.data.rawCSVButtonTooltip": "按原样下载数据,例如将日期作为时间戳下载",
"inspector.reqTimestampDescription": "记录请求启动的时间",
"inspector.reqTimestampKey": "请求时间戳",
"inspector.requests.descriptionRowIconAriaLabel": "描述",