mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge branch 'master' into eui/33.0
This commit is contained in:
commit
9698262124
27 changed files with 738 additions and 289 deletions
|
@ -577,15 +577,11 @@
|
|||
"deprecated": false,
|
||||
"children": [
|
||||
{
|
||||
"parentPluginId": "spacesOss",
|
||||
"id": "def-public.SpacesApi.activeSpace$",
|
||||
"type": "Object",
|
||||
"tags": [],
|
||||
"label": "activeSpace$",
|
||||
"description": [
|
||||
"\nObservable representing the currently active space.\nThe details of the space can change without a full page reload (such as display name, color, etc.)"
|
||||
],
|
||||
"id": "def-public.SpacesApi.getActiveSpace$",
|
||||
"type": "Function",
|
||||
"label": "getActiveSpace$",
|
||||
"signature": [
|
||||
"() => ",
|
||||
"Observable",
|
||||
"<",
|
||||
{
|
||||
|
@ -597,11 +593,16 @@
|
|||
},
|
||||
">"
|
||||
],
|
||||
"description": [
|
||||
"\nObservable representing the currently active space.\nThe details of the space can change without a full page reload (such as display name, color, etc.)"
|
||||
],
|
||||
"children": [],
|
||||
"tags": [],
|
||||
"returnComment": [],
|
||||
"source": {
|
||||
"path": "src/plugins/spaces_oss/public/api.ts",
|
||||
"lineNumber": 22
|
||||
},
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"parentPluginId": "spacesOss",
|
||||
|
|
|
@ -140,7 +140,7 @@ export default function (/* { providerAPI } */) {
|
|||
-----------
|
||||
|
||||
**Services**:::
|
||||
Services are named singleton values produced by a Service Provider. Tests and other services can retrieve service instances by asking for them by name. All functionality except the mocha API is exposed via services.\
|
||||
Services are named singleton values produced by a Service Provider. Tests and other services can retrieve service instances by asking for them by name. All functionality except the mocha API is exposed via services.
|
||||
|
||||
**Page objects**:::
|
||||
Page objects are a special type of service that encapsulate behaviors common to a particular page or plugin. When you write your own plugin, you’ll likely want to add a page object (or several) that describes the common interactions your tests need to execute.
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
=== TSVB
|
||||
|
||||
*TSVB* enables you to visualize the data from multiple data series, supports <<aggregation-reference,
|
||||
most {es} metric aggregations>>, multiple visualization types, custom functions, and some math. To use *TSVB*, your data must have a date field.
|
||||
most {es} metric aggregations>>, multiple visualization types, custom functions, and some math.
|
||||
To create *TSVB* visualization panels, your data must have a time field.
|
||||
|
||||
[role="screenshot"]
|
||||
image::visualize/images/tsvb-screenshot.png[TSVB overview]
|
||||
|
@ -17,15 +18,19 @@ Open *TSVB*, then make sure the required settings are configured.
|
|||
|
||||
. On the *New visualization* window, click *TSVB*.
|
||||
|
||||
. In *TSVB*, click *Panel options*, then make sure the following settings are configured:
|
||||
. In *TSVB*, click *Panel options*, then specify the required *Data* settings.
|
||||
|
||||
* *Index pattern*
|
||||
* *Time field*
|
||||
* *Interval*
|
||||
.. From the *Index pattern* dropdown, select the index pattern you want to visualize.
|
||||
+
|
||||
To visualize an {es} index, open the *Index pattern select mode* menu, deselect *Use only {kib} index patterns*, then enter the {es} index.
|
||||
|
||||
. Select a *Drop last bucket* option. It is dropped by default because the time filter intersects the time range of the last bucket, but can be enabled to see the partial data.
|
||||
.. From the *Time field* dropdown, select the field you want to visualize, then enter the field *Interval*.
|
||||
|
||||
. In the *Panel filter* field, specify any <<kuery-query, KQL filters>> to select specific documents.
|
||||
.. Select a *Drop last bucket* option.
|
||||
+
|
||||
By default, *TSVB* drops the last bucket because the time filter intersects the time range of the last bucket. To view the partial data, select *No*.
|
||||
|
||||
.. In the *Panel filter* field, enter <<kuery-query, KQL filters>> to view specific documents.
|
||||
|
||||
[float]
|
||||
[[configure-the-data-series]]
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { foldLeftRight, getPaths } from '../test_utils';
|
||||
import { NonEmptyStringArray } from '.';
|
||||
|
||||
describe('non_empty_string_array', () => {
|
||||
|
|
|
@ -84,7 +84,8 @@ export async function mountApp({
|
|||
} = pluginsStart;
|
||||
|
||||
const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined;
|
||||
const activeSpaceId = spacesApi && (await spacesApi.activeSpace$.pipe(first()).toPromise())?.id;
|
||||
const activeSpaceId =
|
||||
spacesApi && (await spacesApi.getActiveSpace$().pipe(first()).toPromise())?.id;
|
||||
let globalEmbedSettings: DashboardEmbedSettings | undefined;
|
||||
|
||||
const dashboardServices: DashboardAppServices = {
|
||||
|
|
|
@ -64,6 +64,29 @@ describe('Range filter builder', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should convert strings to numbers if the field is scripted and type number', () => {
|
||||
const field = getField('script number');
|
||||
|
||||
expect(buildRangeFilter(field, { gte: '1', lte: '3' }, indexPattern)).toEqual({
|
||||
meta: {
|
||||
field: 'script number',
|
||||
index: 'id',
|
||||
params: {},
|
||||
},
|
||||
script: {
|
||||
script: {
|
||||
lang: 'expression',
|
||||
source: '(' + field!.script + ')>=gte && (' + field!.script + ')<=lte',
|
||||
params: {
|
||||
value: '>=1 <=3',
|
||||
gte: 1,
|
||||
lte: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap painless scripts in comparator lambdas', () => {
|
||||
const field = getField('script date');
|
||||
const expected =
|
||||
|
|
|
@ -138,7 +138,10 @@ export const buildRangeFilter = (
|
|||
};
|
||||
|
||||
export const getRangeScript = (field: IFieldType, params: RangeFilterParams) => {
|
||||
const knownParams = pickBy(params, (val, key: any) => key in operators);
|
||||
const knownParams = mapValues(
|
||||
pickBy(params, (val, key: any) => key in operators),
|
||||
(value) => (field.type === 'number' && typeof value === 'string' ? parseFloat(value) : value)
|
||||
);
|
||||
let script = map(
|
||||
knownParams,
|
||||
(val: any, key: string) => '(' + field.script + ')' + get(operators, key) + key
|
||||
|
|
|
@ -124,6 +124,33 @@ describe('DiscoverGrid', () => {
|
|||
expect(getDisplayedDocNr(component)).toBe(5);
|
||||
});
|
||||
|
||||
test('showing selected documents, underlying data changes, all documents are displayed, selection is gone', async () => {
|
||||
await toggleDocSelection(component, esHits[0]);
|
||||
await toggleDocSelection(component, esHits[1]);
|
||||
expect(getSelectedDocNr(component)).toBe(2);
|
||||
findTestSubject(component, 'dscGridSelectionBtn').simulate('click');
|
||||
findTestSubject(component, 'dscGridShowSelectedDocuments').simulate('click');
|
||||
expect(getDisplayedDocNr(component)).toBe(2);
|
||||
component.setProps({
|
||||
rows: [
|
||||
{
|
||||
_index: 'i',
|
||||
_id: '6',
|
||||
_score: 1,
|
||||
_type: '_doc',
|
||||
_source: {
|
||||
date: '2020-20-02T12:12:12.128',
|
||||
name: 'test6',
|
||||
extension: 'doc',
|
||||
bytes: 50,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(getDisplayedDocNr(component)).toBe(1);
|
||||
expect(getSelectedDocNr(component)).toBe(0);
|
||||
});
|
||||
|
||||
test('showing only selected documents and remove filter deselecting each doc manually', async () => {
|
||||
await toggleDocSelection(component, esHits[0]);
|
||||
findTestSubject(component, 'dscGridSelectionBtn').simulate('click');
|
||||
|
|
|
@ -164,17 +164,33 @@ export const DiscoverGrid = ({
|
|||
const [isFilterActive, setIsFilterActive] = useState(false);
|
||||
const displayedColumns = getDisplayedColumns(columns, indexPattern);
|
||||
const defaultColumns = displayedColumns.includes('_source');
|
||||
const usedSelectedDocs = useMemo(() => {
|
||||
if (!selectedDocs.length || !rows?.length) {
|
||||
return [];
|
||||
}
|
||||
const idMap = rows.reduce((map, row) => map.set(getDocId(row), true), new Map());
|
||||
// filter out selected docs that are no longer part of the current data
|
||||
const result = selectedDocs.filter((docId) => idMap.get(docId));
|
||||
if (result.length === 0 && isFilterActive) {
|
||||
setIsFilterActive(false);
|
||||
}
|
||||
return result;
|
||||
}, [selectedDocs, rows, isFilterActive]);
|
||||
|
||||
const displayedRows = useMemo(() => {
|
||||
if (!rows) {
|
||||
return [];
|
||||
}
|
||||
if (!isFilterActive || selectedDocs.length === 0) {
|
||||
if (!isFilterActive || usedSelectedDocs.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
return rows.filter((row) => {
|
||||
return selectedDocs.includes(getDocId(row));
|
||||
});
|
||||
}, [rows, selectedDocs, isFilterActive]);
|
||||
const rowsFiltered = rows.filter((row) => usedSelectedDocs.includes(getDocId(row)));
|
||||
if (!rowsFiltered.length) {
|
||||
// in case the selected docs are no longer part of the sample of 500, show all docs
|
||||
return rows;
|
||||
}
|
||||
return rowsFiltered;
|
||||
}, [rows, usedSelectedDocs, isFilterActive]);
|
||||
|
||||
/**
|
||||
* Pagination
|
||||
|
@ -258,16 +274,16 @@ export const DiscoverGrid = ({
|
|||
|
||||
const additionalControls = useMemo(
|
||||
() =>
|
||||
selectedDocs.length ? (
|
||||
usedSelectedDocs.length ? (
|
||||
<DiscoverGridDocumentToolbarBtn
|
||||
isFilterActive={isFilterActive}
|
||||
rows={rows!}
|
||||
selectedDocs={selectedDocs}
|
||||
selectedDocs={usedSelectedDocs}
|
||||
setSelectedDocs={setSelectedDocs}
|
||||
setIsFilterActive={setIsFilterActive}
|
||||
/>
|
||||
) : null,
|
||||
[selectedDocs, isFilterActive, rows, setIsFilterActive]
|
||||
[usedSelectedDocs, isFilterActive, rows, setIsFilterActive]
|
||||
);
|
||||
|
||||
if (!rowCount) {
|
||||
|
@ -291,7 +307,7 @@ export const DiscoverGrid = ({
|
|||
onFilter,
|
||||
indexPattern,
|
||||
isDarkMode: services.uiSettings.get('theme:darkMode'),
|
||||
selectedDocs,
|
||||
selectedDocs: usedSelectedDocs,
|
||||
setSelectedDocs: (newSelectedDocs) => {
|
||||
setSelectedDocs(newSelectedDocs);
|
||||
if (isFilterActive && newSelectedDocs.length === 0) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { of } from 'rxjs';
|
|||
import type { SpacesApi, SpacesApiUi, SpacesApiUiComponent } from './api';
|
||||
|
||||
const createApiMock = (): jest.Mocked<SpacesApi> => ({
|
||||
activeSpace$: of(),
|
||||
getActiveSpace$: jest.fn().mockReturnValue(of()),
|
||||
getActiveSpace: jest.fn(),
|
||||
ui: createApiUiMock(),
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ export interface SpacesApi {
|
|||
* Observable representing the currently active space.
|
||||
* The details of the space can change without a full page reload (such as display name, color, etc.)
|
||||
*/
|
||||
readonly activeSpace$: Observable<Space>;
|
||||
getActiveSpace$(): Observable<Space>;
|
||||
|
||||
/**
|
||||
* Retrieve the currently active space.
|
||||
|
|
|
@ -128,6 +128,10 @@ describe('SearchUILogic', () => {
|
|||
validFields: ['test'],
|
||||
validSortFields: ['test'],
|
||||
validFacetFields: ['test'],
|
||||
defaultValues: {
|
||||
urlField: 'url',
|
||||
titleField: 'title',
|
||||
},
|
||||
};
|
||||
|
||||
describe('loadFieldData', () => {
|
||||
|
@ -142,7 +146,13 @@ describe('SearchUILogic', () => {
|
|||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/api/app_search/engines/engine1/search_ui/field_config'
|
||||
);
|
||||
expect(SearchUILogic.actions.onFieldDataLoaded).toHaveBeenCalledWith(MOCK_RESPONSE);
|
||||
expect(SearchUILogic.actions.onFieldDataLoaded).toHaveBeenCalledWith({
|
||||
validFields: ['test'],
|
||||
validSortFields: ['test'],
|
||||
validFacetFields: ['test'],
|
||||
urlField: 'url',
|
||||
titleField: 'title',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
|
|
|
@ -17,6 +17,8 @@ interface InitialFieldValues {
|
|||
validFields: string[];
|
||||
validSortFields: string[];
|
||||
validFacetFields: string[];
|
||||
urlField?: string;
|
||||
titleField?: string;
|
||||
}
|
||||
interface SearchUIActions {
|
||||
loadFieldData(): void;
|
||||
|
@ -61,8 +63,20 @@ export const SearchUILogic = kea<MakeLogicType<SearchUIValues, SearchUIActions>>
|
|||
validFields: [[], { onFieldDataLoaded: (_, { validFields }) => validFields }],
|
||||
validSortFields: [[], { onFieldDataLoaded: (_, { validSortFields }) => validSortFields }],
|
||||
validFacetFields: [[], { onFieldDataLoaded: (_, { validFacetFields }) => validFacetFields }],
|
||||
titleField: ['', { onTitleFieldChange: (_, { titleField }) => titleField }],
|
||||
urlField: ['', { onUrlFieldChange: (_, { urlField }) => urlField }],
|
||||
titleField: [
|
||||
'',
|
||||
{
|
||||
onTitleFieldChange: (_, { titleField }) => titleField,
|
||||
onFieldDataLoaded: (_, { titleField }) => titleField || '',
|
||||
},
|
||||
],
|
||||
urlField: [
|
||||
'',
|
||||
{
|
||||
onUrlFieldChange: (_, { urlField }) => urlField,
|
||||
onFieldDataLoaded: (_, { urlField }) => urlField || '',
|
||||
},
|
||||
],
|
||||
facetFields: [[], { onFacetFieldsChange: (_, { facetFields }) => facetFields }],
|
||||
sortFields: [[], { onSortFieldsChange: (_, { sortFields }) => sortFields }],
|
||||
activeField: [ActiveField.None, { onActiveFieldChange: (_, { activeField }) => activeField }],
|
||||
|
@ -76,8 +90,20 @@ export const SearchUILogic = kea<MakeLogicType<SearchUIValues, SearchUIActions>>
|
|||
|
||||
try {
|
||||
const initialFieldValues = await http.get(url);
|
||||
const {
|
||||
defaultValues: { urlField, titleField },
|
||||
validFields,
|
||||
validSortFields,
|
||||
validFacetFields,
|
||||
} = initialFieldValues;
|
||||
|
||||
actions.onFieldDataLoaded(initialFieldValues);
|
||||
actions.onFieldDataLoaded({
|
||||
validFields,
|
||||
validSortFields,
|
||||
validFacetFields,
|
||||
urlField,
|
||||
titleField,
|
||||
});
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
FIELD_ORIGIN,
|
||||
SOURCE_TYPES,
|
||||
STYLE_TYPE,
|
||||
COLOR_MAP_TYPE,
|
||||
} from '../../../../../../../maps/common/constants';
|
||||
import { EMSTermJoinConfig } from '../../../../../../../maps/public';
|
||||
import { FieldVisStats } from '../../types';
|
||||
import { VectorLayerDescriptor } from '../../../../../../../maps/common/descriptor_types';
|
||||
import { EmbeddedMapComponent } from '../../../embedded_map';
|
||||
|
||||
export const getChoroplethTopValuesLayer = (
|
||||
fieldName: string,
|
||||
topValues: Array<{ key: any; doc_count: number }>,
|
||||
{ layerId, field }: EMSTermJoinConfig
|
||||
): VectorLayerDescriptor => {
|
||||
return {
|
||||
id: htmlIdGenerator()(),
|
||||
label: i18n.translate('xpack.fileDataVisualizer.choroplethMap.topValuesCount', {
|
||||
defaultMessage: 'Top values count for {fieldName}',
|
||||
values: { fieldName },
|
||||
}),
|
||||
joins: [
|
||||
{
|
||||
// Left join is the id from the type of field (e.g. world_countries)
|
||||
leftField: field,
|
||||
right: {
|
||||
id: 'anomaly_count',
|
||||
type: SOURCE_TYPES.TABLE_SOURCE,
|
||||
__rows: topValues,
|
||||
__columns: [
|
||||
{
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'doc_count',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
// Right join/term is the field in the doc you’re trying to join it to (foreign key - e.g. US)
|
||||
term: 'key',
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceDescriptor: {
|
||||
type: 'EMS_FILE',
|
||||
id: layerId,
|
||||
},
|
||||
style: {
|
||||
type: 'VECTOR',
|
||||
// @ts-ignore missing style properties. Remove once 'VectorLayerDescriptor' type is updated
|
||||
properties: {
|
||||
icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } },
|
||||
fillColor: {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: 'Blue to Red',
|
||||
colorCategory: 'palette_0',
|
||||
fieldMetaOptions: { isEnabled: true, sigma: 3 },
|
||||
type: COLOR_MAP_TYPE.ORDINAL,
|
||||
field: {
|
||||
name: 'doc_count',
|
||||
origin: FIELD_ORIGIN.JOIN,
|
||||
},
|
||||
useCustomColorRamp: false,
|
||||
},
|
||||
},
|
||||
lineColor: {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: { fieldMetaOptions: { isEnabled: true } },
|
||||
},
|
||||
lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } },
|
||||
},
|
||||
isTimeAware: true,
|
||||
},
|
||||
type: 'VECTOR',
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
stats: FieldVisStats | undefined;
|
||||
suggestion: EMSTermJoinConfig;
|
||||
}
|
||||
|
||||
export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
|
||||
const { fieldName, isTopValuesSampled, topValues, topValuesSamplerShardSize } = stats!;
|
||||
|
||||
const layerList: VectorLayerDescriptor[] = useMemo(
|
||||
() => [getChoroplethTopValuesLayer(fieldName || '', topValues || [], suggestion)],
|
||||
[suggestion, fieldName, topValues]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={'fileDataVisualizerChoroplethMapTopValues'}>
|
||||
<div style={{ width: '100%', minHeight: 300 }}>
|
||||
<EmbeddedMapComponent layerList={layerList} />
|
||||
</div>
|
||||
{isTopValuesSampled === true && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" textAlign={'left'}>
|
||||
<FormattedMessage
|
||||
id="xpack.fileDataVisualizer.fieldDataCardExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription"
|
||||
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
|
||||
values={{
|
||||
topValuesSamplerShardSize,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -5,21 +5,55 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import type { FieldDataRowProps } from '../../types/field_data_row';
|
||||
import { TopValues } from '../../../top_values';
|
||||
import { EMSTermJoinConfig } from '../../../../../../../maps/public';
|
||||
import { useFileDataVisualizerKibana } from '../../../../kibana_context';
|
||||
import { DocumentStatsTable } from './document_stats';
|
||||
import { ExpandedRowContent } from './expanded_row_content';
|
||||
import { ChoroplethMap } from './choropleth_map';
|
||||
|
||||
const COMMON_EMS_LAYER_IDS = [
|
||||
'world_countries',
|
||||
'administrative_regions_lvl2',
|
||||
'usa_zip_codes',
|
||||
'usa_states',
|
||||
];
|
||||
|
||||
export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => {
|
||||
const { stats } = config;
|
||||
const [EMSSuggestion, setEMSSuggestion] = useState<EMSTermJoinConfig | null | undefined>();
|
||||
const { stats, fieldName } = config;
|
||||
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
|
||||
const {
|
||||
services: { maps: mapsPlugin },
|
||||
} = useFileDataVisualizerKibana();
|
||||
|
||||
const loadEMSTermSuggestions = useCallback(async () => {
|
||||
if (!mapsPlugin) return;
|
||||
const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({
|
||||
emsLayerIds: COMMON_EMS_LAYER_IDS,
|
||||
sampleValues: Array.isArray(stats?.topValues)
|
||||
? stats?.topValues.map((value) => value.key)
|
||||
: [],
|
||||
sampleValuesColumnName: fieldName || '',
|
||||
});
|
||||
setEMSSuggestion(suggestion);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fieldName]);
|
||||
|
||||
useEffect(
|
||||
function getInitialEMSTermSuggestion() {
|
||||
loadEMSTermSuggestions();
|
||||
},
|
||||
[loadEMSTermSuggestions]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandedRowContent dataTestSubj={'mlDVKeywordContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
|
||||
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
|
||||
{EMSSuggestion && stats && <ChoroplethMap stats={stats} suggestion={EMSSuggestion} />}
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||
import useInterval from 'react-use/lib/useInterval';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { SavedView } from '../../../../containers/saved_view/saved_view';
|
||||
import { AutoSizer } from '../../../../components/auto_sizer';
|
||||
import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
|
||||
import { NodesOverview } from './nodes_overview';
|
||||
|
@ -26,182 +27,191 @@ import { ViewSwitcher } from './waffle/view_switcher';
|
|||
import { IntervalLabel } from './waffle/interval_label';
|
||||
import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter';
|
||||
import { createLegend } from '../lib/create_legend';
|
||||
import { useSavedViewContext } from '../../../../containers/saved_view/saved_view';
|
||||
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
|
||||
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
|
||||
import { BottomDrawer } from './bottom_drawer';
|
||||
import { Legend } from './waffle/legend';
|
||||
|
||||
export const Layout = () => {
|
||||
const [showLoading, setShowLoading] = useState(true);
|
||||
const { sourceId, source } = useSourceContext();
|
||||
const { currentView, shouldLoadDefault } = useSavedViewContext();
|
||||
const {
|
||||
metric,
|
||||
groupBy,
|
||||
sort,
|
||||
nodeType,
|
||||
accountId,
|
||||
region,
|
||||
changeView,
|
||||
view,
|
||||
autoBounds,
|
||||
boundsOverride,
|
||||
legend,
|
||||
} = useWaffleOptionsContext();
|
||||
const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
|
||||
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
|
||||
const { loading, nodes, reload, interval } = useSnapshot(
|
||||
filterQueryAsJson,
|
||||
[metric],
|
||||
groupBy,
|
||||
nodeType,
|
||||
sourceId,
|
||||
currentTime,
|
||||
accountId,
|
||||
region,
|
||||
false
|
||||
);
|
||||
export const Layout = React.memo(
|
||||
({
|
||||
shouldLoadDefault,
|
||||
currentView,
|
||||
}: {
|
||||
shouldLoadDefault: boolean;
|
||||
currentView: SavedView<any> | null;
|
||||
}) => {
|
||||
const [showLoading, setShowLoading] = useState(true);
|
||||
const { sourceId, source } = useSourceContext();
|
||||
const {
|
||||
metric,
|
||||
groupBy,
|
||||
sort,
|
||||
nodeType,
|
||||
accountId,
|
||||
region,
|
||||
changeView,
|
||||
view,
|
||||
autoBounds,
|
||||
boundsOverride,
|
||||
legend,
|
||||
} = useWaffleOptionsContext();
|
||||
const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
|
||||
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
|
||||
const { loading, nodes, reload, interval } = useSnapshot(
|
||||
filterQueryAsJson,
|
||||
[metric],
|
||||
groupBy,
|
||||
nodeType,
|
||||
sourceId,
|
||||
currentTime,
|
||||
accountId,
|
||||
region,
|
||||
false
|
||||
);
|
||||
|
||||
const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette;
|
||||
const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps;
|
||||
const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors;
|
||||
const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette;
|
||||
const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps;
|
||||
const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors;
|
||||
|
||||
const options = {
|
||||
formatter: InfraFormatterType.percent,
|
||||
formatTemplate: '{{value}}',
|
||||
legend: createLegend(legendPalette, legendSteps, legendReverseColors),
|
||||
metric,
|
||||
sort,
|
||||
fields: source?.configuration?.fields,
|
||||
groupBy,
|
||||
};
|
||||
const options = {
|
||||
formatter: InfraFormatterType.percent,
|
||||
formatTemplate: '{{value}}',
|
||||
legend: createLegend(legendPalette, legendSteps, legendReverseColors),
|
||||
metric,
|
||||
sort,
|
||||
fields: source?.configuration?.fields,
|
||||
groupBy,
|
||||
};
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
if (!loading) {
|
||||
jumpToTime(Date.now());
|
||||
}
|
||||
},
|
||||
isAutoReloading ? 5000 : null
|
||||
);
|
||||
useInterval(
|
||||
() => {
|
||||
if (!loading) {
|
||||
jumpToTime(Date.now());
|
||||
}
|
||||
},
|
||||
isAutoReloading ? 5000 : null
|
||||
);
|
||||
|
||||
const intervalAsString = convertIntervalToString(interval);
|
||||
const dataBounds = calculateBoundsFromNodes(nodes);
|
||||
const bounds = autoBounds ? dataBounds : boundsOverride;
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]);
|
||||
const { viewState, onViewChange } = useWaffleViewState();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView) {
|
||||
onViewChange(currentView);
|
||||
}
|
||||
}, [currentView, onViewChange]);
|
||||
|
||||
useEffect(() => {
|
||||
// load snapshot data after default view loaded, unless we're not loading a view
|
||||
if (currentView != null || !shouldLoadDefault) {
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* INFO: why disable exhaustive-deps
|
||||
* We need to wait on the currentView not to be null because it is loaded async and could change the view state.
|
||||
* We don't actually need to watch the value of currentView though, since the view state will be synched up by the
|
||||
* changing params in the reload method so we should only "watch" the reload method.
|
||||
*
|
||||
* TODO: Should refactor this in the future to make it more clear where all the view state is coming
|
||||
* from and it's precedence [query params, localStorage, defaultView, out of the box view]
|
||||
*/
|
||||
const intervalAsString = convertIntervalToString(interval);
|
||||
const dataBounds = calculateBoundsFromNodes(nodes);
|
||||
const bounds = autoBounds ? dataBounds : boundsOverride;
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [reload, shouldLoadDefault]);
|
||||
const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]);
|
||||
const { viewState, onViewChange } = useWaffleViewState();
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoading(true);
|
||||
}, [options.metric, nodeType]);
|
||||
useEffect(() => {
|
||||
if (currentView) {
|
||||
onViewChange(currentView);
|
||||
}
|
||||
}, [currentView, onViewChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasNodes = nodes && nodes.length;
|
||||
// Don't show loading screen when we're auto-reloading
|
||||
setShowLoading(!hasNodes);
|
||||
}, [nodes]);
|
||||
useEffect(() => {
|
||||
// load snapshot data after default view loaded, unless we're not loading a view
|
||||
if (currentView != null || !shouldLoadDefault) {
|
||||
reload();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContent>
|
||||
<AutoSizer bounds>
|
||||
{({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => (
|
||||
<MainContainer ref={pageMeasureRef}>
|
||||
<AutoSizer bounds>
|
||||
{({ measureRef: topActionMeasureRef, bounds: { height: topActionHeight = 0 } }) => (
|
||||
<>
|
||||
<TopActionContainer ref={topActionMeasureRef}>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
gutterSize="m"
|
||||
>
|
||||
<Toolbar nodeType={nodeType} currentTime={currentTime} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<IntervalLabel intervalAsString={intervalAsString} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewSwitcher view={view} onChange={changeView} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<SavedViewContainer>
|
||||
<SavedViewsToolbarControls viewState={viewState} />
|
||||
</SavedViewContainer>
|
||||
</TopActionContainer>
|
||||
<AutoSizer bounds>
|
||||
{({ measureRef, bounds: { height = 0 } }) => (
|
||||
<>
|
||||
<NodesOverview
|
||||
nodes={nodes}
|
||||
options={options}
|
||||
nodeType={nodeType}
|
||||
loading={loading}
|
||||
showLoading={showLoading}
|
||||
reload={reload}
|
||||
onDrilldown={applyFilterQuery}
|
||||
currentTime={currentTime}
|
||||
view={view}
|
||||
autoBounds={autoBounds}
|
||||
boundsOverride={boundsOverride}
|
||||
formatter={formatter}
|
||||
bottomMargin={height}
|
||||
topMargin={topActionHeight}
|
||||
/>
|
||||
{view === 'map' && (
|
||||
<BottomDrawer
|
||||
measureRef={measureRef}
|
||||
interval={interval}
|
||||
/**
|
||||
* INFO: why disable exhaustive-deps
|
||||
* We need to wait on the currentView not to be null because it is loaded async and could change the view state.
|
||||
* We don't actually need to watch the value of currentView though, since the view state will be synched up by the
|
||||
* changing params in the reload method so we should only "watch" the reload method.
|
||||
*
|
||||
* TODO: Should refactor this in the future to make it more clear where all the view state is coming
|
||||
* from and it's precedence [query params, localStorage, defaultView, out of the box view]
|
||||
*/
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [reload, shouldLoadDefault]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoading(true);
|
||||
}, [options.metric, nodeType]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasNodes = nodes && nodes.length;
|
||||
// Don't show loading screen when we're auto-reloading
|
||||
setShowLoading(!hasNodes);
|
||||
}, [nodes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContent>
|
||||
<AutoSizer bounds>
|
||||
{({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => (
|
||||
<MainContainer ref={pageMeasureRef}>
|
||||
<AutoSizer bounds>
|
||||
{({
|
||||
measureRef: topActionMeasureRef,
|
||||
bounds: { height: topActionHeight = 0 },
|
||||
}) => (
|
||||
<>
|
||||
<TopActionContainer ref={topActionMeasureRef}>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
gutterSize="m"
|
||||
>
|
||||
<Toolbar nodeType={nodeType} currentTime={currentTime} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<IntervalLabel intervalAsString={intervalAsString} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewSwitcher view={view} onChange={changeView} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<SavedViewContainer>
|
||||
<SavedViewsToolbarControls viewState={viewState} />
|
||||
</SavedViewContainer>
|
||||
</TopActionContainer>
|
||||
<AutoSizer bounds>
|
||||
{({ measureRef, bounds: { height = 0 } }) => (
|
||||
<>
|
||||
<NodesOverview
|
||||
nodes={nodes}
|
||||
options={options}
|
||||
nodeType={nodeType}
|
||||
loading={loading}
|
||||
showLoading={showLoading}
|
||||
reload={reload}
|
||||
onDrilldown={applyFilterQuery}
|
||||
currentTime={currentTime}
|
||||
view={view}
|
||||
autoBounds={autoBounds}
|
||||
boundsOverride={boundsOverride}
|
||||
formatter={formatter}
|
||||
width={width}
|
||||
>
|
||||
<Legend
|
||||
bottomMargin={height}
|
||||
topMargin={topActionHeight}
|
||||
/>
|
||||
{view === 'map' && (
|
||||
<BottomDrawer
|
||||
measureRef={measureRef}
|
||||
interval={interval}
|
||||
formatter={formatter}
|
||||
bounds={bounds}
|
||||
dataBounds={dataBounds}
|
||||
legend={options.legend}
|
||||
/>
|
||||
</BottomDrawer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</MainContainer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
width={width}
|
||||
>
|
||||
<Legend
|
||||
formatter={formatter}
|
||||
bounds={bounds}
|
||||
dataBounds={dataBounds}
|
||||
legend={options.legend}
|
||||
/>
|
||||
</BottomDrawer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</MainContainer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const MainContainer = euiStyled.div`
|
||||
position: relative;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useSavedViewContext } from '../../../../containers/saved_view/saved_view';
|
||||
import { Layout } from './layout';
|
||||
|
||||
export const LayoutView = () => {
|
||||
const { shouldLoadDefault, currentView } = useSavedViewContext();
|
||||
return <Layout shouldLoadDefault={shouldLoadDefault} currentView={currentView} />;
|
||||
};
|
|
@ -21,7 +21,7 @@ import { ViewSourceConfigurationButton } from '../../../components/source_config
|
|||
import { Source } from '../../../containers/metrics_source';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { Layout } from './components/layout';
|
||||
import { LayoutView } from './components/layout_view';
|
||||
import { useLinkProps } from '../../../hooks/use_link_props';
|
||||
import { SavedViewProvider } from '../../../containers/saved_view/saved_view';
|
||||
import { DEFAULT_WAFFLE_VIEW_STATE } from './hooks/use_waffle_view_state';
|
||||
|
@ -69,7 +69,7 @@ export const SnapshotPage = () => {
|
|||
viewType={'inventory-view'}
|
||||
defaultViewState={DEFAULT_WAFFLE_VIEW_STATE}
|
||||
>
|
||||
<Layout />
|
||||
<LayoutView />
|
||||
</SavedViewProvider>
|
||||
</>
|
||||
) : hasFailedLoadingSource ? (
|
||||
|
|
|
@ -782,7 +782,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
|
||||
const filterExpr = getPointFilterExpression(this.hasJoins());
|
||||
if (filterExpr !== mbMap.getFilter(pointLayerId)) {
|
||||
if (!_.isEqual(filterExpr, mbMap.getFilter(pointLayerId))) {
|
||||
mbMap.setFilter(pointLayerId, filterExpr);
|
||||
mbMap.setFilter(textLayerId, filterExpr);
|
||||
}
|
||||
|
@ -818,7 +818,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
|
||||
const filterExpr = getPointFilterExpression(this.hasJoins());
|
||||
if (filterExpr !== mbMap.getFilter(symbolLayerId)) {
|
||||
if (!_.isEqual(filterExpr, mbMap.getFilter(symbolLayerId))) {
|
||||
mbMap.setFilter(symbolLayerId, filterExpr);
|
||||
}
|
||||
|
||||
|
@ -900,14 +900,14 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
this.syncVisibilityWithMb(mbMap, fillLayerId);
|
||||
mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||
const fillFilterExpr = getFillFilterExpression(hasJoins);
|
||||
if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) {
|
||||
if (!_.isEqual(fillFilterExpr, mbMap.getFilter(fillLayerId))) {
|
||||
mbMap.setFilter(fillLayerId, fillFilterExpr);
|
||||
}
|
||||
|
||||
this.syncVisibilityWithMb(mbMap, lineLayerId);
|
||||
mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||
const lineFilterExpr = getLineFilterExpression(hasJoins);
|
||||
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
|
||||
if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) {
|
||||
mbMap.setFilter(lineLayerId, lineFilterExpr);
|
||||
}
|
||||
|
||||
|
@ -931,7 +931,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
|
||||
const filterExpr = getCentroidFilterExpression(this.hasJoins());
|
||||
if (filterExpr !== mbMap.getFilter(centroidLayerId)) {
|
||||
if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) {
|
||||
mbMap.setFilter(centroidLayerId, filterExpr);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,61 +15,55 @@ import {
|
|||
export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true];
|
||||
const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
|
||||
|
||||
const VISIBILITY_FILTER_CLAUSE = ['all', ['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]];
|
||||
// Kibana features are features added by kibana that do not exist in real data
|
||||
const EXCLUDE_KBN_FEATURES = ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES];
|
||||
function getFilterExpression(geometryFilter: unknown[], hasJoins: boolean) {
|
||||
const filters: unknown[] = [
|
||||
EXCLUDE_TOO_MANY_FEATURES_BOX,
|
||||
EXCLUDE_CENTROID_FEATURES,
|
||||
geometryFilter,
|
||||
];
|
||||
|
||||
const CLOSED_SHAPE_MB_FILTER = [
|
||||
...EXCLUDE_KBN_FEATURES,
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
|
||||
],
|
||||
];
|
||||
if (hasJoins) {
|
||||
filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
|
||||
}
|
||||
|
||||
const VISIBLE_CLOSED_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CLOSED_SHAPE_MB_FILTER];
|
||||
|
||||
const ALL_SHAPE_MB_FILTER = [
|
||||
...EXCLUDE_KBN_FEATURES,
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
|
||||
],
|
||||
];
|
||||
|
||||
const VISIBLE_ALL_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ALL_SHAPE_MB_FILTER];
|
||||
|
||||
const POINT_MB_FILTER = [
|
||||
...EXCLUDE_KBN_FEATURES,
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
|
||||
],
|
||||
];
|
||||
|
||||
const VISIBLE_POINT_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, POINT_MB_FILTER];
|
||||
|
||||
const CENTROID_MB_FILTER = ['all', ['==', ['get', KBN_IS_CENTROID_FEATURE], true]];
|
||||
|
||||
const VISIBLE_CENTROID_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CENTROID_MB_FILTER];
|
||||
return ['all', ...filters];
|
||||
}
|
||||
|
||||
export function getFillFilterExpression(hasJoins: boolean): unknown[] {
|
||||
return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER;
|
||||
return getFilterExpression(
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
|
||||
],
|
||||
hasJoins
|
||||
);
|
||||
}
|
||||
|
||||
export function getLineFilterExpression(hasJoins: boolean): unknown[] {
|
||||
return hasJoins ? VISIBLE_ALL_SHAPE_MB_FILTER : ALL_SHAPE_MB_FILTER;
|
||||
return getFilterExpression(
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
|
||||
],
|
||||
hasJoins
|
||||
);
|
||||
}
|
||||
|
||||
export function getPointFilterExpression(hasJoins: boolean): unknown[] {
|
||||
return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER;
|
||||
return getFilterExpression(
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
|
||||
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
|
||||
],
|
||||
hasJoins
|
||||
);
|
||||
}
|
||||
|
||||
export function getCentroidFilterExpression(hasJoins: boolean): unknown[] {
|
||||
return hasJoins ? VISIBLE_CENTROID_MB_FILTER : CENTROID_MB_FILTER;
|
||||
return getFilterExpression(['==', ['get', KBN_IS_CENTROID_FEATURE], true], hasJoins);
|
||||
}
|
||||
|
|
13
x-pack/plugins/ml/common/constants/embeddable_map.ts
Normal file
13
x-pack/plugins/ml/common/constants/embeddable_map.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const COMMON_EMS_LAYER_IDS = [
|
||||
'world_countries',
|
||||
'administrative_regions_lvl2',
|
||||
'usa_zip_codes',
|
||||
'usa_states',
|
||||
];
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
FIELD_ORIGIN,
|
||||
SOURCE_TYPES,
|
||||
STYLE_TYPE,
|
||||
COLOR_MAP_TYPE,
|
||||
} from '../../../../../../../../maps/common/constants';
|
||||
import { EMSTermJoinConfig } from '../../../../../../../../maps/public';
|
||||
import { FieldVisStats } from '../../../../stats_table/types';
|
||||
import { VectorLayerDescriptor } from '../../../../../../../../maps/common/descriptor_types';
|
||||
import { MlEmbeddedMapComponent } from '../../../../../components/ml_embedded_map';
|
||||
|
||||
export const getChoroplethTopValuesLayer = (
|
||||
fieldName: string,
|
||||
topValues: Array<{ key: any; doc_count: number }>,
|
||||
{ layerId, field }: EMSTermJoinConfig
|
||||
): VectorLayerDescriptor => {
|
||||
return {
|
||||
id: htmlIdGenerator()(),
|
||||
label: i18n.translate('xpack.ml.dataviz.choroplethMap.topValuesCount', {
|
||||
defaultMessage: 'Top values count for {fieldName}',
|
||||
values: { fieldName },
|
||||
}),
|
||||
joins: [
|
||||
{
|
||||
// Left join is the id from the type of field (e.g. world_countries)
|
||||
leftField: field,
|
||||
right: {
|
||||
id: 'anomaly_count',
|
||||
type: SOURCE_TYPES.TABLE_SOURCE,
|
||||
__rows: topValues,
|
||||
__columns: [
|
||||
{
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'doc_count',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
// Right join/term is the field in the doc you’re trying to join it to (foreign key - e.g. US)
|
||||
term: 'key',
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceDescriptor: {
|
||||
type: 'EMS_FILE',
|
||||
id: layerId,
|
||||
},
|
||||
style: {
|
||||
type: 'VECTOR',
|
||||
// @ts-ignore missing style properties. Remove once 'VectorLayerDescriptor' type is updated
|
||||
properties: {
|
||||
icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } },
|
||||
fillColor: {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: 'Blue to Red',
|
||||
colorCategory: 'palette_0',
|
||||
fieldMetaOptions: { isEnabled: true, sigma: 3 },
|
||||
type: COLOR_MAP_TYPE.ORDINAL,
|
||||
field: {
|
||||
name: 'doc_count',
|
||||
origin: FIELD_ORIGIN.JOIN,
|
||||
},
|
||||
useCustomColorRamp: false,
|
||||
},
|
||||
},
|
||||
lineColor: {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: { fieldMetaOptions: { isEnabled: true } },
|
||||
},
|
||||
lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } },
|
||||
},
|
||||
isTimeAware: true,
|
||||
},
|
||||
type: 'VECTOR',
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
stats: FieldVisStats | undefined;
|
||||
suggestion: EMSTermJoinConfig;
|
||||
}
|
||||
|
||||
export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
|
||||
const { fieldName, isTopValuesSampled, topValues, topValuesSamplerShardSize } = stats!;
|
||||
|
||||
const layerList: VectorLayerDescriptor[] = useMemo(
|
||||
() => [getChoroplethTopValuesLayer(fieldName || '', topValues || [], suggestion)],
|
||||
[suggestion, stats]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={'mlChoroplethMapTopValues'}>
|
||||
<div style={{ width: '100%', minHeight: 300 }}>
|
||||
<MlEmbeddedMapComponent layerList={layerList} />
|
||||
</div>
|
||||
{isTopValuesSampled === true && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" textAlign={'left'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.fieldDataCard.choroplethMapTopValues.calculatedFromSampleDescription"
|
||||
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
|
||||
values={{
|
||||
topValuesSamplerShardSize,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ChoroplethMap } from './choropleth_map';
|
|
@ -5,21 +5,50 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import type { FieldDataRowProps } from '../../types/field_data_row';
|
||||
import { TopValues } from '../../../index_based/components/field_data_row/top_values';
|
||||
import { ChoroplethMap } from '../../../index_based/components/field_data_row/choropleth_map';
|
||||
import { useMlKibana } from '../../../../../application/contexts/kibana';
|
||||
import { EMSTermJoinConfig } from '../../../../../../../maps/public';
|
||||
import { COMMON_EMS_LAYER_IDS } from '../../../../../../common/constants/embeddable_map';
|
||||
import { DocumentStatsTable } from './document_stats';
|
||||
import { ExpandedRowContent } from './expanded_row_content';
|
||||
|
||||
export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => {
|
||||
const { stats } = config;
|
||||
const [EMSSuggestion, setEMSSuggestion] = useState<EMSTermJoinConfig | null | undefined>();
|
||||
const { stats, fieldName } = config;
|
||||
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
|
||||
const {
|
||||
services: { maps: mapsPlugin },
|
||||
} = useMlKibana();
|
||||
|
||||
const loadEMSTermSuggestions = async () => {
|
||||
if (!mapsPlugin) return;
|
||||
const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({
|
||||
emsLayerIds: COMMON_EMS_LAYER_IDS,
|
||||
sampleValues: Array.isArray(stats?.topValues)
|
||||
? stats?.topValues.map((value) => value.key)
|
||||
: [],
|
||||
sampleValuesColumnName: fieldName || '',
|
||||
});
|
||||
setEMSSuggestion(suggestion);
|
||||
};
|
||||
|
||||
useEffect(
|
||||
function getInitialEMSTermSuggestion() {
|
||||
loadEMSTermSuggestions();
|
||||
},
|
||||
[config?.fieldName]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandedRowContent dataTestSubj={'mlDVKeywordContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
|
||||
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
|
||||
{EMSSuggestion && stats && <ChoroplethMap stats={stats} suggestion={EMSSuggestion} />}
|
||||
{EMSSuggestion === null && (
|
||||
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
|
||||
)}
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,14 +28,9 @@ import { isDefined } from '../../../common/types/guards';
|
|||
import { MlEmbeddedMapComponent } from '../components/ml_embedded_map';
|
||||
import { EMSTermJoinConfig } from '../../../../maps/public';
|
||||
import { AnomaliesTableRecord } from '../../../common/types/anomalies';
|
||||
import { COMMON_EMS_LAYER_IDS } from '../../../common/constants/embeddable_map';
|
||||
|
||||
const MAX_ENTITY_VALUES = 3;
|
||||
const COMMON_EMS_LAYER_IDS = [
|
||||
'world_countries',
|
||||
'administrative_regions_lvl2',
|
||||
'usa_zip_codes',
|
||||
'usa_states',
|
||||
];
|
||||
|
||||
function getAnomalyRows(anomalies: AnomaliesTableRecord[], jobId: string) {
|
||||
const anomalyRows: Record<
|
||||
|
|
|
@ -59,7 +59,7 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
|
|||
spacesManager: this.spacesManager,
|
||||
getStartServices: core.getStartServices,
|
||||
}),
|
||||
activeSpace$: this.spacesManager.onActiveSpaceChange$,
|
||||
getActiveSpace$: () => this.spacesManager.onActiveSpaceChange$,
|
||||
getActiveSpace: () => this.spacesManager.getActiveSpace(),
|
||||
};
|
||||
|
||||
|
|
|
@ -40,13 +40,10 @@ export default function ({ getPageObjects, getService }) {
|
|||
maxzoom: 24,
|
||||
filter: [
|
||||
'all',
|
||||
['!=', ['get', '__kbn_too_many_features__'], true],
|
||||
['!=', ['get', '__kbn_is_centroid_feature__'], true],
|
||||
['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']],
|
||||
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
|
||||
[
|
||||
'all',
|
||||
['!=', ['get', '__kbn_too_many_features__'], true],
|
||||
['!=', ['get', '__kbn_is_centroid_feature__'], true],
|
||||
['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']],
|
||||
],
|
||||
],
|
||||
layout: { visibility: 'visible' },
|
||||
paint: {
|
||||
|
@ -124,17 +121,10 @@ export default function ({ getPageObjects, getService }) {
|
|||
maxzoom: 24,
|
||||
filter: [
|
||||
'all',
|
||||
['!=', ['get', '__kbn_too_many_features__'], true],
|
||||
['!=', ['get', '__kbn_is_centroid_feature__'], true],
|
||||
['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
|
||||
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
|
||||
[
|
||||
'all',
|
||||
['!=', ['get', '__kbn_too_many_features__'], true],
|
||||
['!=', ['get', '__kbn_is_centroid_feature__'], true],
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], 'Polygon'],
|
||||
['==', ['geometry-type'], 'MultiPolygon'],
|
||||
],
|
||||
],
|
||||
],
|
||||
layout: { visibility: 'visible' },
|
||||
paint: {
|
||||
|
@ -208,19 +198,16 @@ export default function ({ getPageObjects, getService }) {
|
|||
maxzoom: 24,
|
||||
filter: [
|
||||
'all',
|
||||
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
|
||||
['!=', ['get', '__kbn_too_many_features__'], true],
|
||||
['!=', ['get', '__kbn_is_centroid_feature__'], true],
|
||||
[
|
||||
'all',
|
||||
['!=', ['get', '__kbn_too_many_features__'], true],
|
||||
['!=', ['get', '__kbn_is_centroid_feature__'], true],
|
||||
[
|
||||
'any',
|
||||
['==', ['geometry-type'], 'Polygon'],
|
||||
['==', ['geometry-type'], 'MultiPolygon'],
|
||||
['==', ['geometry-type'], 'LineString'],
|
||||
['==', ['geometry-type'], 'MultiLineString'],
|
||||
],
|
||||
'any',
|
||||
['==', ['geometry-type'], 'Polygon'],
|
||||
['==', ['geometry-type'], 'MultiPolygon'],
|
||||
['==', ['geometry-type'], 'LineString'],
|
||||
['==', ['geometry-type'], 'MultiLineString'],
|
||||
],
|
||||
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
|
||||
],
|
||||
layout: { visibility: 'visible' },
|
||||
paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue