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 (
-
-
-
+ {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 {