diff --git a/x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.js index db427fd11c7651..96be9eed2b4679 100644 --- a/x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.js @@ -1200,11 +1200,14 @@ module.exports = (function () { } - //Add missing links between existing nodes - this.fillInGraph = function () { + /** + * Add missing links between existing nodes + * @param maxNewEdges Max number of new edges added. Avoid adding too many new edges + * at once into the graph otherwise disorientating + */ + this.fillInGraph = function (maxNewEdges = 10) { let nodesForLinking = self.getSelectedOrAllTopNodes(); - const maxNewEdges = 10; // Avoid adding too many new edges at once into the graph otherwise disorientating const maxNumVerticesSearchable = 100; diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/index.html b/x-pack/legacy/plugins/graph/public/angular/templates/index.html index 3ed9b390c6a787..07b57ee3225482 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -12,15 +12,17 @@ on-index-pattern-selected="uiSelectIndex" on-query-submit="submit" is-loading="loading" + is-initialized="!!workspace || savedWorkspace.id" initial-query="initialQuery" state="reduxState" dispatch="reduxDispatch" + on-fill-workspace="fillWorkspace" autocomplete-start="autocompleteStart" core-start="coreStart" store="store" > -
+
{ + try { + const fields = selectedFieldsSelector(store.getState()); + const topTermNodes = await fetchTopNodes( + npStart.core.http.post, + $scope.selectedIndex.title, + fields + ); + initWorkspaceIfRequired(); + $scope.workspace.mergeGraph({ + nodes: topTermNodes, + edges: [] + }); + $scope.workspace.fillInGraph(fields.length * 10); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.graph.fillWorkspaceError', + { defaultMessage: 'Fetching top terms failed: {message}', values: { message: e.message } } + ), + }); + } + }; + $scope.submit = function (searchTerm) { initWorkspaceIfRequired(); const numHops = 2; @@ -846,9 +873,6 @@ app.controller('graphuiPlugin', function ( } else { $route.current.locals.SavedWorkspacesProvider.get().then(function (newWorkspace) { $scope.savedWorkspace = newWorkspace; - openSourceModal(npStart.core, indexPattern => { - $scope.indexSelected(indexPattern); - }); }); } diff --git a/x-pack/legacy/plugins/graph/public/components/_index.scss b/x-pack/legacy/plugins/graph/public/components/_index.scss index 85bbf4fcc3adef..a06209e7e4d344 100644 --- a/x-pack/legacy/plugins/graph/public/components/_index.scss +++ b/x-pack/legacy/plugins/graph/public/components/_index.scss @@ -1,5 +1,7 @@ @import './app'; @import './search_bar'; +@import './source_modal'; +@import './guidance_panel/index'; @import './graph_visualization/index'; @import './venn_diagram/index'; @import './settings/index'; diff --git a/x-pack/legacy/plugins/graph/public/components/_source_modal.scss b/x-pack/legacy/plugins/graph/public/components/_source_modal.scss new file mode 100644 index 00000000000000..fbc293442f3316 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/_source_modal.scss @@ -0,0 +1,4 @@ +.gphSourceModal { + width: 720px; + min-height: 530px; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/graph/public/components/app.tsx b/x-pack/legacy/plugins/graph/public/components/app.tsx index 907e7e4cecdcd5..894c6b9ef45ac6 100644 --- a/x-pack/legacy/plugins/graph/public/components/app.tsx +++ b/x-pack/legacy/plugins/graph/public/components/app.tsx @@ -5,12 +5,16 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useState } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; import { Storage } from 'ui/storage'; import { CoreStart } from 'kibana/public'; import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; import { FieldManagerProps, FieldManager } from './field_manager'; import { SearchBarProps, SearchBar } from './search_bar'; +import { GuidancePanel } from './guidance_panel'; +import { selectedFieldsSelector } from '../state_management'; +import { openSourceModal } from '../services/source_modal'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -18,28 +22,47 @@ export interface GraphAppProps extends FieldManagerProps, SearchBarProps { coreStart: CoreStart; autocompleteStart: AutocompletePublicPluginStart; store: Storage; + onFillWorkspace: () => void; + isInitialized: boolean; } export function GraphApp(props: GraphAppProps) { + const [pickerOpen, setPickerOpen] = useState(false); + return ( - -
- - - - - - - - -
-
+ + +
+ + + + + + + + +
+ {!props.isInitialized && ( + 0} + onFillWorkspace={props.onFillWorkspace} + onOpenFieldPicker={() => { + setPickerOpen(true); + }} + onOpenDatasourcePicker={() => { + openSourceModal(props.coreStart, props.onIndexPatternSelected); + }} + /> + )} +
+
); } diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx index 93561e17229360..429eec19a47fa7 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx @@ -16,7 +16,7 @@ function getIconForDataType(dataType: string) { boolean: 'invert', date: 'calendar', geo_point: 'globe', - ip: 'link', + ip: 'storage', }; return icons[dataType] || ICON_TYPES.find(t => t === dataType) || 'document'; } diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.test.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.test.tsx index 32cc546a3ad0ce..fb715e759c62db 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.test.tsx @@ -18,6 +18,7 @@ describe('field_manager', () => { let store: GraphStore; let instance: ShallowWrapper; let dispatchSpy: jest.Mock; + let openSpy: jest.Mock; beforeEach(() => { store = createGraphStore(); @@ -52,8 +53,16 @@ describe('field_manager', () => { ); dispatchSpy = jest.fn(store.dispatch); - - instance = shallow(); + openSpy = jest.fn(); + + instance = shallow( + + ); }); function update() { @@ -80,13 +89,19 @@ describe('field_manager', () => { }); it('should select fields from picker', () => { - const fieldPicker = instance.find(FieldPicker).dive(); - act(() => { - (fieldPicker.find(EuiPopover).prop('button')! as ReactElement).props.onClick(); + (instance + .find(FieldPicker) + .dive() + .find(EuiPopover) + .prop('button')! as ReactElement).props.onClick(); }); - fieldPicker.update(); + expect(openSpy).toHaveBeenCalled(); + + instance.setProps({ pickerOpen: true }); + + const fieldPicker = instance.find(FieldPicker).dive(); expect( fieldPicker diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.tsx index 7f89b555c9f7a0..e44ad248e279de 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_manager.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { bindActionCreators } from 'redux'; @@ -24,9 +23,11 @@ import { export interface FieldManagerProps { state: GraphState; dispatch: GraphDispatch; + pickerOpen: boolean; + setPickerOpen: (open: boolean) => void; } -export function FieldManager({ state, dispatch }: FieldManagerProps) { +export function FieldManager({ state, dispatch, pickerOpen, setPickerOpen }: FieldManagerProps) { const fieldMap = fieldMapSelector(state); const allFields = fieldsSelector(state); const selectedFields = selectedFieldsSelector(state); @@ -41,17 +42,20 @@ export function FieldManager({ state, dispatch }: FieldManagerProps) { ); return ( - - - {selectedFields.map(field => ( - - - - ))} - - + + {selectedFields.map(field => ( + + - - + ))} + + + + ); } diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx index b1ddce4fa1744e..8ef566e881989f 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx @@ -16,11 +16,17 @@ export interface FieldPickerProps { fieldMap: Record; selectField: (fieldName: string) => void; deselectField: (fieldName: string) => void; + open: boolean; + setOpen: (open: boolean) => void; } -export function FieldPicker({ fieldMap, selectField, deselectField }: FieldPickerProps) { - const [open, setOpen] = useState(false); - +export function FieldPicker({ + fieldMap, + selectField, + deselectField, + open, + setOpen, +}: FieldPickerProps) { const allFields = Object.values(fieldMap); const unselectedFields = allFields.filter(field => !field.selected); const hasSelectedFields = unselectedFields.length < allFields.length; diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/_guidance_panel.scss b/x-pack/legacy/plugins/graph/public/components/guidance_panel/_guidance_panel.scss new file mode 100644 index 00000000000000..f1c332eba1aa8a --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/_guidance_panel.scss @@ -0,0 +1,47 @@ +.gphGuidancePanel { + max-width: 580px; + margin: $euiSizeL 0; +} + +.gphGuidancePanel__list { + list-style: none; + margin: 0; + padding: 0; +} + +.gphGuidancePanel__item { + display: block; + max-width: 420px; + position: relative; + padding-left: $euiSizeXL; + margin-bottom: $euiSizeL; + + button { + // make buttons wrap lines like regular text + display: contents; + } +} + +.gphGuidancePanel__item--disabled { + color: $euiColorDarkShade; + pointer-events: none; + + button { + color: $euiColorDarkShade !important; + } +} + +.gphGuidancePanel__itemIcon { + position: absolute; + left: 0; + top: -($euiSizeXS / 2); + width: $euiSizeL; + height: $euiSizeL; + padding: $euiSizeXS; + + &--done { + background-color: $euiColorSecondary; + color: $euiColorEmptyShade; + border-radius: 50%; + } +} diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/_index.scss b/x-pack/legacy/plugins/graph/public/components/guidance_panel/_index.scss new file mode 100644 index 00000000000000..65c71cc17ba354 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/_index.scss @@ -0,0 +1 @@ +@import './_guidance_panel'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx new file mode 100644 index 00000000000000..62d8bbb03bc3ff --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface GuidancePanelProps { + onFillWorkspace: () => void; + onOpenFieldPicker: () => void; + onOpenDatasourcePicker: () => void; + hasDatasource: boolean; + hasFields: boolean; +} + +function ListItem({ + children, + state, +}: { + state: 'done' | 'active' | 'disabled'; + children: ReactNode; +}) { + return ( +
  • + {state !== 'disabled' && ( + + + + )} + {children} +
  • + ); +} + +export function GuidancePanel(props: GuidancePanelProps) { + const { + onFillWorkspace, + onOpenFieldPicker, + onOpenDatasourcePicker, + hasDatasource, + hasFields, + } = props; + + return ( + + + + + + + + + +

    + {i18n.translate('xpack.graph.guidancePanel.title', { + defaultMessage: "Let's get started!", + })} +

    +
    +
    + +
      + + + {i18n.translate( + 'xpack.graph.guidancePanel.datasourceItem.indexPatternButtonLabel', + { + defaultMessage: 'index pattern', + } + )} + + ), + }} + /> + + + + {i18n.translate( + 'xpack.graph.guidancePanel.fieldsItem.fieldsButtonLabel', + { + defaultMessage: 'Select fields', + } + )} + + ), + }} + /> + + + + {i18n.translate( + 'xpack.graph.guidancePanel.nodesItem.topTermsButtonLabel', + { + defaultMessage: 'show correlations of the top terms', + } + )} + + ), + }} + /> + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/index.ts b/x-pack/legacy/plugins/graph/public/components/guidance_panel/index.ts new file mode 100644 index 00000000000000..8704eb2eb6761c --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './guidance_panel'; diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index 80b1c3c3439427..dbad0e01078fd5 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -51,6 +51,7 @@ describe('search_bar', () => { onIndexPatternSelected: () => {}, onQuerySubmit: querySubmit, currentIndexPattern: { title: 'Testpattern' } as IndexPattern, + coreStart: {} as CoreStart, }) ); act(() => { @@ -72,6 +73,7 @@ describe('search_bar', () => { onIndexPatternSelected: () => {}, onQuerySubmit: querySubmit, currentIndexPattern: { title: 'Testpattern', fields: [{ name: 'test' }] } as IndexPattern, + coreStart: {} as CoreStart, }) ); act(() => { @@ -97,6 +99,7 @@ describe('search_bar', () => { onIndexPatternSelected: indexPatternSelected, onQuerySubmit: () => {}, currentIndexPattern: { title: 'Testpattern' } as IndexPattern, + coreStart: {} as CoreStart, }) ); diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index 226f6f829d8a44..18eca326776f5e 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -8,9 +8,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { IDataPluginServices } from 'src/legacy/core_plugins/data/public/types'; +import { CoreStart } from 'kibana/public'; import { QueryBarInput, Query, @@ -21,6 +21,7 @@ import { openSourceModal } from '../services/source_modal'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; export interface SearchBarProps { + coreStart: CoreStart; isLoading: boolean; currentIndexPattern?: IndexPattern; initialQuery?: string; @@ -54,71 +55,61 @@ export function SearchBar(props: SearchBarProps) { } = props; const [query, setQuery] = useState({ language: 'kuery', query: initialQuery || '' }); const kibana = useKibana(); - const { overlays, uiSettings, savedObjects } = kibana.services; + const { overlays } = kibana.services; if (!overlays) return null; return ( - -
    { - e.preventDefault(); - if (!isLoading && currentIndexPattern) { - onQuerySubmit(queryToString(query, currentIndexPattern)); - } - }} - > - - - { + e.preventDefault(); + if (!isLoading && currentIndexPattern) { + onQuerySubmit(queryToString(query, currentIndexPattern)); + } + }} + > + + + + { + openSourceModal(props.coreStart, onIndexPatternSelected); + }} > - { - openSourceModal( - { - overlays, - savedObjects, - uiSettings, - }, - onIndexPatternSelected - ); - }} - > - {currentIndexPattern - ? currentIndexPattern.title - : // This branch will be shown if the user exits the - // initial picker modal - i18n.translate('xpack.graph.bar.pickSourceLabel', { - defaultMessage: 'Click here to pick a data source', - })} - - - } - onChange={setQuery} - /> - - - - {i18n.translate('xpack.graph.bar.exploreLabel', { defaultMessage: 'Explore' })} - - - - -
    + {currentIndexPattern + ? currentIndexPattern.title + : // This branch will be shown if the user exits the + // initial picker modal + i18n.translate('xpack.graph.bar.pickSourceLabel', { + defaultMessage: 'Click here to pick a data source', + })} + + + } + onChange={setQuery} + /> + + + + {i18n.translate('xpack.graph.bar.exploreLabel', { defaultMessage: 'Explore' })} + + + + ); } diff --git a/x-pack/legacy/plugins/graph/public/components/source_modal.tsx b/x-pack/legacy/plugins/graph/public/components/source_modal.tsx index 4c3b3c8be9110b..5829370b030e65 100644 --- a/x-pack/legacy/plugins/graph/public/components/source_modal.tsx +++ b/x-pack/legacy/plugins/graph/public/components/source_modal.tsx @@ -12,7 +12,7 @@ import { SourcePicker, SourcePickerProps } from './source_picker'; export function SourceModal(props: SourcePickerProps) { return ( - <> +
    - +
    ); } diff --git a/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts new file mode 100644 index 00000000000000..0a0fc8cae5d269 --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSuitableIcon } from '../helpers/style_choices'; +import { fetchTopNodes } from './fetch_top_nodes'; + +const icon = getSuitableIcon(''); + +describe('fetch_top_nodes', () => { + it('should build terms agg', async () => { + const postMock = jest.fn(() => Promise.resolve({ resp: {} })); + await fetchTopNodes(postMock, 'test', [ + { color: '', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' }, + { color: '', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' }, + ]); + expect(postMock).toHaveBeenCalledWith('../api/graph/searchProxy', { + body: JSON.stringify({ + index: 'test', + body: { + size: 0, + aggs: { + sample: { + sampler: { + shard_size: 5000, + }, + aggs: { + top_values_field1: { + terms: { + field: 'field1', + size: 10, + }, + }, + top_values_field2: { + terms: { + field: 'field2', + size: 10, + }, + }, + }, + }, + }, + }, + }), + }); + }); + + it('should map result to nodes', async () => { + const postMock = jest.fn(() => + Promise.resolve({ + resp: { + aggregations: { + sample: { + top_values_field1: { + buckets: [{ key: 'A' }, { key: 'B' }], + }, + top_values_field2: { + buckets: [{ key: 'C' }, { key: 'D' }], + }, + }, + }, + }, + }) + ); + const result = await fetchTopNodes(postMock, 'test', [ + { color: 'red', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' }, + { color: 'blue', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' }, + ]); + expect(result.length).toEqual(4); + expect(result[0]).toEqual({ + color: 'red', + data: { + field: 'field1', + term: 'A', + }, + field: 'field1', + icon, + id: '', + label: 'A', + term: 'A', + }); + expect(result[2]).toEqual({ + color: 'blue', + data: { + field: 'field2', + term: 'C', + }, + field: 'field2', + icon, + id: '', + label: 'C', + term: 'C', + }); + }); +}); diff --git a/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.ts b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.ts new file mode 100644 index 00000000000000..87b33cbe35f82c --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'src/core/public'; +import { WorkspaceField, ServerResultNode } from '../types'; + +const DEFAULT_SHARD_SIZE = 5000; + +function createSamplerSearchBody(aggs: object, shardSize: number = DEFAULT_SHARD_SIZE) { + return { + size: 0, + aggs: { + sample: { + sampler: { + shard_size: shardSize, + }, + aggs, + }, + }, + }; +} + +function createTopTermsAggName(fieldName: string) { + return `top_values_${fieldName}`; +} + +function createTopTermsSubAgg(field: string, size: number = 10) { + return { + [createTopTermsAggName(field)]: { + terms: { + field, + size, + }, + }, + }; +} + +// TODO use elasticsearch types here +interface TopTermsAggResponse { + aggregations?: { + sample: Record< + string, + { + buckets: Array<{ key: string; doc_count: number }>; + } + >; + }; +} + +function getTopTermsResult(response: TopTermsAggResponse, fieldName: string) { + if (!response.aggregations) { + return []; + } + return response.aggregations.sample[createTopTermsAggName(fieldName)].buckets.map( + bucket => bucket.key + ); +} + +export function createServerResultNode( + fieldName: string, + term: string, + allFields: WorkspaceField[] +): ServerResultNode { + const field = allFields.find(({ name }) => name === fieldName); + + if (!field) { + throw new Error('Invariant error: field not found'); + } + + return { + field: fieldName, + term, + id: '', + color: field.color, + icon: field.icon, + data: { + field: fieldName, + term, + }, + label: term, + }; +} + +export async function fetchTopNodes( + post: CoreStart['http']['post'], + index: string, + fields: WorkspaceField[] +) { + const aggs = fields + .map(({ name }) => name) + .map(fieldName => createTopTermsSubAgg(fieldName)) + .reduce((allAggs, subAgg) => ({ ...allAggs, ...subAgg })); + const body = createSamplerSearchBody(aggs); + + const response: TopTermsAggResponse = (await post('../api/graph/searchProxy', { + body: JSON.stringify({ index, body }), + })).resp; + + const nodes: ServerResultNode[] = []; + + fields.forEach(({ name }) => { + const topTerms = getTopTermsResult(response, name); + const fieldNodes = topTerms.map(term => createServerResultNode(name, term, fields)); + + nodes.push(...fieldNodes); + }); + + return nodes; +} diff --git a/x-pack/legacy/plugins/graph/public/types/workspace_state.ts b/x-pack/legacy/plugins/graph/public/types/workspace_state.ts index 54666c48161e68..fab093535cb63c 100644 --- a/x-pack/legacy/plugins/graph/public/types/workspace_state.ts +++ b/x-pack/legacy/plugins/graph/public/types/workspace_state.ts @@ -41,27 +41,31 @@ export interface WorkspaceEdge { isSelected?: boolean; } -export interface GraphData { - nodes: Array<{ +export interface ServerResultNode { + field: string; + term: string; + id: string; + label: string; + color: string; + icon: FontawesomeIcon; + data: { field: string; term: string; - id: string; - label: string; - color: string; - icon: FontawesomeIcon; - data: { - field: string; - term: string; - }; - }>; - edges: Array<{ - source: number; - target: number; - weight: number; - width: number; - doc_count?: number; - inferred: boolean; - }>; + }; +} + +export interface ServerResultEdge { + source: number; + target: number; + weight: number; + width: number; + doc_count?: number; + inferred: boolean; +} + +export interface GraphData { + nodes: ServerResultNode[]; + edges: ServerResultEdge[]; } export interface Workspace {