Add pluggable panel action tests (#20163) (#21378)

* Add pluggable panel action tests

* address code review comments

* update inspector snapshot

* remove temp declared ts module now that eui has EuiFlyout typings

* address code comments
This commit is contained in:
Stacey Gammon 2018-07-29 19:24:40 -04:00 committed by GitHub
parent 77f8cc3b6e
commit 38978a07ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 611 additions and 400 deletions

View file

@ -21,4 +21,5 @@ require('../src/setup_node_env');
require('../packages/kbn-test').runTestsCli([
require.resolve('../test/functional/config.js'),
require.resolve('../test/api_integration/config.js'),
require.resolve('../test/panel_actions/config.js'),
]);

View file

@ -0,0 +1,115 @@
/*
* 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 { EuiFlyout } from '@elastic/eui';
import { EventEmitter } from 'events';
import ReactDOM from 'react-dom';
let activeSession: FlyoutSession | null = null;
const CONTAINER_ID = 'flyout-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;
}
/**
* A FlyoutSession describes the session of one opened flyout panel. It offers
* methods to close the flyout panel again. If you open a flyout panel you should make
* sure you call {@link FlyoutSession#close} when it should be closed.
* Since a flyout 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 flyout will be closed and you should throw
* away your reference to this instance whenever you receive that event.
* @extends EventEmitter
*/
class FlyoutSession extends EventEmitter {
/**
* Binds the current flyout session to an Angular scope, meaning this flyout
* session will be closed as soon as the Angular scope gets destroyed.
* @param {object} scope - An 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 flyout as long as it's still the open one.
* If this is not the active session anymore, this method won't do anything.
* If this session was still active and a flyout was closed, the 'closed'
* event will be emitted on this FlyoutSession instance.
*/
public close(): void {
if (activeSession === this) {
const container = document.getElementById(CONTAINER_ID);
if (container) {
ReactDOM.unmountComponentAtNode(container);
this.emit('closed');
}
}
}
}
/**
* Opens a flyout panel with the given component inside. You can use
* {@link FlyoutSession#close} on the return value to close the flyout.
*
* @param flyoutChildren - Mounts the children inside a fly out panel
* @return {FlyoutSession} The session instance for the opened flyout panel.
*/
export function openFlyout(
flyoutChildren: React.ReactNode,
flyoutProps: {
onClose?: () => void;
'data-test-subj'?: string;
} = {}
): FlyoutSession {
// If there is an active inspector session close it before opening a new one.
if (activeSession) {
activeSession.close();
}
const container = getOrCreateContainerElement();
const session = (activeSession = new FlyoutSession());
const onClose = () => {
if (flyoutProps.onClose) {
flyoutProps.onClose();
}
session.close();
};
ReactDOM.render(
<EuiFlyout {...flyoutProps} onClose={onClose}>
{flyoutChildren}
</EuiFlyout>,
container
);
return session;
}
export { FlyoutSession };

View 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 * from './flyout_session';

View file

@ -16,67 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { EventEmitter } from 'events';
import React from 'react';
import ReactDOM from 'react-dom';
import { FlyoutSession, openFlyout } from 'ui/flyout';
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.
*
@ -98,6 +44,8 @@ interface InspectorOptions {
title?: string;
}
export type InspectorSession = FlyoutSession;
/**
* 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
@ -110,11 +58,6 @@ interface InspectorOptions {
* @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
@ -124,20 +67,9 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess
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;
return openFlyout(<InspectorPanel views={views} adapters={adapters} title={options.title} />, {
'data-test-subj': 'inspectorPanel',
});
}
const Inspector = {

View file

@ -34,313 +34,213 @@ exports[`InspectorPanel should render as expected 1`] = `
]
}
>
<EuiFlyout
data-test-subj="inspectorPanel"
hideCloseButton={false}
onClose={[Function]}
ownFocus={false}
size="m"
<EuiFlyoutHeader
hasBorder={true}
>
<span>
<FocusTrap
_createFocusTrap={[Function]}
active={true}
focusTrapOptions={
Object {
"clickOutsideDeactivates": true,
"fallbackFocus": [Function],
}
}
paused={false}
tag="div"
<div
className="euiFlyoutHeader euiFlyoutHeader--hasBorder"
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<div>
<div
className="euiFlyout euiFlyout--medium"
data-test-subj="inspectorPanel"
onKeyDown={[Function]}
role="dialog"
tabIndex={0}
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiButtonIcon
aria-label="Closes this dialog"
className="euiFlyout__closeButton"
color="text"
data-test-subj="euiFlyoutCloseButton"
iconType="cross"
onClick={[Function]}
type="button"
<div
className="euiFlexItem"
>
<button
aria-label="Closes this dialog"
className="euiButtonIcon euiButtonIcon--text euiFlyout__closeButton"
data-test-subj="euiFlyoutCloseButton"
onClick={[Function]}
type="button"
<EuiTitle
size="s"
>
<EuiIcon
aria-hidden="true"
className="euiButtonIcon__icon"
size="m"
type="cross"
<h1
className="euiTitle euiTitle--small"
>
<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"
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"
>
<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}
View:
View 1
</EuiButtonEmpty>
}
closePopover={[Function]}
id="inspectorViewChooser"
isOpen={false}
ownFocus={true}
panelPaddingSize="none"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
<EuiOutsideClickDetector
onOutsideClick={[Function]}
>
<EuiFlexItem
component="div"
grow={true}
<div
className="euiPopover euiPopover--anchorDownRight"
id="inspectorViewChooser"
onClick={[Function]}
onKeyDown={[Function]}
>
<div
className="euiFlexItem"
className="euiPopover__anchor"
>
<EuiTitle
<EuiButtonEmpty
color="primary"
data-test-subj="inspectorViewChooser"
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
size="s"
type="button"
>
<h1
className="euiTitle euiTitle--small"
<button
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small euiButtonEmpty--iconRight"
data-test-subj="inspectorViewChooser"
onClick={[Function]}
type="button"
>
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"
<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
</EuiButtonEmpty>
}
closePopover={[Function]}
id="inspectorViewChooser"
isOpen={false}
ownFocus={true}
panelPaddingSize="none"
>
<EuiOutsideClickDetector
onOutsideClick={[Function]}
>
<div
className="euiPopover euiPopover--anchorDownRight"
id="inspectorViewChooser"
onClick={[Function]}
onKeyDown={[Function]}
>
<div
className="euiPopover__anchor"
>
<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>
</div>
</EuiOutsideClickDetector>
</EuiPopover>
</InspectorViewChooser>
</span>
</span>
</button>
</EuiButtonEmpty>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</div>
</EuiFlyoutHeader>
<component
adapters={
Object {
"bardapter": Object {},
"foodapter": Object {
"foo": [Function],
},
}
}
title="Inspector"
>
<h1>
View 1
</h1>
</component>
</div>
</div>
</EuiOutsideClickDetector>
</EuiPopover>
</InspectorViewChooser>
</div>
</EuiFlexItem>
</div>
</FocusTrap>
</span>
</EuiFlyout>
</EuiFlexGroup>
</div>
</EuiFlyoutHeader>
<component
adapters={
Object {
"bardapter": Object {},
"foodapter": Object {
"foo": [Function],
},
}
}
title="Inspector"
>
<h1>
View 1
</h1>
</component>
</InspectorPanel>
`;

View file

@ -22,7 +22,6 @@ import { Adapters, InspectorViewDescription } from '../types';
interface InspectorPanelProps {
adapters: Adapters;
onClose: () => void;
title?: string;
views: InspectorViewDescription[];
}

View file

@ -22,7 +22,6 @@ import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
} from '@elastic/eui';
@ -79,14 +78,11 @@ class InspectorPanel extends Component {
}
render() {
const { views, onClose, title } = this.props;
const { views, title } = this.props;
const { selectedView } = this.state;
return (
<EuiFlyout
onClose={onClose}
data-test-subj="inspectorPanel"
>
<React.Fragment>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
justifyContent="spaceBetween"
@ -107,7 +103,7 @@ class InspectorPanel extends Component {
</EuiFlexGroup>
</EuiFlyoutHeader>
{ this.renderSelectedPanel() }
</EuiFlyout>
</React.Fragment>
);
}
}
@ -125,7 +121,6 @@ InspectorPanel.propTypes = {
);
}
},
onClose: PropTypes.func.isRequired,
title: PropTypes.string,
};

View file

@ -156,6 +156,17 @@ module.exports = function (grunt) {
],
},
panelActionTests: {
cmd: process.execPath,
args: [
'scripts/functional_tests',
'--config', 'test/panel_actions/config.js',
'--esFrom', 'source',
'--bail',
'--debug',
],
},
functionalTests: {
cmd: process.execPath,
args: [

View file

@ -31,16 +31,9 @@ module.exports = function (grunt) {
}
);
grunt.registerTask('test:server', [
'checkPlugins',
'run:mocha',
]);
grunt.registerTask('test:server', ['checkPlugins', 'run:mocha']);
grunt.registerTask('test:browser', [
'checkPlugins',
'run:browserTestServer',
'karma:unit',
]);
grunt.registerTask('test:browser', ['checkPlugins', 'run:browserTestServer', 'karma:unit']);
grunt.registerTask('test:browser-ci', () => {
const ciShardTasks = keys(grunt.config.get('karma'))
@ -49,13 +42,10 @@ module.exports = function (grunt) {
grunt.log.ok(`Running UI tests in ${ciShardTasks.length} shards`);
grunt.task.run([
'run:browserTestServer',
...ciShardTasks
]);
grunt.task.run(['run:browserTestServer', ...ciShardTasks]);
});
grunt.registerTask('test:coverage', [ 'run:testCoverageServer', 'karma:coverage' ]);
grunt.registerTask('test:coverage', ['run:testCoverageServer', 'karma:coverage']);
grunt.registerTask('test:quick', [
'checkPlugins',
@ -65,26 +55,24 @@ module.exports = function (grunt) {
'test:jest_integration',
'test:projects',
'test:browser',
'run:apiIntegrationTests'
'run:apiIntegrationTests',
]);
grunt.registerTask('test:dev', [
'checkPlugins',
'run:devBrowserTestServer',
'karma:dev'
]);
grunt.registerTask('test:dev', ['checkPlugins', 'run:devBrowserTestServer', 'karma:dev']);
grunt.registerTask('test', subTask => {
if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`);
grunt.task.run(_.compact([
!grunt.option('quick') && 'run:eslint',
!grunt.option('quick') && 'run:tslint',
'run:checkFileCasing',
'licenses',
'test:quick',
'verifyTranslations',
]));
grunt.task.run(
_.compact([
!grunt.option('quick') && 'run:eslint',
!grunt.option('quick') && 'run:tslint',
'run:checkFileCasing',
'licenses',
'test:quick',
'verifyTranslations',
])
);
});
grunt.registerTask('quick-test', ['test:quick']); // historical alias
@ -98,7 +86,7 @@ module.exports = function (grunt) {
const serverCmd = {
cmd: 'yarn',
args: ['kbn', 'run', 'test', '--exclude', 'kibana', '--oss', '--skip-kibana-extra'],
opts: { stdio: 'inherit' }
opts: { stdio: 'inherit' },
};
return new Promise((resolve, reject) => {

View file

@ -0,0 +1,50 @@
/*
* 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 path from 'path';
export default async function ({ readConfigFile }) {
const functionalConfig = await readConfigFile(require.resolve('../functional/config'));
return {
testFiles: [
require.resolve('./index'),
],
services: functionalConfig.get('services'),
pageObjects: functionalConfig.get('pageObjects'),
servers: functionalConfig.get('servers'),
env: functionalConfig.get('env'),
esTestCluster: functionalConfig.get('esTestCluster'),
apps: functionalConfig.get('apps'),
esArchiver: {
directory: path.resolve(__dirname, '../es_archives')
},
screenshots: functionalConfig.get('screenshots'),
junit: {
reportName: 'Panel Actions Functional Tests',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${path.resolve(__dirname, './sample_panel_action')}`,
],
},
};
}

View file

@ -0,0 +1,50 @@
/*
* 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 path from 'path';
export const KIBANA_ARCHIVE_PATH = path.resolve(__dirname, '../functional/fixtures/es_archiver/dashboard/current/kibana');
export const DATA_ARCHIVE_PATH = path.resolve(__dirname, '../functional/fixtures/es_archiver/dashboard/current/data');
export default function ({ getService, getPageObjects, loadTestFile }) {
const remote = getService('remote');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['dashboard']);
describe('pluggable panel actions', function () {
before(async () => {
await remote.setWindowSize(1300, 900);
await PageObjects.dashboard.initTests({
kibanaIndex: KIBANA_ARCHIVE_PATH,
dataIndex: DATA_ARCHIVE_PATH,
defaultIndex: 'logstash-*',
});
await PageObjects.dashboard.preserveCrossAppState();
});
after(async function () {
await PageObjects.dashboard.clearSavedObjectsFromAppLinks();
await esArchiver.unload(KIBANA_ARCHIVE_PATH);
await esArchiver.unload(DATA_ARCHIVE_PATH);
});
loadTestFile(require.resolve('./panel_actions'));
});
}

View file

@ -0,0 +1,54 @@
/*
* 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 expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const dashboardPanelActions = getService('dashboardPanelActions');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['dashboard']);
describe('Panel Actions', () => {
before(async () => {
await PageObjects.dashboard.loadSavedDashboard('few panels');
});
it('Sample action appears in context menu in view mode', async () => {
await dashboardPanelActions.openContextMenu();
const newPanelActionExists = await testSubjects.exists(
'dashboardPanelAction-samplePanelAction'
);
expect(newPanelActionExists).to.be(true);
});
it('Clicking sample action shows a flyout', async () => {
await dashboardPanelActions.openContextMenu();
await testSubjects.click('dashboardPanelAction-samplePanelAction');
const flyoutExists = await testSubjects.exists('samplePanelActionFlyout');
expect(flyoutExists).to.be(true);
});
it('flyout shows the correct contents', async () => {
const titleExists = await testSubjects.exists('samplePanelActionTitle');
expect(titleExists).to.be(true);
const bodyExists = await testSubjects.exists('samplePanelActionBody');
expect(bodyExists).to.be(true);
});
});
}

View file

@ -0,0 +1,33 @@
/*
* 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.
*/
function samplePanelAction(kibana) {
return new kibana.Plugin({
uiExports: {
dashboardPanelActions: ['plugins/sample_panel_action/sample_panel_action'],
},
});
}
module.exports = function (kibana) {
return [
samplePanelAction(kibana),
];
};

View file

@ -0,0 +1,8 @@
{
"name": "sample_panel_action",
"version": "7.0.0-alpha1",
"dependencies": {
"@elastic/eui": "0.0.55",
"react": "^16.4.1"
}
}

View file

@ -0,0 +1,55 @@
/*
* 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 { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
import React from 'react';
import { openFlyout } from '../../../../src/ui/public/flyout';
import {
DashboardPanelAction,
DashboardPanelActionsRegistryProvider,
} from '../../../../src/ui/public/dashboard_panel_actions';
class SamplePanelAction extends DashboardPanelAction {
constructor() {
super({
displayName: 'Sample Panel Action',
id: 'samplePanelAction',
parentPanelId: 'mainMenu',
});
}
onClick({ embeddable }) {
openFlyout(
<React.Fragment>
<EuiFlyoutHeader>
<EuiTitle size="s" data-test-subj="samplePanelActionTitle">
<h1>{embeddable.metadata.title}</h1>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<h1 data-test-subj="samplePanelActionBody">This is a sample action</h1>
</EuiFlyoutBody>
</React.Fragment>,
{
'data-test-subj': 'samplePanelActionFlyout',
},
);
}
}
DashboardPanelActionsRegistryProvider.register(() => new SamplePanelAction());