mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Replace spy panels by Inspector (#16387) * Add Inspector feature * So long, and thanks for all the fish, spy panel * Fix several functional tests * Fix unit tests * Fix spy panel button tests * Replace old spy panel documentation * Disable test temporarily until we have dashboard triggers * Enter edit mode for dark theme test * Fix some more functional tests * Fix more functional tests * More test fixing * Fix more functional tests * Allow opening the inspector via loader handler * Refactor InspectorViewChooser, remove unused CSS * Remove dead code * Fix data download button style * Remove redundant code * Load inspectorViews for dashboard_viewer * Extract inspector views to custom core_plugin * Switch API to TypeScript 🎉 * Design changes * Remove icons from views * Design changes * Improve typings of API * Add typing to all adapters * Show loading spinner in request selector * Rewrite InspectorView to TypeScript * Fix help text for data view * Remove deprecated React lifecycle methods * Embed inspector into dashboard panel actions * Remove temporary inspector trigger * Remove old CSS * Fix dashboard trigger for new panel action * Add tests for InspectorPanel and DataAdapter * Produce a hierarchical table if the vis is hierarchical * Remove allowJs option again * Add missing Apache license headers * Close inspector on dashboard when navigating away * Use proper title for dashboard panels * Fix functional tests * Skip broken test for now * Flush view chooser button * Add request adapter tests * Skip more tests, broken due to typescript * Add Request Time description * Add description for courier request * Fix tests * Replace icon by new (not yet released) icon * Finalize design of inspector * Remove discover test, that relied on spy panels * Change API to be properly mockable in tests * Add aria-live region for request status * Replace old method in functional tests * Replace abitrary magic number * Use object destructuring in vis * Fix issue with crashing requests view * Add request time tooltip * Get request body of correct search source * Make filter buttons properly keyboard accessible * Follow Dave's design suggestions * Remove redundant request from name * Remove unneeded comments * WIP raw-formatted values * Fix filtering issue * Fix tests and more license headers * Add data view tests * Remove search from table * Fix typos * Implement review suggestion * Remove artificial delays for testing * Fix new panel action structure * Minor design adjustments * Fix failing functional test * Update failing snapshot test * Implement final wording * Apply new EUI styling * Fix closing inspector in tests * Fix sorting of table * Align punctuation between tooltips * Fix test that doesn't exist on master * Fix one inspector test (#20072) * Remove file that came accidentally back due to merging * Fix accidental merge paste * Fix 6.4 test failures
This commit is contained in:
parent
5d29b10412
commit
9ba0689dcd
126 changed files with 4221 additions and 1502 deletions
|
@ -9,7 +9,7 @@ An aggregate list of available UiExport types:
|
|||
| hacks | Any module that should be included in every application
|
||||
| visTypes | Modules that register providers with the `ui/registry/vis_types` registry.
|
||||
| fieldFormats | Modules that register providers with the `ui/registry/field_formats` registry.
|
||||
| spyModes | Modules that register providers with the `ui/registry/spy_modes` registry.
|
||||
| inspectorViews | Modules that register custom inspector views via the `viewRegistry` in `ui/inspector`.
|
||||
| chromeNavControls | Modules that register providers with the `ui/registry/chrome_nav_controls` registry.
|
||||
| navbarExtensions | Modules that register providers with the `ui/registry/navbar_extensions` registry.
|
||||
| docViews | Modules that register providers with the `ui/registry/doc_views` registry.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 330 KiB |
|
@ -162,6 +162,6 @@ include::visualize/tagcloud.asciidoc[]
|
|||
|
||||
include::visualize/heatmap.asciidoc[]
|
||||
|
||||
include::visualize/visualization-raw-data.asciidoc[]
|
||||
|
||||
include::visualize/vega.asciidoc[]
|
||||
|
||||
include::visualize/inspector.asciidoc[]
|
||||
|
|
17
docs/visualize/inspector.asciidoc
Normal file
17
docs/visualize/inspector.asciidoc
Normal file
|
@ -0,0 +1,17 @@
|
|||
[[vis-inspector]]
|
||||
== Inspector
|
||||
|
||||
Many visualizations have an Inspector that can help you gain insight to the data
|
||||
behind the visualization.
|
||||
|
||||
To open the Inspector use the Inspector button while in the editor or select Inspector
|
||||
from the Dashboard panel menu.
|
||||
|
||||
From the Inspector, you can download the visualization data as a comma separated
|
||||
values (CSV) file in Formatted or Raw format. Formatted downloads the data in table format.
|
||||
Raw downloads the data as provided -- dates are timestamps, numbers don’t have
|
||||
thousand separators, and so on.
|
||||
|
||||
To view the requests that collected the data, select Requests from the View menu in the upper right.
|
||||
|
||||
Which views are available depends on the inspected visualization.
|
|
@ -1,29 +0,0 @@
|
|||
[[vis-spy]]
|
||||
== Visualization Spy
|
||||
|
||||
To display the raw data behind the visualization, click the image:images/spy-open-button.png[] button in the bottom left corner of the container. The visualization spy panel will open. Use the select input to view detailed information about the raw data.
|
||||
|
||||
image:images/spy-panel.png[]
|
||||
|
||||
.Table
|
||||
A representation of the underlying data, presented as a paginated data grid. You can sort the items
|
||||
in the table by clicking on the table headers at the top of each column.
|
||||
|
||||
.Request
|
||||
The raw request used to query the server, presented in JSON format.
|
||||
|
||||
.Response
|
||||
The raw response from the server, presented in JSON format.
|
||||
|
||||
.Statistics
|
||||
A summary of the statistics related to the request and the response, presented as a data grid. The data
|
||||
grid includes the query duration, the request duration, the total number of records found on the server, and the
|
||||
index pattern used to make the query.
|
||||
|
||||
.Debug
|
||||
The visualization saved state presented in JSON format.
|
||||
|
||||
To export the raw data behind the visualization as a comma-separated-values (CSV) file, click on either the
|
||||
*Raw* or *Formatted* links at the bottom of any of the detailed information tabs. A raw export contains the data as it
|
||||
is stored in Elasticsearch. A formatted export contains the results of any applicable Kibana
|
||||
<<managing-fields,field formatters>>.
|
|
@ -225,6 +225,8 @@
|
|||
"@kbn/eslint-plugin-license-header": "link:packages/kbn-eslint-plugin-license-header",
|
||||
"@kbn/plugin-generator": "link:packages/kbn-plugin-generator",
|
||||
"@kbn/test": "link:packages/kbn-test",
|
||||
"@types/angular": "^1.6.45",
|
||||
"@types/classnames": "^2.2.3",
|
||||
"@types/eslint": "^4.16.2",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/getopts": "^2.0.0",
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "dev_mode",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<div>
|
||||
<div class="euiFlexGroup euiFlexGroup--responsive euiFlexGroup--gutterMedium">
|
||||
<div class="euiFlexItem">
|
||||
<h3 class="euiTitle euiTitle--small">
|
||||
Vis State
|
||||
</h3>
|
||||
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--fontSmall euiCodeBlock--paddingSmall">
|
||||
<code class="euiCodeBlock__code">
|
||||
<pre class="euiCodeBlock__pre">{{vis.getEnabledState() | json}}</pre>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="euiFlexItem">
|
||||
<h3 class="euiTitle euiTitle--small">Details</h3>
|
||||
<dl class="euiDescriptionList euiDescriptionList--column">
|
||||
<dt class="euiDescriptionList__title">Type Name</dt>
|
||||
<dd class="euiDescriptionList__description">{{vis.type.name}}</dd>
|
||||
<dt class="euiDescriptionList__title">Hierarchical Data</dt>
|
||||
<dd class="euiDescriptionList__description">{{vis.isHierarchical()}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -20,9 +20,8 @@
|
|||
export default function (kibana) {
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
spyModes: [
|
||||
'plugins/spy_modes/table_spy_mode',
|
||||
'plugins/spy_modes/req_resp_stats_spy_mode'
|
||||
inspectorViews: [
|
||||
'plugins/inspector_views/register_views'
|
||||
]
|
||||
}
|
||||
});
|
4
src/core_plugins/inspector_views/package.json
Normal file
4
src/core_plugins/inspector_views/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "inspector_views",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Inspector Data View component should render empty state 1`] = `
|
||||
<DataViewComponent
|
||||
adapters={
|
||||
Object {
|
||||
"data": DataAdapter {
|
||||
"_events": Object {
|
||||
"change": [Function],
|
||||
},
|
||||
"_eventsCount": 1,
|
||||
"_maxListeners": undefined,
|
||||
"domain": null,
|
||||
"tabular": [Function],
|
||||
"tabularOptions": Object {},
|
||||
},
|
||||
}
|
||||
}
|
||||
title="Test Data"
|
||||
>
|
||||
<InspectorView
|
||||
useFlex={true}
|
||||
>
|
||||
<EuiFlyoutBody
|
||||
className="inspector-view__flex"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody inspector-view__flex"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<UNDEFINED>
|
||||
<p>
|
||||
The element did not provide any data.
|
||||
</p>
|
||||
</UNDEFINED>
|
||||
}
|
||||
iconColor="subdued"
|
||||
title={
|
||||
<h2>
|
||||
No data available
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiEmptyPrompt"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<span
|
||||
className="euiTextColor euiTextColor--subdued"
|
||||
>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h2
|
||||
className="euiTitle euiTitle--medium"
|
||||
>
|
||||
No data available
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--m"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<EuiText
|
||||
grow={true}
|
||||
>
|
||||
<div
|
||||
className="euiText"
|
||||
>
|
||||
<p>
|
||||
The element did not provide any data.
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--l"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
</span>
|
||||
</EuiTextColor>
|
||||
</div>
|
||||
</EuiEmptyPrompt>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</InspectorView>
|
||||
</DataViewComponent>
|
||||
`;
|
||||
|
||||
exports[`Inspector Data View component should render loading state 1`] = `
|
||||
<DataViewComponent
|
||||
adapters={
|
||||
Object {
|
||||
"data": DataAdapter {
|
||||
"_events": Object {
|
||||
"change": [Function],
|
||||
},
|
||||
"_eventsCount": 1,
|
||||
"_maxListeners": undefined,
|
||||
"domain": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
title="Test Data"
|
||||
>
|
||||
<InspectorView
|
||||
useFlex={true}
|
||||
>
|
||||
<EuiFlyoutBody
|
||||
className="inspector-view__flex"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody inspector-view__flex"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="center"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<EuiPanel
|
||||
className="eui-textCenter"
|
||||
grow={true}
|
||||
hasShadow={false}
|
||||
paddingSize="m"
|
||||
>
|
||||
<div
|
||||
className="euiPanel euiPanel--paddingMedium eui-textCenter"
|
||||
>
|
||||
<EuiLoadingChart
|
||||
mono={false}
|
||||
size="m"
|
||||
>
|
||||
<div
|
||||
className="euiLoadingChart euiLoadingChart--medium"
|
||||
>
|
||||
<div
|
||||
className="euiLoadingChart__bar"
|
||||
/>
|
||||
<div
|
||||
className="euiLoadingChart__bar"
|
||||
/>
|
||||
<div
|
||||
className="euiLoadingChart__bar"
|
||||
/>
|
||||
<div
|
||||
className="euiLoadingChart__bar"
|
||||
/>
|
||||
</div>
|
||||
</EuiLoadingChart>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<EuiText
|
||||
grow={true}
|
||||
>
|
||||
<div
|
||||
className="euiText"
|
||||
>
|
||||
<p>
|
||||
Gathering data
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</InspectorView>
|
||||
</DataViewComponent>
|
||||
`;
|
144
src/core_plugins/inspector_views/public/data/data_table.js
Normal file
144
src/core_plugins/inspector_views/public/data/data_table.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './data_table.less';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DataDownloadOptions } from './download_options';
|
||||
|
||||
class DataTableFormat extends Component {
|
||||
|
||||
state = { };
|
||||
|
||||
static renderCell(col, value, isFormatted) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{ isFormatted ? value.formatted : value }
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
alignItems="center"
|
||||
>
|
||||
{ col.filter &&
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content="Filter for value"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
color="text"
|
||||
aria-label="Filter for value"
|
||||
className="inspector-table__filter"
|
||||
onClick={() => col.filter(value)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
{ col.filterOut &&
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content="Filter out value"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="minusInCircle"
|
||||
color="text"
|
||||
aria-label="Filter out value"
|
||||
className="inspector-table__filter"
|
||||
onClick={() => col.filterOut(value)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps({ data, isFormatted }) {
|
||||
if (!data) {
|
||||
return {
|
||||
columns: null,
|
||||
rows: null,
|
||||
};
|
||||
}
|
||||
|
||||
const columns = data.columns.map(col => ({
|
||||
name: col.name,
|
||||
field: col.field,
|
||||
sortable: isFormatted ? row => row[col.field].raw : true,
|
||||
render: (value) => DataTableFormat.renderCell(col, value, isFormatted),
|
||||
}));
|
||||
|
||||
return { columns, rows: data.rows };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { columns, rows } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={true} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<DataDownloadOptions
|
||||
columns={this.state.columns}
|
||||
rows={this.state.rows}
|
||||
isFormatted={this.props.isFormatted}
|
||||
title={this.props.exportTitle}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="inspectorTable"
|
||||
columns={columns}
|
||||
items={rows}
|
||||
sorting={true}
|
||||
pagination={true}
|
||||
compressed={true}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DataTableFormat.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
exportTitle: PropTypes.string.isRequired,
|
||||
isFormatted: PropTypes.bool,
|
||||
};
|
||||
|
||||
export { DataTableFormat };
|
|
@ -0,0 +1,8 @@
|
|||
.inspector-table__filter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
tr:hover .inspector-table__filter,
|
||||
.inspector-table__filter:focus {
|
||||
opacity: 1;
|
||||
}
|
160
src/core_plugins/inspector_views/public/data/data_view.js
Normal file
160
src/core_plugins/inspector_views/public/data/data_view.js
Normal file
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingChart,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { InspectorView } from 'ui/inspector';
|
||||
|
||||
import {
|
||||
DataTableFormat,
|
||||
} from './data_table';
|
||||
|
||||
class DataViewComponent extends Component {
|
||||
|
||||
_isMounted = false;
|
||||
state = {
|
||||
tabularData: null,
|
||||
tabularOptions: {},
|
||||
tabularLoader: null,
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps) {
|
||||
return {
|
||||
tabularData: null,
|
||||
tabularOptions: {},
|
||||
tabularPromise: nextProps.adapters.data.getTabular(),
|
||||
};
|
||||
}
|
||||
|
||||
onUpdateData = (type) => {
|
||||
if (type === 'tabular') {
|
||||
this.setState({
|
||||
tabularData: null,
|
||||
tabularOptions: {},
|
||||
tabularPromise: this.props.adapters.data.getTabular(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
finishLoadingData() {
|
||||
if (this.state.tabularPromise) {
|
||||
this.state.tabularPromise.then(({ data, options }) => {
|
||||
// Only update the data if the promise resolved before unmounting the component
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
tabularData: data,
|
||||
tabularOptions: options,
|
||||
tabularPromise: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.props.adapters.data.on('change', this.onUpdateData);
|
||||
this.finishLoadingData();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this.props.adapters.data.removeListener('change', this.onUpdateData);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.finishLoadingData();
|
||||
}
|
||||
|
||||
renderNoData() {
|
||||
return (
|
||||
<InspectorView useFlex={true}>
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>No data available</h2>}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>The element did not provide any data.</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</InspectorView>
|
||||
);
|
||||
}
|
||||
|
||||
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>Gathering data</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</InspectorView>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.tabularPromise) {
|
||||
return this.renderLoading();
|
||||
} else if (!this.state.tabularData) {
|
||||
return this.renderNoData();
|
||||
}
|
||||
|
||||
return (
|
||||
<InspectorView>
|
||||
<DataTableFormat
|
||||
data={this.state.tabularData}
|
||||
isFormatted={this.state.tabularOptions.returnsFormattedValues}
|
||||
exportTitle={this.props.title}
|
||||
/>
|
||||
</InspectorView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const DataView = {
|
||||
title: 'Data',
|
||||
order: 10,
|
||||
help: `View the data behind the visualization`,
|
||||
shouldShow(adapters) {
|
||||
return Boolean(adapters.data);
|
||||
},
|
||||
component: DataViewComponent
|
||||
};
|
||||
|
||||
export { DataView };
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { DataView } from './data_view';
|
||||
import { DataAdapter } from 'ui/inspector/adapters';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
jest.mock('./lib/export_csv', () => ({
|
||||
exportAsCsv: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Inspector Data View', () => {
|
||||
|
||||
it('should only show if data adapter is present', () => {
|
||||
const adapter = new DataAdapter();
|
||||
expect(DataView.shouldShow({ data: adapter })).toBe(true);
|
||||
expect(DataView.shouldShow({})).toBe(false);
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
|
||||
let adapters;
|
||||
|
||||
beforeEach(() => {
|
||||
adapters = { data: new DataAdapter() };
|
||||
});
|
||||
|
||||
it('should render loading state', () => {
|
||||
const component = mount(
|
||||
<DataView.component
|
||||
title="Test Data"
|
||||
adapters={adapters}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render empty state', async () => {
|
||||
const component = mount(
|
||||
<DataView.component
|
||||
title="Test Data"
|
||||
adapters={adapters}
|
||||
/>
|
||||
);
|
||||
const tabularLoader = Promise.resolve(null);
|
||||
adapters.data.setTabularLoader(() => tabularLoader);
|
||||
await tabularLoader;
|
||||
// After the loader has resolved we'll still need one update, to "flush" the state changes
|
||||
component.update();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
127
src/core_plugins/inspector_views/public/data/download_options.js
Normal file
127
src/core_plugins/inspector_views/public/data/download_options.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { exportAsCsv } from './lib/export_csv';
|
||||
|
||||
class DataDownloadOptions extends Component {
|
||||
|
||||
state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
onTogglePopover = () => {
|
||||
this.setState(state => ({
|
||||
isPopoverOpen: !state.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
exportCsv = () => {
|
||||
exportAsCsv(`${this.props.title}.csv`, this.props.columns, this.props.rows);
|
||||
}
|
||||
|
||||
exportFormattedCsv = () => {
|
||||
exportAsCsv(`${this.props.title}.csv`, this.props.columns, this.props.rows, item => item.formatted);
|
||||
};
|
||||
|
||||
exportFormattedAsRawCsv = () => {
|
||||
exportAsCsv(`${this.props.title}.csv`, this.props.columns, this.props.rows, item => item.raw);
|
||||
};
|
||||
|
||||
renderUnformattedDownload() {
|
||||
return (
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={this.exportCsv}
|
||||
>
|
||||
Download CSV
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
|
||||
renderFormattedDownloads() {
|
||||
const button = (
|
||||
<EuiButton
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
size="s"
|
||||
onClick={this.onTogglePopover}
|
||||
>
|
||||
Download CSV
|
||||
</EuiButton>
|
||||
);
|
||||
const items = [
|
||||
<EuiContextMenuItem
|
||||
key="csv"
|
||||
onClick={this.exportFormattedCsv}
|
||||
toolTipContent="Download the data in table format"
|
||||
toolTipPosition="left"
|
||||
>
|
||||
Formatted CSV
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="rawCsv"
|
||||
onClick={this.exportFormattedAsRawCsv}
|
||||
toolTipContent={`Download the data as provided, for example, dates as timestamps`}
|
||||
toolTipPosition="left"
|
||||
>
|
||||
Raw CSV
|
||||
</EuiContextMenuItem>
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="inspectorDownloadData"
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
className="eui-textNoWrap"
|
||||
items={items}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.isFormatted) {
|
||||
return this.renderUnformattedDownload();
|
||||
}
|
||||
|
||||
return this.renderFormattedDownloads();
|
||||
}
|
||||
}
|
||||
|
||||
export { DataDownloadOptions };
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import { saveAs } from '@elastic/filesaver';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
function buildCsv(columns, rows, valueFormatter) {
|
||||
const settings = chrome.getUiSettingsClient();
|
||||
const csvSeparator = settings.get('csv:separator', ',');
|
||||
const quoteValues = settings.get('csv:quoteValues', true);
|
||||
|
||||
const nonAlphaNumRE = /[^a-zA-Z0-9]/;
|
||||
const allDoubleQuoteRE = /"/g;
|
||||
|
||||
function escape(val) {
|
||||
if (_.isObject(val)) val = val.valueOf();
|
||||
val = String(val);
|
||||
if (quoteValues && nonAlphaNumRE.test(val)) {
|
||||
val = `"${val.replace(allDoubleQuoteRE, '""')}"`;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// Build the header row by its names
|
||||
const header = columns.map(col => escape(col.name));
|
||||
|
||||
// Convert the array of row objects to an array of row arrays
|
||||
const orderedFieldNames = columns.map(col => col.field);
|
||||
const csvRows = rows.map(row => {
|
||||
return orderedFieldNames.map(field =>
|
||||
escape(valueFormatter ? valueFormatter(row[field]) : row[field])
|
||||
);
|
||||
});
|
||||
|
||||
return [header, ...csvRows]
|
||||
.map(row => row.join(csvSeparator))
|
||||
.join('\r\n')
|
||||
+ '\r\n'; // Add \r\n after last line
|
||||
}
|
||||
|
||||
function exportAsCsv(filename, columns, rows, valueFormatter) {
|
||||
const csv = new Blob([buildCsv(columns, rows, valueFormatter)], { type: 'text/plain;charset=utf-8' });
|
||||
saveAs(csv, filename);
|
||||
}
|
||||
|
||||
export { exportAsCsv };
|
|
@ -17,21 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export default (kibana) => {
|
||||
return new kibana.Plugin({
|
||||
id: 'dev_mode',
|
||||
import { DataView } from './data/data_view';
|
||||
import { RequestsView } from './requests/requests_view';
|
||||
|
||||
isEnabled(config) {
|
||||
return (
|
||||
config.get('env.dev') &&
|
||||
config.get('dev_mode.enabled')
|
||||
);
|
||||
},
|
||||
import { viewRegistry } from 'ui/inspector';
|
||||
|
||||
uiExports: {
|
||||
spyModes: [
|
||||
'plugins/dev_mode/vis_debug_spy_panel'
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
viewRegistry.register(DataView);
|
||||
viewRegistry.register(RequestsView);
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './req_details_description';
|
||||
export * from './req_details_request';
|
||||
export * from './req_details_response';
|
||||
export * from './req_details_stats';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
function RequestDetailsDescription(props) {
|
||||
return (
|
||||
<EuiText className="requests-details__description">
|
||||
{ props.request.description }
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
RequestDetailsDescription.shouldShow = (request) => !!request.description;
|
||||
|
||||
RequestDetailsDescription.propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { RequestDetailsDescription };
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
|
||||
function RequestDetailsRequest(props) {
|
||||
return (
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
paddingSize="s"
|
||||
data-test-subj="inspectorRequestBody"
|
||||
>
|
||||
{ JSON.stringify(props.request.json, null, 2) }
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
RequestDetailsRequest.shouldShow = (request) => !!request.json;
|
||||
|
||||
export { RequestDetailsRequest };
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
|
||||
function RequestDetailsResponse(props) {
|
||||
return (
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
paddingSize="s"
|
||||
data-test-subj="inspectorResponseBody"
|
||||
>
|
||||
{ JSON.stringify(props.request.response.json, null, 2) }
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
RequestDetailsResponse.shouldShow = (request) => request.response && request.response.json;
|
||||
|
||||
export { RequestDetailsResponse };
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiTable,
|
||||
EuiTableBody,
|
||||
EuiTableRow,
|
||||
EuiTableRowCell,
|
||||
} from '@elastic/eui';
|
||||
|
||||
class RequestDetailsStats extends Component {
|
||||
|
||||
static shouldShow = (request) => !!request.stats && Object.keys(request.stats).length;
|
||||
|
||||
renderStatRow = (stat) => {
|
||||
return [
|
||||
<EuiTableRow
|
||||
key={stat.name}
|
||||
>
|
||||
<EuiTableRowCell>
|
||||
<span className="requests-stats__icon">
|
||||
{ stat.description &&
|
||||
<EuiIconTip
|
||||
aria-label="Description"
|
||||
type="questionInCircle"
|
||||
color="subdued"
|
||||
content={stat.description}
|
||||
/>
|
||||
}
|
||||
{ !stat.description &&
|
||||
<EuiIcon
|
||||
type="empty"
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
{stat.name}
|
||||
</EuiTableRowCell>
|
||||
<EuiTableRowCell>{stat.value}</EuiTableRowCell>
|
||||
</EuiTableRow>
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { stats } = this.props.request;
|
||||
const sortedStats = Object.keys(stats).sort().map(name => ({ name, ...stats[name] }));
|
||||
// TODO: Replace by property once available
|
||||
return (
|
||||
<EuiTable style={{ tableLayout: 'auto' }}>
|
||||
<EuiTableBody>
|
||||
{ sortedStats.map(this.renderStatRow) }
|
||||
</EuiTableBody>
|
||||
</EuiTable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RequestDetailsStats.propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { RequestDetailsStats };
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
RequestDetailsDescription,
|
||||
RequestDetailsRequest,
|
||||
RequestDetailsResponse,
|
||||
RequestDetailsStats,
|
||||
} from './details';
|
||||
|
||||
const DETAILS = [
|
||||
{ name: 'Description', component: RequestDetailsDescription },
|
||||
{ name: 'Statistics', component: RequestDetailsStats },
|
||||
{ name: 'Request', component: RequestDetailsRequest },
|
||||
{ name: 'Response', component: RequestDetailsResponse },
|
||||
];
|
||||
|
||||
class RequestDetails extends Component {
|
||||
|
||||
state = {
|
||||
availableDetails: [],
|
||||
selectedDetail: null,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const selectedDetail = prevState && prevState.selectedDetail;
|
||||
const availableDetails = DETAILS.filter(detail =>
|
||||
!detail.component.shouldShow || detail.component.shouldShow(nextProps.request)
|
||||
);
|
||||
// If the previously selected detail is still available we want to stay
|
||||
// on this tab and not set another selectedDetail.
|
||||
if (selectedDetail && availableDetails.includes(selectedDetail)) {
|
||||
return { availableDetails };
|
||||
}
|
||||
|
||||
return {
|
||||
availableDetails: availableDetails,
|
||||
selectedDetail: availableDetails[0]
|
||||
};
|
||||
}
|
||||
|
||||
selectDetailsTab = (detail) => {
|
||||
if (detail !== this.state.selectedDetail) {
|
||||
this.setState({
|
||||
selectedDetail: detail
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderDetailTab = (detail) => {
|
||||
return (
|
||||
<EuiTab
|
||||
key={detail.name}
|
||||
isSelected={detail === this.state.selectedDetail}
|
||||
onClick={() => this.selectDetailsTab(detail)}
|
||||
data-test-subj={`inspectorRequestDetail${detail.name}`}
|
||||
>
|
||||
{detail.name}
|
||||
</EuiTab>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.availableDetails.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const DetailComponent = this.state.selectedDetail.component;
|
||||
return (
|
||||
<div>
|
||||
<EuiTabs size="s">
|
||||
{ this.state.availableDetails.map(this.renderDetailTab) }
|
||||
</EuiTabs>
|
||||
<DetailComponent
|
||||
request={this.props.request}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RequestDetails.propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { RequestDetails };
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
EuiTextColor,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { RequestStatus } from 'ui/inspector/adapters';
|
||||
|
||||
class RequestSelector extends Component {
|
||||
state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
}
|
||||
|
||||
closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
renderRequestDropdownItem = (request, index) => {
|
||||
const hasFailed = request.status === RequestStatus.ERROR;
|
||||
const inProgress = request.status === RequestStatus.PENDING;
|
||||
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={index}
|
||||
icon={request === this.props.selectedRequest ? 'check' : 'empty'}
|
||||
onClick={() => {
|
||||
this.props.onRequestChanged(request);
|
||||
this.closePopover();
|
||||
}}
|
||||
toolTipContent={request.description}
|
||||
toolTipPosition="left"
|
||||
>
|
||||
<EuiTextColor color={hasFailed ? 'danger' : 'default'}>
|
||||
{request.name}
|
||||
{ hasFailed && ' (failed)' }
|
||||
{ inProgress &&
|
||||
<EuiLoadingSpinner
|
||||
size="s"
|
||||
aria-label="Request in progress"
|
||||
className="inspector-request-chooser__menu-spinner"
|
||||
/>
|
||||
}
|
||||
</EuiTextColor>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderRequestDropdown() {
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
size="s"
|
||||
className="inspector-request-chooser__request-title"
|
||||
onClick={this.togglePopover}
|
||||
>
|
||||
{this.props.selectedRequest.name}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="inspectorRequestChooser"
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
className="inspector-request-chooser__menu-panel"
|
||||
items={this.props.requests.map(this.renderRequestDropdownItem)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedRequest, requests } = this.props;
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className="inspector-request-chooser__request-title"
|
||||
>
|
||||
Request:
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
{requests.length <= 1 &&
|
||||
<div className="inspector-request-chooser__single-request">
|
||||
{selectedRequest.name}
|
||||
</div>
|
||||
}
|
||||
{requests.length > 1 && this.renderRequestDropdown()}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{ selectedRequest.status !== RequestStatus.PENDING &&
|
||||
<EuiToolTip
|
||||
position="left"
|
||||
title={selectedRequest.status === RequestStatus.OK ? 'Request succeeded' : 'Request failed'}
|
||||
content="The total time the request took."
|
||||
>
|
||||
<EuiBadge
|
||||
color={selectedRequest.status === RequestStatus.OK ? 'secondary' : 'danger'}
|
||||
iconType={selectedRequest.status === RequestStatus.OK ? 'check' : 'cross'}
|
||||
>
|
||||
{selectedRequest.time}ms
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
}
|
||||
{ selectedRequest.status === RequestStatus.PENDING &&
|
||||
<EuiLoadingSpinner
|
||||
size="m"
|
||||
aria-label="Request in progress"
|
||||
/>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RequestSelector.propTypes = {
|
||||
requests: PropTypes.array.isRequired,
|
||||
selectedRequest: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { RequestSelector };
|
|
@ -0,0 +1,27 @@
|
|||
.requests-details__description {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.requests-stats__icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.inspector-request-chooser__menu-panel {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.inspector-request-chooser__request-title {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.inspector-request-chooser__single-request {
|
||||
.inspector-request-chooser__request-title();
|
||||
height: 30px; // height of EuiEmptyButton small
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inspector-request-chooser__menu-spinner {
|
||||
margin-left: 8px;
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { InspectorView } from 'ui/inspector';
|
||||
import { RequestStatus } from 'ui/inspector/adapters';
|
||||
|
||||
import { RequestSelector } from './request_selector';
|
||||
import { RequestDetails } from './request_details';
|
||||
|
||||
import './requests_inspector.less';
|
||||
|
||||
class RequestsViewComponent extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.adapters.requests.on('change', this._onRequestsChange);
|
||||
|
||||
const requests = props.adapters.requests.getRequests();
|
||||
this.state = {
|
||||
requests: requests,
|
||||
request: requests.length ? requests[0] : null
|
||||
};
|
||||
}
|
||||
|
||||
_onRequestsChange = () => {
|
||||
const requests = this.props.adapters.requests.getRequests();
|
||||
const newState = { requests };
|
||||
if (!requests.includes(this.state.request)) {
|
||||
newState.request = requests.length ? requests[0] : null;
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
selectRequest = (request) => {
|
||||
if (request !== this.state.request) {
|
||||
this.setState({ request });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.adapters.requests.removeListener('change', this._onRequestsChange);
|
||||
}
|
||||
|
||||
renderEmptyRequests() {
|
||||
return (
|
||||
<InspectorView useFlex={true}>
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>No requests logged</h2>}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>The element hasn't logged any requests (yet).</p>
|
||||
<p>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.requests || !this.state.requests.length) {
|
||||
return this.renderEmptyRequests();
|
||||
}
|
||||
|
||||
const failedCount = this.state.requests.filter(
|
||||
req => req.status === RequestStatus.ERROR
|
||||
).length;
|
||||
|
||||
return (
|
||||
<InspectorView>
|
||||
<EuiText size="xs">
|
||||
<p role="status" aria-live="polite" aria-atomic="true">
|
||||
{this.state.requests.length}
|
||||
{this.state.requests.length !== 1 ? ' requests were' : ' request was'} made
|
||||
{failedCount > 0 &&
|
||||
<React.Fragment>
|
||||
, {' '}
|
||||
<EuiTextColor
|
||||
color="danger"
|
||||
>
|
||||
{failedCount} had a failure
|
||||
</EuiTextColor>
|
||||
</React.Fragment>
|
||||
}
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs"/>
|
||||
<RequestSelector
|
||||
requests={this.state.requests}
|
||||
selectedRequest={this.state.request}
|
||||
onRequestChanged={this.selectRequest}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{ this.state.request &&
|
||||
<RequestDetails
|
||||
request={this.state.request}
|
||||
/>
|
||||
}
|
||||
</InspectorView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RequestsViewComponent.propTypes = {
|
||||
adapters: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const RequestsView = {
|
||||
title: 'Requests',
|
||||
order: 20,
|
||||
help: `View the requests that collected the data`,
|
||||
shouldShow(adapters) {
|
||||
return Boolean(adapters.requests);
|
||||
},
|
||||
component: RequestsViewComponent
|
||||
};
|
||||
|
||||
export { RequestsView };
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import {
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Inspector } from 'ui/inspector';
|
||||
import { DashboardPanelAction } from 'ui/dashboard_panel_actions';
|
||||
|
||||
/**
|
||||
* Returns the dashboard panel action for opening an inspector for a specific panel.
|
||||
* This will check if the embeddable inside the panel actually exposes inspector adapters
|
||||
* via its embeddable.getInspectorAdapters() method. If so - and if an inspector
|
||||
* could be shown for those adapters - the inspector icon will be visible.
|
||||
* @return {DashboardPanelAction}
|
||||
*/
|
||||
export function getInspectorPanelAction({ closeContextMenu, panelTitle }) {
|
||||
return new DashboardPanelAction(
|
||||
{
|
||||
id: 'openInspector',
|
||||
displayName: 'Inspector',
|
||||
parentPanelId: 'mainMenu',
|
||||
},
|
||||
{
|
||||
icon: <EuiIcon type="inspect" />,
|
||||
onClick: ({ embeddable }) => {
|
||||
closeContextMenu();
|
||||
const session = Inspector.open(embeddable.getInspectorAdapters(), {
|
||||
title: panelTitle,
|
||||
});
|
||||
// Overwrite the embeddables.destroy() function to close the inspector
|
||||
// before calling the original destroy method
|
||||
const originalDestroy = embeddable.destroy;
|
||||
embeddable.destroy = () => {
|
||||
session.close();
|
||||
if (originalDestroy) {
|
||||
originalDestroy.call(embeddable);
|
||||
}
|
||||
};
|
||||
// In case the inspector gets closed (otherwise), restore the original destroy function
|
||||
session.on('closed', () => {
|
||||
embeddable.destroy = originalDestroy;
|
||||
});
|
||||
},
|
||||
isVisible: ({ embeddable }) => (
|
||||
embeddable && Inspector.isAvailable(embeddable.getInspectorAdapters())
|
||||
),
|
||||
});
|
||||
}
|
|
@ -22,3 +22,4 @@ export { getRemovePanelAction } from './get_remove_panel_action';
|
|||
export { buildEuiContextMenuPanels } from './build_context_menu';
|
||||
export { getCustomizePanelAction } from './get_customize_panel_action';
|
||||
export { getToggleExpandPanelAction } from './get_toggle_expand_panel_action';
|
||||
export { getInspectorPanelAction } from './get_inspector_panel_action';
|
||||
|
|
|
@ -25,6 +25,7 @@ import { PanelOptionsMenu } from './panel_options_menu';
|
|||
import {
|
||||
buildEuiContextMenuPanels,
|
||||
getEditPanelAction,
|
||||
getInspectorPanelAction,
|
||||
getRemovePanelAction,
|
||||
getCustomizePanelAction,
|
||||
getToggleExpandPanelAction,
|
||||
|
@ -124,6 +125,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
|||
});
|
||||
|
||||
const actions = [
|
||||
getInspectorPanelAction({
|
||||
closeContextMenu: closeMyContextMenuPanel,
|
||||
panelTitle,
|
||||
}),
|
||||
getEditPanelAction(),
|
||||
getCustomizePanelAction({
|
||||
onResetPanelTitle,
|
||||
|
|
|
@ -175,10 +175,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.visualize-show-spy {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
cursor: move;
|
||||
}
|
||||
|
@ -316,10 +312,6 @@ dashboard-viewport-provider {
|
|||
}
|
||||
}
|
||||
|
||||
.visualize-show-spy {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Use opacity to make this element accessible to screen readers and keyboard.
|
||||
* 2. Show on focus to enable keyboard accessibility.
|
||||
|
@ -351,9 +343,6 @@ dashboard-viewport-provider {
|
|||
.panel-heading-floater {
|
||||
opacity: 1;
|
||||
}
|
||||
.visualize-show-spy {
|
||||
visibility: visible;
|
||||
}
|
||||
.viewModeOpenContextMenuIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -134,7 +134,6 @@
|
|||
vis="vis"
|
||||
ui-state="uiState"
|
||||
vis-data="visData"
|
||||
show-spy-panel="true"
|
||||
search-source="searchSource"
|
||||
style="height: 200px"
|
||||
>
|
||||
|
|
|
@ -31,7 +31,6 @@ import 'uiExports/visResponseHandlers';
|
|||
import 'uiExports/visRequestHandlers';
|
||||
import 'uiExports/visEditorTypes';
|
||||
import 'uiExports/savedObjectTypes';
|
||||
import 'uiExports/spyModes';
|
||||
import 'uiExports/fieldFormats';
|
||||
import 'uiExports/fieldFormatEditors';
|
||||
import 'uiExports/navbarExtensions';
|
||||
|
@ -40,6 +39,7 @@ import 'uiExports/managementSections';
|
|||
import 'uiExports/devTools';
|
||||
import 'uiExports/docViews';
|
||||
import 'uiExports/embeddableFactories';
|
||||
import 'uiExports/inspectorViews';
|
||||
|
||||
import 'ui/autoload/all';
|
||||
import './home';
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
ui-state="uiState"
|
||||
time-range="timeRange"
|
||||
editor-mode="chrome.getVisible()"
|
||||
show-spy-panel="chrome.getVisible()"
|
||||
>
|
||||
|
||||
</visualize>
|
||||
|
|
|
@ -130,6 +130,21 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
|
|||
description: 'Share Visualization',
|
||||
template: require('plugins/kibana/visualize/editor/panels/share.html'),
|
||||
testId: 'visualizeShareButton',
|
||||
}, {
|
||||
key: 'inspector',
|
||||
description: 'Open Inspector for visualization',
|
||||
testId: 'openInspectorButton',
|
||||
disableButton() {
|
||||
return !vis.hasInspector();
|
||||
},
|
||||
run() {
|
||||
vis.openInspector().bindToAngularScope($scope);
|
||||
},
|
||||
tooltip() {
|
||||
if (!vis.hasInspector()) {
|
||||
return 'This visualization doesn\'t support any inspectors.';
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'refresh',
|
||||
description: 'Refresh',
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
import { PersistedState } from 'ui/persisted_state';
|
||||
import { Embeddable } from 'ui/embeddable';
|
||||
import chrome from 'ui/chrome';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class VisualizeEmbeddable extends Embeddable {
|
||||
|
@ -46,6 +45,10 @@ export class VisualizeEmbeddable extends Embeddable {
|
|||
this._onEmbeddableStateChanged(this.getEmbeddableState());
|
||||
};
|
||||
|
||||
getInspectorAdapters() {
|
||||
return this.savedVisualization.vis.API.inspectorAdapters;
|
||||
}
|
||||
|
||||
getEmbeddableState() {
|
||||
return {
|
||||
customization: this.customization,
|
||||
|
@ -146,7 +149,6 @@ export class VisualizeEmbeddable extends Embeddable {
|
|||
cssClass: `panel-content panel-content--fullWidth`,
|
||||
// The chrome is permanently hidden in "embed mode" in which case we don't want to show the spy pane, since
|
||||
// we deem that situation to be more public facing and want to hide more detailed information.
|
||||
showSpyPanel: !chrome.getIsChromePermanentlyHidden(),
|
||||
dataAttrs: {
|
||||
'shared-item': '',
|
||||
title: this.panelTitle,
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "spy_modes",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
<div ng-if="!req.stopped" class="visualize-spy-loading">
|
||||
<div class="visualize-spy-loading-text">
|
||||
Request in progress
|
||||
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="req.stopped && req.success === false" ng-init="console.log(req)" class="alert alert-danger">
|
||||
<i class="fa fa-danger"></i> Request Failed
|
||||
</div>
|
||||
|
||||
<div ng-if="mode === 'request'">
|
||||
<h3 class="euiTitle euiTitle--small">
|
||||
Elasticsearch request body
|
||||
</h3>
|
||||
<div
|
||||
data-test-subj="visualizationEsRequestBody"
|
||||
class="euiCodeBlock euiCodeBlock--light euiCodeBlock--fontSmall euiCodeBlock--paddingSmall"
|
||||
>
|
||||
<code class="euiCodeBlock__code">
|
||||
<pre class="euiCodeBlock__pre">{{req.fetchParams.body | json}}</pre>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="mode === 'response'">
|
||||
<h3 class="euiTitle euiTitle--small">
|
||||
Elasticsearch response body
|
||||
</h3>
|
||||
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--fontSmall euiCodeBlock--paddingSmall">
|
||||
<code class="euiCodeBlock__code">
|
||||
<pre class="euiCodeBlock__pre">{{req.resp | json}}</pre>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="mode === 'stats'">
|
||||
<table class="table">
|
||||
<tr ng-repeat="pair in stats">
|
||||
<td>{{pair[0]}}</td>
|
||||
<td>{{pair[1]}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -1,84 +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 reqRespStatsHTML from './req_resp_stats_spy_mode.html';
|
||||
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
|
||||
|
||||
const linkReqRespStats = function (mode, $scope) {
|
||||
$scope.mode = mode;
|
||||
$scope.$bind('req', 'searchSource.history[searchSource.history.length - 1]');
|
||||
$scope.$watchMulti([
|
||||
'req',
|
||||
'req.started',
|
||||
'req.stopped',
|
||||
'searchSource'
|
||||
], function () {
|
||||
if (!$scope.searchSource || !$scope.req) return;
|
||||
|
||||
const req = $scope.req;
|
||||
const resp = $scope.req.resp;
|
||||
const stats = $scope.stats = [];
|
||||
|
||||
if (resp && resp.took != null) stats.push(['Query Duration', resp.took + 'ms']);
|
||||
if (req && req.ms != null) stats.push(['Request Duration', req.ms + 'ms']);
|
||||
if (resp && resp.hits) stats.push(['Hits', resp.hits.total]);
|
||||
|
||||
if (req.fetchParams && req.fetchParams.index) {
|
||||
if (req.fetchParams.index.title) stats.push(['Index', req.fetchParams.index.title]);
|
||||
if (req.fetchParams.index.type) stats.push(['Type', req.fetchParams.index.type]);
|
||||
if (req.fetchParams.index.id) stats.push(['Id', req.fetchParams.index.id]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function shouldShowSpyMode(vis) {
|
||||
return vis.type.requestHandler === 'courier' && vis.type.requiresSearch;
|
||||
}
|
||||
|
||||
SpyModesRegistryProvider
|
||||
.register(function () {
|
||||
return {
|
||||
name: 'request',
|
||||
display: 'Request',
|
||||
order: 2,
|
||||
template: reqRespStatsHTML,
|
||||
showMode: shouldShowSpyMode,
|
||||
link: linkReqRespStats.bind(null, 'request')
|
||||
};
|
||||
})
|
||||
.register(function () {
|
||||
return {
|
||||
name: 'response',
|
||||
display: 'Response',
|
||||
order: 3,
|
||||
template: reqRespStatsHTML,
|
||||
showMode: shouldShowSpyMode,
|
||||
link: linkReqRespStats.bind(null, 'response')
|
||||
};
|
||||
})
|
||||
.register(function () {
|
||||
return {
|
||||
name: 'stats',
|
||||
display: 'Statistics',
|
||||
order: 4,
|
||||
template: reqRespStatsHTML,
|
||||
showMode: shouldShowSpyMode,
|
||||
link: linkReqRespStats.bind(null, 'stats')
|
||||
};
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
<kbn-agg-table
|
||||
table="table"
|
||||
export-title="vis.title"
|
||||
per-page="rowsPerPage">
|
||||
</kbn-agg-table>
|
|
@ -1,56 +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 'ui/agg_table';
|
||||
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
|
||||
import tableSpyModeTemplate from './table_spy_mode.html';
|
||||
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
|
||||
|
||||
function VisSpyTableProvider(Notifier, $filter, $rootScope) {
|
||||
const PER_PAGE_DEFAULT = 10;
|
||||
|
||||
return {
|
||||
name: 'table',
|
||||
display: 'Table',
|
||||
order: 1,
|
||||
template: tableSpyModeTemplate,
|
||||
showMode: vis => vis.type.requestHandler === 'courier' && vis.type.requiresSearch,
|
||||
link: function tableLinkFn($scope) {
|
||||
$rootScope.$watchMulti.call($scope, [
|
||||
'vis',
|
||||
'searchSource.rawResponse'
|
||||
], function () {
|
||||
if (!$scope.vis || !$scope.searchSource.rawResponse) {
|
||||
$scope.table = null;
|
||||
} else {
|
||||
$scope.rowsPerPage = PER_PAGE_DEFAULT;
|
||||
|
||||
$scope.table = tabifyAggResponse($scope.vis.getAggConfig().getResponseAggs(), $scope.searchSource.rawResponse, {
|
||||
canSplit: false,
|
||||
asAggConfigResults: true,
|
||||
partialRows: true,
|
||||
isHierarchical: $scope.vis.isHierarchical()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SpyModesRegistryProvider.register(VisSpyTableProvider);
|
|
@ -66,6 +66,6 @@ export default function TimelionVisProvider(Private) {
|
|||
showIndexSelection: false,
|
||||
showQueryBar: false,
|
||||
showFilterBar: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ export const CopySourceTask = {
|
|||
'!src/**/{__tests__,__snapshots__}/**',
|
||||
'!src/test_utils/**',
|
||||
'!src/fixtures/**',
|
||||
'!src/core_plugins/dev_mode/**',
|
||||
'!src/core_plugins/tests_bundle/**',
|
||||
'!src/core_plugins/testbed/**',
|
||||
'!src/core_plugins/console/public/tests/**',
|
||||
|
|
|
@ -25,6 +25,8 @@ import { createFilterTerms } from './create_filter/terms';
|
|||
import orderAggTemplate from '../controls/order_agg.html';
|
||||
import orderAndSizeTemplate from '../controls/order_and_size.html';
|
||||
import otherBucketTemplate from '../controls/other_bucket.html';
|
||||
|
||||
import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils';
|
||||
import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from './_terms_other_bucket_helper';
|
||||
import { toastNotifications } from '../../notify';
|
||||
|
||||
|
@ -94,7 +96,20 @@ export const termsBucketAgg = new BucketAggType({
|
|||
if (aggConfig.params.otherBucket) {
|
||||
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
|
||||
nestedSearchSource.set('aggs', filterAgg);
|
||||
|
||||
const request = aggConfigs.vis.API.inspectorAdapters.requests.start('Other bucket', {
|
||||
description: `This request counts the number of documents that fall
|
||||
outside the criterion of the data buckets.`
|
||||
});
|
||||
nestedSearchSource.getSearchRequestBody().then(body => {
|
||||
request.json(body);
|
||||
});
|
||||
request.stats(getRequestInspectorStats(nestedSearchSource));
|
||||
|
||||
const response = await nestedSearchSource.fetchAsRejectablePromise();
|
||||
request
|
||||
.stats(getResponseInspectorStats(nestedSearchSource, response))
|
||||
.ok({ json: response });
|
||||
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
|
||||
}
|
||||
if (aggConfig.params.missingBucket) {
|
||||
|
|
74
src/ui/public/courier/utils/courier_inspector_utils.js
Normal file
74
src/ui/public/courier/utils/courier_inspector_utils.js
Normal 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function collects statistics from a SearchSource and a response
|
||||
* for the usage in the inspector stats panel. Pass in a searchSource and a response
|
||||
* and the returned object can be passed to the `stats` method of the request
|
||||
* logger.
|
||||
*/
|
||||
function getRequestInspectorStats(searchSource) {
|
||||
const stats = {};
|
||||
const index = searchSource.get('index');
|
||||
|
||||
if (index) {
|
||||
stats['Index pattern'] = {
|
||||
value: index.title,
|
||||
description: 'The index pattern that connected to the Elasticsearch indices.',
|
||||
};
|
||||
stats ['Index pattern ID'] = {
|
||||
value: index.id,
|
||||
description: 'The ID in the .kibana index.',
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
function getResponseInspectorStats(searchSource, resp) {
|
||||
const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1];
|
||||
const stats = {};
|
||||
|
||||
if (resp && resp.took) {
|
||||
stats['Query time'] = {
|
||||
value: `${resp.took}ms`,
|
||||
description: `The time it took to process the query.
|
||||
Does not include the time to send the request or parse it in the browser.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (resp && resp.hits) {
|
||||
stats.Hits = {
|
||||
value: `${resp.hits.total}`,
|
||||
description: 'The number of documents that match the query.',
|
||||
};
|
||||
}
|
||||
|
||||
if (lastRequest && (lastRequest.ms === 0 || lastRequest.ms)) {
|
||||
stats['Request time'] = {
|
||||
value: `${lastRequest.ms}ms`,
|
||||
description: `The time of the request from the browser to Elasticsearch and back.
|
||||
Does not include the time the requested waited in the queue.`
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
export { getRequestInspectorStats, getResponseInspectorStats };
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { Adapters } from 'ui/inspector';
|
||||
import { ContainerState } from './types';
|
||||
|
||||
// TODO: we'll be able to get rid of this shape once all of dashboard is typescriptified too.
|
||||
|
@ -89,6 +90,15 @@ export abstract class Embeddable {
|
|||
containerState: ContainerState
|
||||
): void;
|
||||
|
||||
/**
|
||||
* An embeddable can return inspector adapters if it want the inspector to be
|
||||
* available via the context menu of that panel.
|
||||
* @return Inspector adapters that will be used to open an inspector for.
|
||||
*/
|
||||
public getInspectorAdapters(): Adapters | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
return;
|
||||
}
|
||||
|
|
127
src/ui/public/inspector/README.md
Normal file
127
src/ui/public/inspector/README.md
Normal file
|
@ -0,0 +1,127 @@
|
|||
# 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.
|
57
src/ui/public/inspector/adapters/data/data_adapter.ts
Normal file
57
src/ui/public/inspector/adapters/data/data_adapter.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
// TODO: add a more specific TabularData type.
|
||||
type TabularData = any;
|
||||
type TabularCallback = () => TabularData | Promise<TabularData>;
|
||||
|
||||
interface TabularHolder {
|
||||
data: TabularData | null;
|
||||
options: TabularLoaderOptions;
|
||||
}
|
||||
|
||||
interface TabularLoaderOptions {
|
||||
returnsFormattedValues?: boolean;
|
||||
}
|
||||
|
||||
class DataAdapter extends EventEmitter {
|
||||
private tabular?: TabularCallback;
|
||||
private tabularOptions?: TabularLoaderOptions;
|
||||
|
||||
public setTabularLoader(
|
||||
callback: TabularCallback,
|
||||
options: TabularLoaderOptions = {}
|
||||
): void {
|
||||
this.tabular = callback;
|
||||
this.tabularOptions = options;
|
||||
this.emit('change', 'tabular');
|
||||
}
|
||||
|
||||
public getTabular(): Promise<TabularHolder> {
|
||||
if (!this.tabular || !this.tabularOptions) {
|
||||
return Promise.resolve({ data: null, options: {} });
|
||||
}
|
||||
const options = this.tabularOptions;
|
||||
return Promise.resolve(this.tabular()).then(data => ({ data, options }));
|
||||
}
|
||||
}
|
||||
|
||||
export { DataAdapter };
|
67
src/ui/public/inspector/adapters/data/data_adapters.test.ts
Normal file
67
src/ui/public/inspector/adapters/data/data_adapters.test.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { DataAdapter } from './data_adapter';
|
||||
|
||||
describe('DataAdapter', () => {
|
||||
let adapter: DataAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new DataAdapter();
|
||||
});
|
||||
|
||||
describe('getTabular()', () => {
|
||||
it('should return a null promise when called before initialized', () => {
|
||||
expect(adapter.getTabular()).resolves.toEqual({
|
||||
data: null,
|
||||
options: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the provided callback and resolve with its value', async () => {
|
||||
const spy = jest.fn(() => 'foo');
|
||||
adapter.setTabularLoader(spy);
|
||||
expect(spy).not.toBeCalled();
|
||||
const result = await adapter.getTabular();
|
||||
expect(spy).toBeCalled();
|
||||
expect(result.data).toBe('foo');
|
||||
});
|
||||
|
||||
it('should pass through options specified via setTabularLoader', async () => {
|
||||
adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true });
|
||||
const result = await adapter.getTabular();
|
||||
expect(result.options).toEqual({ returnsFormattedValues: true });
|
||||
});
|
||||
|
||||
it('should return options set when starting loading data', async () => {
|
||||
adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true });
|
||||
const waitForResult = adapter.getTabular();
|
||||
adapter.setTabularLoader(() => 'bar', { returnsFormattedValues: false });
|
||||
const result = await waitForResult;
|
||||
expect(result.options).toEqual({ returnsFormattedValues: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit a "tabular" event when a new tabular loader is specified', () => {
|
||||
const spy = jest.fn();
|
||||
adapter.once('change', spy);
|
||||
adapter.setTabularLoader(() => 42);
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -17,10 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { uiRegistry } from './_registry';
|
||||
class FormattedData {
|
||||
constructor(readonly raw: any, readonly formatted: any) {}
|
||||
}
|
||||
|
||||
export const SpyModesRegistryProvider = uiRegistry({
|
||||
name: 'spyModes',
|
||||
index: ['name'],
|
||||
order: ['order']
|
||||
});
|
||||
export { FormattedData };
|
21
src/ui/public/inspector/adapters/data/index.ts
Normal file
21
src/ui/public/inspector/adapters/data/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { FormattedData } from './formatted_data';
|
||||
export { DataAdapter } from './data_adapter';
|
21
src/ui/public/inspector/adapters/index.ts
Normal file
21
src/ui/public/inspector/adapters/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { DataAdapter, FormattedData } from './data';
|
||||
export { RequestAdapter, RequestStatus } from './request';
|
22
src/ui/public/inspector/adapters/request/index.ts
Normal file
22
src/ui/public/inspector/adapters/request/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { RequestStatus } from './types';
|
||||
|
||||
export { RequestAdapter } from './request_adapter';
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { RequestAdapter } from './request_adapter';
|
||||
import { Request } from './types';
|
||||
|
||||
describe('RequestAdapter', () => {
|
||||
let adapter: RequestAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new RequestAdapter();
|
||||
});
|
||||
|
||||
describe('getRequests()', () => {
|
||||
function requestNames(requests: Request[]) {
|
||||
return requests.map(req => req.name);
|
||||
}
|
||||
|
||||
it('should return all started requests', () => {
|
||||
adapter.start('req1');
|
||||
adapter.start('req2');
|
||||
expect(adapter.getRequests().length).toBe(2);
|
||||
expect(requestNames(adapter.getRequests())).toEqual(['req1', 'req2']);
|
||||
});
|
||||
|
||||
it('should reset when calling reset()', () => {
|
||||
adapter.start('req1');
|
||||
expect(adapter.getRequests().length).toBe(1);
|
||||
adapter.reset();
|
||||
expect(adapter.getRequests()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not return requests started before reset, but finished after it', () => {
|
||||
const req = adapter.start('req1');
|
||||
expect(adapter.getRequests().length).toBe(1);
|
||||
adapter.reset();
|
||||
req.ok({ json: {} });
|
||||
expect(adapter.getRequests()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change events', () => {
|
||||
it('should emit it when starting a new request', () => {
|
||||
const spy = jest.fn();
|
||||
adapter.once('change', spy);
|
||||
expect(spy).not.toBeCalled();
|
||||
adapter.start('request');
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit it when updating the request', () => {
|
||||
const spy = jest.fn();
|
||||
adapter.on('change', spy);
|
||||
expect(spy).not.toBeCalled();
|
||||
const req = adapter.start('request');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
req.json({ my: 'request' });
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
req.stats({ foo: 42, bar: 'test' });
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
req.ok({ json: {} });
|
||||
expect(spy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
});
|
70
src/ui/public/inspector/adapters/request/request_adapter.ts
Normal file
70
src/ui/public/inspector/adapters/request/request_adapter.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { RequestResponder } from './request_responder';
|
||||
import { Request, RequestParams, RequestStatus } from './types';
|
||||
|
||||
/**
|
||||
* An generic inspector adapter to log requests.
|
||||
* These can be presented in the inspector using the requests view.
|
||||
* The adapter is not coupled to a specific implementation or even Elasticsearch
|
||||
* instead it offers a generic API to log requests of any kind.
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
class RequestAdapter extends EventEmitter {
|
||||
private requests: Request[] = [];
|
||||
|
||||
/**
|
||||
* Start logging a new request into this request adapter. The new request will
|
||||
* by default be in a processing state unless you explicitly finish it via
|
||||
* {@link RequestResponder#finish}, {@link RequestResponder#ok} or
|
||||
* {@link RequestResponder#error}.
|
||||
*
|
||||
* @param {string} name The name of this request as it should be shown in the UI.
|
||||
* @param {object} args Additional arguments for the request.
|
||||
* @return {RequestResponder} An instance to add information to the request and finish it.
|
||||
*/
|
||||
public start(name: string, params: RequestParams = {}): RequestResponder {
|
||||
const req: Request = {
|
||||
...params,
|
||||
name,
|
||||
startTime: Date.now(),
|
||||
status: RequestStatus.PENDING,
|
||||
};
|
||||
this.requests.push(req);
|
||||
this._onChange();
|
||||
return new RequestResponder(req, () => this._onChange());
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.requests = [];
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
public getRequests(): Request[] {
|
||||
return this.requests;
|
||||
}
|
||||
|
||||
private _onChange(): void {
|
||||
this.emit('change');
|
||||
}
|
||||
}
|
||||
|
||||
export { RequestAdapter };
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { Request, RequestStatistics, RequestStatus, Response } from './types';
|
||||
|
||||
/**
|
||||
* An API to specify information about a specific request that will be logged.
|
||||
* Create a new instance to log a request using {@link RequestAdapter#start}.
|
||||
*/
|
||||
export class RequestResponder {
|
||||
private readonly request: Request;
|
||||
private readonly onChange: () => void;
|
||||
|
||||
constructor(request: Request, onChange: () => void) {
|
||||
this.request = request;
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
public json(reqJson: object): RequestResponder {
|
||||
this.request.json = reqJson;
|
||||
this.onChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public stats(stats: RequestStatistics): RequestResponder {
|
||||
this.request.stats = {
|
||||
...(this.request.stats || {}),
|
||||
...stats,
|
||||
};
|
||||
this.onChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public finish(status: RequestStatus, response: Response): void {
|
||||
this.request.time = Date.now() - this.request.startTime;
|
||||
this.request.status = status;
|
||||
this.request.response = response;
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
public ok(response: Response): void {
|
||||
this.finish(RequestStatus.OK, response);
|
||||
}
|
||||
|
||||
public error(response: Response): void {
|
||||
this.finish(RequestStatus.ERROR, response);
|
||||
}
|
||||
}
|
58
src/ui/public/inspector/adapters/request/types.ts
Normal file
58
src/ui/public/inspector/adapters/request/types.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The status a request can have.
|
||||
*/
|
||||
export enum RequestStatus {
|
||||
/**
|
||||
* The request hasn't finished yet.
|
||||
*/
|
||||
PENDING,
|
||||
/**
|
||||
* The request has successfully finished.
|
||||
*/
|
||||
OK,
|
||||
/**
|
||||
* The request failed.
|
||||
*/
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export interface Request extends RequestParams {
|
||||
name: string;
|
||||
json?: object;
|
||||
response?: Response;
|
||||
startTime: number;
|
||||
stats?: RequestStatistics;
|
||||
status: RequestStatus;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
export interface RequestParams {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface RequestStatistics {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
json?: object;
|
||||
}
|
26
src/ui/public/inspector/index.ts
Normal file
26
src/ui/public/inspector/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { InspectorView } from './ui';
|
||||
|
||||
export { Inspector } from './inspector';
|
||||
|
||||
export { viewRegistry } from './view_registry';
|
||||
|
||||
export { Adapters } from './types';
|
109
src/ui/public/inspector/inspector.test.js
Normal file
109
src/ui/public/inspector/inspector.test.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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',
|
||||
}));
|
||||
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();
|
||||
});
|
||||
|
||||
describe('return value', () => {
|
||||
beforeEach(() => {
|
||||
setViews([{}]);
|
||||
});
|
||||
|
||||
it('should be an object with a close function', () => {
|
||||
const session = Inspector.open({});
|
||||
expect(typeof session.close).toBe('function');
|
||||
});
|
||||
|
||||
it('should emit the "closed" event if another inspector opens', () => {
|
||||
const session = Inspector.open({});
|
||||
const spy = jest.fn();
|
||||
session.on('closed', spy);
|
||||
Inspector.open({});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit the "closed" event if you call close', () => {
|
||||
const session = Inspector.open({});
|
||||
const spy = jest.fn();
|
||||
session.on('closed', spy);
|
||||
session.close();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can be bound to an angular scope', () => {
|
||||
const session = Inspector.open({});
|
||||
const spy = jest.fn();
|
||||
session.on('closed', spy);
|
||||
const scope = {
|
||||
$on: jest.fn(() => () => {})
|
||||
};
|
||||
session.bindToAngularScope(scope);
|
||||
expect(scope.$on).toHaveBeenCalled();
|
||||
const onCall = scope.$on.mock.calls[0];
|
||||
expect(onCall[0]).toBe('$destroy');
|
||||
expect(typeof onCall[1]).toBe('function');
|
||||
// Call $destroy callback, as angular would when the scope gets destroyed
|
||||
onCall[1]();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('will remove from angular scope when closed', () => {
|
||||
const session = Inspector.open({});
|
||||
const unwatchSpy = jest.fn();
|
||||
const scope = {
|
||||
$on: jest.fn(() => unwatchSpy)
|
||||
};
|
||||
session.bindToAngularScope(scope);
|
||||
session.close();
|
||||
expect(unwatchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
151
src/ui/public/inspector/inspector.tsx
Normal file
151
src/ui/public/inspector/inspector.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Adapters } from './types';
|
||||
import { InspectorPanel } from './ui/inspector_panel';
|
||||
import { viewRegistry } from './view_registry';
|
||||
|
||||
let activeSession: InspectorSession | null = null;
|
||||
|
||||
const CONTAINER_ID = 'inspector-container';
|
||||
|
||||
function getOrCreateContainerElement() {
|
||||
let container = document.getElementById(CONTAINER_ID);
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = CONTAINER_ID;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* An InspectorSession describes the session of one opened inspector. It offers
|
||||
* methods to close the inspector again. If you open an inspector you should make
|
||||
* sure you call {@link InspectorSession#close} when it should be closed.
|
||||
* Since an inspector could also be closed without calling this method (e.g. because
|
||||
* the user closes it), you must listen to the "closed" event on this instance.
|
||||
* It will be emitted whenever the inspector will be closed and you should throw
|
||||
* away your reference to this instance whenever you receive that event.
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
class InspectorSession extends EventEmitter {
|
||||
/**
|
||||
* Binds the current inspector session to an Angular scope, meaning this inspector
|
||||
* session will be closed as soon as the Angular scope gets destroyed.
|
||||
* @param {object} scope - And angular scope object to bind to.
|
||||
*/
|
||||
public bindToAngularScope(scope: ng.IScope): void {
|
||||
const removeWatch = scope.$on('$destroy', () => this.close());
|
||||
this.on('closed', () => removeWatch());
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the opened inspector as long as it's stil the open one.
|
||||
* If this is not the active session anymore, this method won't do anything.
|
||||
* If this session was still active and an inspector was closed, the 'closed'
|
||||
* event will be emitted on this InspectorSession instance.
|
||||
*/
|
||||
public close(): void {
|
||||
if (activeSession === this) {
|
||||
const container = document.getElementById(CONTAINER_ID);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
this.emit('closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
interface InspectorOptions {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// If there is an active inspector session close it before opening a new one.
|
||||
if (activeSession) {
|
||||
activeSession.close();
|
||||
}
|
||||
|
||||
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.`);
|
||||
}
|
||||
|
||||
const container = getOrCreateContainerElement();
|
||||
const session = (activeSession = new InspectorSession());
|
||||
|
||||
ReactDOM.render(
|
||||
<InspectorPanel
|
||||
views={views}
|
||||
adapters={adapters}
|
||||
onClose={() => session.close()}
|
||||
title={options.title}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
const Inspector = {
|
||||
isAvailable,
|
||||
open,
|
||||
};
|
||||
|
||||
export { Inspector };
|
63
src/ui/public/inspector/types.ts
Normal file
63
src/ui/public/inspector/types.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The interface that the adapters used to open spy panels 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<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;
|
||||
}
|
|
@ -0,0 +1,339 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InspectorPanel should render as expected 1`] = `
|
||||
<InspectorPanel
|
||||
adapters={
|
||||
Object {
|
||||
"bardapter": Object {},
|
||||
"foodapter": Object {
|
||||
"foo": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
onClose={[Function]}
|
||||
title="Inspector"
|
||||
views={
|
||||
Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 200,
|
||||
"title": "View 1",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 100,
|
||||
"shouldShow": [Function],
|
||||
"title": "Foo View",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 200,
|
||||
"shouldShow": [Function],
|
||||
"title": "Never",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<EuiFlyout
|
||||
data-test-subj="inspectorPanel"
|
||||
hideCloseButton={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<span>
|
||||
<FocusTrap
|
||||
_createFocusTrap={[Function]}
|
||||
active={true}
|
||||
focusTrapOptions={
|
||||
Object {
|
||||
"clickOutsideDeactivates": true,
|
||||
"fallbackFocus": [Function],
|
||||
}
|
||||
}
|
||||
paused={false}
|
||||
tag="div"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="euiFlyout euiFlyout--medium"
|
||||
data-test-subj="inspectorPanel"
|
||||
onKeyDown={[Function]}
|
||||
role="dialog"
|
||||
tabIndex={0}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Closes this dialog"
|
||||
className="euiFlyout__closeButton"
|
||||
color="text"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
aria-label="Closes this dialog"
|
||||
className="euiButtonIcon euiButtonIcon--text euiFlyout__closeButton"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<EuiIcon
|
||||
aria-hidden="true"
|
||||
className="euiButtonIcon__icon"
|
||||
size="m"
|
||||
type="cross"
|
||||
>
|
||||
<cross
|
||||
aria-hidden="true"
|
||||
className="euiIcon euiIcon--medium euiButtonIcon__icon"
|
||||
focusable="false"
|
||||
height="16"
|
||||
style={
|
||||
Object {
|
||||
"fill": undefined,
|
||||
}
|
||||
}
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="euiIcon euiIcon--medium euiButtonIcon__icon"
|
||||
focusable="false"
|
||||
height="16"
|
||||
style={
|
||||
Object {
|
||||
"fill": undefined,
|
||||
}
|
||||
}
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M7.293 8l-4.147 4.146a.5.5 0 0 0 .708.708L8 8.707l4.146 4.147a.5.5 0 0 0 .708-.708L8.707 8l4.147-4.146a.5.5 0 0 0-.708-.708L8 7.293 3.854 3.146a.5.5 0 1 0-.708.708L7.293 8z"
|
||||
id="cross-a"
|
||||
/>
|
||||
</defs>
|
||||
<use
|
||||
fillRule="nonzero"
|
||||
xlinkHref="#cross-a"
|
||||
/>
|
||||
</svg>
|
||||
</cross>
|
||||
</EuiIcon>
|
||||
</button>
|
||||
</EuiButtonIcon>
|
||||
<EuiFlyoutHeader
|
||||
hasBorder={true}
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutHeader euiFlyoutHeader--hasBorder"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem"
|
||||
>
|
||||
<EuiTitle
|
||||
size="s"
|
||||
>
|
||||
<h1
|
||||
className="euiTitle euiTitle--small"
|
||||
>
|
||||
Inspector
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<InspectorViewChooser
|
||||
onViewSelected={[Function]}
|
||||
selectedView={
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 200,
|
||||
"title": "View 1",
|
||||
}
|
||||
}
|
||||
views={
|
||||
Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 200,
|
||||
"title": "View 1",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 100,
|
||||
"shouldShow": [Function],
|
||||
"title": "Foo View",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"order": 200,
|
||||
"shouldShow": [Function],
|
||||
"title": "Never",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
data-test-subj="inspectorViewChooser"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
View:
|
||||
View 1
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
id="inspectorViewChooser"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiOutsideClickDetector
|
||||
onOutsideClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiPopover euiPopover--anchorDownRight"
|
||||
id="inspectorViewChooser"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
data-test-subj="inspectorViewChooser"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small euiButtonEmpty--iconRight"
|
||||
data-test-subj="inspectorViewChooser"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="euiButtonEmpty__content"
|
||||
>
|
||||
<EuiIcon
|
||||
aria-hidden="true"
|
||||
className="euiButtonEmpty__icon"
|
||||
size="m"
|
||||
type="arrowDown"
|
||||
>
|
||||
<arrowDown
|
||||
aria-hidden="true"
|
||||
className="euiIcon euiIcon--medium euiButtonEmpty__icon"
|
||||
focusable="false"
|
||||
height="16"
|
||||
style={
|
||||
Object {
|
||||
"fill": undefined,
|
||||
}
|
||||
}
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="euiIcon euiIcon--medium euiButtonEmpty__icon"
|
||||
focusable="false"
|
||||
height="16"
|
||||
style={
|
||||
Object {
|
||||
"fill": undefined,
|
||||
}
|
||||
}
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
|
||||
id="arrow_down-a"
|
||||
/>
|
||||
</defs>
|
||||
<use
|
||||
fillRule="nonzero"
|
||||
xlinkHref="#arrow_down-a"
|
||||
/>
|
||||
</svg>
|
||||
</arrowDown>
|
||||
</EuiIcon>
|
||||
<span>
|
||||
View:
|
||||
View 1
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</EuiOutsideClickDetector>
|
||||
</EuiPopover>
|
||||
</InspectorViewChooser>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiFlyoutHeader>
|
||||
<component
|
||||
adapters={
|
||||
Object {
|
||||
"bardapter": Object {},
|
||||
"foodapter": Object {
|
||||
"foo": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
title="Inspector"
|
||||
>
|
||||
<h1>
|
||||
View 1
|
||||
</h1>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</span>
|
||||
</EuiFlyout>
|
||||
</InspectorPanel>
|
||||
`;
|
20
src/ui/public/inspector/ui/index.ts
Normal file
20
src/ui/public/inspector/ui/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { InspectorView } from './inspector_view';
|
7
src/ui/public/inspector/ui/inspector.less
Normal file
7
src/ui/public/inspector/ui/inspector.less
Normal file
|
@ -0,0 +1,7 @@
|
|||
.inspector-view-chooser__icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.inspector-view__flex {
|
||||
display: flex;
|
||||
}
|
|
@ -16,18 +16,15 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ComponentClass } from 'react';
|
||||
|
||||
import visDebugSpyPanelTemplate from './vis_debug_spy_panel.html';
|
||||
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
|
||||
import { Adapters, InspectorViewDescription } from '../types';
|
||||
|
||||
function VisDetailsSpyProvider() {
|
||||
return {
|
||||
name: 'debug',
|
||||
display: 'Debug',
|
||||
template: visDebugSpyPanelTemplate,
|
||||
order: 5
|
||||
};
|
||||
interface InspectorPanelProps {
|
||||
adapters: Adapters;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
views: InspectorViewDescription[];
|
||||
}
|
||||
|
||||
// register the spy mode or it won't show up in the spys
|
||||
SpyModesRegistryProvider.register(VisDetailsSpyProvider);
|
||||
export const InspectorPanel: ComponentClass<InspectorPanelProps>;
|
132
src/ui/public/inspector/ui/inspector_panel.js
Normal file
132
src/ui/public/inspector/ui/inspector_panel.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { InspectorViewChooser } from './inspector_view_chooser';
|
||||
|
||||
import './inspector.less';
|
||||
|
||||
function hasAdaptersChanged(oldAdapters, newAdapters) {
|
||||
return Object.keys(oldAdapters).length !== Object.keys(newAdapters).length
|
||||
|| Object.keys(oldAdapters).some(key => oldAdapters[key] !== newAdapters[key]);
|
||||
}
|
||||
|
||||
class InspectorPanel extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHelpPopoverOpen: false,
|
||||
selectedView: props.views[0],
|
||||
views: props.views,
|
||||
// Clone adapters array so we can validate that this prop never change
|
||||
adapters: { ...props.adapters },
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
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);
|
||||
return {
|
||||
views: nextProps.views,
|
||||
selectedView: selectedViewMustChange ? nextProps.views[0] : prevState.selectedView,
|
||||
};
|
||||
}
|
||||
|
||||
onViewSelected = (view) => {
|
||||
if (view !== this.state.selectedView) {
|
||||
this.setState({
|
||||
selectedView: view
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderSelectedPanel() {
|
||||
return (
|
||||
<this.state.selectedView.component
|
||||
adapters={this.props.adapters}
|
||||
title={this.props.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { views, onClose, title } = this.props;
|
||||
const { selectedView } = this.state;
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="inspectorPanel"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="s">
|
||||
<h1>{ title }</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<InspectorViewChooser
|
||||
views={views}
|
||||
onViewSelected={this.onViewSelected}
|
||||
selectedView={selectedView}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
{ this.renderSelectedPanel() }
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InspectorPanel.defaultProps = {
|
||||
title: 'Inspector',
|
||||
};
|
||||
|
||||
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}.`
|
||||
);
|
||||
}
|
||||
},
|
||||
onClose: PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
export { InspectorPanel };
|
83
src/ui/public/inspector/ui/inspector_panel.test.js
Normal file
83
src/ui/public/inspector/ui/inspector_panel.test.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { InspectorPanel } from './inspector_panel';
|
||||
|
||||
describe('InspectorPanel', () => {
|
||||
|
||||
let adapters;
|
||||
let views;
|
||||
|
||||
beforeEach(() => {
|
||||
adapters = {
|
||||
foodapter: {
|
||||
foo() { return 42; }
|
||||
},
|
||||
bardapter: {
|
||||
|
||||
}
|
||||
};
|
||||
views = [
|
||||
{
|
||||
title: 'View 1',
|
||||
order: 200,
|
||||
component: () => (<h1>View 1</h1>),
|
||||
}, {
|
||||
title: 'Foo View',
|
||||
order: 100,
|
||||
component: () => (<h1>Foo view</h1>),
|
||||
shouldShow(adapters) {
|
||||
return adapters.foodapter;
|
||||
}
|
||||
}, {
|
||||
title: 'Never',
|
||||
order: 200,
|
||||
component: () => null,
|
||||
shouldShow() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
it('should render as expected', () => {
|
||||
const component = mount(
|
||||
<InspectorPanel
|
||||
adapters={adapters}
|
||||
onClose={() => true}
|
||||
views={views}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not allow updating adapters', () => {
|
||||
const component = mount(
|
||||
<InspectorPanel
|
||||
adapters={adapters}
|
||||
onClose={() => true}
|
||||
views={views}
|
||||
/>
|
||||
);
|
||||
adapters.notAllowed = {};
|
||||
expect(() => component.setProps({ adapters })).toThrow();
|
||||
});
|
||||
});
|
53
src/ui/public/inspector/ui/inspector_view.tsx
Normal file
53
src/ui/public/inspector/ui/inspector_view.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
// TODO: Remove once EUI has typing for EuiFlyoutBody
|
||||
declare module '@elastic/eui' {
|
||||
export const EuiFlyoutBody: React.SFC<any>;
|
||||
}
|
||||
|
||||
import { EuiFlyoutBody } from '@elastic/eui';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const InspectorView: React.SFC<{ useFlex?: boolean }> = ({
|
||||
useFlex,
|
||||
children,
|
||||
}) => {
|
||||
const classes = classNames({
|
||||
'inspector-view__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 };
|
124
src/ui/public/inspector/ui/inspector_view_chooser.js
Normal file
124
src/ui/public/inspector/ui/inspector_view_chooser.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
class InspectorViewChooser extends Component {
|
||||
|
||||
state = {
|
||||
isSelectorOpen: false
|
||||
};
|
||||
|
||||
toggleSelector = () => {
|
||||
this.setState((prev) => ({
|
||||
isSelectorOpen: !prev.isSelectorOpen
|
||||
}));
|
||||
};
|
||||
|
||||
closeSelector = () => {
|
||||
this.setState({
|
||||
isSelectorOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
renderView = (view, index) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
this.props.onViewSelected(view);
|
||||
this.closeSelector();
|
||||
}}
|
||||
toolTipContent={view.help}
|
||||
toolTipPosition="left"
|
||||
data-test-subj={`inspectorViewChooser${view.title}`}
|
||||
>
|
||||
{view.title}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderViewButton() {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={this.toggleSelector}
|
||||
data-test-subj="inspectorViewChooser"
|
||||
>
|
||||
View: { this.props.selectedView.title }
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
renderSingleView() {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={this.props.selectedView.help}
|
||||
>
|
||||
<span>View: { this.props.selectedView.title }</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { views } = this.props;
|
||||
|
||||
if (views.length < 2) {
|
||||
return this.renderSingleView();
|
||||
}
|
||||
|
||||
const triggerButton = this.renderViewButton();
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="inspectorViewChooser"
|
||||
ownFocus
|
||||
button={triggerButton}
|
||||
isOpen={this.state.isSelectorOpen}
|
||||
closePopover={this.closeSelector}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={views.map(this.renderView)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InspectorViewChooser.propTypes = {
|
||||
views: PropTypes.array.isRequired,
|
||||
onViewSelected: PropTypes.func.isRequired,
|
||||
selectedView: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { InspectorViewChooser };
|
104
src/ui/public/inspector/view_registry.test.ts
Normal file
104
src/ui/public/inspector/view_registry.test.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 {
|
||||
InspectorViewDescription,
|
||||
InspectorViewRegistry,
|
||||
} from './view_registry';
|
||||
|
||||
import { Adapters } from './types';
|
||||
|
||||
function createMockView(
|
||||
params: {
|
||||
help?: string;
|
||||
order?: number;
|
||||
shouldShow?: (view?: Adapters) => boolean;
|
||||
title?: string;
|
||||
} = {}
|
||||
): InspectorViewDescription {
|
||||
return {
|
||||
component: () => null,
|
||||
help: params.help || 'help text',
|
||||
order: params.order,
|
||||
shouldShow: params.shouldShow,
|
||||
title: params.title || 'view',
|
||||
};
|
||||
}
|
||||
|
||||
describe('InspectorViewRegistry', () => {
|
||||
let registry: InspectorViewRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new InspectorViewRegistry();
|
||||
});
|
||||
|
||||
it('should emit a change event when registering a view', () => {
|
||||
const listener = jest.fn();
|
||||
registry.once('change', listener);
|
||||
registry.register(createMockView());
|
||||
expect(listener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return views ordered by their order property', () => {
|
||||
const view1 = createMockView({ title: 'view1', order: 2000 });
|
||||
const view2 = createMockView({ title: 'view2', order: 1000 });
|
||||
registry.register(view1);
|
||||
registry.register(view2);
|
||||
const views = registry.getAll();
|
||||
expect(views.map(v => v.title)).toEqual(['view2', 'view1']);
|
||||
});
|
||||
|
||||
describe('getVisible()', () => {
|
||||
it('should return empty array on passing undefined to the registry', () => {
|
||||
const view1 = createMockView({ title: 'view1', shouldShow: () => true });
|
||||
const view2 = createMockView({ title: 'view2', shouldShow: () => false });
|
||||
registry.register(view1);
|
||||
registry.register(view2);
|
||||
const views = registry.getVisible();
|
||||
expect(views).toEqual([]);
|
||||
});
|
||||
|
||||
it('should only return matching views', () => {
|
||||
const view1 = createMockView({ title: 'view1', shouldShow: () => true });
|
||||
const view2 = createMockView({ title: 'view2', shouldShow: () => false });
|
||||
registry.register(view1);
|
||||
registry.register(view2);
|
||||
const views = registry.getVisible({});
|
||||
expect(views.map(v => v.title)).toEqual(['view1']);
|
||||
});
|
||||
|
||||
it('views without shouldShow should be included', () => {
|
||||
const view1 = createMockView({ title: 'view1', shouldShow: () => true });
|
||||
const view2 = createMockView({ title: 'view2' });
|
||||
registry.register(view1);
|
||||
registry.register(view2);
|
||||
const views = registry.getVisible({});
|
||||
expect(views.map(v => v.title)).toEqual(['view1', 'view2']);
|
||||
});
|
||||
|
||||
it('should pass the adapters to the callbacks', () => {
|
||||
const shouldShow = jest.fn();
|
||||
const view1 = createMockView({ shouldShow });
|
||||
registry.register(view1);
|
||||
const adapter = { foo: () => null };
|
||||
registry.getVisible(adapter);
|
||||
expect(shouldShow).toHaveBeenCalledWith(adapter);
|
||||
});
|
||||
});
|
||||
});
|
86
src/ui/public/inspector/view_registry.ts
Normal file
86
src/ui/public/inspector/view_registry.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
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 };
|
|
@ -20,6 +20,7 @@
|
|||
aria-label="{{::menuItem.description}}"
|
||||
aria-haspopup="{{!menuItem.hasFunction}}"
|
||||
aria-expanded="{{kbnTopNav.isCurrent(menuItem.key)}}"
|
||||
aria-disabled="{{menuItem.disableButton()}}"
|
||||
ng-class="{'kuiLocalMenuItem-isSelected': kbnTopNav.isCurrent(menuItem.key), 'kuiLocalMenuItem-isDisabled': menuItem.disableButton()}"
|
||||
ng-click="kbnTopNav.handleClick(menuItem)"
|
||||
ng-bind="menuItem.label"
|
||||
|
|
|
@ -274,25 +274,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// /src/ui/public/visualize/visualize.less
|
||||
.visualize-show-spy {
|
||||
border-top-color: @visualize-show-spy-border;
|
||||
}
|
||||
|
||||
visualize-spy {
|
||||
background-color: @visualize-spy-container-pre-bg;
|
||||
}
|
||||
|
||||
.visualize-spy-container {
|
||||
pre {
|
||||
border-color: @visualize-show-spy-border;
|
||||
color: @visualize-spy-container-pre-color;
|
||||
opacity: 1;
|
||||
background-color: @visualize-spy-container-pre-bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// /src/ui/public/vislib/styles/_svg.less
|
||||
.axis {
|
||||
line, path {
|
||||
|
|
|
@ -77,14 +77,6 @@
|
|||
@collapser-hover-bg: @gray6;
|
||||
@collapser-hover-color: @gray3;
|
||||
|
||||
@visualize-show-spy-border: @collapser-border;
|
||||
@visualize-show-spy-bg: @collapser-bg;
|
||||
@visualize-show-spy-color: @collapser-color;
|
||||
@visualize-show-spy-hover-bg: @collapser-hover-bg;
|
||||
@visualize-show-spy-hover-color: @collapser-hover-color;
|
||||
@visualize-spy-container-pre-color: #a6a6a6;
|
||||
@visualize-spy-container-pre-bg: darken(@panel-bg, 5%);
|
||||
|
||||
@svg-axis-color: @gray8;
|
||||
@svg-tick-text-color: @gray10;
|
||||
@svg-brush-color: @white;
|
||||
|
|
|
@ -137,13 +137,6 @@
|
|||
@settings-add-data-wizard-parse-csv-container-border: @kibanaBlue3;
|
||||
|
||||
// Visualize ===================================================================
|
||||
@visualize-show-spy-border: @gray-lighter;
|
||||
@visualize-show-spy-bg: @white;
|
||||
@visualize-show-spy-color: @gray3;
|
||||
|
||||
@visualize-show-spy-hover-bg: @gray-lighter;
|
||||
@visualize-show-spy-hover-color: @gray3;
|
||||
|
||||
@visualize-info-bg: @globalColorLightGray;
|
||||
|
||||
|
||||
|
@ -172,8 +165,8 @@
|
|||
@vis-editor-agg-wide-btn-border: @gray-lighter;
|
||||
@vis-editor-agg-wide-btn-bg: @body-bg;
|
||||
|
||||
@vis-editor-agg-wide-btn-hover-bg: @visualize-show-spy-hover-bg;
|
||||
@vis-editor-agg-wide-btn-hover-color: @visualize-show-spy-hover-color;
|
||||
@vis-editor-agg-wide-btn-hover-bg: @gray-lighter;
|
||||
@vis-editor-agg-wide-btn-hover-color: @gray3;
|
||||
|
||||
@vis-editor-agg-editor-order-bg: transparent;
|
||||
@vis-editor-agg-editor-order-border: @gray-lighter;
|
||||
|
|
|
@ -19,10 +19,13 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { VisProvider } from '..';
|
||||
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import { VisTypesRegistryProvider } from '../../registry/vis_types';
|
||||
import { DataAdapter, RequestAdapter } from '../../inspector/adapters';
|
||||
import { Inspector } from '../../inspector/inspector';
|
||||
|
||||
describe('Vis Class', function () {
|
||||
let indexPattern;
|
||||
|
@ -106,4 +109,151 @@ describe('Vis Class', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('inspector', () => {
|
||||
|
||||
// Wrap the given vis type definition in a state, that can be passed to vis
|
||||
const state = (type) => ({
|
||||
type: {
|
||||
visConfig: { defaults: {} },
|
||||
...type,
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasInspector()', () => {
|
||||
it('should forward to inspectors hasInspector', () => {
|
||||
const vis = new Vis(indexPattern, state({
|
||||
inspectorAdapters: {
|
||||
data: true,
|
||||
requests: true,
|
||||
}
|
||||
}));
|
||||
sinon.spy(Inspector, 'isAvailable');
|
||||
vis.hasInspector();
|
||||
expect(Inspector.isAvailable.calledOnce).to.be(true);
|
||||
const adapters = Inspector.isAvailable.lastCall.args[0];
|
||||
expect(adapters.data).to.be.a(DataAdapter);
|
||||
expect(adapters.requests).to.be.a(RequestAdapter);
|
||||
});
|
||||
|
||||
it('should return hasInspectors result', () => {
|
||||
const vis = new Vis(indexPattern, state({}));
|
||||
const stub = sinon.stub(Inspector, 'isAvailable');
|
||||
stub.returns(true);
|
||||
expect(vis.hasInspector()).to.be(true);
|
||||
stub.returns(false);
|
||||
expect(vis.hasInspector()).to.be(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Inspector.isAvailable.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('openInspector()', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(Inspector, 'open');
|
||||
});
|
||||
|
||||
it('should call openInspector with all attached inspectors', () => {
|
||||
const Foodapter = class {};
|
||||
const vis = new Vis(indexPattern, state({
|
||||
inspectorAdapters: {
|
||||
data: true,
|
||||
custom: {
|
||||
foo: Foodapter
|
||||
}
|
||||
}
|
||||
}));
|
||||
vis.openInspector();
|
||||
expect(Inspector.open.calledOnce).to.be(true);
|
||||
const adapters = Inspector.open.lastCall.args[0];
|
||||
expect(adapters).to.be(vis.API.inspectorAdapters);
|
||||
});
|
||||
|
||||
it('should pass the vis title to the openInspector call', () => {
|
||||
const vis = new Vis(indexPattern, { ...state(), title: 'beautifulVis' });
|
||||
vis.openInspector();
|
||||
expect(Inspector.open.calledOnce).to.be(true);
|
||||
const params = Inspector.open.lastCall.args[1];
|
||||
expect(params.title).to.be('beautifulVis');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Inspector.open.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspectorAdapters', () => {
|
||||
|
||||
it('should register none for none requestHandler', () => {
|
||||
const vis = new Vis(indexPattern, state({ requestHandler: 'none' }));
|
||||
expect(vis.API.inspectorAdapters).to.eql({});
|
||||
});
|
||||
|
||||
it('should attach data and request handler for courier', () => {
|
||||
const vis = new Vis(indexPattern, state({ requestHandler: 'courier' }));
|
||||
expect(vis.API.inspectorAdapters.data).to.be.a(DataAdapter);
|
||||
expect(vis.API.inspectorAdapters.requests).to.be.a(RequestAdapter);
|
||||
});
|
||||
|
||||
it('should allow enabling data adapter manually', () => {
|
||||
const vis = new Vis(indexPattern, state({
|
||||
requestHandler: 'none',
|
||||
inspectorAdapters: {
|
||||
data: true,
|
||||
}
|
||||
}));
|
||||
expect(vis.API.inspectorAdapters.data).to.be.a(DataAdapter);
|
||||
});
|
||||
|
||||
it('should allow enabling requests adapter manually', () => {
|
||||
const vis = new Vis(indexPattern, state({
|
||||
requestHandler: 'none',
|
||||
inspectorAdapters: {
|
||||
requests: true,
|
||||
}
|
||||
}));
|
||||
expect(vis.API.inspectorAdapters.requests).to.be.a(RequestAdapter);
|
||||
});
|
||||
|
||||
it('should allow adding custom inspector adapters via the custom key', () => {
|
||||
const Foodapter = class {};
|
||||
const Bardapter = class {};
|
||||
const vis = new Vis(indexPattern, state({
|
||||
requestHandler: 'none',
|
||||
inspectorAdapters: {
|
||||
custom: {
|
||||
foo: Foodapter,
|
||||
bar: Bardapter,
|
||||
}
|
||||
}
|
||||
}));
|
||||
expect(vis.API.inspectorAdapters.foo).to.be.a(Foodapter);
|
||||
expect(vis.API.inspectorAdapters.bar).to.be.a(Bardapter);
|
||||
});
|
||||
|
||||
it('should not share adapter instances between vis instances', () => {
|
||||
const Foodapter = class {};
|
||||
const visState = state({
|
||||
inspectorAdapters: {
|
||||
data: true,
|
||||
custom: {
|
||||
foo: Foodapter
|
||||
}
|
||||
}
|
||||
});
|
||||
const vis1 = new Vis(indexPattern, visState);
|
||||
const vis2 = new Vis(indexPattern, visState);
|
||||
expect(vis1.API.inspectorAdapters.foo).to.be.a(Foodapter);
|
||||
expect(vis2.API.inspectorAdapters.foo).to.be.a(Foodapter);
|
||||
expect(vis1.API.inspectorAdapters.foo).not.to.be(vis2.API.inspectorAdapters.foo);
|
||||
expect(vis1.API.inspectorAdapters.data).to.be.a(DataAdapter);
|
||||
expect(vis2.API.inspectorAdapters.data).to.be.a(DataAdapter);
|
||||
expect(vis1.API.inspectorAdapters.data).not.to.be(vis2.API.inspectorAdapters.data);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
vis-data="visData"
|
||||
ui-state="uiState"
|
||||
search-source="searchSource"
|
||||
show-spy-panel="showSpyPanel"
|
||||
listen-on-change="false"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -34,10 +34,9 @@ const defaultEditor = function ($rootScope, $compile) {
|
|||
return class DefaultEditor {
|
||||
static key = 'default';
|
||||
|
||||
constructor(el, vis, showSpyPanel) {
|
||||
constructor(el, vis) {
|
||||
this.el = $(el);
|
||||
this.vis = vis;
|
||||
this.showSpyPanel = showSpyPanel;
|
||||
|
||||
if (!this.vis.type.editorConfig.optionTabs && this.vis.type.editorConfig.optionsTemplate) {
|
||||
this.vis.type.editorConfig.optionTabs = [
|
||||
|
@ -50,7 +49,6 @@ const defaultEditor = function ($rootScope, $compile) {
|
|||
let $scope;
|
||||
|
||||
const updateScope = () => {
|
||||
$scope.showSpyPanel = this.showSpyPanel;
|
||||
$scope.vis = this.vis;
|
||||
$scope.visData = visData;
|
||||
$scope.uiState = uiState;
|
||||
|
|
|
@ -21,10 +21,56 @@ import _ from 'lodash';
|
|||
import { SearchSourceProvider } from '../../courier/data_source/search_source';
|
||||
import { VisRequestHandlersRegistryProvider } from '../../registry/vis_request_handlers';
|
||||
import { calculateObjectHash } from '../lib/calculate_object_hash';
|
||||
import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils';
|
||||
import { tabifyAggResponse } from '../../agg_response/tabify/tabify';
|
||||
|
||||
import { FormattedData } from '../../inspector/adapters';
|
||||
|
||||
const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
|
||||
const SearchSource = Private(SearchSourceProvider);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async function buildTabularInspectorData(vis, searchSource) {
|
||||
const table = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), searchSource.finalResponse, {
|
||||
canSplit: false,
|
||||
asAggConfigResults: false,
|
||||
partialRows: true,
|
||||
isHierarchical: vis.isHierarchical(),
|
||||
});
|
||||
const columns = table.columns.map((col, index) => {
|
||||
const field = col.aggConfig.getField();
|
||||
const isCellContentFilterable =
|
||||
col.aggConfig.isFilterable()
|
||||
&& (!field || field.filterable);
|
||||
return ({
|
||||
name: col.title,
|
||||
field: `col${index}`,
|
||||
filter: isCellContentFilterable && ((value) => {
|
||||
const filter = col.aggConfig.createFilter(value.raw);
|
||||
vis.API.queryFilter.addFilters(filter);
|
||||
}),
|
||||
filterOut: isCellContentFilterable && ((value) => {
|
||||
const filter = col.aggConfig.createFilter(value.raw);
|
||||
filter.meta = filter.meta || {};
|
||||
filter.meta.negate = true;
|
||||
vis.API.queryFilter.addFilters(filter);
|
||||
}),
|
||||
});
|
||||
});
|
||||
const rows = table.rows.map(row => {
|
||||
return row.reduce((prev, cur, index) => {
|
||||
const fieldFormatter = table.columns[index].aggConfig.fieldFormatter('text');
|
||||
prev[`col${index}`] = new FormattedData(cur, fieldFormatter(cur));
|
||||
return prev;
|
||||
}, {});
|
||||
});
|
||||
|
||||
return { columns, rows };
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'courier',
|
||||
handler: function (vis, { searchSource, timeRange, query, filters, forceFetch }) {
|
||||
|
@ -75,8 +121,19 @@ const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
|
|||
return requestSearchSource.getSearchRequestBody().then(q => {
|
||||
const queryHash = calculateObjectHash(q);
|
||||
if (shouldQuery(queryHash)) {
|
||||
vis.API.inspectorAdapters.requests.reset();
|
||||
const request = vis.API.inspectorAdapters.requests.start('Data', {
|
||||
description: `This request queries Elasticsearch to fetch the data for the visualization.`,
|
||||
});
|
||||
request.stats(getRequestInspectorStats(requestSearchSource));
|
||||
|
||||
requestSearchSource.onResults().then(resp => {
|
||||
searchSource.lastQuery = queryHash;
|
||||
|
||||
request
|
||||
.stats(getResponseInspectorStats(searchSource, resp))
|
||||
.ok({ json: resp });
|
||||
|
||||
searchSource.rawResponse = resp;
|
||||
return _.cloneDeep(resp);
|
||||
}).then(async resp => {
|
||||
|
@ -88,9 +145,19 @@ const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
|
|||
}
|
||||
|
||||
searchSource.finalResponse = resp;
|
||||
|
||||
vis.API.inspectorAdapters.data.setTabularLoader(
|
||||
() => buildTabularInspectorData(vis, searchSource),
|
||||
{ returnsFormattedValues: true }
|
||||
);
|
||||
|
||||
resolve(resp);
|
||||
}).catch(e => reject(e));
|
||||
|
||||
requestSearchSource.getSearchRequestBody().then(req => {
|
||||
request.json(req);
|
||||
});
|
||||
|
||||
courier.fetch();
|
||||
} else {
|
||||
resolve(searchSource.finalResponse);
|
||||
|
|
|
@ -40,6 +40,9 @@ import { queryManagerFactory } from '../query_manager';
|
|||
import { SearchSourceProvider } from '../courier/data_source/search_source';
|
||||
import { SavedObjectsClientProvider } from '../saved_objects';
|
||||
|
||||
import { Inspector } from '../inspector';
|
||||
import { RequestAdapter, DataAdapter } from '../inspector/adapters';
|
||||
|
||||
const getTerms = (table, columnIndex, rowIndex) => {
|
||||
// get only rows where cell value matches current row for all the fields before columnIndex
|
||||
const rows = table.rows.filter(row => row.every((cell, i) => cell === table.rows[rowIndex][i] || i >= columnIndex));
|
||||
|
@ -114,10 +117,59 @@ export function VisProvider(Private, Promise, indexPatterns, timefilter, getAppS
|
|||
throw new Error('Unable to inherit search source, visualize saved object does not have search source.');
|
||||
}
|
||||
return new SearchSource().inherits(parentSearchSource);
|
||||
}
|
||||
},
|
||||
inspectorAdapters: this._getActiveInspectorAdapters(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the inspector for this visualization.
|
||||
* @return {InspectorSession} the handler for the session of this inspector.
|
||||
*/
|
||||
openInspector() {
|
||||
return Inspector.open(this.API.inspectorAdapters, {
|
||||
title: this.title
|
||||
});
|
||||
}
|
||||
|
||||
hasInspector() {
|
||||
return Inspector.isAvailable(this.API.inspectorAdapters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object of all inspectors for this vis object.
|
||||
* This must only be called after this.type has properly be initialized,
|
||||
* since we need to read out data from the the vis type to check which
|
||||
* inspectors are available.
|
||||
*/
|
||||
_getActiveInspectorAdapters() {
|
||||
const adapters = {};
|
||||
const { inspectorAdapters: typeAdapters } = this.type;
|
||||
|
||||
// Add the requests inspector adapters if the vis type explicitly requested it via
|
||||
// inspectorAdapters.requests: true in its definition or if it's using the courier
|
||||
// request handler, since that will automatically log its requests.
|
||||
if (typeAdapters && typeAdapters.requests || this.type.requestHandler === 'courier') {
|
||||
adapters.requests = new RequestAdapter();
|
||||
}
|
||||
|
||||
// Add the data inspector adapter if the vis type requested it or if the
|
||||
// vis is using courier, since we know that courier supports logging
|
||||
// its data.
|
||||
if (typeAdapters && typeAdapters.data || this.type.requestHandler === 'courier') {
|
||||
adapters.data = new DataAdapter();
|
||||
}
|
||||
|
||||
// Add all inspectors, that are explicitly registered with this vis type
|
||||
if (typeAdapters && typeAdapters.custom) {
|
||||
Object.entries(typeAdapters.custom).forEach(([key, Adapter]) => {
|
||||
adapters[key] = new Adapter();
|
||||
});
|
||||
}
|
||||
|
||||
return adapters;
|
||||
}
|
||||
|
||||
isEditorMode() {
|
||||
return this.editorMode || false;
|
||||
}
|
||||
|
|
|
@ -1,331 +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 $ from 'jquery';
|
||||
import sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import ngMock from 'ng_mock';
|
||||
import angular from 'angular';
|
||||
import { VisProvider } from '../../vis';
|
||||
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
|
||||
import { uiRegistry } from '../../registry/_registry';
|
||||
import { SpyModesRegistryProvider } from '../../registry/spy_modes';
|
||||
import mockUiState from 'fixtures/mock_ui_state';
|
||||
|
||||
describe('visualize spy panel', function () {
|
||||
let $scope;
|
||||
let $compile;
|
||||
let $timeout;
|
||||
let $el;
|
||||
let visElement;
|
||||
let Vis;
|
||||
let indexPattern;
|
||||
let fixtures;
|
||||
let searchSource;
|
||||
let vis;
|
||||
let spyModeStubRegistry;
|
||||
|
||||
beforeEach(ngMock.module('kibana', 'kibana/table_vis', (PrivateProvider) => {
|
||||
spyModeStubRegistry = uiRegistry({
|
||||
name: 'spyModes',
|
||||
index: ['name'],
|
||||
order: ['order']
|
||||
});
|
||||
|
||||
PrivateProvider.swap(SpyModesRegistryProvider, spyModeStubRegistry);
|
||||
}));
|
||||
|
||||
beforeEach(ngMock.inject(function (Private, $injector) {
|
||||
$scope = $injector.get('$rootScope').$new();
|
||||
$timeout = $injector.get('$timeout');
|
||||
$compile = $injector.get('$compile');
|
||||
visElement = angular.element('<div>');
|
||||
visElement.width(500);
|
||||
visElement.height(500);
|
||||
fixtures = require('fixtures/fake_hierarchical_data');
|
||||
Vis = Private(VisProvider);
|
||||
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
searchSource = Private(FixturesStubbedSearchSourceProvider);
|
||||
vis = new CreateVis(null, false);
|
||||
init(vis, fixtures.oneRangeBucket);
|
||||
}));
|
||||
|
||||
// basically a parameterized beforeEach
|
||||
function init(vis, esResponse) {
|
||||
vis.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); });
|
||||
mockUiState._reset();
|
||||
$scope.vis = vis;
|
||||
$scope.esResponse = esResponse;
|
||||
$scope.uiState = mockUiState;
|
||||
$scope.searchSource = searchSource;
|
||||
$scope.visElement = visElement;
|
||||
}
|
||||
|
||||
function CreateVis(params, requiresSearch) {
|
||||
const vis = new Vis(indexPattern, {
|
||||
type: 'table',
|
||||
params: params || {},
|
||||
aggs: [
|
||||
{ type: 'count', schema: 'metric' },
|
||||
{
|
||||
type: 'range',
|
||||
schema: 'bucket',
|
||||
params: {
|
||||
field: 'bytes',
|
||||
ranges: [
|
||||
{ from: 0, to: 1000 },
|
||||
{ from: 1000, to: 2000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
vis.type.requestHandler = requiresSearch ? 'default' : 'none';
|
||||
vis.type.responseHandler = 'none';
|
||||
vis.type.requiresSearch = false;
|
||||
return vis;
|
||||
}
|
||||
|
||||
|
||||
function compile() {
|
||||
const spyElem = $('<visualize-spy vis="vis" vis-element="visElement" search-source="searchSource" ui-state="uiState">');
|
||||
const $el = $compile(spyElem)($scope);
|
||||
$scope.$apply();
|
||||
|
||||
$el.toggleButton = $el.find('[data-test-subj="spyToggleButton"]');
|
||||
$el.maximizedButton = $el.find('[data-test-subj="toggleSpyFullscreen"]');
|
||||
$el.panel = $el.find('[data-test-subj="spyContainer"]');
|
||||
$el.tabs = $el.find('[data-test-subj="spyModTabs"]');
|
||||
|
||||
return $el;
|
||||
}
|
||||
|
||||
function fillRegistryAndCompile() {
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'spymode1',
|
||||
display: 'SpyMode1',
|
||||
order: 1,
|
||||
template: '<div></div>',
|
||||
}));
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'spymode2',
|
||||
display: 'SpyMode2',
|
||||
order: 2,
|
||||
template: '<div></div>',
|
||||
}));
|
||||
|
||||
$el = compile();
|
||||
}
|
||||
|
||||
function openSpy(el = $el) {
|
||||
el.toggleButton.click();
|
||||
}
|
||||
|
||||
// Returns an array of the title of all shown mode tabs.
|
||||
function getModeTabTitles($el) {
|
||||
const tabElems = $el.tabs.find('button').get();
|
||||
return tabElems.map(btn => btn.textContent.trim());
|
||||
}
|
||||
|
||||
describe('toggle button', () => {
|
||||
|
||||
it('should not be shown if no spy mode is registered', () => {
|
||||
const $el = compile();
|
||||
expect($el.toggleButton.length).to.equal(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('open and closing the spy panel', () => {
|
||||
|
||||
beforeEach(fillRegistryAndCompile);
|
||||
|
||||
it('should show spy-panel on toggle click', () => {
|
||||
expect($el.panel.hasClass('ng-hide')).to.equal(true);
|
||||
$el.toggleButton.click();
|
||||
expect($el.panel.hasClass('ng-hide')).to.equal(false);
|
||||
});
|
||||
|
||||
it('should hide spy-panel on toggle button, when opened', () => {
|
||||
$el.toggleButton.click();
|
||||
expect($el.panel.hasClass('ng-hide')).to.equal(false);
|
||||
$el.toggleButton.click();
|
||||
expect($el.panel.hasClass('ng-hide')).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maximized mode', () => {
|
||||
|
||||
beforeEach(fillRegistryAndCompile);
|
||||
|
||||
it('should toggle to maximized mode when maximized button is clicked', () => {
|
||||
openSpy();
|
||||
$el.maximizedButton.click();
|
||||
expect($el.panel.hasClass('only')).to.equal(true);
|
||||
expect(visElement.hasClass('spy-only')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should exit maximized mode on a second click', () => {
|
||||
openSpy();
|
||||
$el.maximizedButton.click();
|
||||
expect($el.panel.hasClass('only')).to.equal(true);
|
||||
expect(visElement.hasClass('spy-only')).to.equal(true);
|
||||
$el.maximizedButton.click();
|
||||
expect($el.panel.hasClass('only')).to.equal(false);
|
||||
expect(visElement.hasClass('spy-only')).to.equal(false);
|
||||
});
|
||||
|
||||
it('will be forced when vis would be too small otherwise', () => {
|
||||
visElement.height(50);
|
||||
openSpy();
|
||||
$timeout.flush();
|
||||
expect($el.panel.hasClass('only')).to.equal(true);
|
||||
expect(visElement.hasClass('spy-only')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should not trigger forced maximized mode, when spy is not shown', () => {
|
||||
visElement.height(50);
|
||||
compile();
|
||||
$timeout.flush();
|
||||
expect(visElement.hasClass('spy-only')).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('spy modes', () => {
|
||||
|
||||
function registerRegularPanels() {
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'spymode2',
|
||||
display: 'SpyMode2',
|
||||
order: 2,
|
||||
template: '<div class="spymode2"></div>',
|
||||
}));
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'spymode1',
|
||||
display: 'SpyMode1',
|
||||
order: 1,
|
||||
template: '<div class="spymode1"></div>',
|
||||
}));
|
||||
}
|
||||
|
||||
it('should show registered spy modes as tabs', () => {
|
||||
registerRegularPanels();
|
||||
const $el = compile();
|
||||
openSpy($el);
|
||||
expect($el.tabs.find('button').length).to.equal(2);
|
||||
expect(getModeTabTitles($el)).to.eql(['SpyMode1', 'SpyMode2']);
|
||||
});
|
||||
|
||||
it('should by default be on the first spy mode when opening', async () => {
|
||||
registerRegularPanels();
|
||||
const $el = compile();
|
||||
openSpy($el);
|
||||
expect($el.panel.find('.spymode1').length).to.equal(1);
|
||||
});
|
||||
|
||||
describe('conditional spy modes', () => {
|
||||
|
||||
let filterOutSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
filterOutSpy = sinon.spy(() => false);
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'test',
|
||||
display: 'ShouldBeFiltered',
|
||||
showMode: filterOutSpy,
|
||||
order: 1,
|
||||
template: '<div></div>'
|
||||
}));
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'test2',
|
||||
display: 'ShouldNotBeFiltered',
|
||||
order: 2,
|
||||
template: '<div></div>'
|
||||
}));
|
||||
spyModeStubRegistry.register(() => ({
|
||||
name: 'test3',
|
||||
display: 'Test3',
|
||||
order: 3,
|
||||
showMode: () => true,
|
||||
template: '<div></div>'
|
||||
}));
|
||||
|
||||
$el = compile();
|
||||
openSpy();
|
||||
});
|
||||
|
||||
it('should filter out panels, that return false in showMode', () => {
|
||||
expect(getModeTabTitles($el)).not.to.include('ShouldBeFiltered');
|
||||
});
|
||||
|
||||
it('should show modes without a showMode function', () => {
|
||||
expect(getModeTabTitles($el)).to.include('ShouldNotBeFiltered');
|
||||
});
|
||||
|
||||
it('should show mods whose showMode returns true', () => {
|
||||
expect(getModeTabTitles($el)).to.include('Test3');
|
||||
});
|
||||
|
||||
it('should pass the visualization to the showMode method', () => {
|
||||
expect(filterOutSpy.called).to.equal(true);
|
||||
expect(filterOutSpy.getCall(0).args[0]).to.equal(vis);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('uiState', () => {
|
||||
beforeEach(fillRegistryAndCompile);
|
||||
|
||||
it('should sync the active tab to the uiState', () => {
|
||||
expect($scope.uiState.get('spy.mode.name', null)).to.be.null;
|
||||
openSpy();
|
||||
expect($scope.uiState.get('spy.mode.name', null)).to.equal('spymode1');
|
||||
});
|
||||
|
||||
it('should sync uiState when closing the panel', () => {
|
||||
openSpy();
|
||||
expect($scope.uiState.get('spy.mode.name', null)).to.equal('spymode1');
|
||||
$el.toggleButton.click();
|
||||
expect($scope.uiState.get('spy.mode.name', null)).to.equal(null);
|
||||
});
|
||||
|
||||
it('should sync uiState when maximizing', () => {
|
||||
openSpy();
|
||||
expect($scope.uiState.get('spy.mode.fill', null)).to.equal(null);
|
||||
$el.maximizedButton.click(); // Maximize it initially
|
||||
expect($scope.uiState.get('spy.mode.fill', false)).to.equal(true);
|
||||
$el.maximizedButton.click(); // Reset maximized state again
|
||||
expect($scope.uiState.get('spy.mode.fill', false)).to.equal(false);
|
||||
});
|
||||
|
||||
it('should also reset fullscreen when closing panel', () => {
|
||||
openSpy();
|
||||
$el.maximizedButton.click();
|
||||
expect($scope.uiState.get('spy.mode.fill', false)).to.equal(true);
|
||||
$el.toggleButton.click(); // Close spy panel
|
||||
expect($scope.uiState.get('spy.mode.fill', null)).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -30,6 +30,7 @@ import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logsta
|
|||
import { VisProvider } from '../../../vis';
|
||||
import { getVisualizeLoader } from '../loader';
|
||||
import { EmbeddedVisualizeHandler } from '../embedded_visualize_handler';
|
||||
import { Inspector } from '../../../inspector/inspector';
|
||||
|
||||
describe('visualize loader', () => {
|
||||
|
||||
|
@ -228,6 +229,15 @@ describe('visualize loader', () => {
|
|||
expect(handler.getElement().jquery).to.be.ok();
|
||||
});
|
||||
|
||||
it('should allow opening the inspector of the visualization and return its session', () => {
|
||||
const handler = loader.embedVisualizationWithSavedObject(newContainer(), createSavedObject(), {});
|
||||
sinon.spy(Inspector, 'open');
|
||||
const inspectorSession = handler.openInspector();
|
||||
expect(Inspector.open.calledOnce).to.be(true);
|
||||
expect(inspectorSession.close).to.be.a('function');
|
||||
inspectorSession.close();
|
||||
});
|
||||
|
||||
it('should have whenFirstRenderComplete returns a promise resolving on first renderComplete event', async () => {
|
||||
const container = newContainer();
|
||||
const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
|
||||
|
|
|
@ -26,9 +26,10 @@ const RENDER_COMPLETE_EVENT = 'render_complete';
|
|||
* with the visualization.
|
||||
*/
|
||||
export class EmbeddedVisualizeHandler {
|
||||
constructor(element, scope) {
|
||||
constructor(element, scope, savedObject) {
|
||||
this._element = element;
|
||||
this._scope = scope;
|
||||
this._savedObject = savedObject;
|
||||
this._listeners = new EventEmitter();
|
||||
// Listen to the first RENDER_COMPLETE_EVENT to resolve this promise
|
||||
this._firstRenderComplete = new Promise(resolve => {
|
||||
|
@ -88,6 +89,15 @@ export class EmbeddedVisualizeHandler {
|
|||
return this._element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the inspector for the embedded visualization. This will return an
|
||||
* handler to the inspector to close and interact with it.
|
||||
* @return {InspectorSession} An inspector session to interact with the opened inspector.
|
||||
*/
|
||||
openInspector() {
|
||||
return this._savedObject.vis.openInspector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise, that will resolve (without a value) once the first rendering of
|
||||
* the visualization has finished. If you want to listen to concecutive rendering
|
||||
|
|
|
@ -42,10 +42,6 @@ import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
|
|||
* @property {object} timeRange An object with a from/to key, that must be
|
||||
* either a date in ISO format, or a valid datetime Elasticsearch expression,
|
||||
* e.g.: { from: 'now-7d/d', to: 'now' }
|
||||
* @property {boolean} showSpyPanel Whether or not the spy panel should be available
|
||||
* on this chart. If set to true, spy panels will only be shown if there are
|
||||
* spy panels available for this specific visualization, since not every visualization
|
||||
* supports all spy panels. (default: false)
|
||||
* @property {boolean} append If set to true, the visualization will be appended
|
||||
* to the passed element instead of replacing all its content. (default: false)
|
||||
* @property {string} cssClass If specified this CSS class (or classes with space separated)
|
||||
|
@ -66,7 +62,6 @@ const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations) => {
|
|||
scope.timeRange = params.timeRange;
|
||||
scope.filters = params.filters;
|
||||
scope.query = params.query;
|
||||
scope.showSpyPanel = params.showSpyPanel;
|
||||
|
||||
const container = angular.element(el);
|
||||
|
||||
|
@ -91,7 +86,7 @@ const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations) => {
|
|||
container.html(visHtml);
|
||||
}
|
||||
|
||||
return new EmbeddedVisualizeHandler(visHtml, scope);
|
||||
return new EmbeddedVisualizeHandler(visHtml, scope, savedObj);
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,5 @@
|
|||
time-range="timeRange"
|
||||
filters="filters"
|
||||
query="query"
|
||||
show-spy-panel="showSpyPanel"
|
||||
render-complete
|
||||
></visualize>
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<div
|
||||
class="visualize-show-spy"
|
||||
ng-if="modes.length > 0"
|
||||
>
|
||||
<button
|
||||
data-test-subj="spyToggleButton"
|
||||
ng-click="toggleDisplay()"
|
||||
class="kuiCollapseButton visualize-show-spy-tab"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="kuiIcon"
|
||||
ng-class="shouldShowSpyPanel() ? 'fa-chevron-circle-down' : 'fa-chevron-circle-up'"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-test-subj="spyContainer"
|
||||
class="visualize-spy-container"
|
||||
ng-class="{ 'only': maximizedSpy || forceMaximized }"
|
||||
ng-show="shouldShowSpyPanel()"
|
||||
>
|
||||
<div class="visualize-spy__tab-container euiTabs euiTabs--small">
|
||||
<div role="tablist" class="visualize-spy__tabs" data-test-subj="spyModTabs">
|
||||
<button
|
||||
role="tab"
|
||||
class="euiTab"
|
||||
ng-repeat="mode in modes"
|
||||
aria-selected="{{mode.name === currentMode}}"
|
||||
ng-class="{'euiTab-isSelected': mode.name === currentMode}"
|
||||
data-test-subj="spyModeSelect-{{::mode.name}}"
|
||||
ng-click="setSpyMode(mode.name)"
|
||||
>
|
||||
<span class="euiTab__content">
|
||||
{{::mode.display}}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-test-subj="toggleSpyFullscreen"
|
||||
aria-label="Toggle spy panel fullscreen"
|
||||
class="kuiButton kuiButton--hollow"
|
||||
ng-hide="forceMaximized"
|
||||
ng-click="toggleMaximize()"
|
||||
>
|
||||
<span
|
||||
class="kuiIcon"
|
||||
ng-class="currentMode.fill ? 'fa-compress' : 'fa-expand'"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-test-subj="spyContentContainer"
|
||||
data-spy-content-container
|
||||
class="kuiVerticalRhythm visualize-spy__content-container"
|
||||
></div>
|
||||
</div>
|
|
@ -1,201 +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 $ from 'jquery';
|
||||
import { SpyModesRegistryProvider } from '../registry/spy_modes';
|
||||
import { uiModules } from '../modules';
|
||||
import spyTemplate from './spy.html';
|
||||
import { PersistedState } from '../persisted_state';
|
||||
|
||||
uiModules
|
||||
.get('app/visualize')
|
||||
.directive('visualizeSpy', function (Private, $compile, $timeout) {
|
||||
|
||||
const spyModes = Private(SpyModesRegistryProvider);
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: spyTemplate,
|
||||
scope: {
|
||||
vis: '<',
|
||||
searchSource: '<',
|
||||
uiState: '<',
|
||||
visElement: '<',
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
|
||||
// If no uiState has been passed, create a local one for this spy.
|
||||
if (!$scope.uiState) $scope.uiState = new PersistedState({});
|
||||
|
||||
let currentSpy;
|
||||
let defaultModeName;
|
||||
|
||||
const $container = $el.find('[data-spy-content-container]');
|
||||
|
||||
$scope.modes = [];
|
||||
|
||||
$scope.currentMode = null;
|
||||
$scope.maximizedSpy = false;
|
||||
$scope.forceMaximized = false;
|
||||
|
||||
function checkForcedMaximized() {
|
||||
$timeout(() => {
|
||||
if ($scope.visElement && $scope.currentMode && $scope.visElement.height() < 180) {
|
||||
$scope.forceMaximized = true;
|
||||
} else {
|
||||
$scope.forceMaximized = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkForcedMaximized();
|
||||
|
||||
|
||||
/**
|
||||
* Filter for modes that should actually be active for this visualization.
|
||||
* This will call the showMode method of the mode, pass it the vis object.
|
||||
* Depending on whether or not that returns a truthy value, it will be shown
|
||||
* or not. If the method is not present, the mode will always be shown.
|
||||
*/
|
||||
function filterModes() {
|
||||
$scope.modes = spyModes.inOrder.filter(mode =>
|
||||
mode.showMode ? mode.showMode($scope.vis) : true
|
||||
);
|
||||
defaultModeName = $scope.modes.length > 0 ? $scope.modes[0].name : null;
|
||||
}
|
||||
|
||||
filterModes();
|
||||
$scope.$watch('vis', filterModes);
|
||||
|
||||
function syncFromUiState() {
|
||||
$scope.currentMode = $scope.uiState.get('spy.mode.name');
|
||||
$scope.maximizedSpy = $scope.uiState.get('spy.mode.fill');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write our current state into the uiState.
|
||||
* This will write the name and fill (maximized) into the uiState
|
||||
* if a panel is opened (currentMode is set) or it will otherwise
|
||||
* remove the spy key from the uiState.
|
||||
*/
|
||||
function updateUiState() {
|
||||
if ($scope.currentMode) {
|
||||
$scope.uiState.set('spy.mode', {
|
||||
name: $scope.currentMode,
|
||||
fill: $scope.maximizedSpy,
|
||||
});
|
||||
} else {
|
||||
$scope.uiState.set('spy', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Initially sync the panel state from the uiState.
|
||||
syncFromUiState();
|
||||
|
||||
// Whenever the uiState changes, update the settings from it.
|
||||
$scope.uiState.on('change', syncFromUiState);
|
||||
$scope.$on('$destroy', () => $scope.uiState.off('change', syncFromUiState));
|
||||
|
||||
$scope.setSpyMode = function setSpyMode(modeName) {
|
||||
$scope.currentMode = modeName;
|
||||
updateUiState();
|
||||
$scope.$emit('render');
|
||||
};
|
||||
|
||||
$scope.toggleDisplay = function () {
|
||||
// If the spy panel is already shown (a currentMode is set),
|
||||
// close the panel by setting the name to null, otherwise open the
|
||||
// panel (i.e. set it to the default mode name).
|
||||
if ($scope.currentMode) {
|
||||
$scope.setSpyMode(null);
|
||||
$scope.forceMaximized = false;
|
||||
} else {
|
||||
$scope.setSpyMode(defaultModeName);
|
||||
checkForcedMaximized();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Should we currently show the spy panel. True if a currentMode has been set.
|
||||
*/
|
||||
$scope.shouldShowSpyPanel = () => {
|
||||
return !!$scope.currentMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle maximized state of spy panel and update the UI state.
|
||||
*/
|
||||
$scope.toggleMaximize = function () {
|
||||
$scope.maximizedSpy = !$scope.maximizedSpy;
|
||||
updateUiState();
|
||||
};
|
||||
|
||||
/**
|
||||
* Whenever the maximized state changes, we also need to toggle the class
|
||||
* of the visualization.
|
||||
*/
|
||||
$scope.$watchMulti(['maximizedSpy', 'forceMaximized'], () => {
|
||||
$scope.visElement.toggleClass('spy-only', $scope.maximizedSpy || $scope.forceMaximized);
|
||||
});
|
||||
|
||||
/**
|
||||
* Watch for changes of the currentMode. Whenever it changes, we render
|
||||
* the new mode into the template. Therefore we remove the previously rendered
|
||||
* mode (if existing) and compile and bind the template of the new mode.
|
||||
*/
|
||||
$scope.$watch('currentMode', (mode, prevMode) => {
|
||||
if (mode === prevMode && (currentSpy && currentSpy.mode === mode)) {
|
||||
// When the mode hasn't changed and we have already rendered it, return.
|
||||
return;
|
||||
}
|
||||
|
||||
const newMode = spyModes.byName[mode];
|
||||
|
||||
if (currentSpy) {
|
||||
// If we already have a spy loaded, remove that HTML element and
|
||||
// destroy the previous Angular scope.
|
||||
currentSpy.$container.remove();
|
||||
currentSpy.$scope.$destroy();
|
||||
currentSpy = null;
|
||||
}
|
||||
|
||||
// If we want haven't specified a new mode we won't do anything further.
|
||||
if (!newMode) {
|
||||
// Reset the forced maximized flag if we are about to close the panel.
|
||||
$scope.forceMaximized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScope = $scope.$new();
|
||||
const contentContainer = $('<div class="visualize-spy-content">');
|
||||
contentContainer.append($compile(newMode.template)(contentScope));
|
||||
|
||||
$container.append(contentContainer);
|
||||
|
||||
currentSpy = {
|
||||
$scope: contentScope,
|
||||
$container: contentContainer,
|
||||
mode: mode,
|
||||
};
|
||||
|
||||
newMode.link && newMode.link(currentSpy.$scope, currentSpy.$element);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -20,10 +20,3 @@
|
|||
ng-class="{ loading: vis.type.requiresSearch && searchSource.activeFetchCount > 0 }"
|
||||
class="visualize-chart"></div>
|
||||
</div>
|
||||
<visualize-spy
|
||||
ng-if="showSpyPanel"
|
||||
vis="vis"
|
||||
vis-element="visElement"
|
||||
search-source="searchSource"
|
||||
ui-state="uiState"
|
||||
></visualize-spy>
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
import * as Rx from 'rxjs';
|
||||
import { tap, debounceTime, filter, share, switchMap } from 'rxjs/operators';
|
||||
import './spy';
|
||||
import './visualize.less';
|
||||
import _ from 'lodash';
|
||||
import { uiModules } from '../modules';
|
||||
|
@ -36,7 +35,6 @@ uiModules
|
|||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
showSpyPanel: '=?',
|
||||
vis: '=',
|
||||
visData: '=',
|
||||
uiState: '=?',
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './spy';
|
||||
import './visualize.less';
|
||||
import './visualize_legend';
|
||||
import { uiModules } from '../modules';
|
||||
|
@ -33,7 +32,6 @@ uiModules
|
|||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
showSpyPanel: '=',
|
||||
vis: '=',
|
||||
visData: '=',
|
||||
uiState: '=?',
|
||||
|
@ -44,7 +42,7 @@ uiModules
|
|||
const vis = $scope.vis;
|
||||
const Editor = typeof vis.type.editor === 'function' ? vis.type.editor :
|
||||
editorTypes.find(editor => editor.key === vis.type.editor);
|
||||
const editor = new Editor(element[0], vis, $scope.showSpyPanel);
|
||||
const editor = new Editor(element[0], vis);
|
||||
|
||||
$scope.renderFunction = () => {
|
||||
if (!$scope.vis) return;
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
ui-state="uiState"
|
||||
class="vis-editor-content"
|
||||
search-source="savedObj.searchSource"
|
||||
show-spy-panel="showSpyPanel"
|
||||
/>
|
||||
<visualization
|
||||
ng-if="editorMode!==true"
|
||||
|
@ -16,5 +15,4 @@
|
|||
ui-state="uiState"
|
||||
listen-on-change="false"
|
||||
search-source="savedObj.searchSource"
|
||||
show-spy-panel="showSpyPanel"
|
||||
/>
|
||||
|
|
|
@ -48,7 +48,6 @@ uiModules
|
|||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
showSpyPanel: '=?',
|
||||
editorMode: '=?',
|
||||
savedObj: '=?',
|
||||
appState: '=?',
|
||||
|
@ -113,8 +112,8 @@ uiModules
|
|||
requestHandler($scope.vis, handlerParams)
|
||||
.then(requestHandlerResponse => {
|
||||
|
||||
//No need to call the response handler when there have been no data nor has been there changes
|
||||
//in the vis-state (response handler does not depend on uiStat
|
||||
//No need to call the response handler when there have been no data nor has been there changes
|
||||
//in the vis-state (response handler does not depend on uiStat
|
||||
const canSkipResponseHandler = (
|
||||
$scope.previousRequestHandlerResponse && $scope.previousRequestHandlerResponse === requestHandlerResponse &&
|
||||
$scope.previousVisState && _.isEqual($scope.previousVisState, $scope.vis.getState())
|
||||
|
|
|
@ -34,10 +34,6 @@ visualization {
|
|||
flex: 1 0;
|
||||
}
|
||||
|
||||
&.spy-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
@ -66,125 +62,3 @@ visualization {
|
|||
.item { }
|
||||
.bottom { align-self: flext-end; }
|
||||
}
|
||||
|
||||
visualize-spy {
|
||||
background-color: #ffffff;
|
||||
z-index: 1000;
|
||||
|
||||
// this element should flex
|
||||
flex: 0 1 auto;
|
||||
padding: 0px 0px 0px 15px;
|
||||
|
||||
// it's children should also flex vertically
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.only {
|
||||
flex: 1 1 auto;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Prevent clipping the focused state of buttons at the top of the container.
|
||||
*/
|
||||
.visualize-spy-container {
|
||||
padding-top: 2px; /* 1 */
|
||||
|
||||
&.only {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Restrict height of the spy and scroll if the content exceeds this height. This prevents
|
||||
* the spy from pushing surrounding content around, e.g. pushing the table down in Discover.
|
||||
*/
|
||||
.visualize-spy-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 482px; /* 1 */
|
||||
overflow-y: auto; /* 1 */
|
||||
|
||||
header {
|
||||
padding: 0 0 15px;
|
||||
}
|
||||
|
||||
header + * {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> .alert {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
tr > td {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
.visualize-spy-nav {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.visualize-spy-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.visualize-spy-loading {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.visualize-spy-loading-text {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
background: @alert-info-bg;
|
||||
color: @alert-info-text;
|
||||
padding: 5px 10px;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
.spinner > * {
|
||||
background-color: @alert-info-text;
|
||||
}
|
||||
}
|
||||
|
||||
.visualize-show-spy {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.visualize-show-spy-tab {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
left: 5px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.visualize-spy__content-container {
|
||||
overflow: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.visualize-spy__tab-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.visualize-spy__tabs {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export {
|
|||
embeddableFactories,
|
||||
fieldFormats,
|
||||
fieldFormatEditors,
|
||||
spyModes,
|
||||
inspectorViews,
|
||||
chromeNavControls,
|
||||
navbarExtensions,
|
||||
dashboardPanelActions,
|
||||
|
|
|
@ -42,7 +42,6 @@ export const embeddableFactories = appExtension;
|
|||
export const dashboardPanelActions = appExtension;
|
||||
export const fieldFormats = appExtension;
|
||||
export const fieldFormatEditors = appExtension;
|
||||
export const spyModes = appExtension;
|
||||
export const chromeNavControls = appExtension;
|
||||
export const navbarExtensions = appExtension;
|
||||
export const managementSections = appExtension;
|
||||
|
@ -50,6 +49,7 @@ export const devTools = appExtension;
|
|||
export const docViews = appExtension;
|
||||
export const hacks = appExtension;
|
||||
export const home = appExtension;
|
||||
export const inspectorViews = appExtension;
|
||||
|
||||
// aliases visTypeEnhancers to the visTypes group
|
||||
export const visTypeEnhancers = wrap(alias('visTypes'), appExtension);
|
||||
|
|
|
@ -153,7 +153,6 @@ module.exports = function (grunt) {
|
|||
flags: [
|
||||
...funcTestServerFlags,
|
||||
'--dev',
|
||||
'--dev_mode.enabled=false',
|
||||
'--no-base-path',
|
||||
'--optimize.watchPort=5611',
|
||||
'--optimize.watchPrebuild=true',
|
||||
|
|
|
@ -125,11 +125,9 @@ export default function ({ getService, getPageObjects }) {
|
|||
await dashboardAddPanel.addVisualization('Visualization TileMap');
|
||||
await PageObjects.dashboard.saveDashboard('No local edits');
|
||||
|
||||
await testSubjects.moveMouseTo('dashboardPanel');
|
||||
await PageObjects.visualize.openSpyPanel();
|
||||
const tileMapData = await PageObjects.visualize.getDataTableData();
|
||||
await testSubjects.moveMouseTo('dashboardPanel');
|
||||
await PageObjects.visualize.closeSpyPanel();
|
||||
await dashboardPanelActions.openInspector();
|
||||
const tileMapData = await PageObjects.visualize.getInspectorTableData();
|
||||
await PageObjects.visualize.closeInspector();
|
||||
|
||||
await PageObjects.dashboard.clickEdit();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
|
@ -143,11 +141,9 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
await PageObjects.header.clickDashboard();
|
||||
|
||||
await testSubjects.moveMouseTo('dashboardPanel');
|
||||
await PageObjects.visualize.openSpyPanel();
|
||||
const changedTileMapData = await PageObjects.visualize.getDataTableData();
|
||||
await testSubjects.moveMouseTo('dashboardPanel');
|
||||
await PageObjects.visualize.closeSpyPanel();
|
||||
await dashboardPanelActions.openInspector();
|
||||
const changedTileMapData = await PageObjects.visualize.getInspectorTableData();
|
||||
await PageObjects.visualize.closeInspector();
|
||||
expect(changedTileMapData.length).to.not.equal(tileMapData.length);
|
||||
});
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import expect from 'expect.js';
|
|||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const retry = getService('retry');
|
||||
const remote = getService('remote');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const PageObjects = getPageObjects(['dashboard', 'visualize', 'header']);
|
||||
|
||||
|
@ -38,37 +37,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not show the spy pane toggle if mouse is not hovering', async () => {
|
||||
// move mouse off the panel.
|
||||
await PageObjects.header.clickTimepicker();
|
||||
|
||||
// no spy pane without hover
|
||||
const spyToggleExists = await PageObjects.visualize.getSpyToggleExists();
|
||||
expect(spyToggleExists).to.be(false);
|
||||
});
|
||||
|
||||
it('shows the spy pane toggle on hover', async () => {
|
||||
const panels = await PageObjects.dashboard.getDashboardPanels();
|
||||
// Simulate hover
|
||||
await remote.moveMouseTo(panels[0]);
|
||||
const spyToggleExists = await PageObjects.visualize.getSpyToggleExists();
|
||||
expect(spyToggleExists).to.be(true);
|
||||
});
|
||||
|
||||
// This was an actual bug that appeared, where the spy pane appeared on panels after adding them, but
|
||||
// disappeared when a new dashboard was opened up.
|
||||
it('shows the spy pane toggle directly after opening a dashboard', async () => {
|
||||
await PageObjects.dashboard.clickEdit();
|
||||
await PageObjects.dashboard.saveDashboard('spy pane test', { saveAsNew: true });
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.loadSavedDashboard('spy pane test');
|
||||
const panels = await PageObjects.dashboard.getDashboardPanels();
|
||||
// Simulate hover
|
||||
await remote.moveMouseTo(panels[1]);
|
||||
const spyToggleExists = await PageObjects.visualize.getSpyToggleExists();
|
||||
expect(spyToggleExists).to.be(true);
|
||||
});
|
||||
|
||||
it('shows other panels after being minimized', async () => {
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
// Panels are all minimized on a fresh open of a dashboard, so we need to re-expand in order to then minimize.
|
||||
|
|
|
@ -87,57 +87,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
});
|
||||
|
||||
it.skip('doc view should show oldest time first', function () {
|
||||
// Note: Could just check the timestamp, but might as well check that the whole doc is as expected.
|
||||
const ExpectedDoc =
|
||||
'September 22nd 2015, 23:50:13.253\ntype:apache index:logstash-2015.09.22 @timestamp:September 22nd 2015, 23:50:13.253'
|
||||
+ ' ip:238.171.34.42 extension:jpg response:200 geo.coordinates:{ "lat": 38.66494528, "lon": -88.45299556'
|
||||
+ ' } geo.src:FR geo.dest:KH geo.srcdest:FR:KH @tags:success, info utc_time:September 22nd 2015,'
|
||||
+ ' 23:50:13.253 referer:http://twitter.com/success/nancy-currie agent:Mozilla/4.0 (compatible; MSIE 6.0;'
|
||||
+ ' Windows NT 5.1; SV1; .NET CLR 1.1.4322) clientip:238.171.34.42 bytes:7,124'
|
||||
+ ' host:media-for-the-masses.theacademyofperformingartsandscience.org request:/uploads/karl-henize.jpg'
|
||||
+ ' url:https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/karl-henize.jpg'
|
||||
+ ' @message:238.171.34.42 - - [2015-09-22T23:50:13.253Z] "GET /uploads/karl-henize.jpg HTTP/1.1" 200 7124'
|
||||
+ ' "-" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)" spaces:this is a'
|
||||
+ ' thing with lots of spaces wwwwoooooo xss:<script>console.log("xss")</script>'
|
||||
+ ' headings:<h3>alexander-viktorenko</h5>, http://nytimes.com/warning/michael-massimino'
|
||||
+ ' links:@www.slate.com, http://www.slate.com/security/frederick-w-leslie, www.www.slate.com'
|
||||
+ ' relatedContent:{ "url": "http://www.laweekly.com/music/bjork-at-the-nokia-theatre-12-12-2408191",'
|
||||
+ ' "og:type": "article", "og:title": "Bjork at the Nokia Theatre, 12/12", "og:description": "Bjork at the'
|
||||
+ ' Nokia Theater, December 12 By Randall Roberts Last night’s Bjork show at the Dystopia –'
|
||||
+ ' er, I mean Nokia -- Theatre downtown di...", "og:url": "'
|
||||
+ 'http://www.laweekly.com/music/bjork-at-the-nokia-theatre-12-12-2408191", "article:published_time":'
|
||||
+ ' "2007-12-13T12:19:35-08:00", "article:modified_time": "2014-11-27T08:28:42-08:00", "article:section":'
|
||||
+ ' "Music", "og:image": "'
|
||||
+ 'http://IMAGES1.laweekly.com/imager/bjork-at-the-nokia-theatre-12-12/u/original/2470701/bjorktn003.jpg",'
|
||||
+ ' "og:image:height": "334", "og:image:width": "480", "og:site_name": "LA Weekly", "twitter:title":'
|
||||
+ ' "Bjork at the Nokia Theatre, 12/12", "twitter:description": "Bjork at the Nokia Theater, December 12'
|
||||
+ ' By Randall Roberts Last night’s Bjork show at the Dystopia – er, I mean Nokia -- Theatre'
|
||||
+ ' downtown di...", "twitter:card": "summary", "twitter:image": "'
|
||||
+ 'http://IMAGES1.laweekly.com/imager/bjork-at-the-nokia-theatre-12-12/u/original/2470701/bjorktn003.jpg",'
|
||||
+ ' "twitter:site": "@laweekly" }, { "url": "'
|
||||
+ 'http://www.laweekly.com/music/the-rapture-at-the-mayan-7-25-2401011", "og:type": "article", "og:title":'
|
||||
+ ' "The Rapture at the Mayan, 7/25", "og:description": "If you haven’t yet experienced the'
|
||||
+ ' phenomenon of people walk-dancing, apparently the best place to witness this is at a Rapture show.'
|
||||
+ ' Here’s...", "og:url": "http://www.laweekly.com/music/the-rapture-at-the-mayan-7-25-2401011",'
|
||||
+ ' "article:published_time": "2007-07-26T12:42:30-07:00", "article:modified_time":'
|
||||
+ ' "2014-11-27T08:00:51-08:00", "article:section": "Music", "og:image": "'
|
||||
+ 'http://IMAGES1.laweekly.com/imager/the-rapture-at-the-mayan-7-25/u/original/2463272/rapturetn05.jpg",'
|
||||
+ ' "og:image:height": "321", "og:image:width": "480", "og:site_name": "LA Weekly", "twitter:title": "The'
|
||||
+ ' Rapture at the Mayan, 7/25", "twitter:description": "If you haven’t yet experienced the'
|
||||
+ ' phenomenon of people walk-dancing, apparently the best place to witness this is at a Rapture show.'
|
||||
+ ' Here’s...", "twitter:card": "summary", "twitter:image": "'
|
||||
+ 'http://IMAGES1.laweekly.com/imager/the-rapture-at-the-mayan-7-25/u/original/2463272/rapturetn05.jpg",'
|
||||
+ ' "twitter:site": "@laweekly" } machine.os:win 7 machine.ram:7,516,192,768 _id:AU_x3_g4GFA8no6QjkYX'
|
||||
+ ' _type:doc _index:logstash-2015.09.22 _score: - relatedContent.article:modified_time:November 27th'
|
||||
+ ' 2014, 16:00:51.000, November 27th 2014, 16:28:42.000 relatedContent.article:published_time:July 26th'
|
||||
+ ' 2007, 19:42:30.000, December 13th 2007, 20:19:35.000';
|
||||
return PageObjects.discover.getDocTableIndex(1)
|
||||
.then(function (rowData) {
|
||||
expect(rowData).to.be(ExpectedDoc);
|
||||
});
|
||||
});
|
||||
|
||||
it('doc view should sort ascending', function () {
|
||||
// Note: Could just check the timestamp, but might as well check that the whole doc is as expected.
|
||||
const ExpectedDoc =
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue