mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
inspector table adapter cleanup (#84232)
This commit is contained in:
parent
df32ff80ca
commit
b93034b72c
81 changed files with 719 additions and 890 deletions
|
@ -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<DataStartDependencies, DataPublicPluginStart></code> | |
|
||||
| { bfetch, expressions, uiActions, usageCollection } | <code>DataSetupDependencies</code> | |
|
||||
| { bfetch, expressions, uiActions, usageCollection, inspector } | <code>DataSetupDependencies</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) > [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md)
|
||||
|
||||
## Adapters.data property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
data?: DataAdapter;
|
||||
```
|
|
@ -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> | |
|
||||
|
||||
|
|
|
@ -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<!-- --><<!-- -->Executor\['registerFunction'\]<!-- -->> ): ReturnType<!-- --><<!-- -->Executor\['registerFunction'\]<!-- -->> =<!-- -->> 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
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) > [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`
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [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) | | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) > [tables](./kibana-plugin-plugins-expressions-public.tablesadapter.tables.md)
|
||||
|
||||
## TablesAdapter.tables property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
get tables(): {
|
||||
[key: string]: Datatable;
|
||||
};
|
||||
```
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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']>;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"bfetch",
|
||||
"expressions",
|
||||
"uiActions",
|
||||
"share"
|
||||
"share",
|
||||
"inspector"
|
||||
],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"extraPublicDirs": ["common"],
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
@import './ui/index';
|
||||
@import './utils/table_inspector_view/index';
|
||||
|
|
|
@ -70,6 +70,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 {
|
||||
|
@ -104,7 +105,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);
|
||||
|
||||
|
@ -141,6 +142,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,
|
||||
|
|
|
@ -20,6 +20,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';
|
||||
|
@ -1888,7 +1890,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)
|
||||
|
@ -2560,7 +2562,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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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();
|
|
@ -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;
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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),
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -19,3 +19,4 @@
|
|||
|
||||
export * from './create_error';
|
||||
export * from './get_by_alias';
|
||||
export * from './tables_adapter';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -117,4 +117,5 @@ export {
|
|||
ExpressionsService,
|
||||
ExpressionsServiceSetup,
|
||||
ExpressionsServiceStart,
|
||||
TablesAdapter,
|
||||
} from '../common';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -17,6 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './data';
|
||||
export * from './request';
|
||||
export * from './types';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -19,15 +19,9 @@
|
|||
|
||||
export {
|
||||
Adapters,
|
||||
DataAdapter,
|
||||
FormattedData,
|
||||
RequestAdapter,
|
||||
RequestStatistic,
|
||||
RequestStatistics,
|
||||
RequestStatus,
|
||||
RequestResponder,
|
||||
TabularData,
|
||||
TabularDataColumn,
|
||||
TabularDataRow,
|
||||
TabularDataValue,
|
||||
} from './adapters';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
@import './data/index';
|
||||
@import './requests/index';
|
||||
|
|
|
@ -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);
|
|
@ -17,5 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { getDataViewDescription } from './data';
|
||||
export { getRequestsViewDescription } from './requests';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' }, {
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -7,6 +7,3 @@
|
|||
import { Datatable } from 'src/plugins/expressions';
|
||||
|
||||
export type TableInspectorAdapter = Record<string, Datatable>;
|
||||
export interface LensInspectorAdapters {
|
||||
tables?: TableInspectorAdapter;
|
||||
}
|
||||
|
|
|
@ -2711,22 +2711,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": "説明",
|
||||
|
|
|
@ -2712,22 +2712,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": "描述",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue