Inspector 👉 New Platform (#42164)

* 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 <gammon@elastic.co>

* [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 <gammon@elastic.co>

* Update src/legacy/ui/public/inspector/build_tabular_inspector_data.ts

Co-Authored-By: Stacey Gammon <gammon@elastic.co>

* feat: 🎸 improve types as per review

* fix: 🐛 remove <InspectorView> comp and fix view layouts

* test: 💍 improve mocks
This commit is contained in:
Vadim Dalecky 2019-08-02 17:00:35 +02:00 committed by GitHub
parent b94e94ea94
commit 39b233a24d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1144 additions and 906 deletions

View file

@ -24,7 +24,8 @@
"timelion": "src/legacy/core_plugins/timelion",
"tagCloud": "src/legacy/core_plugins/tagcloud",
"tsvb": "src/legacy/core_plugins/metrics",
"kbnESQuery": "packages/kbn-es-query"
"kbnESQuery": "packages/kbn-es-query",
"inspector": "src/plugins/inspector"
},
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
"translations": []

View file

@ -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: {

View file

@ -24,3 +24,5 @@ jest.doMock('ui/capabilities', () => ({
},
},
}));
jest.mock('ui/new_platform');

View file

@ -119,93 +119,77 @@ exports[`Inspector Data View component should render empty state 1`] = `
}
title="Test Data"
>
<InspectorView
useFlex={true}
<EuiEmptyPrompt
body={
<React.Fragment>
<p>
<FormattedMessage
defaultMessage="The element did not provide any data."
id="inspectorViews.data.noDataAvailableDescription"
values={Object {}}
/>
</p>
</React.Fragment>
}
iconColor="subdued"
title={
<h2>
<FormattedMessage
defaultMessage="No data available"
id="inspectorViews.data.noDataAvailableTitle"
values={Object {}}
/>
</h2>
}
>
<EuiFlyoutBody
className="kbnInspectorView--flex"
<div
className="euiEmptyPrompt"
>
<div
className="euiFlyoutBody kbnInspectorView--flex"
<EuiTextColor
color="subdued"
>
<div
className="euiFlyoutBody__overflow"
<span
className="euiTextColor euiTextColor--subdued"
>
<EuiEmptyPrompt
body={
<React.Fragment>
<p>
<FormattedMessage
defaultMessage="The element did not provide any data."
id="inspectorViews.data.noDataAvailableDescription"
values={Object {}}
/>
</p>
</React.Fragment>
}
iconColor="subdued"
title={
<h2>
<FormattedMessage
defaultMessage="No data available"
id="inspectorViews.data.noDataAvailableTitle"
values={Object {}}
/>
</h2>
}
<EuiTitle>
<h2
className="euiTitle euiTitle--medium"
>
<FormattedMessage
defaultMessage="No data available"
id="inspectorViews.data.noDataAvailableTitle"
values={Object {}}
>
No data available
</FormattedMessage>
</h2>
</EuiTitle>
<EuiSpacer
size="m"
>
<div
className="euiEmptyPrompt"
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<EuiText>
<div
className="euiText euiText--medium"
>
<EuiTextColor
color="subdued"
>
<span
className="euiTextColor euiTextColor--subdued"
<p>
<FormattedMessage
defaultMessage="The element did not provide any data."
id="inspectorViews.data.noDataAvailableDescription"
values={Object {}}
>
<EuiTitle>
<h2
className="euiTitle euiTitle--medium"
>
<FormattedMessage
defaultMessage="No data available"
id="inspectorViews.data.noDataAvailableTitle"
values={Object {}}
>
No data available
</FormattedMessage>
</h2>
</EuiTitle>
<EuiSpacer
size="m"
>
<div
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<EuiText>
<div
className="euiText euiText--medium"
>
<p>
<FormattedMessage
defaultMessage="The element did not provide any data."
id="inspectorViews.data.noDataAvailableDescription"
values={Object {}}
>
The element did not provide any data.
</FormattedMessage>
</p>
</div>
</EuiText>
</span>
</EuiTextColor>
The element did not provide any data.
</FormattedMessage>
</p>
</div>
</EuiEmptyPrompt>
</div>
</div>
</EuiFlyoutBody>
</InspectorView>
</EuiText>
</span>
</EuiTextColor>
</div>
</EuiEmptyPrompt>
</DataViewComponent>
`;
@ -328,91 +312,85 @@ exports[`Inspector Data View component should render loading state 1`] = `
}
title="Test Data"
>
<InspectorView
useFlex={true}
<EuiFlexGroup
alignItems="center"
justifyContent="center"
style={
Object {
"height": "100%",
}
}
>
<EuiFlyoutBody
className="kbnInspectorView--flex"
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
style={
Object {
"height": "100%",
}
}
>
<div
className="euiFlyoutBody kbnInspectorView--flex"
<EuiFlexItem
grow={false}
>
<div
className="euiFlyoutBody__overflow"
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiFlexGroup
alignItems="center"
justifyContent="center"
<EuiPanel
className="eui-textCenter"
grow={true}
hasShadow={false}
paddingSize="m"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
className="euiPanel euiPanel--paddingMedium eui-textCenter"
>
<EuiFlexItem
grow={false}
<EuiLoadingChart
size="m"
>
<span
className="euiLoadingChart euiLoadingChart--medium"
>
<span
className="euiLoadingChart__bar"
/>
<span
className="euiLoadingChart__bar"
/>
<span
className="euiLoadingChart__bar"
/>
<span
className="euiLoadingChart__bar"
/>
</span>
</EuiLoadingChart>
<EuiSpacer
size="s"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
className="euiSpacer euiSpacer--s"
/>
</EuiSpacer>
<EuiText>
<div
className="euiText euiText--medium"
>
<EuiPanel
className="eui-textCenter"
grow={true}
hasShadow={false}
paddingSize="m"
>
<div
className="euiPanel euiPanel--paddingMedium eui-textCenter"
<p>
<FormattedMessage
defaultMessage="Gathering data"
id="inspectorViews.data.gatheringDataLabel"
values={Object {}}
>
<EuiLoadingChart
size="m"
>
<span
className="euiLoadingChart euiLoadingChart--medium"
>
<span
className="euiLoadingChart__bar"
/>
<span
className="euiLoadingChart__bar"
/>
<span
className="euiLoadingChart__bar"
/>
<span
className="euiLoadingChart__bar"
/>
</span>
</EuiLoadingChart>
<EuiSpacer
size="s"
>
<div
className="euiSpacer euiSpacer--s"
/>
</EuiSpacer>
<EuiText>
<div
className="euiText euiText--medium"
>
<p>
<FormattedMessage
defaultMessage="Gathering data"
id="inspectorViews.data.gatheringDataLabel"
values={Object {}}
>
Gathering data
</FormattedMessage>
</p>
</div>
</EuiText>
</div>
</EuiPanel>
Gathering data
</FormattedMessage>
</p>
</div>
</EuiFlexItem>
</EuiText>
</div>
</EuiFlexGroup>
</EuiPanel>
</div>
</div>
</EuiFlyoutBody>
</InspectorView>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</DataViewComponent>
`;

View file

@ -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 (
<InspectorView useFlex={true}>
<EuiEmptyPrompt
title={
<h2>
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="inspectorViews.data.noDataAvailableTitle"
defaultMessage="No data available"
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="inspectorViews.data.noDataAvailableTitle"
defaultMessage="No data available"
id="inspectorViews.data.noDataAvailableDescription"
defaultMessage="The element did not provide any data."
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="inspectorViews.data.noDataAvailableDescription"
defaultMessage="The element did not provide any data."
/>
</p>
</React.Fragment>
}
/>
</InspectorView>
</p>
</React.Fragment>
}
/>
);
}
renderLoading() {
return (
<InspectorView useFlex={true}>
<EuiFlexGroup
justifyContent="center"
alignItems="center"
>
<EuiFlexItem grow={false}>
<EuiPanel className="eui-textCenter">
<EuiLoadingChart size="m" />
<EuiSpacer size="s" />
<EuiText>
<p>
<FormattedMessage
id="inspectorViews.data.gatheringDataLabel"
defaultMessage="Gathering data"
/>
</p>
</EuiText>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</InspectorView>
<EuiFlexGroup
justifyContent="center"
alignItems="center"
style={{ height: '100%' }}
>
<EuiFlexItem grow={false}>
<EuiPanel className="eui-textCenter">
<EuiLoadingChart size="m" />
<EuiSpacer size="s" />
<EuiText>
<p>
<FormattedMessage
id="inspectorViews.data.gatheringDataLabel"
defaultMessage="Gathering data"
/>
</p>
</EuiText>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}
@ -161,13 +156,11 @@ class DataViewComponent extends Component {
}
return (
<InspectorView>
<DataTableFormat
data={this.state.tabularData}
isFormatted={this.state.tabularOptions.returnsFormattedValues}
exportTitle={this.props.title}
/>
</InspectorView>
<DataTableFormat
data={this.state.tabularData}
isFormatted={this.state.tabularOptions.returnsFormattedValues}
exportTitle={this.props.title}
/>
);
}
}

View file

@ -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(),
}));

View file

@ -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 (
<InspectorView useFlex={true}>
<EuiEmptyPrompt
data-test-subj="inspectorNoRequestsMessage"
title={
<h2>
<EuiEmptyPrompt
data-test-subj="inspectorNoRequestsMessage"
title={
<h2>
<FormattedMessage
id="inspectorViews.requests.noRequestsLoggedTitle"
defaultMessage="No requests logged"
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="inspectorViews.requests.noRequestsLoggedTitle"
defaultMessage="No requests logged"
id="inspectorViews.requests.noRequestsLoggedDescription.elementHasNotLoggedAnyRequestsText"
defaultMessage="The element hasn't logged any requests (yet)."
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="inspectorViews.requests.noRequestsLoggedDescription.elementHasNotLoggedAnyRequestsText"
defaultMessage="The element hasn't logged any requests (yet)."
/>
</p>
<p>
<FormattedMessage
id="inspectorViews.requests.noRequestsLoggedDescription.whatDoesItUsuallyMeanText"
defaultMessage="This usually means that there was no need to fetch any data or
that the element has not yet started fetching data."
/>
</p>
</React.Fragment>
}
/>
</InspectorView>
</p>
<p>
<FormattedMessage
id="inspectorViews.requests.noRequestsLoggedDescription.whatDoesItUsuallyMeanText"
defaultMessage="This usually means that there was no need to fetch any data or
that the element has not yet started fetching data."
/>
</p>
</React.Fragment>
}
/>
);
}
@ -111,7 +108,7 @@ class RequestsViewComponent extends Component {
).length;
return (
<InspectorView>
<>
<EuiText size="xs">
<p role="status" aria-live="polite" aria-atomic="true">
<FormattedMessage
@ -152,7 +149,7 @@ class RequestsViewComponent extends Component {
request={this.state.request}
/>
}
</InspectorView>
</>
);
}
}

View file

@ -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 {

View file

@ -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: {

View file

@ -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;
}
});

View file

@ -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';

View file

@ -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 (
<InspectorView>
{ /* Always use InspectorView as the wrapping element! */ }
</InspectorView>
);
}
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.

View file

@ -1 +0,0 @@
@import './ui/index';

View file

@ -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';

View file

@ -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 };
}

View file

@ -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<Record<string, FormattedData>>((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 };
}

View file

@ -17,10 +17,6 @@
* under the License.
*/
export { InspectorView } from './ui';
export { Inspector } from './inspector';
export { viewRegistry } from './view_registry';
export { Adapters } from './types';

View file

@ -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();
});
});
});

View file

@ -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.
* Do not use this, use New Platform `inspector` plugin instead.
*/
function isAvailable(adapters?: Adapters): boolean {
return viewRegistry.getVisible(adapters).length > 0;
}
export const Inspector = {
/**
* @deprecated
*
* Do not use this, use New Platform `inspector` plugin instead.
*/
isAvailable: npStart.plugins.inspector.isAvailable,
/**
* 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.
*/
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(
<InspectorPanel views={views} adapters={adapters} title={options.title} />,
{
'data-test-subj': 'inspectorPanel',
closeButtonAriaLabel: closeButtonLabel,
}
);
}
const Inspector = {
isAvailable,
open,
/**
* @deprecated
*
* Do not use this, use New Platform `inspector` plugin instead.
*/
open: npStart.plugins.inspector.open,
};
export { Inspector };

View file

@ -18,46 +18,16 @@
*/
/**
* The interface that the adapters used to open an inspector have to fullfill.
* Do not import these types from here, instead import them from `inspector` plugin.
*
* ```ts
* import { InspectorViewDescription } from 'src/plugins/inspector/public';
* ```
*
* @deprecated
*/
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<InspectorViewProps>} 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.
*/
export interface InspectorViewDescription {
component: React.ComponentType<InspectorViewProps>;
help?: string;
order?: number;
shouldShow?: (adapters: Adapters) => boolean;
title: string;
}
export {
Adapters,
InspectorViewProps,
InspectorViewDescription,
} from '../../../../plugins/inspector/public';

View file

@ -1 +0,0 @@
@import './inspector';

View file

@ -1,3 +0,0 @@
.kbnInspectorView--flex {
display: flex;
}

View file

@ -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<InspectorPanelProps>;
/**
* Do not use this, use NP `inspector` plugin instead.
*
* @deprecated
*/
export * from '../../../../../plugins/inspector/public/ui/inspector_panel';

View file

@ -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 <EuiFlyoutBody className={classes}>{children}</EuiFlyoutBody>;
};
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';

View file

@ -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.
* Do not use this, instead use `inspector` plugin directly.
*
* ```ts
* import { npSetup } from 'ui/new_platform';
*
* npSetup.plugins.inspector.registerView(view);
* ```
*
* @deprecated
*/
/**
* 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.
*/
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;

View file

@ -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(),
}),
};

View file

@ -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),
}),
},
},
};

View file

@ -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<DataPlugin['setup']>;
inspector: InspectorSetup;
}
export interface PluginsStart {
data: ReturnType<DataPlugin['start']>;
inspector: InspectorStart;
}
export const npSetup = {

View file

@ -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', () => {

View file

@ -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.

View file

@ -0,0 +1,6 @@
{
"id": "inspector",
"version": "kibana",
"server": false,
"ui": true
}

View file

@ -17,4 +17,5 @@
* under the License.
*/
export { InspectorView } from './inspector_view';
export { DataAdapter, FormattedData } from './data';
export { RequestAdapter, RequestStatus } from './request';

View file

@ -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',
}),
};

View file

@ -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';

View file

@ -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<PluginSetup>;
export type Start = jest.Mocked<PluginStart>;
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<Start['open']>;
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,
};

View file

@ -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<Setup, Start> {
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(
<InspectorPanel views={views} adapters={adapters} title={options.title} />,
{
'data-test-subj': 'inspectorPanel',
closeButtonAriaLabel: closeButtonLabel,
}
);
};
return {
isAvailable,
open,
};
}
public stop() {}
}

View file

@ -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);
});
});
});

View file

@ -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();
});
});
});

View file

@ -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<InspectorViewProps>} 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<InspectorViewProps>;
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;

View file

@ -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`] = `
>
<FormattedMessage
defaultMessage="View: {viewName}"
id="common.ui.inspector.view"
id="inspector.view"
values={
Object {
"viewName": "View 1",
@ -298,7 +297,7 @@ exports[`InspectorPanel should render as expected 1`] = `
>
<FormattedMessage
defaultMessage="View: {viewName}"
id="common.ui.inspector.view"
id="inspector.view"
values={
Object {
"viewName": "View 1",
@ -322,20 +321,30 @@ exports[`InspectorPanel should render as expected 1`] = `
</EuiFlexGroup>
</div>
</EuiFlyoutHeader>
<component
adapters={
Object {
"bardapter": Object {},
"foodapter": Object {
"foo": [Function],
},
}
}
title="Inspector"
>
<h1>
View 1
</h1>
</component>
<EuiFlyoutBody>
<div
className="euiFlyoutBody"
>
<div
className="euiFlyoutBody__overflow"
>
<component
adapters={
Object {
"bardapter": Object {},
"foodapter": Object {
"foo": [Function],
},
}
}
title="Inspector"
>
<h1>
View 1
</h1>
</component>
</div>
</div>
</EuiFlyoutBody>
</InspectorPanel>
`;

View file

@ -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: () => (<h1>View 1</h1>),
}, {
component: () => <h1>View 1</h1>,
},
{
title: 'Foo View',
order: 100,
component: () => (<h1>Foo view</h1>),
shouldShow(adapters) {
return adapters.foodapter;
}
}, {
component: () => <h1>Foo view</h1>,
shouldShow(adapters2: Adapters) {
return adapters2.foodapter;
},
},
{
title: 'Never',
order: 200,
component: () => null,
shouldShow() {
return false;
}
}
},
},
];
});
it('should render as expected', () => {
const component = mountWithIntl(
<InspectorPanel
adapters={adapters}
onClose={() => true}
views={views}
/>
);
const component = mountWithIntl(<InspectorPanel adapters={adapters} views={views} />);
expect(component).toMatchSnapshot();
});
it('should not allow updating adapters', () => {
const component = mountWithIntl(
<InspectorPanel
adapters={adapters}
onClose={() => true}
views={views}
/>
);
const component = mountWithIntl(<InspectorPanel adapters={adapters} views={views} />);
adapters.notAllowed = {};
expect(() => component.setProps({ adapters })).toThrow();
});

View file

@ -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;
}
static getDerivedStateFromProps(nextProps, prevState) {
export class InspectorPanel extends Component<InspectorPanelProps, InspectorPanelState> {
static defaultProps = {
title: inspectorTitle,
};
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 (
<this.state.selectedView.component
adapters={this.props.adapters}
title={this.props.title}
title={this.props.title || ''}
/>
);
}
@ -86,13 +107,10 @@ class InspectorPanel extends Component {
return (
<React.Fragment>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
justifyContent="spaceBetween"
alignItems="center"
>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={true}>
<EuiTitle size="s">
<h1>{ title }</h1>
<h1>{title}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -104,26 +122,8 @@ class InspectorPanel extends Component {
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
{ this.renderSelectedPanel() }
<EuiFlyoutBody>{this.renderSelectedPanel()}</EuiFlyoutBody>
</React.Fragment>
);
}
}
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 };

View file

@ -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';
class InspectorViewChooser extends Component {
interface Props {
views: InspectorViewDescription[];
onViewSelected: (view: InspectorViewDescription) => void;
selectedView: InspectorViewDescription;
}
state = {
isSelectorOpen: false
interface State {
isSelectorOpen: boolean;
}
export class InspectorViewChooser extends Component<Props, State> {
static propTypes = {
views: PropTypes.array.isRequired,
onViewSelected: PropTypes.func.isRequired,
selectedView: PropTypes.object.isRequired,
};
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 (
<EuiContextMenuItem
key={index}
@ -62,7 +77,7 @@ class InspectorViewChooser extends Component {
{view.title}
</EuiContextMenuItem>
);
}
};
renderViewButton() {
return (
@ -74,7 +89,7 @@ class InspectorViewChooser extends Component {
data-test-subj="inspectorViewChooser"
>
<FormattedMessage
id="common.ui.inspector.view"
id="inspector.view"
defaultMessage="View: {viewName}"
values={{ viewName: this.props.selectedView.title }}
/>
@ -84,12 +99,9 @@ class InspectorViewChooser extends Component {
renderSingleView() {
return (
<EuiToolTip
position="bottom"
content={this.props.selectedView.help}
>
<EuiToolTip position="bottom" content={this.props.selectedView.help}>
<FormattedMessage
id="common.ui.inspector.view"
id="inspector.view"
defaultMessage="View: {viewName}"
values={{ viewName: this.props.selectedView.title }}
/>
@ -117,18 +129,8 @@ class InspectorViewChooser extends Component {
anchorPosition="downRight"
repositionOnScroll
>
<EuiContextMenuPanel
items={views.map(this.renderView)}
/>
<EuiContextMenuPanel items={views.map(this.renderView)} />
</EuiPopover>
);
}
}
InspectorViewChooser.propTypes = {
views: PropTypes.array.isRequired,
onViewSelected: PropTypes.func.isRequired,
selectedView: PropTypes.object.isRequired,
};
export { InspectorViewChooser };

View file

@ -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';

View file

@ -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));
}
}

View file

@ -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';

View file

@ -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 (
<InspectorView>
<MapDetails
centerLon={this.state.stats.center[0]}
centerLat={this.state.stats.center[1]}
zoom={this.state.stats.zoom}
mapStyle={this.state.mapStyle}
/>
</InspectorView>
<MapDetails
centerLon={this.state.stats.center[0]}
centerLat={this.state.stats.center[1]}
zoom={this.state.stats.zoom}
mapStyle={this.state.mapStyle}
/>
);
}
}

View file

@ -481,11 +481,11 @@
"common.ui.indexPattern.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するにな、ページを更新してください。",
"common.ui.indexPattern.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。",
"common.ui.indexPattern.unknownFieldHeader": "不明なフィールドタイプ {type}",
"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": "管理",

View file

@ -481,11 +481,11 @@
"common.ui.indexPattern.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。",
"common.ui.indexPattern.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。",
"common.ui.indexPattern.unknownFieldHeader": "未知字段类型 {type}",
"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": "管理",