From 6b6e979063719c19586344429bf63519da047d5c Mon Sep 17 00:00:00 2001
From: Zacqary Adam Xeper
Date: Fri, 2 Aug 2019 11:29:45 -0500
Subject: [PATCH 001/118] [Logs UI][a11y] Announce name of column on remove
column button (#41695) (#42529)
* [Logs UI][a11y] Announce name of column on remove column button
* Use title instead of aria-label
* ariaColumnName => columnDescription
* Move template string out of i18n
* Revert label id change
* Inject i18n to field title
* Change title to Remove {columnDescription} column
* Replace injectI18N with i18n.translate
* Fix i18n
---
.../log_columns_configuration_panel.tsx | 76 +++++++++++--------
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
3 files changed, 43 insertions(+), 35 deletions(-)
diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx
index bbc3cde41794c..53d426eba0449 100644
--- a/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx
@@ -19,7 +19,8 @@ import {
EuiDroppable,
EuiIcon,
} from '@elastic/eui';
-import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import React, { useCallback } from 'react';
import { DragHandleProps, DropResult } from '../../../../../common/eui_draggable';
@@ -167,29 +168,35 @@ const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
remove,
},
dragHandleProps,
-}) => (
-
-
-
-
-
-
-
-
-
-
-
- {field}
-
-
-
-
-
-
-);
+}) => {
+ const fieldLogColumnTitle = i18n.translate(
+ 'xpack.infra.sourceConfiguration.fieldLogColumnTitle',
+ {
+ defaultMessage: 'Field',
+ }
+ );
+ return (
+
+
+
+
+
+
+
+ {fieldLogColumnTitle}
+
+ {field}
+
+
+
+
+
+
+ );
+};
const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
fieldName: React.ReactNode;
@@ -213,23 +220,26 @@ const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
-
+
);
-const RemoveLogColumnButton = injectI18n<{
+const RemoveLogColumnButton: React.FunctionComponent<{
onClick?: () => void;
-}>(({ intl, onClick }) => {
- const removeColumnLabel = intl.formatMessage({
- id: 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel',
- defaultMessage: 'Remove this column',
- });
+ columnDescription: string;
+}> = ({ onClick, columnDescription }) => {
+ const removeColumnLabel = i18n.translate(
+ 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel',
+ {
+ defaultMessage: 'Remove {columnDescription} column',
+ values: { columnDescription },
+ }
+ );
return (
);
-});
+};
const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => (
Date: Fri, 2 Aug 2019 18:36:18 +0200
Subject: [PATCH 002/118] =?UTF-8?q?[7.x]=20Inspector=20=F0=9F=91=89=20New?=
=?UTF-8?q?=20Platform=20(#42164)=20(#42530)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor: 💡 remove SASS and clean up InspectorView component
* refactor: 💡 clean up inspector, convert .js -> .ts
* feat: 🎸 add Inspector NP plugin boilerplate
* feat: 🎸 move view registry to NP, move types, add registerView
* docs: ✏️ move inspector README to NP plugin
* refactor: 💡 move ui/inspector/ui to NP
* refactor: 💡 move Inspector adapters to NP
* refactor: 💡 move Inspector.isAvailable to New Platform
* refactor: 💡 move Inspector.open to New Platform plugin
* test: 💍 move Inspector tests to NP plugin
* chore: 🤖 fix imports
* feat: 🎸 update translations
* test: 💍 fix failing translation snapshot
* test: 💍 fix yarn test:browser tests
* Update src/legacy/ui/public/inspector/build_tabular_inspector_data.ts
Co-Authored-By: Stacey Gammon
* [ML] [Job wizards] switching to new kibana context provider
* fix: 🐛 specify translation path directly to the plugin
* docs: ✏️ add comment about Webpack config fix
* Update src/legacy/ui/public/inspector/build_tabular_inspector_data.ts
Co-Authored-By: Stacey Gammon
* Update src/legacy/ui/public/inspector/build_tabular_inspector_data.ts
Co-Authored-By: Stacey Gammon
* feat: 🎸 improve types as per review
* fix: 🐛 remove comp and fix view layouts
* test: 💍 improve mocks
---
.i18nrc.json | 3 +-
.../public/np_core.test.mocks.ts | 26 +-
.../public/ui_capabilities.test.mocks.ts | 2 +
.../data/__snapshots__/data_view.test.js.snap | 276 ++++++++----------
.../inspector_views/public/data/data_view.js | 93 +++---
.../public/data/data_view.test.js | 1 +
.../public/requests/requests_view.js | 59 ++--
.../migrations/migrate_to_730_panels.test.ts | 2 +
.../public/dashboard/np_core.test.mocks.ts | 26 +-
src/legacy/core_plugins/tests_bundle/index.js | 16 +-
src/legacy/ui/public/_index.scss | 1 -
src/legacy/ui/public/inspector/README.md | 129 +-------
src/legacy/ui/public/inspector/_index.scss | 1 -
.../ui/public/inspector/adapters/index.ts | 10 +-
.../inspector/build_tabular_inspector_data.js | 68 -----
.../inspector/build_tabular_inspector_data.ts | 103 +++++++
src/legacy/ui/public/inspector/index.ts | 4 -
.../ui/public/inspector/inspector.test.js | 66 -----
src/legacy/ui/public/inspector/inspector.tsx | 82 ++----
src/legacy/ui/public/inspector/types.ts | 54 +---
src/legacy/ui/public/inspector/ui/_index.scss | 1 -
.../ui/public/inspector/ui/_inspector.scss | 3 -
...spector_panel.d.ts => inspector_panel.tsx} | 16 +-
...or_view.tsx => inspector_view_chooser.tsx} | 28 +-
.../ui/public/inspector/view_registry.ts | 73 +----
.../public/new_platform/__mocks__/helpers.ts | 6 +-
.../new_platform/new_platform.karma_mock.js | 15 +
.../ui/public/new_platform/new_platform.ts | 6 +
.../loader/__tests__/visualize_loader.js | 3 +-
src/plugins/inspector/README.md | 122 ++++++++
src/plugins/inspector/kibana.json | 6 +
.../public}/adapters/data/data_adapter.ts | 0
.../adapters/data/data_adapters.test.ts | 0
.../public}/adapters/data/formatted_data.ts | 0
.../inspector/public}/adapters/data/index.ts | 0
.../inspector/public/adapters}/index.ts | 3 +-
.../public}/adapters/request/index.ts | 0
.../adapters/request/request_adapter.test.ts | 0
.../adapters/request/request_adapter.ts | 0
.../adapters/request/request_responder.ts | 4 +-
.../public}/adapters/request/types.ts | 0
src/plugins/inspector/public/index.ts | 28 ++
src/plugins/inspector/public/mocks.ts | 78 +++++
src/plugins/inspector/public/plugin.tsx | 112 +++++++
.../public/test/is_available.test.ts | 52 ++++
.../inspector/public/test/open.test.ts | 30 ++
src/plugins/inspector/public/types.ts | 75 +++++
.../inspector_panel.test.tsx.snap} | 45 +--
.../public/ui/inspector_panel.test.tsx} | 52 ++--
.../inspector/public/ui/inspector_panel.tsx} | 102 +++----
.../public/ui/inspector_view_chooser.tsx} | 54 ++--
.../inspector/public}/view_registry.test.ts | 3 +-
src/plugins/inspector/public/view_registry.ts | 74 +++++
.../plugins/kbn_tp_run_pipeline/public/app.js | 3 +-
.../maps/public/inspector/views/map_view.js | 16 +-
.../translations/translations/ja-JP.json | 10 +-
.../translations/translations/zh-CN.json | 10 +-
57 files changed, 1145 insertions(+), 907 deletions(-)
delete mode 100644 src/legacy/ui/public/inspector/_index.scss
delete mode 100644 src/legacy/ui/public/inspector/build_tabular_inspector_data.js
create mode 100644 src/legacy/ui/public/inspector/build_tabular_inspector_data.ts
delete mode 100644 src/legacy/ui/public/inspector/inspector.test.js
delete mode 100644 src/legacy/ui/public/inspector/ui/_index.scss
delete mode 100644 src/legacy/ui/public/inspector/ui/_inspector.scss
rename src/legacy/ui/public/inspector/ui/{inspector_panel.d.ts => inspector_panel.tsx} (74%)
rename src/legacy/ui/public/inspector/ui/{inspector_view.tsx => inspector_view_chooser.tsx} (52%)
create mode 100644 src/plugins/inspector/README.md
create mode 100644 src/plugins/inspector/kibana.json
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/data/data_adapter.ts (100%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/data/data_adapters.test.ts (100%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/data/formatted_data.ts (100%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/data/index.ts (100%)
rename src/{legacy/ui/public/inspector/ui => plugins/inspector/public/adapters}/index.ts (87%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/request/index.ts (100%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/request/request_adapter.test.ts (100%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/request/request_adapter.ts (100%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/request/request_responder.ts (93%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/adapters/request/types.ts (100%)
create mode 100644 src/plugins/inspector/public/index.ts
create mode 100644 src/plugins/inspector/public/mocks.ts
create mode 100644 src/plugins/inspector/public/plugin.tsx
create mode 100644 src/plugins/inspector/public/test/is_available.test.ts
create mode 100644 src/plugins/inspector/public/test/open.test.ts
create mode 100644 src/plugins/inspector/public/types.ts
rename src/{legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap => plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap} (94%)
rename src/{legacy/ui/public/inspector/ui/inspector_panel.test.js => plugins/inspector/public/ui/inspector_panel.test.tsx} (71%)
rename src/{legacy/ui/public/inspector/ui/inspector_panel.js => plugins/inspector/public/ui/inspector_panel.tsx} (52%)
rename src/{legacy/ui/public/inspector/ui/inspector_view_chooser.js => plugins/inspector/public/ui/inspector_view_chooser.tsx} (74%)
rename src/{legacy/ui/public/inspector => plugins/inspector/public}/view_registry.test.ts (96%)
create mode 100644 src/plugins/inspector/public/view_registry.ts
diff --git a/.i18nrc.json b/.i18nrc.json
index bc557db8fdf14..23ed590974c5a 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -58,7 +58,8 @@
"xpack.spaces": "x-pack/legacy/plugins/spaces",
"xpack.upgradeAssistant": "x-pack/legacy/plugins/upgrade_assistant",
"xpack.uptime": "x-pack/legacy/plugins/uptime",
- "xpack.watcher": "x-pack/legacy/plugins/watcher"
+ "xpack.watcher": "x-pack/legacy/plugins/watcher",
+ "inspector": "src/plugins/inspector"
},
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
"translations": [
diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts
index a3909bc556b57..1e71e2b26970b 100644
--- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts
+++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts
@@ -17,35 +17,11 @@
* under the License.
*/
-import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../core/public/mocks';
-
let modalContents: React.Component;
export const getModalContents = () => modalContents;
-jest.doMock('ui/new_platform', () => {
- return {
- npStart: {
- core: {
- overlays: {
- openFlyout: jest.fn(),
- openModal: (component: React.Component) => {
- modalContents = component;
- return {
- close: jest.fn(),
- };
- },
- },
- },
- },
- npSetup: {
- core: {
- fatalErrors: fatalErrorsServiceMock.createSetupContract(),
- notifications: notificationServiceMock.createSetupContract(),
- },
- },
- };
-});
+jest.mock('ui/new_platform');
jest.doMock('ui/metadata', () => ({
metadata: {
diff --git a/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts b/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts
index b7e4a12fac247..69163522c2d99 100644
--- a/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts
+++ b/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts
@@ -24,3 +24,5 @@ jest.doMock('ui/capabilities', () => ({
},
},
}));
+
+jest.mock('ui/new_platform');
diff --git a/src/legacy/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap b/src/legacy/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap
index dc1f6c17c93e4..11180877e04ab 100644
--- a/src/legacy/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap
+++ b/src/legacy/core_plugins/inspector_views/public/data/__snapshots__/data_view.test.js.snap
@@ -119,93 +119,77 @@ exports[`Inspector Data View component should render empty state 1`] = `
}
title="Test Data"
>
-
+
+
+
+
+ }
+ iconColor="subdued"
+ title={
+
+
+
+ }
>
-
-
-
-
-
-
-
-
- }
- iconColor="subdued"
- title={
-
-
-
- }
+
+
+
+ No data available
+
+
+
+
+
+
+
-
-
+
-
-
-
- No data available
-
-
-
-
-
-
-
-
-
-
- The element did not provide any data.
-
-
-
-
-
-
+ The element did not provide any data.
+
+
-
-
-
-
-
+
+
+
+
+
`;
@@ -328,91 +312,85 @@ exports[`Inspector Data View component should render loading state 1`] = `
}
title="Test Data"
>
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Gathering data
-
-
-
-
-
-
+ Gathering data
+
+
-
+
-
+
-
-
-
+
+
+
`;
diff --git a/src/legacy/core_plugins/inspector_views/public/data/data_view.js b/src/legacy/core_plugins/inspector_views/public/data/data_view.js
index 32c340f110fc1..265e4dde29636 100644
--- a/src/legacy/core_plugins/inspector_views/public/data/data_view.js
+++ b/src/legacy/core_plugins/inspector_views/public/data/data_view.js
@@ -29,8 +29,6 @@ import {
EuiText,
} from '@elastic/eui';
-import { InspectorView } from 'ui/inspector';
-
import {
DataTableFormat,
} from './data_table';
@@ -102,54 +100,51 @@ class DataViewComponent extends Component {
renderNoData() {
return (
-
-
+
+
+
+ }
+ body={
+
+
-
- }
- body={
-
-
-
-
-
- }
- />
-
+
+
+ }
+ />
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
@@ -161,13 +156,11 @@ class DataViewComponent extends Component {
}
return (
-
-
-
+
);
}
}
diff --git a/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js b/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js
index 29d536d60e83e..c616a7504d3e9 100644
--- a/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js
+++ b/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js
@@ -22,6 +22,7 @@ import { DataView } from './data_view';
import { DataAdapter } from 'ui/inspector/adapters';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
+jest.mock('ui/new_platform');
jest.mock('./lib/export_csv', () => ({
exportAsCsv: jest.fn(),
}));
diff --git a/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js b/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js
index 2ce47f62ef956..a949b2ebe6ea1 100644
--- a/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js
+++ b/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js
@@ -26,7 +26,6 @@ import {
EuiTextColor,
} from '@elastic/eui';
-import { InspectorView } from 'ui/inspector';
import { RequestStatus } from 'ui/inspector/adapters';
import { RequestSelector } from './request_selector';
@@ -68,36 +67,34 @@ class RequestsViewComponent extends Component {
renderEmptyRequests() {
return (
-
-
+
+
+
+ }
+ body={
+
+
-
- }
- body={
-
-
-
-
-
-
-
-
- }
- />
-
+
+
+
+
+
+ }
+ />
);
}
@@ -111,7 +108,7 @@ class RequestsViewComponent extends Component {
).length;
return (
-
+ <>
}
-
+ >
);
}
}
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts
index f18a1b29f7181..2c9c6ca65a92e 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts
@@ -34,6 +34,8 @@ jest.mock(
{ virtual: true }
);
+jest.mock('ui/new_platform');
+
import { migratePanelsTo730 } from './migrate_to_730_panels';
import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from '../types';
import {
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts
index fff5aeab599ea..e1d9cfac95268 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts
@@ -17,35 +17,11 @@
* under the License.
*/
-import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks';
-
let modalContents: React.Component;
export const getModalContents = () => modalContents;
-jest.doMock('ui/new_platform', () => {
- return {
- npStart: {
- core: {
- overlays: {
- openFlyout: jest.fn(),
- openModal: (component: React.Component) => {
- modalContents = component;
- return {
- close: jest.fn(),
- };
- },
- },
- },
- },
- npSetup: {
- core: {
- fatalErrors: fatalErrorsServiceMock.createSetupContract(),
- notifications: notificationServiceMock.createSetupContract(),
- },
- },
- };
-});
+jest.mock('ui/new_platform');
jest.doMock('ui/metadata', () => ({
metadata: {
diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js
index e10582df46b13..ae14438119da3 100644
--- a/src/legacy/core_plugins/tests_bundle/index.js
+++ b/src/legacy/core_plugins/tests_bundle/index.js
@@ -104,11 +104,25 @@ export default (kibana) => {
modules: [...modules],
template: createTestEntryTemplate(uiSettingDefaults),
extendConfig(webpackConfig) {
- return webpackMerge({
+ const mergedConfig = webpackMerge({
resolve: {
extensions: ['.karma_mock.js', '.karma_mock.tsx', '.karma_mock.ts']
}
}, webpackConfig);
+
+ /**
+ * [..] it removes the commons bundle creation from the webpack
+ * config when we're building the bundle for the browser tests. It
+ * shouldn't be created, and by default isn't, but something is
+ * triggering it in webpack which breaks the tests so if we just
+ * remove the optimization config it will never happen and the tests
+ * will keep working [..]
+ *
+ * TLDR: If you have any questions about this line, ask Spencer.
+ */
+ delete mergedConfig.optimization.splitChunks.cacheGroups.commons;
+
+ return mergedConfig;
}
});
diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss
index a54c100d52eae..3ae2533eb6b9d 100644
--- a/src/legacy/ui/public/_index.scss
+++ b/src/legacy/ui/public/_index.scss
@@ -18,7 +18,6 @@
@import './error_url_overflow/index';
@import './exit_full_screen/index';
@import './field_editor/index';
-@import './inspector/index';
@import './kbn_top_nav/index';
@import './markdown/index';
@import './notify/index';
diff --git a/src/legacy/ui/public/inspector/README.md b/src/legacy/ui/public/inspector/README.md
index b4eb392812aee..c8133d0d9238d 100644
--- a/src/legacy/ui/public/inspector/README.md
+++ b/src/legacy/ui/public/inspector/README.md
@@ -1,127 +1,6 @@
# Inspector
-The inspector is a contextual tool to gain insights into different elements
-in Kibana, e.g. visualizations. It has the form of a flyout panel.
-
-## Inspector Views
-
-The "Inspector Panel" can have multiple so called "Inspector Views" inside of it.
-These views are used to gain different information into the element you are inspecting.
-There is a request inspector view to gain information in the requests done for this
-element or a data inspector view to inspect the underlying data. Whether or not
-a specific view is available depends on the used adapters.
-
-## Inspector Adapters
-
-Since the Inspector panel itself is not tied to a specific type of elements (visualizations,
-saved searches, etc.), everything you need to open the inspector is a collection
-of so called inspector adapters. A single adapter can be any type of JavaScript class.
-
-Most likely an adapter offers some kind of logging capabilities for the element, that
-uses it e.g. the request adapter allows element (like visualizations) to log requests
-they make.
-
-The corresponding inspector view will then use the information inside the adapter
-to present the data in the panel. That concept allows different types of elements
-to use the Inspector panel, while they can use completely or partial different adapters
-and inspector views than other elements.
-
-For example a visualization could provide the request and data adapter while a saved
-search could only provide the request adapter and a Vega visualization could additionally
-provide a Vega adapter.
-
-There is no 1 to 1 relationship between adapters and views. An adapter could be used
-by multiple views and a view can use data from multiple adapters. It's up to the
-view to decide whether or not it wants to be shown for a given adapters list.
-
-## Develop custom inspectors
-
-You can extend the inspector panel by adding custom inspector views and inspector
-adapters via a plugin.
-
-### Develop inspector views
-
-To develop custom inspector views you should first register your file via `uiExports`
-in your plugin config:
-
-```js
-export default (kibana) => {
- return new kibana.Plugin({
- uiExports: {
- inspectorViews: [ 'plugins/your_plugin/custom_view' ],
- }
- });
-};
-```
-
-Within the `custom_view.js` file in your `public` folder, you can define your
-inspector view as follows:
-
-```js
-import React from 'react';
-import { InspectorView, viewRegistry } from 'ui/inspector';
-
-function MyInspectorComponent(props) {
- // props.adapters is the object of all adapters and may vary depending
- // on who and where this inspector was opened. You should check for all
- // adapters you need, in the below shouldShow method, before accessing
- // them here.
- return (
-
- { /* Always use InspectorView as the wrapping element! */ }
-
- );
-}
-
-const MyLittleInspectorView = {
- // Title shown to select this view
- title: 'Display Name',
- // An icon id from the EUI icon list
- icon: 'iconName',
- // An order to sort the views (lower means first)
- order: 10,
- // An additional helptext, that wil
- help: `And additional help text, that will be shown in the inspector help.`,
- shouldShow(adapters) {
- // Only show if `someAdapter` is available. Make sure to check for
- // all adapters that you want to access in your view later on and
- // any additional condition you want to be true to be shown.
- return adapters.someAdapter;
- },
- // A React component, that will be used for rendering
- component: MyInspectorComponent
-};
-
-viewRegistry.register(MyLittleInspectorView);
-```
-
-### Develop custom adapters
-
-An inspector adapter is just a plain JavaScript class, that can e.g. be attached
-to custom visualization types, so an inspector view can show additional information for this
-visualization.
-
-To add additional adapters to your visualization type, use the `inspectorAdapters.custom`
-object when defining the visualization type:
-
-```js
-class MyCustomInspectorAdapter {
- // ....
-}
-
-// inside your visualization type description (usually passed to VisFactory.create...Type)
-{
- // ...
- inspectorAdapters: {
- custom: {
- someAdapter: MyCustomInspectorAdapter
- }
- }
-}
-```
-
-An instance of MyCustomInspectorAdapter will now be available on each visualization
-of that type and can be accessed via `vis.API.inspectorAdapters.someInspector`.
-
-Custom inspector views can now check for the presence of `adapters.someAdapter`
-in their `shouldShow` method and use this adapter in their component.
+- Inspector has been moved to `inspector` New Platform plugin.
+- You can find its documentation in `src/plugins/inspector/README.md`.
+- This folder will be deleted soon, it is deprecated, do not use anything from here.
+- This folder is ready to be deleted, as soon as nothing imports from here anymore.
diff --git a/src/legacy/ui/public/inspector/_index.scss b/src/legacy/ui/public/inspector/_index.scss
deleted file mode 100644
index a51fde079f10b..0000000000000
--- a/src/legacy/ui/public/inspector/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import './ui/index';
diff --git a/src/legacy/ui/public/inspector/adapters/index.ts b/src/legacy/ui/public/inspector/adapters/index.ts
index 8e1979ab33275..55df5a33a178b 100644
--- a/src/legacy/ui/public/inspector/adapters/index.ts
+++ b/src/legacy/ui/public/inspector/adapters/index.ts
@@ -17,5 +17,11 @@
* under the License.
*/
-export { DataAdapter, FormattedData } from './data';
-export { RequestAdapter, RequestStatus } from './request';
+/* eslint-disable */
+
+/**
+ * Do not use this, use NP `inspector` plugin instead.
+ *
+ * @deprecated
+ */
+export * from '../../../../../plugins/inspector/public/adapters/index';
diff --git a/src/legacy/ui/public/inspector/build_tabular_inspector_data.js b/src/legacy/ui/public/inspector/build_tabular_inspector_data.js
deleted file mode 100644
index 8539f0e0833d1..0000000000000
--- a/src/legacy/ui/public/inspector/build_tabular_inspector_data.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { set } from 'lodash';
-import { createFilter } from '../vis/vis_filters';
-import { FormattedData } from './adapters/data';
-
-/**
- * This function builds tabular data from the response and attaches it to the
- * inspector. It will only be called when the data view in the inspector is opened.
- */
-export async function buildTabularInspectorData(table, queryFilter) {
- const aggConfigs = table.columns.map(column => column.aggConfig);
- const rows = table.rows.map(row => {
- return table.columns.reduce((prev, cur, colIndex) => {
- const value = row[cur.id];
- const fieldFormatter = cur.aggConfig.fieldFormatter('text');
- prev[`col-${colIndex}-${cur.aggConfig.id}`] = new FormattedData(value, fieldFormatter(value));
- return prev;
- }, {});
- });
-
- const columns = table.columns.map((col, colIndex) => {
- const field = col.aggConfig.getField();
- const isCellContentFilterable =
- col.aggConfig.isFilterable()
- && (!field || field.filterable);
- return ({
- name: col.name,
- field: `col-${colIndex}-${col.aggConfig.id}`,
- filter: isCellContentFilterable && (value => {
- const rowIndex = rows.findIndex(row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw);
- const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
- queryFilter.addFilters(filter);
- }),
- filterOut: isCellContentFilterable && (value => {
- const rowIndex = rows.findIndex(row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw);
- const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
- const notOther = value.raw !== '__other__';
- const notMissing = value.raw !== '__missing__';
- if (Array.isArray(filter)) {
- filter.forEach(f => set(f, 'meta.negate', (notOther && notMissing)));
- } else {
- set(filter, 'meta.negate', (notOther && notMissing));
- }
- queryFilter.addFilters(filter);
- }),
- });
- });
-
- return { columns, rows };
-}
diff --git a/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts b/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts
new file mode 100644
index 0000000000000..ceeed4e9e1d67
--- /dev/null
+++ b/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts
@@ -0,0 +1,103 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { set } from 'lodash';
+// @ts-ignore
+import { createFilter } from '../vis/vis_filters';
+import { FormattedData } from './adapters';
+
+interface Column {
+ id: string;
+ name: string;
+ aggConfig: any;
+}
+
+interface Row {
+ [key: string]: any;
+}
+
+interface Table {
+ columns: Column[];
+ rows: Row[];
+}
+
+/**
+ * @deprecated
+ *
+ * Do not use this function.
+ *
+ * @todo This function is used only by Courier. Courier will
+ * soon be removed, and this function will be deleted, too. If Courier is not removed,
+ * move this function inside Courier.
+ *
+ * ---
+ *
+ * This function builds tabular data from the response and attaches it to the
+ * inspector. It will only be called when the data view in the inspector is opened.
+ */
+export async function buildTabularInspectorData(
+ table: Table,
+ queryFilter: { addFilters: (filter: any) => void }
+) {
+ const aggConfigs = table.columns.map(column => column.aggConfig);
+ const rows = table.rows.map(row => {
+ return table.columns.reduce>((prev, cur, colIndex) => {
+ const value = row[cur.id];
+ const fieldFormatter = cur.aggConfig.fieldFormatter('text');
+ prev[`col-${colIndex}-${cur.aggConfig.id}`] = new FormattedData(value, fieldFormatter(value));
+ return prev;
+ }, {});
+ });
+
+ const columns = table.columns.map((col, colIndex) => {
+ const field = col.aggConfig.getField();
+ const isCellContentFilterable = col.aggConfig.isFilterable() && (!field || field.filterable);
+ return {
+ name: col.name,
+ field: `col-${colIndex}-${col.aggConfig.id}`,
+ filter:
+ isCellContentFilterable &&
+ ((value: { raw: unknown }) => {
+ const rowIndex = rows.findIndex(
+ row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
+ );
+ const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
+ queryFilter.addFilters(filter);
+ }),
+ filterOut:
+ isCellContentFilterable &&
+ ((value: { raw: unknown }) => {
+ const rowIndex = rows.findIndex(
+ row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
+ );
+ const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
+ const notOther = value.raw !== '__other__';
+ const notMissing = value.raw !== '__missing__';
+ if (Array.isArray(filter)) {
+ filter.forEach(f => set(f, 'meta.negate', notOther && notMissing));
+ } else {
+ set(filter, 'meta.negate', notOther && notMissing);
+ }
+ queryFilter.addFilters(filter);
+ }),
+ };
+ });
+
+ return { columns, rows };
+}
diff --git a/src/legacy/ui/public/inspector/index.ts b/src/legacy/ui/public/inspector/index.ts
index 8169e544cbb45..db82508f36ada 100644
--- a/src/legacy/ui/public/inspector/index.ts
+++ b/src/legacy/ui/public/inspector/index.ts
@@ -17,10 +17,6 @@
* under the License.
*/
-export { InspectorView } from './ui';
-
export { Inspector } from './inspector';
-
export { viewRegistry } from './view_registry';
-
export { Adapters } from './types';
diff --git a/src/legacy/ui/public/inspector/inspector.test.js b/src/legacy/ui/public/inspector/inspector.test.js
deleted file mode 100644
index 75b7804d47ffb..0000000000000
--- a/src/legacy/ui/public/inspector/inspector.test.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { Inspector } from './inspector';
-jest.mock('./view_registry', () => ({
- viewRegistry: {
- getVisible: jest.fn(),
- },
-}));
-jest.mock('./ui/inspector_panel', () => ({
- InspectorPanel: () => 'InspectorPanel',
-}));
-jest.mock('ui/i18n', () => ({ I18nContext: ({ children }) => children }));
-
-jest.mock('ui/new_platform', () => ({
- npStart: {
- core: {
- overlay: {
- openFlyout: jest.fn(),
- },
- }
- },
-}));
-
-import { viewRegistry } from './view_registry';
-
-function setViews(views) {
- viewRegistry.getVisible.mockImplementation(() => views);
-}
-
-describe('Inspector', () => {
- describe('isAvailable()', () => {
- it('should return false if no view would be available', () => {
- setViews([]);
- expect(Inspector.isAvailable({})).toBe(false);
- });
-
- it('should return true if views would be available', () => {
- setViews([{}]);
- expect(Inspector.isAvailable({})).toBe(true);
- });
- });
-
- describe('open()', () => {
- it('should throw an error if no views available', () => {
- setViews([]);
- expect(() => Inspector.open({})).toThrow();
- });
- });
-});
diff --git a/src/legacy/ui/public/inspector/inspector.tsx b/src/legacy/ui/public/inspector/inspector.tsx
index 84e03df440e41..d65245c11cfe1 100644
--- a/src/legacy/ui/public/inspector/inspector.tsx
+++ b/src/legacy/ui/public/inspector/inspector.tsx
@@ -16,75 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { OverlayRef } from '../../../../core/public';
import { npStart } from '../new_platform';
-import { Adapters } from './types';
-import { InspectorPanel } from './ui/inspector_panel';
-import { viewRegistry } from './view_registry';
-
-const closeButtonLabel = i18n.translate('common.ui.inspector.closeButton', {
- defaultMessage: 'Close Inspector',
-});
+export { InspectorSession } from '../../../../plugins/inspector/public';
/**
- * Checks if a inspector panel could be shown based on the passed adapters.
+ * @deprecated
*
- * @param {object} adapters - An object of adapters. This should be the same
- * you would pass into `open`.
- * @returns {boolean} True, if a call to `open` with the same adapters
- * would have shown the inspector panel, false otherwise.
- */
-function isAvailable(adapters?: Adapters): boolean {
- return viewRegistry.getVisible(adapters).length > 0;
-}
-
-/**
- * Options that can be specified when opening the inspector.
- * @property {string} title - An optional title, that will be shown in the header
- * of the inspector. Can be used to give more context about what is being inspected.
+ * Do not use this, use New Platform `inspector` plugin instead.
*/
-interface InspectorOptions {
- title?: string;
-}
-
-export type InspectorSession = OverlayRef;
-
-/**
- * Opens the inspector panel for the given adapters and close any previously opened
- * inspector panel. The previously panel will be closed also if no new panel will be
- * opened (e.g. because of the passed adapters no view is available). You can use
- * {@link InspectorSession#close} on the return value to close that opened panel again.
- *
- * @param {object} adapters - An object of adapters for which you want to show
- * the inspector panel.
- * @param {InspectorOptions} options - Options that configure the inspector. See InspectorOptions type.
- * @return {InspectorSession} The session instance for the opened inspector.
- */
-function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSession {
- const views = viewRegistry.getVisible(adapters);
-
- // Don't open inspector if there are no views available for the passed adapters
- if (!views || views.length === 0) {
- throw new Error(`Tried to open an inspector without views being available.
- Make sure to call Inspector.isAvailable() with the same adapters before to check
- if an inspector can be shown.`);
- }
-
- return npStart.core.overlays.openFlyout(
- ,
- {
- 'data-test-subj': 'inspectorPanel',
- closeButtonAriaLabel: closeButtonLabel,
- }
- );
-}
-
-const Inspector = {
- isAvailable,
- open,
+export const Inspector = {
+ /**
+ * @deprecated
+ *
+ * Do not use this, use New Platform `inspector` plugin instead.
+ */
+ isAvailable: npStart.plugins.inspector.isAvailable,
+
+ /**
+ * @deprecated
+ *
+ * Do not use this, use New Platform `inspector` plugin instead.
+ */
+ open: npStart.plugins.inspector.open,
};
-
-export { Inspector };
diff --git a/src/legacy/ui/public/inspector/types.ts b/src/legacy/ui/public/inspector/types.ts
index 668e9347cc56c..98f2cf487eb43 100644
--- a/src/legacy/ui/public/inspector/types.ts
+++ b/src/legacy/ui/public/inspector/types.ts
@@ -18,46 +18,16 @@
*/
/**
- * The interface that the adapters used to open an inspector have to fullfill.
- */
-export interface Adapters {
- [key: string]: any;
-}
-
-/**
- * The props interface that a custom inspector view component, that will be passed
- * to {@link InspectorViewDescription#component}, must use.
- */
-export interface InspectorViewProps {
- /**
- * The adapters thta has been used to open the inspector.
- */
- adapters: Adapters;
- /**
- * The title that the inspector is currently using e.g. a visualization name.
- */
- title: string;
-}
-
-/**
- * An object describing an inspector view.
- * @typedef {object} InspectorViewDescription
- * @property {string} title - The title that will be used to present that view.
- * @property {string} icon - An icon name to present this view. Must match an EUI icon.
- * @property {React.ComponentType} component - The actual React component to render that
- * that view. It should always return an `InspectorView` element at the toplevel.
- * @property {number} [order=9000] - An order for this view. Views are ordered from lower
- * order values to higher order values in the UI.
- * @property {string} [help=''] - An help text for this view, that gives a brief description
- * of this view.
- * @property {viewShouldShowFunc} [shouldShow] - A function, that determines whether
- * this view should be visible for a given collection of adapters. If not specified
- * the view will always be visible.
+ * Do not import these types from here, instead import them from `inspector` plugin.
+ *
+ * ```ts
+ * import { InspectorViewDescription } from 'src/plugins/inspector/public';
+ * ```
+ *
+ * @deprecated
*/
-export interface InspectorViewDescription {
- component: React.ComponentType;
- help?: string;
- order?: number;
- shouldShow?: (adapters: Adapters) => boolean;
- title: string;
-}
+export {
+ Adapters,
+ InspectorViewProps,
+ InspectorViewDescription,
+} from '../../../../plugins/inspector/public';
diff --git a/src/legacy/ui/public/inspector/ui/_index.scss b/src/legacy/ui/public/inspector/ui/_index.scss
deleted file mode 100644
index e5cd29059a8a6..0000000000000
--- a/src/legacy/ui/public/inspector/ui/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import './inspector';
diff --git a/src/legacy/ui/public/inspector/ui/_inspector.scss b/src/legacy/ui/public/inspector/ui/_inspector.scss
deleted file mode 100644
index 88962905854e0..0000000000000
--- a/src/legacy/ui/public/inspector/ui/_inspector.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.kbnInspectorView--flex {
- display: flex;
-}
diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.d.ts b/src/legacy/ui/public/inspector/ui/inspector_panel.tsx
similarity index 74%
rename from src/legacy/ui/public/inspector/ui/inspector_panel.d.ts
rename to src/legacy/ui/public/inspector/ui/inspector_panel.tsx
index 154b1a58f5732..92ed169bf15e8 100644
--- a/src/legacy/ui/public/inspector/ui/inspector_panel.d.ts
+++ b/src/legacy/ui/public/inspector/ui/inspector_panel.tsx
@@ -16,14 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ComponentClass } from 'react';
-import { Adapters, InspectorViewDescription } from '../types';
+/* eslint-disable */
-interface InspectorPanelProps {
- adapters: Adapters;
- title?: string;
- views: InspectorViewDescription[];
-}
-
-export const InspectorPanel: ComponentClass;
+/**
+ * Do not use this, use NP `inspector` plugin instead.
+ *
+ * @deprecated
+ */
+export * from '../../../../../plugins/inspector/public/ui/inspector_panel';
diff --git a/src/legacy/ui/public/inspector/ui/inspector_view.tsx b/src/legacy/ui/public/inspector/ui/inspector_view_chooser.tsx
similarity index 52%
rename from src/legacy/ui/public/inspector/ui/inspector_view.tsx
rename to src/legacy/ui/public/inspector/ui/inspector_view_chooser.tsx
index 45d7c95d0d7d9..017e5c91095f6 100644
--- a/src/legacy/ui/public/inspector/ui/inspector_view.tsx
+++ b/src/legacy/ui/public/inspector/ui/inspector_view_chooser.tsx
@@ -17,29 +17,11 @@
* under the License.
*/
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import { EuiFlyoutBody } from '@elastic/eui';
+/* eslint-disable */
/**
- * The InspectorView component should be the top most element in every implemented
- * inspector view. It makes sure, that the appropriate stylings are applied to the
- * view.
+ * Do not use this, use NP `inspector` plugin instead.
+ *
+ * @deprecated
*/
-const InspectorView: React.SFC<{ useFlex?: boolean }> = ({ useFlex, children }) => {
- const classes = classNames({
- 'kbnInspectorView--flex': Boolean(useFlex),
- });
- return {children};
-};
-
-InspectorView.propTypes = {
- /**
- * Set to true if the element should have display: flex set.
- */
- useFlex: PropTypes.bool,
-};
-
-export { InspectorView };
+export * from '../../../../../plugins/inspector/public/ui/inspector_view_chooser';
diff --git a/src/legacy/ui/public/inspector/view_registry.ts b/src/legacy/ui/public/inspector/view_registry.ts
index a4be902fc8ee1..199087960ba89 100644
--- a/src/legacy/ui/public/inspector/view_registry.ts
+++ b/src/legacy/ui/public/inspector/view_registry.ts
@@ -17,66 +17,19 @@
* under the License.
*/
-import { EventEmitter } from 'events';
-import { Adapters, InspectorViewDescription } from './types';
+import { npSetup } from 'ui/new_platform';
+export { InspectorViewDescription } from './types';
/**
- * @callback viewShouldShowFunc
- * @param {object} adapters - A list of adapters to check whether or not this view
- * should be shown for.
- * @returns {boolean} true - if this view should be shown for the given adapters.
- */
-
-/**
- * A registry that will hold inspector views.
- */
-class InspectorViewRegistry extends EventEmitter {
- private views: InspectorViewDescription[] = [];
-
- /**
- * Register a new inspector view to the registry. Check the README.md in the
- * inspector directory for more information of the object format to register
- * here. This will also emit a 'change' event on the registry itself.
- *
- * @param {InspectorViewDescription} view - The view description to add to the registry.
- */
- public register(view: InspectorViewDescription): void {
- if (!view) {
- return;
- }
- this.views.push(view);
- // Keep registry sorted by the order property
- this.views.sort((a, b) => (a.order || Number.MAX_VALUE) - (b.order || Number.MAX_VALUE));
- this.emit('change');
- }
-
- /**
- * Retrieve all views currently registered with the registry.
- * @returns {InspectorViewDescription[]} A by `order` sorted list of all registered
- * inspector views.
- */
- public getAll(): InspectorViewDescription[] {
- return this.views;
- }
-
- /**
- * Retrieve all registered views, that want to be visible for the specified adapters.
- * @param {object} adapters - an adapter configuration
- * @returns {InspectorViewDescription[]} All inespector view descriptions visible
- * for the specific adapters.
- */
- public getVisible(adapters?: Adapters): InspectorViewDescription[] {
- if (!adapters) {
- return [];
- }
- return this.views.filter(view => !view.shouldShow || view.shouldShow(adapters));
- }
-}
-
-/**
- * The global view registry. In the long run this should be solved by a registry
- * system introduced by the new platform instead, to not keep global state like that.
+ * Do not use this, instead use `inspector` plugin directly.
+ *
+ * ```ts
+ * import { npSetup } from 'ui/new_platform';
+ *
+ * npSetup.plugins.inspector.registerView(view);
+ * ```
+ *
+ * @deprecated
*/
-const viewRegistry = new InspectorViewRegistry();
-
-export { viewRegistry, InspectorViewRegistry, InspectorViewDescription };
+export const viewRegistry =
+ npSetup.plugins.inspector.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.views;
diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts
index 439d158802659..838d94264848d 100644
--- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts
+++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts
@@ -17,16 +17,20 @@
* under the License.
*/
+/* eslint-disable @kbn/eslint/no-restricted-paths */
import { coreMock } from '../../../../../core/public/mocks';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { dataPluginMock } from '../../../../../plugins/data/public/mocks';
+import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks';
+/* eslint-enable @kbn/eslint/no-restricted-paths */
export const pluginsMock = {
createSetup: () => ({
data: dataPluginMock.createSetupContract(),
+ inspector: inspectorPluginMock.createSetupContract(),
}),
createStart: () => ({
data: dataPluginMock.createStartContract(),
+ inspector: inspectorPluginMock.createStartContract(),
}),
};
diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js
index c66f4aa5bc2b3..ecbf514892a2e 100644
--- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js
+++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js
@@ -29,6 +29,14 @@ export const npSetup = {
registerType: sinon.fake(),
},
},
+ inspector: {
+ registerView: () => undefined,
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
+ views: {
+ register: () => undefined,
+ },
+ },
+ },
},
};
@@ -36,6 +44,13 @@ export const npStart = {
core: {},
plugins: {
data: {},
+ inspector: {
+ isAvailable: () => false,
+ open: () => ({
+ onClose: Promise.resolve(undefined),
+ close: () => Promise.resolve(undefined),
+ }),
+ },
},
};
diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts
index cfcf99fcbc9f2..5e0eb2feeb450 100644
--- a/src/legacy/ui/public/new_platform/new_platform.ts
+++ b/src/legacy/ui/public/new_platform/new_platform.ts
@@ -18,13 +18,19 @@
*/
import { InternalCoreSetup, InternalCoreStart } from '../../../../core/public';
import { Plugin as DataPlugin } from '../../../../plugins/data/public';
+import {
+ Setup as InspectorSetup,
+ Start as InspectorStart,
+} from '../../../../plugins/inspector/public';
export interface PluginsSetup {
data: ReturnType;
+ inspector: InspectorSetup;
}
export interface PluginsStart {
data: ReturnType;
+ inspector: InspectorStart;
}
export const npSetup = {
diff --git a/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js b/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js
index f48e2014385b5..aedb2f016b50d 100644
--- a/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js
+++ b/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js
@@ -36,8 +36,7 @@ import { dispatchRenderComplete } from '../../../render_complete';
import { PipelineDataLoader } from '../pipeline_data_loader';
import { VisualizeDataLoader } from '../visualize_data_loader';
import { PersistedState } from '../../../persisted_state';
-import { DataAdapter } from '../../../inspector/adapters/data';
-import { RequestAdapter } from '../../../inspector/adapters/request';
+import { DataAdapter, RequestAdapter } from '../../../inspector/adapters';
describe('visualize loader', () => {
diff --git a/src/plugins/inspector/README.md b/src/plugins/inspector/README.md
new file mode 100644
index 0000000000000..e56db65cb90cb
--- /dev/null
+++ b/src/plugins/inspector/README.md
@@ -0,0 +1,122 @@
+# Inspector
+
+The inspector is a contextual tool to gain insights into different elements
+in Kibana, e.g. visualizations. It has the form of a flyout panel.
+
+## Inspector Views
+
+The "Inspector Panel" can have multiple so called "Inspector Views" inside of it.
+These views are used to gain different information into the element you are inspecting.
+There is a request inspector view to gain information in the requests done for this
+element or a data inspector view to inspect the underlying data. Whether or not
+a specific view is available depends on the used adapters.
+
+## Inspector Adapters
+
+Since the Inspector panel itself is not tied to a specific type of elements (visualizations,
+saved searches, etc.), everything you need to open the inspector is a collection
+of so called inspector adapters. A single adapter can be any type of JavaScript class.
+
+Most likely an adapter offers some kind of logging capabilities for the element, that
+uses it e.g. the request adapter allows element (like visualizations) to log requests
+they make.
+
+The corresponding inspector view will then use the information inside the adapter
+to present the data in the panel. That concept allows different types of elements
+to use the Inspector panel, while they can use completely or partial different adapters
+and inspector views than other elements.
+
+For example a visualization could provide the request and data adapter while a saved
+search could only provide the request adapter and a Vega visualization could additionally
+provide a Vega adapter.
+
+There is no 1 to 1 relationship between adapters and views. An adapter could be used
+by multiple views and a view can use data from multiple adapters. It's up to the
+view to decide whether or not it wants to be shown for a given adapters list.
+
+## Develop custom inspectors
+
+You can extend the inspector panel by adding custom inspector views and inspector
+adapters via a plugin.
+
+### Develop inspector views
+
+To develop custom inspector views you can define your
+inspector view as follows:
+
+```js
+import React from 'react';
+import { viewRegistry } from 'ui/inspector';
+
+function MyInspectorComponent(props) {
+ // props.adapters is the object of all adapters and may vary depending
+ // on who and where this inspector was opened. You should check for all
+ // adapters you need, in the below shouldShow method, before accessing
+ // them here.
+ return (
+ <>
+ My custom view....
+ >
+ );
+}
+
+const MyLittleInspectorView = {
+ // Title shown to select this view
+ title: 'Display Name',
+ // An icon id from the EUI icon list
+ icon: 'iconName',
+ // An order to sort the views (lower means first)
+ order: 10,
+ // An additional helptext, that wil
+ help: `And additional help text, that will be shown in the inspector help.`,
+ shouldShow(adapters) {
+ // Only show if `someAdapter` is available. Make sure to check for
+ // all adapters that you want to access in your view later on and
+ // any additional condition you want to be true to be shown.
+ return adapters.someAdapter;
+ },
+ // A React component, that will be used for rendering
+ component: MyInspectorComponent
+};
+```
+
+Then register your view in *setup* life-cycle with `inspector` plugin.
+
+```ts
+class MyPlugin extends Plugin {
+ setup(core, { inspector }) {
+ inspector.registerView(MyLittleInspectorView);
+ }
+}
+```
+
+### Develop custom adapters
+
+An inspector adapter is just a plain JavaScript class, that can e.g. be attached
+to custom visualization types, so an inspector view can show additional information for this
+visualization.
+
+To add additional adapters to your visualization type, use the `inspectorAdapters.custom`
+object when defining the visualization type:
+
+```js
+class MyCustomInspectorAdapter {
+ // ....
+}
+
+// inside your visualization type description (usually passed to VisFactory.create...Type)
+{
+ // ...
+ inspectorAdapters: {
+ custom: {
+ someAdapter: MyCustomInspectorAdapter
+ }
+ }
+}
+```
+
+An instance of MyCustomInspectorAdapter will now be available on each visualization
+of that type and can be accessed via `vis.API.inspectorAdapters.someInspector`.
+
+Custom inspector views can now check for the presence of `adapters.someAdapter`
+in their `shouldShow` method and use this adapter in their component.
diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json
new file mode 100644
index 0000000000000..39d3ff65eed53
--- /dev/null
+++ b/src/plugins/inspector/kibana.json
@@ -0,0 +1,6 @@
+{
+ "id": "inspector",
+ "version": "kibana",
+ "server": false,
+ "ui": true
+}
diff --git a/src/legacy/ui/public/inspector/adapters/data/data_adapter.ts b/src/plugins/inspector/public/adapters/data/data_adapter.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/data/data_adapter.ts
rename to src/plugins/inspector/public/adapters/data/data_adapter.ts
diff --git a/src/legacy/ui/public/inspector/adapters/data/data_adapters.test.ts b/src/plugins/inspector/public/adapters/data/data_adapters.test.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/data/data_adapters.test.ts
rename to src/plugins/inspector/public/adapters/data/data_adapters.test.ts
diff --git a/src/legacy/ui/public/inspector/adapters/data/formatted_data.ts b/src/plugins/inspector/public/adapters/data/formatted_data.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/data/formatted_data.ts
rename to src/plugins/inspector/public/adapters/data/formatted_data.ts
diff --git a/src/legacy/ui/public/inspector/adapters/data/index.ts b/src/plugins/inspector/public/adapters/data/index.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/data/index.ts
rename to src/plugins/inspector/public/adapters/data/index.ts
diff --git a/src/legacy/ui/public/inspector/ui/index.ts b/src/plugins/inspector/public/adapters/index.ts
similarity index 87%
rename from src/legacy/ui/public/inspector/ui/index.ts
rename to src/plugins/inspector/public/adapters/index.ts
index c295c8eca1afc..8e1979ab33275 100644
--- a/src/legacy/ui/public/inspector/ui/index.ts
+++ b/src/plugins/inspector/public/adapters/index.ts
@@ -17,4 +17,5 @@
* under the License.
*/
-export { InspectorView } from './inspector_view';
+export { DataAdapter, FormattedData } from './data';
+export { RequestAdapter, RequestStatus } from './request';
diff --git a/src/legacy/ui/public/inspector/adapters/request/index.ts b/src/plugins/inspector/public/adapters/request/index.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/request/index.ts
rename to src/plugins/inspector/public/adapters/request/index.ts
diff --git a/src/legacy/ui/public/inspector/adapters/request/request_adapter.test.ts b/src/plugins/inspector/public/adapters/request/request_adapter.test.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/request/request_adapter.test.ts
rename to src/plugins/inspector/public/adapters/request/request_adapter.test.ts
diff --git a/src/legacy/ui/public/inspector/adapters/request/request_adapter.ts b/src/plugins/inspector/public/adapters/request/request_adapter.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/request/request_adapter.ts
rename to src/plugins/inspector/public/adapters/request/request_adapter.ts
diff --git a/src/legacy/ui/public/inspector/adapters/request/request_responder.ts b/src/plugins/inspector/public/adapters/request/request_responder.ts
similarity index 93%
rename from src/legacy/ui/public/inspector/adapters/request/request_responder.ts
rename to src/plugins/inspector/public/adapters/request/request_responder.ts
index 31aa56030d874..36ae6a147b999 100644
--- a/src/legacy/ui/public/inspector/adapters/request/request_responder.ts
+++ b/src/plugins/inspector/public/adapters/request/request_responder.ts
@@ -48,11 +48,11 @@ export class RequestResponder {
const startDate = new Date(this.request.startTime);
this.request.stats.requestTimestamp = {
- label: i18n.translate('common.ui.inspector.reqTimestampKey', {
+ label: i18n.translate('inspector.reqTimestampKey', {
defaultMessage: 'Request timestamp',
}),
value: startDate.toISOString(),
- description: i18n.translate('common.ui.inspector.reqTimestampDescription', {
+ description: i18n.translate('inspector.reqTimestampDescription', {
defaultMessage: 'Time when the start of the request has been logged',
}),
};
diff --git a/src/legacy/ui/public/inspector/adapters/request/types.ts b/src/plugins/inspector/public/adapters/request/types.ts
similarity index 100%
rename from src/legacy/ui/public/inspector/adapters/request/types.ts
rename to src/plugins/inspector/public/adapters/request/types.ts
diff --git a/src/plugins/inspector/public/index.ts b/src/plugins/inspector/public/index.ts
new file mode 100644
index 0000000000000..ad0c9b77e915e
--- /dev/null
+++ b/src/plugins/inspector/public/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PluginInitializerContext } from '../../../core/public';
+import { InspectorPublicPlugin } from './plugin';
+
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new InspectorPublicPlugin(initializerContext);
+}
+
+export { InspectorPublicPlugin as Plugin, Setup, Start } from './plugin';
+export * from './types';
diff --git a/src/plugins/inspector/public/mocks.ts b/src/plugins/inspector/public/mocks.ts
new file mode 100644
index 0000000000000..0e605b1dd3069
--- /dev/null
+++ b/src/plugins/inspector/public/mocks.ts
@@ -0,0 +1,78 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Setup as PluginSetup, Start as PluginStart } from '.';
+import { InspectorViewRegistry } from './view_registry';
+import { plugin as pluginInitializer } from '.';
+// eslint-disable-next-line
+import { coreMock } from '../../../core/public/mocks';
+
+export type Setup = jest.Mocked;
+export type Start = jest.Mocked;
+
+const createSetupContract = (): Setup => {
+ const views = new InspectorViewRegistry();
+
+ const setupContract: Setup = {
+ registerView: jest.fn(views.register.bind(views)),
+
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
+ views,
+ },
+ };
+ return setupContract;
+};
+
+const createStartContract = (): Start => {
+ const startContract: Start = {
+ isAvailable: jest.fn(),
+ open: jest.fn(),
+ };
+
+ const openResult = {
+ onClose: Promise.resolve(undefined),
+ close: jest.fn(() => Promise.resolve(undefined)),
+ } as ReturnType;
+ startContract.open.mockImplementation(() => openResult);
+
+ return startContract;
+};
+
+const createPlugin = async () => {
+ const pluginInitializerContext = coreMock.createPluginInitializerContext();
+ const coreSetup = coreMock.createSetup();
+ const coreStart = coreMock.createStart();
+ const plugin = pluginInitializer(pluginInitializerContext);
+ const setup = await plugin.setup(coreSetup);
+
+ return {
+ pluginInitializerContext,
+ coreSetup,
+ coreStart,
+ plugin,
+ setup,
+ doStart: async () => await plugin.start(coreStart),
+ };
+};
+
+export const inspectorPluginMock = {
+ createSetupContract,
+ createStartContract,
+ createPlugin,
+};
diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx
new file mode 100644
index 0000000000000..53a7adde9b3a6
--- /dev/null
+++ b/src/plugins/inspector/public/plugin.tsx
@@ -0,0 +1,112 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import * as React from 'react';
+import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
+import { InspectorViewRegistry } from './view_registry';
+import { Adapters, InspectorOptions, InspectorSession } from './types';
+import { InspectorPanel } from './ui/inspector_panel';
+
+export interface Setup {
+ registerView: InspectorViewRegistry['register'];
+
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
+ views: InspectorViewRegistry;
+ };
+}
+
+export interface Start {
+ /**
+ * Checks if a inspector panel could be shown based on the passed adapters.
+ *
+ * @param {object} adapters - An object of adapters. This should be the same
+ * you would pass into `open`.
+ * @returns {boolean} True, if a call to `open` with the same adapters
+ * would have shown the inspector panel, false otherwise.
+ */
+ isAvailable: (adapters?: Adapters) => boolean;
+
+ /**
+ * Opens the inspector panel for the given adapters and close any previously opened
+ * inspector panel. The previously panel will be closed also if no new panel will be
+ * opened (e.g. because of the passed adapters no view is available). You can use
+ * {@link InspectorSession#close} on the return value to close that opened panel again.
+ *
+ * @param {object} adapters - An object of adapters for which you want to show
+ * the inspector panel.
+ * @param {InspectorOptions} options - Options that configure the inspector. See InspectorOptions type.
+ * @return {InspectorSession} The session instance for the opened inspector.
+ * @throws {Error}
+ */
+ open: (adapters: Adapters, options?: InspectorOptions) => InspectorSession;
+}
+
+export class InspectorPublicPlugin implements Plugin {
+ views: InspectorViewRegistry | undefined;
+
+ constructor(initializerContext: PluginInitializerContext) {}
+
+ public async setup(core: CoreSetup) {
+ this.views = new InspectorViewRegistry();
+
+ return {
+ registerView: this.views!.register.bind(this.views),
+
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
+ views: this.views,
+ },
+ };
+ }
+
+ public start(core: CoreStart) {
+ const isAvailable: Start['isAvailable'] = adapters =>
+ this.views!.getVisible(adapters).length > 0;
+
+ const closeButtonLabel = i18n.translate('inspector.closeButton', {
+ defaultMessage: 'Close Inspector',
+ });
+
+ const open: Start['open'] = (adapters, options = {}) => {
+ const views = this.views!.getVisible(adapters);
+
+ // Don't open inspector if there are no views available for the passed adapters
+ if (!views || views.length === 0) {
+ throw new Error(`Tried to open an inspector without views being available.
+ Make sure to call Inspector.isAvailable() with the same adapters before to check
+ if an inspector can be shown.`);
+ }
+
+ return core.overlays.openFlyout(
+ ,
+ {
+ 'data-test-subj': 'inspectorPanel',
+ closeButtonAriaLabel: closeButtonLabel,
+ }
+ );
+ };
+
+ return {
+ isAvailable,
+ open,
+ };
+ }
+
+ public stop() {}
+}
diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts
new file mode 100644
index 0000000000000..1aeffd68a9f3d
--- /dev/null
+++ b/src/plugins/inspector/public/test/is_available.test.ts
@@ -0,0 +1,52 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { inspectorPluginMock } from '../mocks';
+import { DataAdapter } from '../adapters/data/data_adapter';
+import { RequestAdapter } from '../adapters/request/request_adapter';
+
+const adapter1 = new DataAdapter();
+const adapter2 = new RequestAdapter();
+
+describe('inspector', () => {
+ describe('isAvailable()', () => {
+ it('should return false if no view would be available', async () => {
+ const { doStart } = await inspectorPluginMock.createPlugin();
+ const start = await doStart();
+ expect(start.isAvailable({ adapter1 })).toBe(false);
+ });
+
+ it('should return true if views would be available, false otherwise', async () => {
+ const { setup, doStart } = await inspectorPluginMock.createPlugin();
+
+ setup.registerView({
+ title: 'title',
+ help: 'help',
+ shouldShow(adapters: any) {
+ return 'adapter1' in adapters;
+ },
+ } as any);
+
+ const start = await doStart();
+
+ expect(start.isAvailable({ adapter1 })).toBe(true);
+ expect(start.isAvailable({ adapter2 })).toBe(false);
+ });
+ });
+});
diff --git a/src/plugins/inspector/public/test/open.test.ts b/src/plugins/inspector/public/test/open.test.ts
new file mode 100644
index 0000000000000..94cf161bb11c2
--- /dev/null
+++ b/src/plugins/inspector/public/test/open.test.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { inspectorPluginMock } from '../mocks';
+
+describe('inspector', () => {
+ describe('open()', () => {
+ it('should throw an error if no views available', async () => {
+ const { doStart } = await inspectorPluginMock.createPlugin();
+ const start = await doStart();
+ expect(() => start.open({})).toThrow();
+ });
+ });
+});
diff --git a/src/plugins/inspector/public/types.ts b/src/plugins/inspector/public/types.ts
new file mode 100644
index 0000000000000..5c3fd770c28d3
--- /dev/null
+++ b/src/plugins/inspector/public/types.ts
@@ -0,0 +1,75 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { OverlayRef } from '../../../core/public';
+
+/**
+ * The interface that the adapters used to open an inspector have to fullfill.
+ */
+export interface Adapters {
+ [key: string]: any;
+}
+
+/**
+ * The props interface that a custom inspector view component, that will be passed
+ * to {@link InspectorViewDescription#component}, must use.
+ */
+export interface InspectorViewProps {
+ /**
+ * Adapters used to open the inspector.
+ */
+ adapters: Adapters;
+ /**
+ * The title that the inspector is currently using e.g. a visualization name.
+ */
+ title: string;
+}
+
+/**
+ * An object describing an inspector view.
+ * @typedef {object} InspectorViewDescription
+ * @property {string} title - The title that will be used to present that view.
+ * @property {string} icon - An icon name to present this view. Must match an EUI icon.
+ * @property {React.ComponentType} component - The actual React component to render that view.
+ * @property {number} [order=9000] - An order for this view. Views are ordered from lower
+ * order values to higher order values in the UI.
+ * @property {string} [help=''] - An help text for this view, that gives a brief description
+ * of this view.
+ * @property {viewShouldShowFunc} [shouldShow] - A function, that determines whether
+ * this view should be visible for a given collection of adapters. If not specified
+ * the view will always be visible.
+ */
+export interface InspectorViewDescription {
+ component: React.ComponentType;
+ help?: string;
+ order?: number;
+ shouldShow?: (adapters: Adapters) => boolean;
+ title: string;
+}
+
+/**
+ * Options that can be specified when opening the inspector.
+ * @property {string} title - An optional title, that will be shown in the header
+ * of the inspector. Can be used to give more context about what is being inspected.
+ */
+export interface InspectorOptions {
+ title?: string;
+}
+
+export type InspectorSession = OverlayRef;
diff --git a/src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap
similarity index 94%
rename from src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap
rename to src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap
index 6506774cca887..843fd78b24be9 100644
--- a/src/legacy/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap
+++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap
@@ -112,7 +112,6 @@ exports[`InspectorPanel should render as expected 1`] = `
"timeZone": null,
}
}
- onClose={[Function]}
title="Inspector"
views={
Array [
@@ -217,7 +216,7 @@ exports[`InspectorPanel should render as expected 1`] = `
>
-
-
- View 1
-
-
+
+
+
`;
diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.test.js b/src/plugins/inspector/public/ui/inspector_panel.test.tsx
similarity index 71%
rename from src/legacy/ui/public/inspector/ui/inspector_panel.test.js
rename to src/plugins/inspector/public/ui/inspector_panel.test.tsx
index d4c72ba0132e4..c482b6fa8033b 100644
--- a/src/legacy/ui/public/inspector/ui/inspector_panel.test.js
+++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx
@@ -18,65 +18,55 @@
*/
import React from 'react';
-import { InspectorPanel } from './inspector_panel';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { InspectorPanel } from './inspector_panel';
+import { Adapters, InspectorViewDescription } from '../types';
describe('InspectorPanel', () => {
-
- let adapters;
- let views;
+ let adapters: Adapters;
+ let views: InspectorViewDescription[];
beforeEach(() => {
adapters = {
foodapter: {
- foo() { return 42; }
+ foo() {
+ return 42;
+ },
},
- bardapter: {
-
- }
+ bardapter: {},
};
views = [
{
title: 'View 1',
order: 200,
- component: () => (View 1
),
- }, {
+ component: () => View 1
,
+ },
+ {
title: 'Foo View',
order: 100,
- component: () => (Foo view
),
- shouldShow(adapters) {
- return adapters.foodapter;
- }
- }, {
+ component: () => Foo view
,
+ shouldShow(adapters2: Adapters) {
+ return adapters2.foodapter;
+ },
+ },
+ {
title: 'Never',
order: 200,
component: () => null,
shouldShow() {
return false;
- }
- }
+ },
+ },
];
});
it('should render as expected', () => {
- const component = mountWithIntl(
- true}
- views={views}
- />
- );
+ const component = mountWithIntl();
expect(component).toMatchSnapshot();
});
it('should not allow updating adapters', () => {
- const component = mountWithIntl(
- true}
- views={views}
- />
- );
+ const component = mountWithIntl();
adapters.notAllowed = {};
expect(() => component.setProps({ adapters })).toThrow();
});
diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.js b/src/plugins/inspector/public/ui/inspector_panel.tsx
similarity index 52%
rename from src/legacy/ui/public/inspector/ui/inspector_panel.js
rename to src/plugins/inspector/public/ui/inspector_panel.tsx
index 26f100b89c172..953bf7e1f073b 100644
--- a/src/legacy/ui/public/inspector/ui/inspector_panel.js
+++ b/src/plugins/inspector/public/ui/inspector_panel.tsx
@@ -20,52 +20,73 @@
import { i18n } from '@kbn/i18n';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiFlyoutHeader,
- EuiTitle,
-} from '@elastic/eui';
-
+import { EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
+import { Adapters, InspectorViewDescription } from '../types';
import { InspectorViewChooser } from './inspector_view_chooser';
-function hasAdaptersChanged(oldAdapters, newAdapters) {
- return Object.keys(oldAdapters).length !== Object.keys(newAdapters).length
- || Object.keys(oldAdapters).some(key => oldAdapters[key] !== newAdapters[key]);
+function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) {
+ return (
+ Object.keys(oldAdapters).length !== Object.keys(newAdapters).length ||
+ Object.keys(oldAdapters).some(key => oldAdapters[key] !== newAdapters[key])
+ );
}
-const inspectorTitle = i18n.translate('common.ui.inspector.title', {
+const inspectorTitle = i18n.translate('inspector.title', {
defaultMessage: 'Inspector',
});
-class InspectorPanel extends Component {
+interface InspectorPanelProps {
+ adapters: Adapters;
+ title?: string;
+ views: InspectorViewDescription[];
+}
- constructor(props) {
- super(props);
- this.state = {
- selectedView: props.views[0],
- views: props.views,
- // Clone adapters array so we can validate that this prop never change
- adapters: { ...props.adapters },
- };
- }
+interface InspectorPanelState {
+ selectedView: InspectorViewDescription;
+ views: InspectorViewDescription[];
+ adapters: Adapters;
+}
+
+export class InspectorPanel extends Component {
+ static defaultProps = {
+ title: inspectorTitle,
+ };
- static getDerivedStateFromProps(nextProps, prevState) {
+ static propTypes = {
+ adapters: PropTypes.object.isRequired,
+ views: (props: InspectorPanelProps, propName: string, componentName: string) => {
+ if (!Array.isArray(props.views) || props.views.length < 1) {
+ throw new Error(
+ `${propName} prop must be an array of at least one element in ${componentName}.`
+ );
+ }
+ },
+ title: PropTypes.string,
+ };
+
+ state: InspectorPanelState = {
+ selectedView: this.props.views[0],
+ views: this.props.views,
+ // Clone adapters array so we can validate that this prop never change
+ adapters: { ...this.props.adapters },
+ };
+
+ static getDerivedStateFromProps(nextProps: InspectorPanelProps, prevState: InspectorPanelState) {
if (hasAdaptersChanged(prevState.adapters, nextProps.adapters)) {
throw new Error('Adapters are not allowed to be changed on an open InspectorPanel.');
}
- const selectedViewMustChange = nextProps.views !== prevState.views
- && !nextProps.views.includes(prevState.selectedView);
+ const selectedViewMustChange =
+ nextProps.views !== prevState.views && !nextProps.views.includes(prevState.selectedView);
return {
views: nextProps.views,
selectedView: selectedViewMustChange ? nextProps.views[0] : prevState.selectedView,
};
}
- onViewSelected = (view) => {
+ onViewSelected = (view: InspectorViewDescription) => {
if (view !== this.state.selectedView) {
this.setState({
- selectedView: view
+ selectedView: view,
});
}
};
@@ -74,7 +95,7 @@ class InspectorPanel extends Component {
return (
);
}
@@ -86,13 +107,10 @@ class InspectorPanel extends Component {
return (
-
+
- { title }
+ {title}
@@ -104,26 +122,8 @@ class InspectorPanel extends Component {
- { this.renderSelectedPanel() }
+ {this.renderSelectedPanel()}
);
}
}
-
-InspectorPanel.defaultProps = {
- title: inspectorTitle,
-};
-
-InspectorPanel.propTypes = {
- adapters: PropTypes.object.isRequired,
- views: (props, propName, componentName) => {
- if (!Array.isArray(props[propName]) || props[propName].length < 1) {
- throw new Error(
- `${propName} prop must be an array of at least one element in ${componentName}.`
- );
- }
- },
- title: PropTypes.string,
-};
-
-export { InspectorPanel };
diff --git a/src/legacy/ui/public/inspector/ui/inspector_view_chooser.js b/src/plugins/inspector/public/ui/inspector_view_chooser.tsx
similarity index 74%
rename from src/legacy/ui/public/inspector/ui/inspector_view_chooser.js
rename to src/plugins/inspector/public/ui/inspector_view_chooser.tsx
index 2ab4c57ce8bb8..ce6027ad383cf 100644
--- a/src/legacy/ui/public/inspector/ui/inspector_view_chooser.js
+++ b/src/plugins/inspector/public/ui/inspector_view_chooser.tsx
@@ -20,7 +20,6 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-
import {
EuiButtonEmpty,
EuiContextMenuItem,
@@ -28,26 +27,42 @@ import {
EuiPopover,
EuiToolTip,
} from '@elastic/eui';
+import { InspectorViewDescription } from '../types';
+
+interface Props {
+ views: InspectorViewDescription[];
+ onViewSelected: (view: InspectorViewDescription) => void;
+ selectedView: InspectorViewDescription;
+}
-class InspectorViewChooser extends Component {
+interface State {
+ isSelectorOpen: boolean;
+}
+
+export class InspectorViewChooser extends Component {
+ static propTypes = {
+ views: PropTypes.array.isRequired,
+ onViewSelected: PropTypes.func.isRequired,
+ selectedView: PropTypes.object.isRequired,
+ };
- state = {
- isSelectorOpen: false
+ state: State = {
+ isSelectorOpen: false,
};
toggleSelector = () => {
- this.setState((prev) => ({
- isSelectorOpen: !prev.isSelectorOpen
+ this.setState(prev => ({
+ isSelectorOpen: !prev.isSelectorOpen,
}));
};
closeSelector = () => {
this.setState({
- isSelectorOpen: false
+ isSelectorOpen: false,
});
};
- renderView = (view, index) => {
+ renderView = (view: InspectorViewDescription, index: number) => {
return (
);
- }
+ };
renderViewButton() {
return (
@@ -74,7 +89,7 @@ class InspectorViewChooser extends Component {
data-test-subj="inspectorViewChooser"
>
@@ -84,12 +99,9 @@ class InspectorViewChooser extends Component {
renderSingleView() {
return (
-
+
@@ -117,18 +129,8 @@ class InspectorViewChooser extends Component {
anchorPosition="downRight"
repositionOnScroll
>
-
+
);
}
}
-
-InspectorViewChooser.propTypes = {
- views: PropTypes.array.isRequired,
- onViewSelected: PropTypes.func.isRequired,
- selectedView: PropTypes.object.isRequired,
-};
-
-export { InspectorViewChooser };
diff --git a/src/legacy/ui/public/inspector/view_registry.test.ts b/src/plugins/inspector/public/view_registry.test.ts
similarity index 96%
rename from src/legacy/ui/public/inspector/view_registry.test.ts
rename to src/plugins/inspector/public/view_registry.test.ts
index e073c6431892f..830ee107213fb 100644
--- a/src/legacy/ui/public/inspector/view_registry.test.ts
+++ b/src/plugins/inspector/public/view_registry.test.ts
@@ -17,7 +17,8 @@
* under the License.
*/
-import { InspectorViewDescription, InspectorViewRegistry } from './view_registry';
+import { InspectorViewRegistry } from './view_registry';
+import { InspectorViewDescription } from './types';
import { Adapters } from './types';
diff --git a/src/plugins/inspector/public/view_registry.ts b/src/plugins/inspector/public/view_registry.ts
new file mode 100644
index 0000000000000..4a35baf1f3ef4
--- /dev/null
+++ b/src/plugins/inspector/public/view_registry.ts
@@ -0,0 +1,74 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EventEmitter } from 'events';
+import { Adapters, InspectorViewDescription } from './types';
+
+/**
+ * @callback viewShouldShowFunc
+ * @param {object} adapters - A list of adapters to check whether or not this view
+ * should be shown for.
+ * @returns {boolean} true - if this view should be shown for the given adapters.
+ */
+
+/**
+ * A registry that will hold inspector views.
+ */
+export class InspectorViewRegistry extends EventEmitter {
+ private views: InspectorViewDescription[] = [];
+
+ /**
+ * Register a new inspector view to the registry. Check the README.md in the
+ * inspector directory for more information of the object format to register
+ * here. This will also emit a 'change' event on the registry itself.
+ *
+ * @param {InspectorViewDescription} view - The view description to add to the registry.
+ */
+ public register(view: InspectorViewDescription): void {
+ if (!view) {
+ return;
+ }
+ this.views.push(view);
+ // Keep registry sorted by the order property
+ this.views.sort((a, b) => (a.order || Number.MAX_VALUE) - (b.order || Number.MAX_VALUE));
+ this.emit('change');
+ }
+
+ /**
+ * Retrieve all views currently registered with the registry.
+ * @returns {InspectorViewDescription[]} A by `order` sorted list of all registered
+ * inspector views.
+ */
+ public getAll(): InspectorViewDescription[] {
+ return this.views;
+ }
+
+ /**
+ * Retrieve all registered views, that want to be visible for the specified adapters.
+ * @param {object} adapters - an adapter configuration
+ * @returns {InspectorViewDescription[]} All inespector view descriptions visible
+ * for the specific adapters.
+ */
+ public getVisible(adapters?: Adapters): InspectorViewDescription[] {
+ if (!adapters) {
+ return [];
+ }
+ return this.views.filter(view => !view.shouldShow || view.shouldShow(adapters));
+ }
+}
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js
index ea5d894b21d46..bd58184cd1185 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js
@@ -23,8 +23,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
-import { RequestAdapter } from 'ui/inspector/adapters/request';
-import { DataAdapter } from 'ui/inspector/adapters/data';
+import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters';
import { runPipeline } from 'ui/visualize/loader/pipeline_helpers';
import { visualizationLoader } from 'ui/visualize/loader/visualization_loader';
diff --git a/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js b/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js
index e1bdcbdf191b7..5be15e1edccb7 100644
--- a/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js
+++ b/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js
@@ -6,8 +6,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-
-import { InspectorView } from 'ui/inspector';
import { MapDetails } from './map_details';
import { i18n } from '@kbn/i18n';
@@ -38,14 +36,12 @@ class MapViewComponent extends Component {
render() {
return (
-
-
-
+
);
}
}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 77b852fd7c3f0..0096654281044 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -484,11 +484,11 @@
"common.ui.indexPattern.unknownFieldHeader": "不明なフィールドタイプ {type}",
"common.ui.indexPattern.warningText": "現在 {index} に一致するすべてのインデックスにクエリを実行しています。{title} はワイルドカードベースのインデックスパターンに移行されるはずです。",
"common.ui.indexPattern.warningTitle": "時間間隔インデックスパターンのサポートは廃止されました",
- "common.ui.inspector.closeButton": "インスペクターを閉じる",
- "common.ui.inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です",
- "common.ui.inspector.reqTimestampKey": "リクエストのタイムスタンプ",
- "common.ui.inspector.title": "インスペクター",
- "common.ui.inspector.view": "{viewName} を表示",
+ "inspector.closeButton": "インスペクターを閉じる",
+ "inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です",
+ "inspector.reqTimestampKey": "リクエストのタイムスタンプ",
+ "inspector.title": "インスペクター",
+ "inspector.view": "{viewName} を表示",
"common.ui.legacyBrowserMessage": "この Kibana インストレーションは、現在ご使用のブラウザが満たしていない厳格なセキュリティ要件が有効になっています。",
"common.ui.legacyBrowserTitle": "ブラウザをアップグレードしてください",
"common.ui.management.breadcrumb": "管理",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 211376f4b7a22..0e04a11bbf833 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -484,11 +484,11 @@
"common.ui.indexPattern.unknownFieldHeader": "未知字段类型 {type}",
"common.ui.indexPattern.warningText": "当前正在查询所有匹配 {index} 的索引。{title} 应迁移到基于通配符的索引模式。",
"common.ui.indexPattern.warningTitle": "已移除对时间间隔索引模式的支持",
- "common.ui.inspector.closeButton": "关闭检查器",
- "common.ui.inspector.reqTimestampDescription": "记录请求启动的时间",
- "common.ui.inspector.reqTimestampKey": "请求时间戳",
- "common.ui.inspector.title": "检查器",
- "common.ui.inspector.view": "视图:{viewName}",
+ "inspector.closeButton": "关闭检查器",
+ "inspector.reqTimestampDescription": "记录请求启动的时间",
+ "inspector.reqTimestampKey": "请求时间戳",
+ "inspector.title": "检查器",
+ "inspector.view": "视图:{viewName}",
"common.ui.legacyBrowserMessage": "此 Kibana 安装启用了当前浏览器未满足的严格安全要求。",
"common.ui.legacyBrowserTitle": "请升级您的浏览器",
"common.ui.management.breadcrumb": "管理",
From ec83196b5556675b2777d7bdc33a8e190acfab58 Mon Sep 17 00:00:00 2001
From: Tyler Smalley
Date: Fri, 2 Aug 2019 12:33:12 -0700
Subject: [PATCH 003/118] Increase max-old-space-size for builds (#42218)
(#42539)
Signed-off-by: Tyler Smalley
---
src/dev/build/tasks/optimize_task.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/dev/build/tasks/optimize_task.js b/src/dev/build/tasks/optimize_task.js
index f6de0c717abfe..bbd8f9622535d 100644
--- a/src/dev/build/tasks/optimize_task.js
+++ b/src/dev/build/tasks/optimize_task.js
@@ -49,7 +49,7 @@ export const OptimizeBuildTask = {
env: {
FORCE_DLL_CREATION: 'true',
KBN_CACHE_LOADER_WRITABLE: 'true',
- NODE_OPTIONS: '--max-old-space-size=2048'
+ NODE_OPTIONS: '--max-old-space-size=3072'
},
});
From d32d59bd21e0f701120c883677ff0ed9b2c89a1d Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Fri, 2 Aug 2019 13:13:47 -0700
Subject: [PATCH 004/118] [7.x] [Infra UI] Add cloud metrics and cloud/host
info to metadata endpoint (#41836) (#42536)
* [Infra UI] Add cloud metrics and cloud/host info to metadata endpoint (#41836)
* [Infra UI] Add cloud metrics and cloud/host info to metadata endpoint
* Adding cloud metrics
* Correcting the pod host/cloud info
* Adding tests to metadata for new cloud/host info
* Fixing test to include machine.type
* Adding aws test data
* Refactor metadata container into hook
* Functionally complete
* updating tests
* Removing Metadata GraphQL endpoint and supporting files
* Moving types under common/http_api and prefixing with Infra
* adding filter for aws.ec2 dataset
* move away from fetch to useHTTPRequest
* Add decode function to useHTTPRequest; rename data to response;
* Changing from Typescript types to IO-TS types and adding checks at client and server
* Fixing field type
---
.../plugins/infra/common/graphql/types.ts | 59 -
.../plugins/infra/common/http_api/index.ts | 2 +
.../infra/common/http_api/metadata_api.ts | 100 +
.../plugins/infra/common/runtime_types.ts | 14 +
.../components/waffle/custom_field_panel.tsx | 2 +-
.../waffle/waffle_group_by_controls.tsx | 2 +-
.../metadata/lib/get_filtered_layouts.ts | 38 +
.../containers/metadata/metadata.gql_query.ts | 22 -
.../containers/metadata/use_metadata.ts | 48 +
.../containers/metadata/with_metadata.tsx | 90 -
.../infra/public/graphql/introspection.json | 161 +-
.../plugins/infra/public/graphql/types.ts | 59 -
.../infra/public/hooks/use_http_request.tsx | 69 +
.../infra/public/pages/metrics/index.tsx | 375 +-
.../public/pages/metrics/page_providers.tsx | 8 +-
.../plugins/infra/server/graphql/index.ts | 2 -
.../infra/server/graphql/metadata/index.ts | 8 -
.../server/graphql/metadata/resolvers.ts | 30 -
.../server/graphql/metadata/schema.gql.ts | 26 -
.../plugins/infra/server/graphql/types.ts | 79 -
.../plugins/infra/server/infra_server.ts | 4 +-
.../lib/adapters/metadata/adapter_types.ts | 23 -
.../elasticsearch_metadata_adapter.ts | 87 -
.../server/lib/adapters/metadata/index.ts | 7 -
.../infra/server/lib/compose/kibana.ts | 5 -
.../plugins/infra/server/lib/constants.ts | 2 +
.../lib/domains/metadata_domain/index.ts | 7 -
.../metadata_domain/metadata_domain.ts | 45 -
.../plugins/infra/server/lib/infra_types.ts | 2 -
.../infra/server/routes/metadata/index.ts | 75 +
.../metadata/lib/get_cloud_metric_metadata.ts | 62 +
.../routes/metadata/lib/get_id_field_name.ts | 18 +
.../metadata/lib/get_metric_metadata.ts | 84 +
.../routes/metadata/lib/get_node_info.ts | 76 +
.../routes/metadata/lib/get_pod_node_name.ts | 48 +
.../routes/metadata/lib/pick_feature_name.ts | 16 +
.../api_integration/apis/infra/constants.ts | 4 +
.../api_integration/apis/infra/metadata.ts | 254 +-
.../logs_and_metrics_with_aws/data.json.gz | Bin 0 -> 9529707 bytes
.../logs_and_metrics_with_aws/mappings.json | 17024 ++++++++++++++++
40 files changed, 18102 insertions(+), 935 deletions(-)
create mode 100644 x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts
create mode 100644 x-pack/legacy/plugins/infra/common/runtime_types.ts
create mode 100644 x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.ts
delete mode 100644 x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts
create mode 100644 x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts
delete mode 100644 x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx
create mode 100644 x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx
delete mode 100644 x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts
delete mode 100644 x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/index.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts
create mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.ts
create mode 100644 x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics_with_aws/data.json.gz
create mode 100644 x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics_with_aws/mappings.json
diff --git a/x-pack/legacy/plugins/infra/common/graphql/types.ts b/x-pack/legacy/plugins/infra/common/graphql/types.ts
index 2da829dbf2936..7843a54b93bed 100644
--- a/x-pack/legacy/plugins/infra/common/graphql/types.ts
+++ b/x-pack/legacy/plugins/infra/common/graphql/types.ts
@@ -28,8 +28,6 @@ export interface InfraSource {
configuration: InfraSourceConfiguration;
/** The status of the source */
status: InfraSourceStatus;
- /** A hierarchy of metadata entries by node */
- metadataByNode: InfraNodeMetadata;
/** A consecutive span of log entries surrounding a point in time */
logEntriesAround: InfraLogEntryInterval;
/** A consecutive span of log entries within an interval */
@@ -132,20 +130,6 @@ export interface InfraIndexField {
/** Whether the field's values can be aggregated */
aggregatable: boolean;
}
-/** One metadata entry for a node. */
-export interface InfraNodeMetadata {
- id: string;
-
- name: string;
-
- features: InfraNodeFeature[];
-}
-
-export interface InfraNodeFeature {
- name: string;
-
- source: string;
-}
/** A consecutive sequence of log entries */
export interface InfraLogEntryInterval {
/** The key corresponding to the start of the interval covered by the entries */
@@ -424,11 +408,6 @@ export interface SourceQueryArgs {
/** The id of the source */
id: string;
}
-export interface MetadataByNodeInfraSourceArgs {
- nodeId: string;
-
- nodeType: InfraNodeType;
-}
export interface LogEntriesAroundInfraSourceArgs {
/** The sort key that corresponds to the point in time */
key: InfraTimeKeyInput;
@@ -722,44 +701,6 @@ export namespace LogSummary {
};
}
-export namespace MetadataQuery {
- export type Variables = {
- sourceId: string;
- nodeId: string;
- nodeType: InfraNodeType;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- id: string;
-
- metadataByNode: MetadataByNode;
- };
-
- export type MetadataByNode = {
- __typename?: 'InfraNodeMetadata';
-
- name: string;
-
- features: Features[];
- };
-
- export type Features = {
- __typename?: 'InfraNodeFeature';
-
- name: string;
-
- source: string;
- };
-}
-
export namespace MetricsQuery {
export type Variables = {
sourceId: string;
diff --git a/x-pack/legacy/plugins/infra/common/http_api/index.ts b/x-pack/legacy/plugins/infra/common/http_api/index.ts
index 90afdcb43ffb1..19a3dcea3bd0d 100644
--- a/x-pack/legacy/plugins/infra/common/http_api/index.ts
+++ b/x-pack/legacy/plugins/infra/common/http_api/index.ts
@@ -6,3 +6,5 @@
export * from './search_results_api';
export * from './search_summary_api';
+export * from './metadata_api';
+export * from './timed_api';
diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts
new file mode 100644
index 0000000000000..796960651122f
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 * as rt from 'io-ts';
+import { InfraWrappableRequest } from '../../server/lib/adapters/framework';
+
+export const InfraMetadataNodeTypeRT = rt.keyof({
+ host: null,
+ pod: null,
+ container: null,
+});
+
+export const InfraMetadataRequestRT = rt.type({
+ nodeId: rt.string,
+ nodeType: InfraMetadataNodeTypeRT,
+ sourceId: rt.string,
+});
+
+export const InfraMetadataFeatureRT = rt.type({
+ name: rt.string,
+ source: rt.string,
+});
+
+export const InfraMetadataOSRT = rt.partial({
+ codename: rt.string,
+ family: rt.string,
+ kernel: rt.string,
+ name: rt.string,
+ platform: rt.string,
+ version: rt.string,
+});
+
+export const InfraMetadataHostRT = rt.partial({
+ name: rt.string,
+ os: InfraMetadataOSRT,
+ architecture: rt.string,
+ containerized: rt.boolean,
+});
+
+export const InfraMetadataInstanceRT = rt.partial({
+ id: rt.string,
+ name: rt.string,
+});
+
+export const InfraMetadataProjectRT = rt.partial({
+ id: rt.string,
+});
+
+export const InfraMetadataMachineRT = rt.partial({
+ interface: rt.string,
+});
+
+export const InfraMetadataCloudRT = rt.partial({
+ instance: InfraMetadataInstanceRT,
+ provider: rt.string,
+ availability_zone: rt.string,
+ project: InfraMetadataProjectRT,
+ machine: InfraMetadataMachineRT,
+});
+
+export const InfraMetadataInfoRT = rt.partial({
+ cloud: InfraMetadataCloudRT,
+ host: InfraMetadataHostRT,
+});
+
+const InfraMetadataRequiredRT = rt.type({
+ name: rt.string,
+ features: rt.array(InfraMetadataFeatureRT),
+});
+
+const InfraMetadataOptionalRT = rt.partial({
+ info: InfraMetadataInfoRT,
+});
+
+export const InfraMetadataRT = rt.intersection([InfraMetadataRequiredRT, InfraMetadataOptionalRT]);
+
+export type InfraMetadata = rt.TypeOf;
+
+export type InfraMetadataRequest = rt.TypeOf;
+
+export type InfraMetadataWrappedRequest = InfraWrappableRequest;
+
+export type InfraMetadataFeature = rt.TypeOf;
+
+export type InfraMetadataInfo = rt.TypeOf;
+
+export type InfraMetadataCloud = rt.TypeOf;
+
+export type InfraMetadataInstance = rt.TypeOf;
+
+export type InfraMetadataProject = rt.TypeOf;
+
+export type InfraMetadataMachine = rt.TypeOf;
+
+export type InfraMetadataHost = rt.TypeOf;
+
+export type InfraMEtadataOS = rt.TypeOf;
diff --git a/x-pack/legacy/plugins/infra/common/runtime_types.ts b/x-pack/legacy/plugins/infra/common/runtime_types.ts
new file mode 100644
index 0000000000000..297743f9b3456
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/common/runtime_types.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { Errors } from 'io-ts';
+import { failure } from 'io-ts/lib/PathReporter';
+
+export const createPlainError = (message: string) => new Error(message);
+
+export const throwErrors = (createError: (message: string) => Error) => (errors: Errors) => {
+ throw createError(failure(errors).join('\n'));
+};
diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx
index 58444ab5e7d29..6ba6d3f6bda4e 100644
--- a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx
@@ -7,7 +7,7 @@
import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
-import { InfraIndexField } from '../../../server/graphql/types';
+import { InfraIndexField } from '../../graphql/types';
interface Props {
onSubmit: (field: string) => void;
fields: InfraIndexField[];
diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx
index 24eacf840fd69..271035bc6ab34 100644
--- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx
@@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
-import { InfraIndexField, InfraNodeType, InfraSnapshotGroupbyInput } from '../../graphql/types';
+import { InfraNodeType, InfraSnapshotGroupbyInput, InfraIndexField } from '../../graphql/types';
import { InfraGroupByOptions } from '../../lib/lib';
import { CustomFieldPanel } from './custom_field_panel';
import { fieldToName } from './lib/field_to_display_name';
diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.ts b/x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.ts
new file mode 100644
index 0000000000000..a543365aa68d5
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { InfraMetadataFeature } from '../../../../common/http_api/metadata_api';
+import { InfraMetricLayout } from '../../../pages/metrics/layouts/types';
+
+export const getFilteredLayouts = (
+ layouts: InfraMetricLayout[],
+ metadata: Array | undefined
+): InfraMetricLayout[] => {
+ if (!metadata) {
+ return layouts;
+ }
+
+ const metricMetadata: Array = metadata
+ .filter(data => data && data.source === 'metrics')
+ .map(data => data && data.name);
+
+ // After filtering out sections that can't be displayed, a layout may end up empty and can be removed.
+ const filteredLayouts = layouts
+ .map(layout => getFilteredLayout(layout, metricMetadata))
+ .filter(layout => layout.sections.length > 0);
+ return filteredLayouts;
+};
+
+export const getFilteredLayout = (
+ layout: InfraMetricLayout,
+ metricMetadata: Array
+): InfraMetricLayout => {
+ // A section is only displayed if at least one of its requirements is met
+ // All others are filtered out.
+ const filteredSections = layout.sections.filter(
+ section => _.intersection(section.requires, metricMetadata).length > 0
+ );
+ return { ...layout, sections: filteredSections };
+};
diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts b/x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts
deleted file mode 100644
index 9a59cfcbee9ec..0000000000000
--- a/x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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 gql from 'graphql-tag';
-
-export const metadataQuery = gql`
- query MetadataQuery($sourceId: ID!, $nodeId: String!, $nodeType: InfraNodeType!) {
- source(id: $sourceId) {
- id
- metadataByNode(nodeId: $nodeId, nodeType: $nodeType) {
- name
- features {
- name
- source
- }
- }
- }
- }
-`;
diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts b/x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts
new file mode 100644
index 0000000000000..941c7532d5b29
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { useEffect } from 'react';
+import { InfraNodeType } from '../../graphql/types';
+import { InfraMetricLayout } from '../../pages/metrics/layouts/types';
+import { InfraMetadata, InfraMetadataRT } from '../../../common/http_api/metadata_api';
+import { getFilteredLayouts } from './lib/get_filtered_layouts';
+import { useHTTPRequest } from '../../hooks/use_http_request';
+import { throwErrors, createPlainError } from '../../../common/runtime_types';
+
+export function useMetadata(
+ nodeId: string,
+ nodeType: InfraNodeType,
+ layouts: InfraMetricLayout[],
+ sourceId: string
+) {
+ const decodeResponse = (response: any) => {
+ return InfraMetadataRT.decode(response).getOrElseL(throwErrors(createPlainError));
+ };
+
+ const { error, loading, response, makeRequest } = useHTTPRequest(
+ '/api/infra/metadata',
+ 'POST',
+ JSON.stringify({
+ nodeId,
+ nodeType,
+ sourceId,
+ decodeResponse,
+ })
+ );
+
+ useEffect(() => {
+ (async () => {
+ await makeRequest();
+ })();
+ }, [makeRequest]);
+
+ return {
+ name: (response && response.name) || '',
+ filteredLayouts: (response && getFilteredLayouts(layouts, response.features)) || [],
+ error: (error && error.message) || null,
+ loading,
+ };
+}
diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx b/x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx
deleted file mode 100644
index 1950ecf436539..0000000000000
--- a/x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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 _ from 'lodash';
-import React from 'react';
-import { Query } from 'react-apollo';
-
-import { InfraNodeType, MetadataQuery } from '../../graphql/types';
-import { InfraMetricLayout } from '../../pages/metrics/layouts/types';
-import { metadataQuery } from './metadata.gql_query';
-
-interface WithMetadataProps {
- children: (args: WithMetadataArgs) => React.ReactNode;
- layouts: InfraMetricLayout[];
- nodeType: InfraNodeType;
- nodeId: string;
- sourceId: string;
-}
-
-interface WithMetadataArgs {
- name: string;
- filteredLayouts: InfraMetricLayout[];
- error?: string | undefined;
- loading: boolean;
-}
-
-export const WithMetadata = ({
- children,
- layouts,
- nodeType,
- nodeId,
- sourceId,
-}: WithMetadataProps) => {
- return (
-
- query={metadataQuery}
- fetchPolicy="no-cache"
- variables={{
- sourceId,
- nodeType,
- nodeId,
- }}
- >
- {({ data, error, loading }) => {
- const metadata = data && data.source && data.source.metadataByNode;
- const filteredLayouts = (metadata && getFilteredLayouts(layouts, metadata.features)) || [];
- return children({
- name: (metadata && metadata.name) || '',
- filteredLayouts,
- error: error && error.message,
- loading,
- });
- }}
-
- );
-};
-
-const getFilteredLayouts = (
- layouts: InfraMetricLayout[],
- metadata: Array | undefined
-): InfraMetricLayout[] => {
- if (!metadata) {
- return layouts;
- }
-
- const metricMetadata: Array = metadata
- .filter(data => data && data.source === 'metrics')
- .map(data => data && data.name);
-
- // After filtering out sections that can't be displayed, a layout may end up empty and can be removed.
- const filteredLayouts = layouts
- .map(layout => getFilteredLayout(layout, metricMetadata))
- .filter(layout => layout.sections.length > 0);
- return filteredLayouts;
-};
-
-const getFilteredLayout = (
- layout: InfraMetricLayout,
- metricMetadata: Array
-): InfraMetricLayout => {
- // A section is only displayed if at least one of its requirements is met
- // All others are filtered out.
- const filteredSections = layout.sections.filter(
- section => _.intersection(section.requires, metricMetadata).length > 0
- );
- return { ...layout, sections: filteredSections };
-};
diff --git a/x-pack/legacy/plugins/infra/public/graphql/introspection.json b/x-pack/legacy/plugins/infra/public/graphql/introspection.json
index c937d4f365e62..055fac61cb93d 100644
--- a/x-pack/legacy/plugins/infra/public/graphql/introspection.json
+++ b/x-pack/legacy/plugins/infra/public/graphql/introspection.json
@@ -137,39 +137,6 @@
"isDeprecated": false,
"deprecationReason": null
},
- {
- "name": "metadataByNode",
- "description": "A hierarchy of metadata entries by node",
- "args": [
- {
- "name": "nodeId",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "nodeType",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "InfraNodeType", "ofType": null }
- },
- "defaultValue": null
- }
- ],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "InfraNodeMetadata", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
{
"name": "logEntriesAround",
"description": "A consecutive span of log entries surrounding a point in time",
@@ -1086,115 +1053,6 @@
"enumValues": null,
"possibleTypes": null
},
- {
- "kind": "ENUM",
- "name": "InfraNodeType",
- "description": "",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- { "name": "pod", "description": "", "isDeprecated": false, "deprecationReason": null },
- {
- "name": "container",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null }
- ],
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "InfraNodeMetadata",
- "description": "One metadata entry for a node.",
- "fields": [
- {
- "name": "id",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "name",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "features",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "InfraNodeFeature", "ofType": null }
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "InfraNodeFeature",
- "description": "",
- "fields": [
- {
- "name": "name",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "source",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
{
"kind": "INPUT_OBJECT",
"name": "InfraTimeKeyInput",
@@ -2039,6 +1897,25 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "ENUM",
+ "name": "InfraNodeType",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ { "name": "pod", "description": "", "isDeprecated": false, "deprecationReason": null },
+ {
+ "name": "container",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null }
+ ],
+ "possibleTypes": null
+ },
{
"kind": "INPUT_OBJECT",
"name": "InfraSnapshotGroupbyInput",
diff --git a/x-pack/legacy/plugins/infra/public/graphql/types.ts b/x-pack/legacy/plugins/infra/public/graphql/types.ts
index 2da829dbf2936..7843a54b93bed 100644
--- a/x-pack/legacy/plugins/infra/public/graphql/types.ts
+++ b/x-pack/legacy/plugins/infra/public/graphql/types.ts
@@ -28,8 +28,6 @@ export interface InfraSource {
configuration: InfraSourceConfiguration;
/** The status of the source */
status: InfraSourceStatus;
- /** A hierarchy of metadata entries by node */
- metadataByNode: InfraNodeMetadata;
/** A consecutive span of log entries surrounding a point in time */
logEntriesAround: InfraLogEntryInterval;
/** A consecutive span of log entries within an interval */
@@ -132,20 +130,6 @@ export interface InfraIndexField {
/** Whether the field's values can be aggregated */
aggregatable: boolean;
}
-/** One metadata entry for a node. */
-export interface InfraNodeMetadata {
- id: string;
-
- name: string;
-
- features: InfraNodeFeature[];
-}
-
-export interface InfraNodeFeature {
- name: string;
-
- source: string;
-}
/** A consecutive sequence of log entries */
export interface InfraLogEntryInterval {
/** The key corresponding to the start of the interval covered by the entries */
@@ -424,11 +408,6 @@ export interface SourceQueryArgs {
/** The id of the source */
id: string;
}
-export interface MetadataByNodeInfraSourceArgs {
- nodeId: string;
-
- nodeType: InfraNodeType;
-}
export interface LogEntriesAroundInfraSourceArgs {
/** The sort key that corresponds to the point in time */
key: InfraTimeKeyInput;
@@ -722,44 +701,6 @@ export namespace LogSummary {
};
}
-export namespace MetadataQuery {
- export type Variables = {
- sourceId: string;
- nodeId: string;
- nodeType: InfraNodeType;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- id: string;
-
- metadataByNode: MetadataByNode;
- };
-
- export type MetadataByNode = {
- __typename?: 'InfraNodeMetadata';
-
- name: string;
-
- features: Features[];
- };
-
- export type Features = {
- __typename?: 'InfraNodeFeature';
-
- name: string;
-
- source: string;
- };
-}
-
export namespace MetricsQuery {
export type Variables = {
sourceId: string;
diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx
new file mode 100644
index 0000000000000..606f7d0aecdc0
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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, { useMemo, useState } from 'react';
+import { kfetch } from 'ui/kfetch';
+import { toastNotifications } from 'ui/notify';
+import { i18n } from '@kbn/i18n';
+import { idx } from '@kbn/elastic-idx/target';
+import { KFetchError } from 'ui/kfetch/kfetch_error';
+import { useTrackedPromise } from '../utils/use_tracked_promise';
+export function useHTTPRequest(
+ pathname: string,
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD',
+ body?: string,
+ decode: (response: any) => Response = response => response
+) {
+ const [response, setResponse] = useState(null);
+ const [error, setError] = useState(null);
+ const [request, makeRequest] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: () =>
+ kfetch({
+ method,
+ pathname,
+ body,
+ }),
+ onResolve: resp => setResponse(decode(resp)),
+ onReject: (e: unknown) => {
+ const err = e as KFetchError;
+ setError(err);
+ toastNotifications.addWarning({
+ title: i18n.translate('xpack.infra.useHTTPRequest.error.title', {
+ defaultMessage: `Error while fetching resource`,
+ }),
+ text: (
+
+
+ {i18n.translate('xpack.infra.useHTTPRequest.error.status', {
+ defaultMessage: `Error`,
+ })}
+
+ {idx(err.res, r => r.statusText)} ({idx(err.res, r => r.status)})
+
+ {i18n.translate('xpack.infra.useHTTPRequest.error.url', {
+ defaultMessage: `URL`,
+ })}
+
+ {idx(err.res, r => r.url)}
+
+ ),
+ });
+ },
+ },
+ [pathname, body, method]
+ );
+
+ const loading = useMemo(() => request.state === 'pending', [request.state]);
+
+ return {
+ response,
+ error,
+ loading,
+ makeRequest,
+ };
+}
diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx
index 1929b3351acb7..54cd1542bc7cc 100644
--- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx
@@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { GraphQLFormattedError } from 'graphql';
-import React from 'react';
+import React, { useCallback, useContext } from 'react';
import { UICapabilities } from 'ui/capabilities';
import { injectUICapabilities } from 'ui/capabilities/react';
import euiStyled, { EuiTheme, withTheme } from '../../../../../common/eui_styled_components';
@@ -29,18 +29,18 @@ import { MetricsSideNav } from '../../components/metrics/side_nav';
import { MetricsTimeControls } from '../../components/metrics/time_controls';
import { ColumnarPage, PageContent } from '../../components/page';
import { SourceConfigurationFlyout } from '../../components/source_configuration';
-import { WithMetadata } from '../../containers/metadata/with_metadata';
import { WithMetrics } from '../../containers/metrics/with_metrics';
import {
WithMetricsTime,
WithMetricsTimeUrlState,
} from '../../containers/metrics/with_metrics_time';
-import { WithSource } from '../../containers/with_source';
import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
import { Error, ErrorPageBody } from '../error';
import { layoutCreators } from './layouts';
import { InfraMetricLayoutSection } from './layouts/types';
-import { MetricDetailPageProviders } from './page_providers';
+import { withMetricPageProviders } from './page_providers';
+import { useMetadata } from '../../containers/metadata/use_metadata';
+import { Source } from '../../containers/source';
const DetailPageContent = euiStyled(PageContent)`
overflow: auto;
@@ -63,206 +63,189 @@ interface Props {
uiCapabilities: UICapabilities;
}
-export const MetricDetail = injectUICapabilities(
- withTheme(
- injectI18n(
- class extends React.PureComponent {
- public static displayName = 'MetricDetailPage';
+export const MetricDetail = withMetricPageProviders(
+ injectUICapabilities(
+ withTheme(
+ injectI18n(({ intl, uiCapabilities, match, theme }: Props) => {
+ const nodeId = match.params.node;
+ const nodeType = match.params.type as InfraNodeType;
+ const layoutCreator = layoutCreators[nodeType];
+ if (!layoutCreator) {
+ return (
+
+ );
+ }
+ const { sourceId } = useContext(Source.Context);
+ const layouts = layoutCreator(theme);
+ const { name, filteredLayouts, loading: metadataLoading } = useMetadata(
+ nodeId,
+ nodeType,
+ layouts,
+ sourceId
+ );
+ const breadcrumbs = [
+ {
+ href: '#/',
+ text: intl.formatMessage({
+ id: 'xpack.infra.header.infrastructureTitle',
+ defaultMessage: 'Infrastructure',
+ }),
+ },
+ { text: name },
+ ];
- public render() {
- const { intl, uiCapabilities } = this.props;
- const nodeId = this.props.match.params.node;
- const nodeType = this.props.match.params.type as InfraNodeType;
- const layoutCreator = layoutCreators[nodeType];
- if (!layoutCreator) {
- return (
-
- );
- }
- const layouts = layoutCreator(this.props.theme);
+ const handleClick = useCallback(
+ (section: InfraMetricLayoutSection) => () => {
+ const id = section.linkToId || section.id;
+ const el = document.getElementById(id);
+ if (el) {
+ el.scrollIntoView();
+ }
+ },
+ []
+ );
- return (
-
-
- {({ sourceId }) => (
-
- {({
- timeRange,
- setTimeRange,
- refreshInterval,
- setRefreshInterval,
- isAutoReloading,
- setAutoReload,
- }) => (
-
- {({ name, filteredLayouts, loading: metadataLoading }) => {
- const breadcrumbs = [
- {
- href: '#/',
- text: intl.formatMessage({
- id: 'xpack.infra.header.infrastructureTitle',
- defaultMessage: 'Infrastructure',
- }),
- },
- { text: name },
- ];
- return (
-
-
-
-
-
+ {({
+ timeRange,
+ setTimeRange,
+ refreshInterval,
+ setRefreshInterval,
+ isAutoReloading,
+ setAutoReload,
+ }) => (
+
+
+
+
+
+
+
+ {({ metrics, error, loading, refetch }) => {
+ if (error) {
+ const invalidNodeError = error.graphQLErrors.some(
+ (err: GraphQLFormattedError) =>
+ err.code === InfraMetricsErrorCodes.invalid_node
+ );
+
+ return (
+ <>
+
+ intl.formatMessage(
{
- id: 'xpack.infra.metricDetailPage.documentTitle',
- defaultMessage: 'Infrastructure | Metrics | {name}',
+ id: 'xpack.infra.metricDetailPage.documentTitleError',
+ defaultMessage: '{previousTitle} | Uh oh',
},
{
- name,
+ previousTitle,
}
- )}
- />
-
-
- {({ metrics, error, loading, refetch }) => {
- if (error) {
- const invalidNodeError = error.graphQLErrors.some(
- (err: GraphQLFormattedError) =>
- err.code === InfraMetricsErrorCodes.invalid_node
- );
-
- return (
- <>
-
- intl.formatMessage(
- {
- id:
- 'xpack.infra.metricDetailPage.documentTitleError',
- defaultMessage: '{previousTitle} | Uh oh',
- },
- {
- previousTitle,
- }
- )
- }
+ )
+ }
+ />
+ {invalidNodeError ? (
+
+ ) : (
+
+ )}
+ >
+ );
+ }
+ return (
+
+
+
+ {({ measureRef, bounds: { width = 0 } }) => {
+ return (
+
+
+
+
+
+
+
+ {name}
+
+
+
- {invalidNodeError ? (
-
- ) : (
-
- )}
- >
- );
- }
- return (
-
-
-
- {({ measureRef, bounds: { width = 0 } }) => {
- return (
-
-
-
-
-
-
-
- {name}
-
-
-
-
-
-
-
-
- 0 && isAutoReloading
- ? false
- : loading
- }
- refetch={refetch}
- onChangeRangeTime={setTimeRange}
- isLiveStreaming={isAutoReloading}
- stopLiveStreaming={() => setAutoReload(false)}
- />
-
-
-
- );
- }}
-
-
- );
- }}
-
-
-
- );
- }}
-
- )}
-
- )}
-
-
- );
- }
+
+
+
- private handleClick = (section: InfraMetricLayoutSection) => () => {
- const id = section.linkToId || section.id;
- const el = document.getElementById(id);
- if (el) {
- el.scrollIntoView();
- }
- };
- }
+
+ 0 && isAutoReloading ? false : loading
+ }
+ refetch={refetch}
+ onChangeRangeTime={setTimeRange}
+ isLiveStreaming={isAutoReloading}
+ stopLiveStreaming={() => setAutoReload(false)}
+ />
+
+
+
+ );
+ }}
+
+
+ );
+ }}
+
+
+
+ )}
+
+ );
+ })
)
)
);
diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx
index 49f07837024fc..6d45f52d541b9 100644
--- a/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx
@@ -10,10 +10,14 @@ import { SourceConfigurationFlyoutState } from '../../components/source_configur
import { MetricsTimeContainer } from '../../containers/metrics/with_metrics_time';
import { Source } from '../../containers/source';
-export const MetricDetailPageProviders: React.FunctionComponent = ({ children }) => (
+export const withMetricPageProviders = (Component: React.ComponentType) => (
+ props: T
+) => (
- {children}
+
+
+
);
diff --git a/x-pack/legacy/plugins/infra/server/graphql/index.ts b/x-pack/legacy/plugins/infra/server/graphql/index.ts
index 552076c05e8e8..81400b74f0539 100644
--- a/x-pack/legacy/plugins/infra/server/graphql/index.ts
+++ b/x-pack/legacy/plugins/infra/server/graphql/index.ts
@@ -7,7 +7,6 @@
import { rootSchema } from '../../common/graphql/root/schema.gql';
import { sharedSchema } from '../../common/graphql/shared/schema.gql';
import { logEntriesSchema } from './log_entries/schema.gql';
-import { metadataSchema } from './metadata/schema.gql';
import { metricsSchema } from './metrics/schema.gql';
import { snapshotSchema } from './snapshot/schema.gql';
import { sourceStatusSchema } from './source_status/schema.gql';
@@ -16,7 +15,6 @@ import { sourcesSchema } from './sources/schema.gql';
export const schemas = [
rootSchema,
sharedSchema,
- metadataSchema,
logEntriesSchema,
snapshotSchema,
sourcesSchema,
diff --git a/x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts b/x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts
deleted file mode 100644
index cda731bdaa9b6..0000000000000
--- a/x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * 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 { createMetadataResolvers } from './resolvers';
-export { metadataSchema } from './schema.gql';
diff --git a/x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts b/x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts
deleted file mode 100644
index 8d1b386af548e..0000000000000
--- a/x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 { InfraSourceResolvers } from '../../graphql/types';
-import { InfraMetadataDomain } from '../../lib/domains/metadata_domain';
-import { ChildResolverOf, InfraResolverOf } from '../../utils/typed_resolvers';
-import { QuerySourceResolver } from '../sources/resolvers';
-
-type InfraSourceMetadataByNodeResolver = ChildResolverOf<
- InfraResolverOf,
- QuerySourceResolver
->;
-
-export const createMetadataResolvers = (libs: {
- metadata: InfraMetadataDomain;
-}): {
- InfraSource: {
- metadataByNode: InfraSourceMetadataByNodeResolver;
- };
-} => ({
- InfraSource: {
- async metadataByNode(source, args, { req }) {
- const result = await libs.metadata.getMetadata(req, source.id, args.nodeId, args.nodeType);
- return result;
- },
- },
-});
diff --git a/x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.ts b/x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.ts
deleted file mode 100644
index 8944e309d463d..0000000000000
--- a/x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 gql from 'graphql-tag';
-
-export const metadataSchema = gql`
- "One metadata entry for a node."
- type InfraNodeMetadata {
- id: ID!
- name: String!
- features: [InfraNodeFeature!]!
- }
-
- type InfraNodeFeature {
- name: String!
- source: String!
- }
-
- extend type InfraSource {
- "A hierarchy of metadata entries by node"
- metadataByNode(nodeId: String!, nodeType: InfraNodeType!): InfraNodeMetadata!
- }
-`;
diff --git a/x-pack/legacy/plugins/infra/server/graphql/types.ts b/x-pack/legacy/plugins/infra/server/graphql/types.ts
index 619166b8b8596..e223ac7b334a2 100644
--- a/x-pack/legacy/plugins/infra/server/graphql/types.ts
+++ b/x-pack/legacy/plugins/infra/server/graphql/types.ts
@@ -56,8 +56,6 @@ export interface InfraSource {
configuration: InfraSourceConfiguration;
/** The status of the source */
status: InfraSourceStatus;
- /** A hierarchy of metadata entries by node */
- metadataByNode: InfraNodeMetadata;
/** A consecutive span of log entries surrounding a point in time */
logEntriesAround: InfraLogEntryInterval;
/** A consecutive span of log entries within an interval */
@@ -160,20 +158,6 @@ export interface InfraIndexField {
/** Whether the field's values can be aggregated */
aggregatable: boolean;
}
-/** One metadata entry for a node. */
-export interface InfraNodeMetadata {
- id: string;
-
- name: string;
-
- features: InfraNodeFeature[];
-}
-
-export interface InfraNodeFeature {
- name: string;
-
- source: string;
-}
/** A consecutive sequence of log entries */
export interface InfraLogEntryInterval {
/** The key corresponding to the start of the interval covered by the entries */
@@ -452,11 +436,6 @@ export interface SourceQueryArgs {
/** The id of the source */
id: string;
}
-export interface MetadataByNodeInfraSourceArgs {
- nodeId: string;
-
- nodeType: InfraNodeType;
-}
export interface LogEntriesAroundInfraSourceArgs {
/** The sort key that corresponds to the point in time */
key: InfraTimeKeyInput;
@@ -663,8 +642,6 @@ export namespace InfraSourceResolvers {
configuration?: ConfigurationResolver;
/** The status of the source */
status?: StatusResolver;
- /** A hierarchy of metadata entries by node */
- metadataByNode?: MetadataByNodeResolver;
/** A consecutive span of log entries surrounding a point in time */
logEntriesAround?: LogEntriesAroundResolver;
/** A consecutive span of log entries within an interval */
@@ -711,17 +688,6 @@ export namespace InfraSourceResolvers {
Parent = InfraSource,
Context = InfraContext
> = Resolver;
- export type MetadataByNodeResolver<
- R = InfraNodeMetadata,
- Parent = InfraSource,
- Context = InfraContext
- > = Resolver;
- export interface MetadataByNodeArgs {
- nodeId: string;
-
- nodeType: InfraNodeType;
- }
-
export type LogEntriesAroundResolver<
R = InfraLogEntryInterval,
Parent = InfraSource,
@@ -1106,51 +1072,6 @@ export namespace InfraIndexFieldResolvers {
Context = InfraContext
> = Resolver;
}
-/** One metadata entry for a node. */
-export namespace InfraNodeMetadataResolvers {
- export interface Resolvers {
- id?: IdResolver;
-
- name?: NameResolver;
-
- features?: FeaturesResolver;
- }
-
- export type IdResolver = Resolver<
- R,
- Parent,
- Context
- >;
- export type NameResolver<
- R = string,
- Parent = InfraNodeMetadata,
- Context = InfraContext
- > = Resolver;
- export type FeaturesResolver<
- R = InfraNodeFeature[],
- Parent = InfraNodeMetadata,
- Context = InfraContext
- > = Resolver;
-}
-
-export namespace InfraNodeFeatureResolvers {
- export interface Resolvers {
- name?: NameResolver;
-
- source?: SourceResolver;
- }
-
- export type NameResolver<
- R = string,
- Parent = InfraNodeFeature,
- Context = InfraContext
- > = Resolver;
- export type SourceResolver<
- R = string,
- Parent = InfraNodeFeature,
- Context = InfraContext
- > = Resolver;
-}
/** A consecutive sequence of log entries */
export namespace InfraLogEntryIntervalResolvers {
export interface Resolvers {
diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts
index 25303a1a29de2..02a1d14152a2a 100644
--- a/x-pack/legacy/plugins/infra/server/infra_server.ts
+++ b/x-pack/legacy/plugins/infra/server/infra_server.ts
@@ -8,7 +8,6 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools';
import { initIpToHostName } from './routes/ip_to_hostname';
import { schemas } from './graphql';
import { createLogEntriesResolvers } from './graphql/log_entries';
-import { createMetadataResolvers } from './graphql/metadata';
import { createMetricResolvers } from './graphql/metrics/resolvers';
import { createSnapshotResolvers } from './graphql/snapshot';
import { createSourceStatusResolvers } from './graphql/source_status';
@@ -16,11 +15,11 @@ import { createSourcesResolvers } from './graphql/sources';
import { InfraBackendLibs } from './lib/infra_types';
import { initLegacyLoggingRoutes } from './logging_legacy';
import { initMetricExplorerRoute } from './routes/metrics_explorer';
+import { initMetadataRoute } from './routes/metadata';
export const initInfraServer = (libs: InfraBackendLibs) => {
const schema = makeExecutableSchema({
resolvers: [
- createMetadataResolvers(libs) as IResolvers,
createLogEntriesResolvers(libs) as IResolvers,
createSnapshotResolvers(libs) as IResolvers,
createSourcesResolvers(libs) as IResolvers,
@@ -35,4 +34,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initLegacyLoggingRoutes(libs.framework);
initIpToHostName(libs);
initMetricExplorerRoute(libs);
+ initMetadataRoute(libs);
};
diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts
deleted file mode 100644
index 3e44120a6c5c4..0000000000000
--- a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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 { InfraSourceConfiguration } from '../../sources';
-import { InfraFrameworkRequest, InfraMetadataAggregationBucket } from '../framework';
-
-export interface InfraMetricsAdapterResponse {
- id: string;
- name?: string;
- buckets: InfraMetadataAggregationBucket[];
-}
-
-export interface InfraMetadataAdapter {
- getMetricMetadata(
- req: InfraFrameworkRequest,
- sourceConfiguration: InfraSourceConfiguration,
- nodeId: string,
- nodeType: string
- ): Promise;
-}
diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts
deleted file mode 100644
index d23b9b9bb142e..0000000000000
--- a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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 { get } from 'lodash';
-import { InfraSourceConfiguration } from '../../sources';
-import {
- InfraBackendFrameworkAdapter,
- InfraFrameworkRequest,
- InfraMetadataAggregationResponse,
-} from '../framework';
-import { InfraMetadataAdapter, InfraMetricsAdapterResponse } from './adapter_types';
-import { NAME_FIELDS } from '../../constants';
-
-export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
- private framework: InfraBackendFrameworkAdapter;
- constructor(framework: InfraBackendFrameworkAdapter) {
- this.framework = framework;
- }
-
- public async getMetricMetadata(
- req: InfraFrameworkRequest,
- sourceConfiguration: InfraSourceConfiguration,
- nodeId: string,
- nodeType: 'host' | 'container' | 'pod'
- ): Promise {
- const idFieldName = getIdFieldName(sourceConfiguration, nodeType);
- const metricQuery = {
- allowNoIndices: true,
- ignoreUnavailable: true,
- index: sourceConfiguration.metricAlias,
- body: {
- query: {
- bool: {
- filter: {
- term: { [idFieldName]: nodeId },
- },
- },
- },
- size: 0,
- aggs: {
- nodeName: {
- terms: {
- field: NAME_FIELDS[nodeType],
- size: 1,
- },
- },
- metrics: {
- terms: {
- field: 'event.dataset',
- size: 1000,
- },
- },
- },
- },
- };
-
- const response = await this.framework.callWithRequest<
- any,
- { metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse }
- >(req, 'search', metricQuery);
-
- const buckets =
- response.aggregations && response.aggregations.metrics
- ? response.aggregations.metrics.buckets
- : [];
-
- return {
- id: nodeId,
- name: get(response, ['aggregations', 'nodeName', 'buckets', 0, 'key'], nodeId),
- buckets,
- };
- }
-}
-
-const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => {
- switch (nodeType) {
- case 'host':
- return sourceConfiguration.fields.host;
- case 'container':
- return sourceConfiguration.fields.container;
- default:
- return sourceConfiguration.fields.pod;
- }
-};
diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts
deleted file mode 100644
index 4e09b5d0e9e2d..0000000000000
--- a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * 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 './adapter_types';
diff --git a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts
index f1691c9f3c7dd..91f049a08ec96 100644
--- a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts
+++ b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts
@@ -10,12 +10,10 @@ import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kiban
import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter';
import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter';
-import { ElasticsearchMetadataAdapter } from '../adapters/metadata/elasticsearch_metadata_adapter';
import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter';
import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status';
import { InfraFieldsDomain } from '../domains/fields_domain';
import { InfraLogEntriesDomain } from '../domains/log_entries_domain';
-import { InfraMetadataDomain } from '../domains/metadata_domain';
import { InfraMetricsDomain } from '../domains/metrics_domain';
import { InfraBackendLibs, InfraDomainLibs } from '../infra_types';
import { InfraSnapshot } from '../snapshot';
@@ -35,9 +33,6 @@ export function compose(server: Server): InfraBackendLibs {
const snapshot = new InfraSnapshot({ sources, framework });
const domainLibs: InfraDomainLibs = {
- metadata: new InfraMetadataDomain(new ElasticsearchMetadataAdapter(framework), {
- sources,
- }),
fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), {
sources,
}),
diff --git a/x-pack/legacy/plugins/infra/server/lib/constants.ts b/x-pack/legacy/plugins/infra/server/lib/constants.ts
index c964e691058cd..4f2fa561da0c5 100644
--- a/x-pack/legacy/plugins/infra/server/lib/constants.ts
+++ b/x-pack/legacy/plugins/infra/server/lib/constants.ts
@@ -20,3 +20,5 @@ export const IP_FIELDS = {
[InfraNodeType.pod]: 'kubernetes.pod.ip',
[InfraNodeType.container]: 'container.ip_address',
};
+
+export const CLOUD_METRICS_MODULES = ['aws'];
diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts
deleted file mode 100644
index 8095e8424873a..0000000000000
--- a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * 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 './metadata_domain';
diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts
deleted file mode 100644
index 5fb86df5a633a..0000000000000
--- a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 { InfraFrameworkRequest, InfraMetadataAggregationBucket } from '../../adapters/framework';
-import { InfraMetadataAdapter } from '../../adapters/metadata';
-import { InfraSources } from '../../sources';
-
-export class InfraMetadataDomain {
- constructor(
- private readonly adapter: InfraMetadataAdapter,
- private readonly libs: { sources: InfraSources }
- ) {}
-
- public async getMetadata(
- req: InfraFrameworkRequest,
- sourceId: string,
- nodeId: string,
- nodeType: string
- ) {
- const { configuration } = await this.libs.sources.getSourceConfiguration(req, sourceId);
- const metricsPromise = this.adapter.getMetricMetadata(req, configuration, nodeId, nodeType);
-
- const metrics = await metricsPromise;
-
- const metricMetadata = pickMetadata(metrics.buckets).map(entry => {
- return { name: entry, source: 'metrics' };
- });
-
- const id = metrics.id;
- const name = metrics.name || id;
- return { id, name, features: metricMetadata };
- }
-}
-
-const pickMetadata = (buckets: InfraMetadataAggregationBucket[]): string[] => {
- if (buckets) {
- const metadata = buckets.map(bucket => bucket.key);
- return metadata;
- } else {
- return [];
- }
-};
diff --git a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts
index c63a82c137e13..394d893ede4f4 100644
--- a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts
+++ b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts
@@ -9,14 +9,12 @@ import { InfraConfigurationAdapter } from './adapters/configuration';
import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework';
import { InfraFieldsDomain } from './domains/fields_domain';
import { InfraLogEntriesDomain } from './domains/log_entries_domain';
-import { InfraMetadataDomain } from './domains/metadata_domain';
import { InfraMetricsDomain } from './domains/metrics_domain';
import { InfraSnapshot } from './snapshot';
import { InfraSourceStatus } from './source_status';
import { InfraSources } from './sources';
export interface InfraDomainLibs {
- metadata: InfraMetadataDomain;
fields: InfraFieldsDomain;
logEntries: InfraLogEntriesDomain;
metrics: InfraMetricsDomain;
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts
new file mode 100644
index 0000000000000..34b3448b86074
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 Boom, { boomify } from 'boom';
+import { get } from 'lodash';
+import {
+ InfraMetadata,
+ InfraMetadataWrappedRequest,
+ InfraMetadataFeature,
+ InfraMetadataRequestRT,
+ InfraMetadataRT,
+} from '../../../common/http_api/metadata_api';
+import { InfraBackendLibs } from '../../lib/infra_types';
+import { getMetricMetadata } from './lib/get_metric_metadata';
+import { pickFeatureName } from './lib/pick_feature_name';
+import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata';
+import { getNodeInfo } from './lib/get_node_info';
+import { throwErrors } from '../../../common/runtime_types';
+
+export const initMetadataRoute = (libs: InfraBackendLibs) => {
+ const { framework } = libs;
+
+ framework.registerRoute>({
+ method: 'POST',
+ path: '/api/infra/metadata',
+ handler: async req => {
+ try {
+ const { nodeId, nodeType, sourceId } = InfraMetadataRequestRT.decode(
+ req.payload
+ ).getOrElseL(throwErrors(Boom.badRequest));
+
+ const { configuration } = await libs.sources.getSourceConfiguration(req, sourceId);
+ const metricsMetadata = await getMetricMetadata(
+ framework,
+ req,
+ configuration,
+ nodeId,
+ nodeType
+ );
+ const metricFeatures = pickFeatureName(metricsMetadata.buckets).map(
+ nameToFeature('metrics')
+ );
+
+ const info = await getNodeInfo(framework, req, configuration, nodeId, nodeType);
+ const cloudInstanceId = get(info, 'cloud.instance.id');
+
+ const cloudMetricsMetadata = cloudInstanceId
+ ? await getCloudMetricsMetadata(framework, req, configuration, cloudInstanceId)
+ : { buckets: [] };
+ const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map(
+ nameToFeature('metrics')
+ );
+
+ const id = metricsMetadata.id;
+ const name = metricsMetadata.name || id;
+ return InfraMetadataRT.decode({
+ id,
+ name,
+ features: [...metricFeatures, ...cloudMetricsFeatures],
+ info,
+ }).getOrElseL(throwErrors(Boom.badImplementation));
+ } catch (error) {
+ throw boomify(error);
+ }
+ },
+ });
+};
+
+const nameToFeature = (source: string) => (name: string): InfraMetadataFeature => ({
+ name,
+ source,
+});
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts
new file mode 100644
index 0000000000000..58b3beab42886
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 {
+ InfraBackendFrameworkAdapter,
+ InfraFrameworkRequest,
+ InfraMetadataAggregationBucket,
+ InfraMetadataAggregationResponse,
+} from '../../../lib/adapters/framework';
+import { InfraSourceConfiguration } from '../../../lib/sources';
+import { CLOUD_METRICS_MODULES } from '../../../lib/constants';
+
+export interface InfraCloudMetricsAdapterResponse {
+ buckets: InfraMetadataAggregationBucket[];
+}
+
+export const getCloudMetricsMetadata = async (
+ framework: InfraBackendFrameworkAdapter,
+ req: InfraFrameworkRequest,
+ sourceConfiguration: InfraSourceConfiguration,
+ instanceId: string
+): Promise => {
+ const metricQuery = {
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ index: sourceConfiguration.metricAlias,
+ body: {
+ query: {
+ bool: {
+ filter: [{ match: { 'cloud.instance.id': instanceId } }],
+ should: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })),
+ },
+ },
+ size: 0,
+ aggs: {
+ metrics: {
+ terms: {
+ field: 'event.dataset',
+ size: 1000,
+ },
+ },
+ },
+ },
+ };
+
+ const response = await framework.callWithRequest<
+ {},
+ {
+ metrics?: InfraMetadataAggregationResponse;
+ }
+ >(req, 'search', metricQuery);
+
+ const buckets =
+ response.aggregations && response.aggregations.metrics
+ ? response.aggregations.metrics.buckets
+ : [];
+
+ return { buckets };
+};
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts
new file mode 100644
index 0000000000000..5f6bdd30fa2b8
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { InfraSourceConfiguration } from '../../../lib/sources';
+
+export const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => {
+ switch (nodeType) {
+ case 'host':
+ return sourceConfiguration.fields.host;
+ case 'container':
+ return sourceConfiguration.fields.container;
+ default:
+ return sourceConfiguration.fields.pod;
+ }
+};
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts
new file mode 100644
index 0000000000000..812bc27fffc8a
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 { get } from 'lodash';
+import {
+ InfraFrameworkRequest,
+ InfraMetadataAggregationBucket,
+ InfraBackendFrameworkAdapter,
+ InfraMetadataAggregationResponse,
+} from '../../../lib/adapters/framework';
+import { InfraSourceConfiguration } from '../../../lib/sources';
+import { getIdFieldName } from './get_id_field_name';
+import { NAME_FIELDS } from '../../../lib/constants';
+
+export interface InfraMetricsAdapterResponse {
+ id: string;
+ name?: string;
+ buckets: InfraMetadataAggregationBucket[];
+}
+
+export const getMetricMetadata = async (
+ framework: InfraBackendFrameworkAdapter,
+ req: InfraFrameworkRequest,
+ sourceConfiguration: InfraSourceConfiguration,
+ nodeId: string,
+ nodeType: 'host' | 'pod' | 'container'
+): Promise => {
+ const idFieldName = getIdFieldName(sourceConfiguration, nodeType);
+
+ const metricQuery = {
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ index: sourceConfiguration.metricAlias,
+ body: {
+ query: {
+ bool: {
+ must_not: [{ match: { 'event.dataset': 'aws.ec2' } }],
+ filter: [
+ {
+ match: { [idFieldName]: nodeId },
+ },
+ ],
+ },
+ },
+ size: 0,
+ aggs: {
+ nodeName: {
+ terms: {
+ field: NAME_FIELDS[nodeType],
+ size: 1,
+ },
+ },
+ metrics: {
+ terms: {
+ field: 'event.dataset',
+ size: 1000,
+ },
+ },
+ },
+ },
+ };
+
+ const response = await framework.callWithRequest<
+ {},
+ {
+ metrics?: InfraMetadataAggregationResponse;
+ nodeName?: InfraMetadataAggregationResponse;
+ }
+ >(req, 'search', metricQuery);
+
+ const buckets =
+ response.aggregations && response.aggregations.metrics
+ ? response.aggregations.metrics.buckets
+ : [];
+
+ return {
+ id: nodeId,
+ name: get(response, ['aggregations', 'nodeName', 'buckets', 0, 'key'], nodeId),
+ buckets,
+ };
+};
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts
new file mode 100644
index 0000000000000..5af25515a42ed
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { first } from 'lodash';
+import {
+ InfraFrameworkRequest,
+ InfraBackendFrameworkAdapter,
+} from '../../../lib/adapters/framework';
+import { InfraSourceConfiguration } from '../../../lib/sources';
+import { InfraNodeType } from '../../../graphql/types';
+import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api';
+import { getPodNodeName } from './get_pod_node_name';
+import { CLOUD_METRICS_MODULES } from '../../../lib/constants';
+import { getIdFieldName } from './get_id_field_name';
+
+export const getNodeInfo = async (
+ framework: InfraBackendFrameworkAdapter,
+ req: InfraFrameworkRequest,
+ sourceConfiguration: InfraSourceConfiguration,
+ nodeId: string,
+ nodeType: 'host' | 'pod' | 'container'
+): Promise => {
+ // If the nodeType is a Kubernetes pod then we need to get the node info
+ // from a host record instead of a pod. This is due to the fact that any host
+ // can report pod details and we can't rely on the host/cloud information associated
+ // with the kubernetes.pod.uid. We need to first lookup the `kubernetes.node.name`
+ // then use that to lookup the host's node information.
+ if (nodeType === InfraNodeType.pod) {
+ const kubernetesNodeName = await getPodNodeName(
+ framework,
+ req,
+ sourceConfiguration,
+ nodeId,
+ nodeType
+ );
+ if (kubernetesNodeName) {
+ return getNodeInfo(
+ framework,
+ req,
+ sourceConfiguration,
+ kubernetesNodeName,
+ InfraNodeType.host
+ );
+ }
+ return {};
+ }
+ const params = {
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ terminateAfter: 1,
+ index: sourceConfiguration.metricAlias,
+ body: {
+ size: 1,
+ _source: ['host.*', 'cloud.*'],
+ query: {
+ bool: {
+ must_not: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })),
+ filter: [{ match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }],
+ },
+ },
+ },
+ };
+ const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>(
+ req,
+ 'search',
+ params
+ );
+ const firstHit = first(response.hits.hits);
+ if (firstHit) {
+ return firstHit._source;
+ }
+ return {};
+};
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts
new file mode 100644
index 0000000000000..893707a4660ee
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { first, get } from 'lodash';
+import {
+ InfraFrameworkRequest,
+ InfraBackendFrameworkAdapter,
+} from '../../../lib/adapters/framework';
+import { InfraSourceConfiguration } from '../../../lib/sources';
+import { getIdFieldName } from './get_id_field_name';
+
+export const getPodNodeName = async (
+ framework: InfraBackendFrameworkAdapter,
+ req: InfraFrameworkRequest,
+ sourceConfiguration: InfraSourceConfiguration,
+ nodeId: string,
+ nodeType: 'host' | 'pod' | 'container'
+): Promise => {
+ const params = {
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ terminateAfter: 1,
+ index: sourceConfiguration.metricAlias,
+ body: {
+ size: 1,
+ _source: ['kubernetes.node.name'],
+ query: {
+ bool: {
+ filter: [
+ { match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } },
+ { exists: { field: `kubernetes.node.name` } },
+ ],
+ },
+ },
+ },
+ };
+ const response = await framework.callWithRequest<
+ { _source: { kubernetes: { node: { name: string } } } },
+ {}
+ >(req, 'search', params);
+ const firstHit = first(response.hits.hits);
+ if (firstHit) {
+ return get(firstHit, '_source.kubernetes.node.name');
+ }
+};
diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.ts
new file mode 100644
index 0000000000000..8b6bb49d9f645
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { InfraMetadataAggregationBucket } from '../../../lib/adapters/framework';
+
+export const pickFeatureName = (buckets: InfraMetadataAggregationBucket[]): string[] => {
+ if (buckets) {
+ const metadata = buckets.map(bucket => bucket.key);
+ return metadata;
+ } else {
+ return [];
+ }
+};
diff --git a/x-pack/test/api_integration/apis/infra/constants.ts b/x-pack/test/api_integration/apis/infra/constants.ts
index 6f47406398491..eb91cdb888eb2 100644
--- a/x-pack/test/api_integration/apis/infra/constants.ts
+++ b/x-pack/test/api_integration/apis/infra/constants.ts
@@ -22,5 +22,9 @@ export const DATES = {
min: 1562786660845,
max: 1562786716965,
},
+ logs_and_metrics_with_aws: {
+ min: 1564083185000,
+ max: 1564083493080,
+ },
},
};
diff --git a/x-pack/test/api_integration/apis/infra/metadata.ts b/x-pack/test/api_integration/apis/infra/metadata.ts
index 9e7802be6b066..8c06f2400b959 100644
--- a/x-pack/test/api_integration/apis/infra/metadata.ts
+++ b/x-pack/test/api_integration/apis/infra/metadata.ts
@@ -5,39 +5,42 @@
*/
import expect from '@kbn/expect';
-
-import { metadataQuery } from '../../../../legacy/plugins/infra/public/containers/metadata/metadata.gql_query';
-import { MetadataQuery } from '../../../../legacy/plugins/infra/public/graphql/types';
+import { InfraNodeType } from '../../../../legacy/plugins/infra/server/graphql/types';
+import {
+ InfraMetadata,
+ InfraMetadataRequest,
+} from '../../../../legacy/plugins/infra/common/http_api/metadata_api';
import { KbnTestProvider } from './types';
const metadataTests: KbnTestProvider = ({ getService }) => {
const esArchiver = getService('esArchiver');
- const client = getService('infraOpsGraphQLClient');
+ const supertest = getService('supertest');
+ const fetchMetadata = async (body: InfraMetadataRequest): Promise => {
+ const response = await supertest
+ .post('/api/infra/metadata')
+ .set('kbn-xsrf', 'xxx')
+ .send(body)
+ .expect(200);
+ return response.body;
+ };
describe('metadata', () => {
describe('7.0.0', () => {
before(() => esArchiver.load('infra/7.0.0/hosts'));
after(() => esArchiver.unload('infra/7.0.0/hosts'));
- it('hosts', () => {
- return client
- .query({
- query: metadataQuery,
- variables: {
- sourceId: 'default',
- nodeId: 'demo-stack-mysql-01',
- nodeType: 'host',
- },
- })
- .then(resp => {
- const metadata = resp.data.source.metadataByNode;
- if (metadata) {
- expect(metadata.features.length).to.be(12);
- expect(metadata.name).to.equal('demo-stack-mysql-01');
- } else {
- throw new Error('Metadata should never be empty');
- }
- });
+ it('hosts', async () => {
+ const metadata = await fetchMetadata({
+ sourceId: 'default',
+ nodeId: 'demo-stack-mysql-01',
+ nodeType: InfraNodeType.host,
+ });
+ if (metadata) {
+ expect(metadata.features.length).to.be(12);
+ expect(metadata.name).to.equal('demo-stack-mysql-01');
+ } else {
+ throw new Error('Metadata should never be empty');
+ }
});
});
@@ -45,25 +48,196 @@ const metadataTests: KbnTestProvider = ({ getService }) => {
before(() => esArchiver.load('infra/6.6.0/docker'));
after(() => esArchiver.unload('infra/6.6.0/docker'));
- it('docker', () => {
- return client
- .query({
- query: metadataQuery,
- variables: {
- sourceId: 'default',
- nodeId: '631f36a845514442b93c3fdd2dc91bcd8feb680b8ac5832c7fb8fdc167bb938e',
- nodeType: 'container',
+ it('docker', async () => {
+ const metadata = await fetchMetadata({
+ sourceId: 'default',
+ nodeId: '631f36a845514442b93c3fdd2dc91bcd8feb680b8ac5832c7fb8fdc167bb938e',
+ nodeType: InfraNodeType.container,
+ });
+ if (metadata) {
+ expect(metadata.features.length).to.be(10);
+ expect(metadata.name).to.equal('docker-autodiscovery_elasticsearch_1');
+ } else {
+ throw new Error('Metadata should never be empty');
+ }
+ });
+ });
+
+ describe('8.0.0', () => {
+ const archiveName = 'infra/8.0.0/logs_and_metrics_with_aws';
+ before(() => esArchiver.load(archiveName));
+ after(() => esArchiver.unload(archiveName));
+
+ it('host', async () => {
+ const metadata = await fetchMetadata({
+ sourceId: 'default',
+ nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc',
+ nodeType: InfraNodeType.host,
+ });
+ if (metadata) {
+ expect(metadata.features.length).to.be(58);
+ expect(metadata.name).to.equal('gke-observability-8--observability-8--bc1afd95-f0zc');
+ expect(metadata.info).to.eql({
+ cloud: {
+ availability_zone: 'europe-west1-c',
+ instance: {
+ name: 'gke-observability-8--observability-8--bc1afd95-f0zc',
+ id: '6200309808276807579',
+ },
+ provider: 'gcp',
+ machine: { type: 'n1-standard-4' },
+ project: { id: 'elastic-observability' },
+ },
+ host: {
+ hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc',
+ os: {
+ kernel: '4.14.127+',
+ codename: 'Core',
+ name: 'CentOS Linux',
+ family: 'redhat',
+ version: '7 (Core)',
+ platform: 'centos',
+ },
+ containerized: false,
+ name: 'gke-observability-8--observability-8--bc1afd95-f0zc',
+ architecture: 'x86_64',
+ },
+ });
+ } else {
+ throw new Error('Metadata should never be empty');
+ }
+ });
+
+ it('host with aws', async () => {
+ const metadata = await fetchMetadata({
+ sourceId: 'default',
+ nodeId: 'ip-172-31-47-9.us-east-2.compute.internal',
+ nodeType: InfraNodeType.host,
+ });
+ if (metadata) {
+ expect(metadata.features.length).to.be(19);
+ expect(metadata.features.some(f => f.name === 'aws.ec2')).to.be(true);
+ expect(metadata.name).to.equal('ip-172-31-47-9.us-east-2.compute.internal');
+ expect(metadata.info).to.eql({
+ cloud: {
+ availability_zone: 'us-east-2c',
+ image: { id: 'ami-0d8f6eb4f641ef691' },
+ instance: { id: 'i-011454f72559c510b' },
+ provider: 'aws',
+ machine: { type: 't2.micro' },
+ region: 'us-east-2',
+ account: { id: '015351775590' },
+ },
+ host: {
+ hostname: 'ip-172-31-47-9.us-east-2.compute.internal',
+ os: {
+ kernel: '4.14.123-111.109.amzn2.x86_64',
+ codename: 'Karoo',
+ name: 'Amazon Linux',
+ family: 'redhat',
+ version: '2',
+ platform: 'amzn',
+ },
+ containerized: false,
+ name: 'ip-172-31-47-9.us-east-2.compute.internal',
+ id: 'ded64cbff86f478990a3dfbb63a8d238',
+ architecture: 'x86_64',
+ },
+ });
+ } else {
+ throw new Error('Metadata should never be empty');
+ }
+ });
+
+ it('pod', async () => {
+ const metadata = await fetchMetadata({
+ sourceId: 'default',
+ nodeId: '14887487-99f8-11e9-9a96-42010a84004d',
+ nodeType: InfraNodeType.pod,
+ });
+ if (metadata) {
+ expect(metadata.features.length).to.be(29);
+ // With this data set the `kubernetes.pod.name` fields have been removed.
+ expect(metadata.name).to.equal('fluentd-gcp-v3.2.0-np7vw');
+ expect(metadata.info).to.eql({
+ cloud: {
+ instance: {
+ id: '6613144177892233360',
+ name: 'gke-observability-8--observability-8--bc1afd95-ngmh',
+ },
+ provider: 'gcp',
+ availability_zone: 'europe-west1-c',
+ machine: {
+ type: 'n1-standard-4',
+ },
+ project: {
+ id: 'elastic-observability',
+ },
+ },
+ host: {
+ hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh',
+ name: 'gke-observability-8--observability-8--bc1afd95-ngmh',
+ os: {
+ codename: 'Core',
+ family: 'redhat',
+ kernel: '4.14.127+',
+ name: 'CentOS Linux',
+ platform: 'centos',
+ version: '7 (Core)',
+ },
+ architecture: 'x86_64',
+ containerized: false,
+ },
+ });
+ } else {
+ throw new Error('Metadata should never be empty');
+ }
+ });
+
+ it('container', async () => {
+ const metadata = await fetchMetadata({
+ sourceId: 'default',
+ nodeId: 'c74b04834c6d7cc1800c3afbe31d0c8c0c267f06e9eb45c2b0c2df3e6cee40c5',
+ nodeType: InfraNodeType.container,
+ });
+ if (metadata) {
+ expect(metadata.features.length).to.be(26);
+ expect(metadata.name).to.equal(
+ 'k8s_prometheus-to-sd-exporter_fluentd-gcp-v3.2.0-w68r5_kube-system_26950cde-9aed-11e9-9a96-42010a84004d_0'
+ );
+ expect(metadata.info).to.eql({
+ cloud: {
+ instance: {
+ id: '4039094952262994102',
+ name: 'gke-observability-8--observability-8--bc1afd95-nhhw',
+ },
+ provider: 'gcp',
+ availability_zone: 'europe-west1-c',
+ machine: {
+ type: 'n1-standard-4',
+ },
+ project: {
+ id: 'elastic-observability',
+ },
+ },
+ host: {
+ hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw',
+ name: 'gke-observability-8--observability-8--bc1afd95-nhhw',
+ os: {
+ codename: 'Core',
+ family: 'redhat',
+ kernel: '4.14.127+',
+ name: 'CentOS Linux',
+ platform: 'centos',
+ version: '7 (Core)',
+ },
+ architecture: 'x86_64',
+ containerized: false,
},
- })
- .then(resp => {
- const metadata = resp.data.source.metadataByNode;
- if (metadata) {
- expect(metadata.features.length).to.be(10);
- expect(metadata.name).to.equal('docker-autodiscovery_elasticsearch_1');
- } else {
- throw new Error('Metadata should never be empty');
- }
});
+ } else {
+ throw new Error('Metadata should never be empty');
+ }
});
});
});
diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics_with_aws/data.json.gz b/x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics_with_aws/data.json.gz
new file mode 100644
index 0000000000000000000000000000000000000000..28f641523191f348a286aa8e2df088806c197ae4
GIT binary patch
literal 9529707
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