mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Context menu (#76497)
* feat: 🎸 add grouping to presentable interface * feat: 🎸 add group to "Explore underlying" data action * refactor: 💡 return panel list and simplify context creation * refactor: 💡 simplify context menu builder code * refactor: 💡 further simplify context menu builder code * feat: 🎸 add grouping to context menu builder * feat: 🎸 add icon to drilldowns group * fix: 🐛 sort in the other order * feat: 🎸 group drilldown actions in edit mode * fix: 🐛 fix TypeScript error * feat: 🎸 wrap long context menu list into a submenu * feat: 🎸 improve context menu long list wrapping * feat: 🎸 display drilldowns panel at the bottom of main panel * feat: 🎸 add separator line for context menu * test: 💍 add basic context menu builder unit tests * feat: 🎸 remove meta decoratiosn from generated menu * test: 💍 add test subject attribute to "More" menu item * chore: 🤖 remove separator line and add comment about EUI * test: 💍 update Jest snapshots * chore: 🤖 revert back change of showing both drilldown options * test: 💍 add context menu samples to example plugin * feat: 🎸 collapse long groups into a sub-panel * test: 💍 add context menu panel edit mode examples * test: 💍 fix OSS functional test * test: 💍 fix X-Pack functional tests * fix: 🐛 re-introduce item sorting by title * test: 💍 allow explicitly opening more menu * test: 💍 try opening more panel in functional tests * test: 💍 disable some tests * chore: 🤖 remove unused code * test: 💍 use action test helper in unit tests * refactor: 💡 add helper utility to generate actions in examples * test: 💍 disable one more functional test * test: 💍 improve how inspector is opened in functional tests * test: 💍 enable functional test * refactor: 💡 convert test suite to typescript * test: 💍 move panel replace tests into a separate test suite * test: 💍 move panel cloning tests to a separate test suite * test: 💍 set up dashboard context menu test suite * test: 💍 enable few panel context menu tests * test: 💍 enable saved search panel tests * test: 💍 enable expanded panel context menu tests * test: 💍 remove render complete awaits Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
337fe73d09
commit
4b49e5a1c8
31 changed files with 1380 additions and 459 deletions
|
@ -37,6 +37,7 @@ import { UiActionsStart, createAction } from '../../../src/plugins/ui_actions/pu
|
|||
import { AppMountParameters, OverlayStart } from '../../../src/core/public';
|
||||
import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '../../ui_action_examples/public';
|
||||
import { TriggerContextExample } from './trigger_context_example';
|
||||
import { ContextMenuExamples } from './context_menu_examples';
|
||||
|
||||
interface Props {
|
||||
uiActionsApi: UiActionsStart;
|
||||
|
@ -109,7 +110,12 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => {
|
|||
</EuiText>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<TriggerContextExample uiActionsApi={uiActionsApi} />
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<ContextMenuExamples />
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
|
|
|
@ -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 React from 'react';
|
||||
import { EuiCode, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { PanelView } from './panel_view';
|
||||
import { PanelViewWithSharing } from './panel_view_with_sharing';
|
||||
import { PanelViewWithSharingLong } from './panel_view_with_sharing_long';
|
||||
import { PanelEdit } from './panel_edit';
|
||||
import { PanelEditWithDrilldowns } from './panel_edit_with_drilldowns';
|
||||
import { PanelEditWithDrilldownsAndContextActions } from './panel_edit_with_drilldowns_and_context_actions';
|
||||
|
||||
export const ContextMenuExamples: React.FC = () => {
|
||||
return (
|
||||
<EuiText>
|
||||
<h1>Context menu examples</h1>
|
||||
<p>
|
||||
Below examples show how context menu panels look with varying number of actions and how the
|
||||
actions can be grouped into different panels using <EuiCode>grouping</EuiCode> field.
|
||||
</p>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<PanelView />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelViewWithSharing />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelViewWithSharingLong />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<PanelEdit />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelEditWithDrilldowns />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelEditWithDrilldownsAndContextActions />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -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 './context_menu_examples';
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { buildContextMenuForActions } from '../../../../src/plugins/ui_actions/public';
|
||||
import { sampleAction } from './util';
|
||||
|
||||
export const PanelEdit: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const context = {};
|
||||
const trigger: any = 'TEST_TRIGGER';
|
||||
const actions = [
|
||||
sampleAction('test-1', 100, 'Edit visualization', 'pencil'),
|
||||
sampleAction('test-2', 99, 'Clone panel', 'partial'),
|
||||
sampleAction('test-3', 98, 'Edit panel title', 'pencil'),
|
||||
sampleAction('test-4', 97, 'Customize time range', 'calendar'),
|
||||
sampleAction('test-5', 96, 'Inspect', 'inspect'),
|
||||
sampleAction('test-6', 95, 'Full screen', 'fullScreen'),
|
||||
sampleAction('test-7', 94, 'Replace panel', 'submodule'),
|
||||
sampleAction('test-8', 93, 'Delete from dashboard', 'trash'),
|
||||
];
|
||||
|
||||
const panels = useAsync(() =>
|
||||
buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context, trigger })),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={<EuiButton onClick={() => setOpen((x) => !x)}>Edit mode</EuiButton>}
|
||||
isOpen={open}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setOpen(false)}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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 * as React from 'react';
|
||||
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { buildContextMenuForActions, Action } from '../../../../src/plugins/ui_actions/public';
|
||||
import { sampleAction } from './util';
|
||||
|
||||
export const PanelEditWithDrilldowns: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const context = {};
|
||||
const trigger: any = 'TEST_TRIGGER';
|
||||
const grouping: Action['grouping'] = [
|
||||
{
|
||||
id: 'drilldowns',
|
||||
getDisplayName: () => 'Drilldowns',
|
||||
getIconType: () => 'popout',
|
||||
order: 20,
|
||||
},
|
||||
];
|
||||
const actions = [
|
||||
sampleAction('test-1', 100, 'Edit visualization', 'pencil'),
|
||||
sampleAction('test-2', 99, 'Clone panel', 'partial'),
|
||||
sampleAction('test-3', 98, 'Edit panel title', 'pencil'),
|
||||
sampleAction('test-4', 97, 'Customize time range', 'calendar'),
|
||||
sampleAction('test-5', 96, 'Inspect', 'inspect'),
|
||||
sampleAction('test-6', 95, 'Full screen', 'fullScreen'),
|
||||
sampleAction('test-7', 94, 'Replace panel', 'submodule'),
|
||||
sampleAction('test-8', 93, 'Delete from dashboard', 'trash'),
|
||||
|
||||
sampleAction('test-9', 10, 'Create drilldown', 'plusInCircle', grouping),
|
||||
sampleAction('test-10', 9, 'Manage drilldowns', 'list', grouping),
|
||||
];
|
||||
|
||||
const panels = useAsync(() =>
|
||||
buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context, trigger })),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={<EuiButton onClick={() => setOpen((x) => !x)}>Edit mode with drilldowns</EuiButton>}
|
||||
isOpen={open}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setOpen(false)}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { buildContextMenuForActions, Action } from '../../../../src/plugins/ui_actions/public';
|
||||
import { sampleAction } from './util';
|
||||
|
||||
export const PanelEditWithDrilldownsAndContextActions: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const context = {};
|
||||
const trigger: any = 'TEST_TRIGGER';
|
||||
const drilldownGrouping: Action['grouping'] = [
|
||||
{
|
||||
id: 'drilldowns',
|
||||
getDisplayName: () => 'Drilldowns',
|
||||
getIconType: () => 'popout',
|
||||
order: 20,
|
||||
},
|
||||
];
|
||||
const customActionGrouping: Action['grouping'] = [
|
||||
{
|
||||
id: 'actions',
|
||||
getDisplayName: () => 'Custom actions',
|
||||
getIconType: () => 'cloudStormy',
|
||||
order: 20,
|
||||
},
|
||||
];
|
||||
const actions = [
|
||||
sampleAction('test-1', 100, 'Edit visualization', 'pencil'),
|
||||
sampleAction('test-2', 99, 'Clone panel', 'partial'),
|
||||
sampleAction('test-3', 98, 'Edit panel title', 'pencil'),
|
||||
sampleAction('test-4', 97, 'Customize time range', 'calendar'),
|
||||
sampleAction('test-5', 96, 'Inspect', 'inspect'),
|
||||
sampleAction('test-6', 95, 'Full screen', 'fullScreen'),
|
||||
sampleAction('test-7', 94, 'Replace panel', 'submodule'),
|
||||
sampleAction('test-8', 93, 'Delete from dashboard', 'trash'),
|
||||
|
||||
sampleAction('test-9', 10, 'Create drilldown', 'plusInCircle', drilldownGrouping),
|
||||
sampleAction('test-10', 9, 'Manage drilldowns', 'list', drilldownGrouping),
|
||||
|
||||
sampleAction('test-11', 10, 'Go to Sales dashboard', 'dashboardApp', customActionGrouping),
|
||||
sampleAction('test-12', 9, 'Go to Traffic dashboard', 'dashboardApp', customActionGrouping),
|
||||
sampleAction('test-13', 8, 'Custom actions', 'cloudStormy', customActionGrouping),
|
||||
sampleAction('test-14', 7, 'View in Salesforce', 'link', customActionGrouping),
|
||||
];
|
||||
|
||||
const panels = useAsync(() =>
|
||||
buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context, trigger })),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButton onClick={() => setOpen((x) => !x)}>
|
||||
Edit mode with drilldowns and custom actions
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={open}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setOpen(false)}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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 * as React from 'react';
|
||||
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { buildContextMenuForActions } from '../../../../src/plugins/ui_actions/public';
|
||||
import { sampleAction } from './util';
|
||||
|
||||
export const PanelView: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const context = {};
|
||||
const trigger: any = 'TEST_TRIGGER';
|
||||
const actions = [
|
||||
sampleAction('test-1', 100, 'Explore underlying data', 'discoverApp'),
|
||||
sampleAction('test-2', 99, 'Customize time range', 'calendar'),
|
||||
sampleAction('test-3', 98, 'Inspect', 'inspect'),
|
||||
sampleAction('test-4', 97, 'Full screen', 'fullScreen'),
|
||||
];
|
||||
|
||||
const panels = useAsync(() =>
|
||||
buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context, trigger })),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={<EuiButton onClick={() => setOpen((x) => !x)}>View mode</EuiButton>}
|
||||
isOpen={open}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setOpen(false)}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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 * as React from 'react';
|
||||
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { buildContextMenuForActions, Action } from '../../../../src/plugins/ui_actions/public';
|
||||
import { sampleAction } from './util';
|
||||
|
||||
export const PanelViewWithSharing: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const context = {};
|
||||
const trigger: any = 'TEST_TRIGGER';
|
||||
const grouping: Action['grouping'] = [
|
||||
{
|
||||
id: 'sharing',
|
||||
getDisplayName: () => 'Sharing',
|
||||
getIconType: () => 'share',
|
||||
order: 50,
|
||||
},
|
||||
];
|
||||
const actions = [
|
||||
sampleAction('test-1', 100, 'Explore underlying data', 'discoverApp'),
|
||||
sampleAction('test-2', 99, 'Customize time range', 'calendar'),
|
||||
sampleAction('test-3', 98, 'Inspect', 'inspect'),
|
||||
sampleAction('test-4', 97, 'Full screen', 'fullScreen'),
|
||||
sampleAction('test-5', 10, 'Copy link', 'link', grouping),
|
||||
sampleAction('test-6', 9, 'Copy .png', 'image', grouping),
|
||||
];
|
||||
|
||||
const panels = useAsync(() =>
|
||||
buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context, trigger })),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButton onClick={() => setOpen((x) => !x)}>View mode with few sharing options</EuiButton>
|
||||
}
|
||||
isOpen={open}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setOpen(false)}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { buildContextMenuForActions, Action } from '../../../../src/plugins/ui_actions/public';
|
||||
import { sampleAction } from './util';
|
||||
|
||||
export const PanelViewWithSharingLong: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const context = {};
|
||||
const trigger: any = 'TEST_TRIGGER';
|
||||
const grouping: Action['grouping'] = [
|
||||
{
|
||||
id: 'sharing',
|
||||
getDisplayName: () => 'Sharing',
|
||||
getIconType: () => 'share',
|
||||
order: 50,
|
||||
},
|
||||
];
|
||||
const actions = [
|
||||
sampleAction('test-1', 100, 'Explore underlying data', 'discoverApp'),
|
||||
sampleAction('test-2', 99, 'Customize time range', 'calendar'),
|
||||
sampleAction('test-3', 98, 'Inspect', 'inspect'),
|
||||
sampleAction('test-4', 97, 'Full screen', 'fullScreen'),
|
||||
sampleAction('test-5', 10, 'Copy link', 'link', grouping),
|
||||
sampleAction('test-6', 9, 'Copy .png', 'image', grouping),
|
||||
sampleAction('test-7', 8, 'Copy .pdf', 'link', grouping),
|
||||
sampleAction('test-8', 7, 'Send to slack', 'link', grouping),
|
||||
sampleAction('test-9', 6, 'Send by e-mail', 'email', grouping),
|
||||
];
|
||||
|
||||
const panels = useAsync(() =>
|
||||
buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context, trigger })),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButton onClick={() => setOpen((x) => !x)}>
|
||||
View mode with many sharing options
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={open}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setOpen(false)}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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 { Action } from '../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export const sampleAction = (
|
||||
id: string,
|
||||
order: number,
|
||||
name: string,
|
||||
icon: string,
|
||||
grouping?: Action['grouping']
|
||||
): Action => {
|
||||
return {
|
||||
id,
|
||||
type: 'SAMPLE' as any,
|
||||
order,
|
||||
getDisplayName: () => name,
|
||||
getIconType: () => icon,
|
||||
isCompatible: async () => true,
|
||||
execute: async () => {},
|
||||
grouping,
|
||||
};
|
||||
};
|
|
@ -37,7 +37,7 @@ export interface PanelHeaderProps {
|
|||
title?: string;
|
||||
isViewMode: boolean;
|
||||
hidePanelTitles: boolean;
|
||||
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor>;
|
||||
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor[]>;
|
||||
closeContextMenu: boolean;
|
||||
badges: Array<Action<EmbeddableContext>>;
|
||||
notifications: Array<Action<EmbeddableContext>>;
|
||||
|
|
|
@ -28,14 +28,14 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
export interface PanelOptionsMenuProps {
|
||||
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor>;
|
||||
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor[]>;
|
||||
isViewMode: boolean;
|
||||
closeContextMenu: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
actionContextMenuPanel?: EuiContextMenuPanelDescriptor;
|
||||
actionContextMenuPanel?: EuiContextMenuPanelDescriptor[];
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
|
|||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId="mainMenu"
|
||||
panels={this.state.actionContextMenuPanel ? [this.state.actionContextMenuPanel] : []}
|
||||
panels={this.state.actionContextMenuPanel || []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
// @ts-ignore
|
||||
import React from 'react';
|
||||
import { Action, ActionContext as Context, ActionDefinition } from './action';
|
||||
import { Presentable } from '../util/presentable';
|
||||
import { Presentable, PresentableGrouping } from '../util/presentable';
|
||||
import { uiToReactComponent } from '../../../kibana_react/public';
|
||||
import { ActionType } from '../types';
|
||||
|
||||
|
@ -36,6 +36,7 @@ export class ActionInternal<A extends ActionDefinition = ActionDefinition>
|
|||
public readonly order: number = this.definition.order || 0;
|
||||
public readonly MenuItem? = this.definition.MenuItem;
|
||||
public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined;
|
||||
public readonly grouping?: PresentableGrouping<Context<A>> = this.definition.grouping;
|
||||
|
||||
public execute(context: Context<A>) {
|
||||
return this.definition.execute(context);
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
|
||||
import { buildContextMenuForActions } from './build_eui_context_menu_panels';
|
||||
import { Action, createAction } from '../actions';
|
||||
|
||||
|
@ -25,9 +26,9 @@ const createTestAction = ({
|
|||
dispayName,
|
||||
order,
|
||||
}: {
|
||||
type: string;
|
||||
type?: string;
|
||||
dispayName: string;
|
||||
order: number;
|
||||
order?: number;
|
||||
}) =>
|
||||
createAction({
|
||||
type: type as any, // mapping doesn't matter for this test
|
||||
|
@ -36,32 +37,36 @@ const createTestAction = ({
|
|||
execute: async () => {},
|
||||
});
|
||||
|
||||
test('contextMenu actions sorting: order, type, displayName', async () => {
|
||||
const resultMapper = (panel: EuiContextMenuPanelDescriptor) => ({
|
||||
items: panel.items ? panel.items.map((item) => ({ name: item.name })) : [],
|
||||
});
|
||||
|
||||
test('sorts items in DESC order by "order" field first, then by display name', async () => {
|
||||
const actions: Action[] = [
|
||||
createTestAction({
|
||||
order: 100,
|
||||
type: '1',
|
||||
dispayName: 'a',
|
||||
order: 1,
|
||||
type: 'foo',
|
||||
dispayName: 'a-1',
|
||||
}),
|
||||
createTestAction({
|
||||
order: 100,
|
||||
type: '1',
|
||||
dispayName: 'b',
|
||||
order: 2,
|
||||
type: 'foo',
|
||||
dispayName: 'a-2',
|
||||
}),
|
||||
createTestAction({
|
||||
order: 0,
|
||||
type: '2',
|
||||
dispayName: 'c',
|
||||
order: 3,
|
||||
type: 'foo',
|
||||
dispayName: 'a-3',
|
||||
}),
|
||||
createTestAction({
|
||||
order: 0,
|
||||
type: '2',
|
||||
dispayName: 'd',
|
||||
order: 2,
|
||||
type: 'foo',
|
||||
dispayName: 'b-2',
|
||||
}),
|
||||
createTestAction({
|
||||
order: 0,
|
||||
type: '3',
|
||||
dispayName: 'aa',
|
||||
order: 2,
|
||||
type: 'foo',
|
||||
dispayName: 'c-2',
|
||||
}),
|
||||
].sort(() => 0.5 - Math.random());
|
||||
|
||||
|
@ -69,13 +74,166 @@ test('contextMenu actions sorting: order, type, displayName', async () => {
|
|||
actions: actions.map((action) => ({ action, context: {}, trigger: '' as any })),
|
||||
});
|
||||
|
||||
expect(result.items?.map((item) => item.name as string)).toMatchInlineSnapshot(`
|
||||
expect(result.map(resultMapper)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"aa",
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"name": "a-3",
|
||||
},
|
||||
Object {
|
||||
"name": "a-2",
|
||||
},
|
||||
Object {
|
||||
"name": "b-2",
|
||||
},
|
||||
Object {
|
||||
"name": "More",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"name": "c-2",
|
||||
},
|
||||
Object {
|
||||
"name": "a-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('builds empty menu when no actions provided', async () => {
|
||||
const menu = await buildContextMenuForActions({
|
||||
actions: [],
|
||||
closeMenu: () => {},
|
||||
});
|
||||
|
||||
expect(menu.map(resultMapper)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"items": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('can build menu with one action', async () => {
|
||||
const menu = await buildContextMenuForActions({
|
||||
actions: [
|
||||
{
|
||||
action: createTestAction({
|
||||
dispayName: 'Foo',
|
||||
}),
|
||||
context: {},
|
||||
trigger: 'TETS_TRIGGER' as any,
|
||||
},
|
||||
],
|
||||
closeMenu: () => {},
|
||||
});
|
||||
|
||||
expect(menu.map(resultMapper)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"name": "Foo",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('orders items according to "order" field', async () => {
|
||||
const actions = [
|
||||
createTestAction({
|
||||
order: 1,
|
||||
dispayName: 'Foo',
|
||||
}),
|
||||
createTestAction({
|
||||
order: 2,
|
||||
dispayName: 'Bar',
|
||||
}),
|
||||
];
|
||||
const menu = await buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })),
|
||||
});
|
||||
|
||||
expect(menu[0].items![0].name).toBe('Bar');
|
||||
expect(menu[0].items![1].name).toBe('Foo');
|
||||
|
||||
const actions2 = [
|
||||
createTestAction({
|
||||
order: 2,
|
||||
dispayName: 'Bar',
|
||||
}),
|
||||
createTestAction({
|
||||
order: 1,
|
||||
dispayName: 'Foo',
|
||||
}),
|
||||
];
|
||||
const menu2 = await buildContextMenuForActions({
|
||||
actions: actions2.map((action) => ({ action, context: {}, trigger: 'TEST' as any })),
|
||||
});
|
||||
|
||||
expect(menu2[0].items![0].name).toBe('Bar');
|
||||
expect(menu2[0].items![1].name).toBe('Foo');
|
||||
});
|
||||
|
||||
test('hides items behind in "More" submenu if there are more than 4 actions', async () => {
|
||||
const actions = [
|
||||
createTestAction({
|
||||
dispayName: 'Foo 1',
|
||||
}),
|
||||
createTestAction({
|
||||
dispayName: 'Foo 2',
|
||||
}),
|
||||
createTestAction({
|
||||
dispayName: 'Foo 3',
|
||||
}),
|
||||
createTestAction({
|
||||
dispayName: 'Foo 4',
|
||||
}),
|
||||
createTestAction({
|
||||
dispayName: 'Foo 5',
|
||||
}),
|
||||
];
|
||||
const menu = await buildContextMenuForActions({
|
||||
actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })),
|
||||
});
|
||||
|
||||
expect(menu.map(resultMapper)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"name": "Foo 1",
|
||||
},
|
||||
Object {
|
||||
"name": "Foo 2",
|
||||
},
|
||||
Object {
|
||||
"name": "Foo 3",
|
||||
},
|
||||
Object {
|
||||
"name": "More",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"name": "Foo 4",
|
||||
},
|
||||
Object {
|
||||
"name": "Foo 5",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -20,10 +20,9 @@
|
|||
import * as React from 'react';
|
||||
import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
|
||||
import _ from 'lodash';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { uiToReactComponent } from '../../../kibana_react/public';
|
||||
import { Action } from '../actions';
|
||||
import { Action, ActionExecutionContext } from '../actions';
|
||||
import { Trigger } from '../triggers';
|
||||
import { BaseContext } from '../types';
|
||||
|
||||
|
@ -31,6 +30,10 @@ export const defaultTitle = i18n.translate('uiActions.actionPanel.title', {
|
|||
defaultMessage: 'Options',
|
||||
});
|
||||
|
||||
export const txtMore = i18n.translate('uiActions.actionPanel.more', {
|
||||
defaultMessage: 'More',
|
||||
});
|
||||
|
||||
interface ActionWithContext<Context extends BaseContext = BaseContext> {
|
||||
action: Action<Context>;
|
||||
context: Context;
|
||||
|
@ -41,6 +44,88 @@ interface ActionWithContext<Context extends BaseContext = BaseContext> {
|
|||
trigger: Trigger;
|
||||
}
|
||||
|
||||
type ItemDescriptor = EuiContextMenuPanelItemDescriptor & {
|
||||
_order: number;
|
||||
_title?: string;
|
||||
};
|
||||
|
||||
type PanelDescriptor = EuiContextMenuPanelDescriptor & {
|
||||
_level?: number;
|
||||
_icon?: string;
|
||||
items: ItemDescriptor[];
|
||||
};
|
||||
|
||||
const onClick = (action: Action, context: ActionExecutionContext<object>, close: () => void) => (
|
||||
event: React.MouseEvent
|
||||
) => {
|
||||
if (event.currentTarget instanceof HTMLAnchorElement) {
|
||||
// from react-router's <Link/>
|
||||
if (
|
||||
!event.defaultPrevented && // onClick prevented default
|
||||
event.button === 0 && // ignore everything but left clicks
|
||||
(!event.currentTarget.target || event.currentTarget.target === '_self') && // let browser handle "target=_blank" etc.
|
||||
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys
|
||||
) {
|
||||
event.preventDefault();
|
||||
action.execute(context);
|
||||
}
|
||||
} else action.execute(context);
|
||||
close();
|
||||
};
|
||||
|
||||
/**
|
||||
* This method adds "More" item to panels, which have more than 4 items; and
|
||||
* moves all items after the thrird one into that "More" sub-menu.
|
||||
*/
|
||||
const wrapMainPanelItemsIntoSubmenu = (panels: Record<string, PanelDescriptor>, id: string) => {
|
||||
const panel = panels[id];
|
||||
if (!panel) return;
|
||||
const maxItemsBeforeWrapping = 4;
|
||||
if (!panel.items) return;
|
||||
if (panel.items.length <= maxItemsBeforeWrapping) return;
|
||||
const visibleItems = panel.items.slice(0, 3) as ItemDescriptor[];
|
||||
const itemsInSubmenu = panel.items.slice(3) as ItemDescriptor[];
|
||||
const morePanelId = panel.id + '__more';
|
||||
const more: ItemDescriptor = {
|
||||
name: txtMore,
|
||||
panel: morePanelId,
|
||||
icon: 'boxesHorizontal',
|
||||
'data-test-subj': `embeddablePanelMore-${id}`,
|
||||
_order: -1,
|
||||
};
|
||||
panel.items = [...visibleItems, more];
|
||||
const subPanel: PanelDescriptor = {
|
||||
id: morePanelId,
|
||||
title: panel.title || defaultTitle,
|
||||
items: itemsInSubmenu,
|
||||
};
|
||||
panels[morePanelId] = subPanel;
|
||||
};
|
||||
|
||||
const removeItemMetaFields = (items: ItemDescriptor[]): EuiContextMenuPanelItemDescriptor[] => {
|
||||
const euiItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
for (const item of items) {
|
||||
const { _order: omit, _title: omit2, ...rest } = item;
|
||||
euiItems.push(rest);
|
||||
}
|
||||
return euiItems;
|
||||
};
|
||||
|
||||
const removePanelMetaFields = (panels: PanelDescriptor[]): EuiContextMenuPanelDescriptor[] => {
|
||||
const euiPanels: EuiContextMenuPanelDescriptor[] = [];
|
||||
for (const panel of panels) {
|
||||
const { _level: omit, _icon: omit2, ...rest } = panel;
|
||||
euiPanels.push({ ...rest, items: removeItemMetaFields(rest.items) });
|
||||
}
|
||||
return euiPanels;
|
||||
};
|
||||
|
||||
export interface BuildContextMenuParams {
|
||||
actions: ActionWithContext[];
|
||||
title?: string;
|
||||
closeMenu?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
|
||||
*/
|
||||
|
@ -48,131 +133,88 @@ export async function buildContextMenuForActions({
|
|||
actions,
|
||||
title = defaultTitle,
|
||||
closeMenu = () => {},
|
||||
}: {
|
||||
actions: ActionWithContext[];
|
||||
title?: string;
|
||||
closeMenu?: () => void;
|
||||
}): Promise<EuiContextMenuPanelDescriptor> {
|
||||
const menuItems = await buildEuiContextMenuPanelItems({
|
||||
actions,
|
||||
closeMenu,
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'mainMenu',
|
||||
title,
|
||||
items: menuItems,
|
||||
}: BuildContextMenuParams): Promise<EuiContextMenuPanelDescriptor[]> {
|
||||
const panels: Record<string, PanelDescriptor> = {
|
||||
mainMenu: {
|
||||
id: 'mainMenu',
|
||||
title,
|
||||
items: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an array of Actions into the shape needed to build an EUIContextMenu
|
||||
*/
|
||||
async function buildEuiContextMenuPanelItems({
|
||||
actions,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: ActionWithContext[];
|
||||
closeMenu: () => void;
|
||||
}) {
|
||||
actions = sortBy(
|
||||
actions,
|
||||
(a) => -1 * (a.action.order ?? 0),
|
||||
(a) => a.action.type,
|
||||
(a) => a.action.getDisplayName({ ...a.context, trigger: a.trigger })
|
||||
);
|
||||
|
||||
const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length);
|
||||
const promises = actions.map(async ({ action, context, trigger }, index) => {
|
||||
const isCompatible = await action.isCompatible({
|
||||
...context,
|
||||
trigger,
|
||||
});
|
||||
if (!isCompatible) {
|
||||
return;
|
||||
const promises = actions.map(async (item) => {
|
||||
const { action } = item;
|
||||
const context: ActionExecutionContext<object> = { ...item.context, trigger: item.trigger };
|
||||
const isCompatible = await item.action.isCompatible(context);
|
||||
if (!isCompatible) return;
|
||||
let parentPanel = '';
|
||||
let currentPanel = '';
|
||||
if (action.grouping) {
|
||||
for (let i = 0; i < action.grouping.length; i++) {
|
||||
const group = action.grouping[i];
|
||||
currentPanel = group.id;
|
||||
if (!panels[currentPanel]) {
|
||||
const name = group.getDisplayName ? group.getDisplayName(context) : group.id;
|
||||
panels[currentPanel] = {
|
||||
id: currentPanel,
|
||||
title: name,
|
||||
items: [],
|
||||
_level: i,
|
||||
_icon: group.getIconType ? group.getIconType(context) : 'empty',
|
||||
};
|
||||
if (parentPanel) {
|
||||
panels[parentPanel].items!.push({
|
||||
name,
|
||||
panel: currentPanel,
|
||||
icon: group.getIconType ? group.getIconType(context) : 'empty',
|
||||
_order: group.order || 0,
|
||||
_title: group.getDisplayName ? group.getDisplayName(context) : '',
|
||||
});
|
||||
}
|
||||
}
|
||||
parentPanel = currentPanel;
|
||||
}
|
||||
}
|
||||
|
||||
items[index] = await convertPanelActionToContextMenuItem({
|
||||
action,
|
||||
actionContext: context,
|
||||
trigger,
|
||||
closeMenu,
|
||||
panels[parentPanel || 'mainMenu'].items!.push({
|
||||
name: action.MenuItem
|
||||
? React.createElement(uiToReactComponent(action.MenuItem), { context })
|
||||
: action.getDisplayName(context),
|
||||
icon: action.getIconType(context),
|
||||
'data-test-subj': `embeddablePanelAction-${action.id}`,
|
||||
onClick: onClick(action, context, closeMenu),
|
||||
href: action.getHref ? await action.getHref(context) : undefined,
|
||||
_order: action.order || 0,
|
||||
_title: action.getDisplayName(context),
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return items.filter(Boolean);
|
||||
}
|
||||
for (const panel of Object.values(panels)) {
|
||||
const items = panel.items.filter(Boolean) as ItemDescriptor[];
|
||||
panel.items = _.sortBy(
|
||||
items,
|
||||
(a) => -1 * (a._order ?? 0),
|
||||
(a) => a._title
|
||||
);
|
||||
}
|
||||
|
||||
async function convertPanelActionToContextMenuItem<Context extends object>({
|
||||
action,
|
||||
actionContext,
|
||||
trigger,
|
||||
closeMenu,
|
||||
}: {
|
||||
action: Action<Context>;
|
||||
actionContext: Context;
|
||||
trigger: Trigger;
|
||||
closeMenu: () => void;
|
||||
}): Promise<EuiContextMenuPanelItemDescriptor> {
|
||||
const menuPanelItem: EuiContextMenuPanelItemDescriptor = {
|
||||
name: action.MenuItem
|
||||
? React.createElement(uiToReactComponent(action.MenuItem), {
|
||||
context: {
|
||||
...actionContext,
|
||||
trigger,
|
||||
},
|
||||
})
|
||||
: action.getDisplayName({
|
||||
...actionContext,
|
||||
trigger,
|
||||
}),
|
||||
icon: action.getIconType({
|
||||
...actionContext,
|
||||
trigger,
|
||||
}),
|
||||
panel: _.get(action, 'childContextMenuPanel.id'),
|
||||
'data-test-subj': `embeddablePanelAction-${action.id}`,
|
||||
};
|
||||
wrapMainPanelItemsIntoSubmenu(panels, 'mainMenu');
|
||||
|
||||
menuPanelItem.onClick = (event) => {
|
||||
if (event.currentTarget instanceof HTMLAnchorElement) {
|
||||
// from react-router's <Link/>
|
||||
if (
|
||||
!event.defaultPrevented && // onClick prevented default
|
||||
event.button === 0 && // ignore everything but left clicks
|
||||
(!event.currentTarget.target || event.currentTarget.target === '_self') && // let browser handle "target=_blank" etc.
|
||||
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys
|
||||
) {
|
||||
event.preventDefault();
|
||||
action.execute({
|
||||
...actionContext,
|
||||
trigger,
|
||||
for (const panel of Object.values(panels)) {
|
||||
if (panel._level === 0) {
|
||||
// TODO: Add separator line here once it is available in EUI.
|
||||
// See https://github.com/elastic/eui/pull/4018
|
||||
if (panel.items.length > 3) {
|
||||
panels.mainMenu.items.push({
|
||||
name: panel.title || panel.id,
|
||||
icon: panel._icon || 'empty',
|
||||
panel: panel.id,
|
||||
});
|
||||
} else {
|
||||
// let browser handle navigation
|
||||
panels.mainMenu.items.push(...panel.items);
|
||||
}
|
||||
} else {
|
||||
// not a link
|
||||
action.execute({
|
||||
...actionContext,
|
||||
trigger,
|
||||
});
|
||||
}
|
||||
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
if (action.getHref) {
|
||||
const href = await action.getHref({
|
||||
...actionContext,
|
||||
trigger,
|
||||
});
|
||||
if (href) {
|
||||
menuPanelItem.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
return menuPanelItem;
|
||||
const panelList = Object.values(panels);
|
||||
return removePanelMetaFields(panelList);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,10 @@ export {
|
|||
IncompatibleActionError,
|
||||
} from './actions';
|
||||
export { buildContextMenuForActions } from './context_menu';
|
||||
export { Presentable as UiActionsPresentable } from './util';
|
||||
export {
|
||||
Presentable as UiActionsPresentable,
|
||||
PresentableGrouping as UiActionsPresentableGrouping,
|
||||
} from './util';
|
||||
export {
|
||||
Trigger,
|
||||
TriggerContext,
|
||||
|
|
|
@ -109,7 +109,7 @@ export class UiActionsExecutionService {
|
|||
}
|
||||
|
||||
private async executeMultipleActions(tasks: ExecuteActionTask[]) {
|
||||
const panel = await buildContextMenuForActions({
|
||||
const panels = await buildContextMenuForActions({
|
||||
actions: tasks.map(({ action, context, trigger }) => ({
|
||||
action,
|
||||
context,
|
||||
|
@ -121,7 +121,7 @@ export class UiActionsExecutionService {
|
|||
session.close();
|
||||
},
|
||||
});
|
||||
const session = openContextMenu([panel], {
|
||||
const session = openContextMenu(panels, {
|
||||
'data-test-subj': 'multipleActionsContextMenu',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -68,4 +68,20 @@ export interface Presentable<Context extends object = object> {
|
|||
* the context and should be displayed to user, otherwise resolves to false.
|
||||
*/
|
||||
isCompatible(context: Context): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Grouping where this item should appear as a submenu. Each entry is a new
|
||||
* sub-menu level. For example, used to show drilldowns and sharing options
|
||||
* in panel context menu in a sub-menu.
|
||||
*/
|
||||
readonly grouping?: PresentableGrouping<Context>;
|
||||
}
|
||||
|
||||
export interface PresentableGroup<Context extends object = object>
|
||||
extends Partial<
|
||||
Pick<Presentable<Context>, 'getDisplayName' | 'getDisplayNameTooltip' | 'getIconType' | 'order'>
|
||||
> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type PresentableGrouping<Context extends object = object> = Array<PresentableGroup<Context>>;
|
||||
|
|
|
@ -96,7 +96,9 @@ export default function ({ getService, loadTestFile }) {
|
|||
|
||||
loadTestFile(require.resolve('./dashboard_time_picker'));
|
||||
loadTestFile(require.resolve('./bwc_shared_urls'));
|
||||
loadTestFile(require.resolve('./panel_controls'));
|
||||
loadTestFile(require.resolve('./panel_replacing'));
|
||||
loadTestFile(require.resolve('./panel_cloning'));
|
||||
loadTestFile(require.resolve('./panel_context_menu'));
|
||||
loadTestFile(require.resolve('./dashboard_state'));
|
||||
});
|
||||
|
||||
|
|
80
test/functional/apps/dashboard/panel_cloning.ts
Normal file
80
test/functional/apps/dashboard/panel_cloning.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 '@kbn/expect';
|
||||
import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const PageObjects = getPageObjects([
|
||||
'dashboard',
|
||||
'header',
|
||||
'visualize',
|
||||
'discover',
|
||||
'timePicker',
|
||||
]);
|
||||
|
||||
describe('dashboard panel cloning', function viewEditModeTests() {
|
||||
before(async function () {
|
||||
await PageObjects.dashboard.initTests();
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setHistoricalDataRange();
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
|
||||
it('clones a panel', async () => {
|
||||
const initialPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
await dashboardPanelActions.clonePanelByTitle(PIE_CHART_VIS_NAME);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const postPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(postPanelTitles.length).to.equal(initialPanelTitles.length + 1);
|
||||
});
|
||||
|
||||
it('appends a clone title tag', async () => {
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles[1]).to.equal(PIE_CHART_VIS_NAME + ' (copy)');
|
||||
});
|
||||
|
||||
it('retains original panel dimensions', async () => {
|
||||
const panelDimensions = await PageObjects.dashboard.getPanelDimensions();
|
||||
expect(panelDimensions[0]).to.eql(panelDimensions[1]);
|
||||
});
|
||||
|
||||
it('gives a correct title to the clone of a clone', async () => {
|
||||
const initialPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
const clonedPanelName = initialPanelTitles[initialPanelTitles.length - 1];
|
||||
await dashboardPanelActions.clonePanelByTitle(clonedPanelName);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const postPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(postPanelTitles.length).to.equal(initialPanelTitles.length + 1);
|
||||
expect(postPanelTitles[postPanelTitles.length - 1]).to.equal(
|
||||
PIE_CHART_VIS_NAME + ' (copy 1)'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
185
test/functional/apps/dashboard/panel_context_menu.ts
Normal file
185
test/functional/apps/dashboard/panel_context_menu.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 '@kbn/expect';
|
||||
import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page';
|
||||
import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const dashboardVisualizations = getService('dashboardVisualizations');
|
||||
const PageObjects = getPageObjects([
|
||||
'dashboard',
|
||||
'header',
|
||||
'visualize',
|
||||
'discover',
|
||||
'timePicker',
|
||||
]);
|
||||
const dashboardName = 'Dashboard Panel Controls Test';
|
||||
|
||||
describe('dashboard panel context menu', function viewEditModeTests() {
|
||||
before(async function () {
|
||||
await PageObjects.dashboard.initTests();
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setHistoricalDataRange();
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
|
||||
it('are hidden in view mode', async function () {
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.expectMissingEditPanelAction();
|
||||
await dashboardPanelActions.expectMissingRemovePanelAction();
|
||||
});
|
||||
|
||||
it('are shown in edit mode', async function () {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
|
||||
const isContextMenuIconVisible = await dashboardPanelActions.isContextMenuIconVisible();
|
||||
expect(isContextMenuIconVisible).to.equal(true);
|
||||
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsClonePanelAction();
|
||||
await dashboardPanelActions.expectExistsReplacePanelAction();
|
||||
await dashboardPanelActions.expectExistsRemovePanelAction();
|
||||
await dashboardPanelActions.expectExistsToggleExpandAction();
|
||||
});
|
||||
|
||||
it('are shown in edit mode after a hard refresh', async () => {
|
||||
// Based off an actual bug encountered in a PR where a hard refresh in
|
||||
// edit mode did not show the edit mode controls.
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
// The second parameter of true will include the timestamp in the url and
|
||||
// trigger a hard refresh.
|
||||
await browser.get(currentUrl.toString(), true);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsClonePanelAction();
|
||||
await dashboardPanelActions.expectExistsReplacePanelAction();
|
||||
await dashboardPanelActions.expectExistsRemovePanelAction();
|
||||
|
||||
// Get rid of the timestamp in the url.
|
||||
await browser.get(currentUrl.toString(), false);
|
||||
});
|
||||
|
||||
describe('visualization object edit menu', () => {
|
||||
it('opens a visualization when edit link is clicked', async () => {
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
expect(currentUrl).to.contain(VisualizeConstants.EDIT_PATH);
|
||||
});
|
||||
|
||||
it('deletes the visualization when delete link is clicked', async () => {
|
||||
await PageObjects.header.clickDashboard();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.removePanel();
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saved search object edit menu', () => {
|
||||
const searchName = 'my search';
|
||||
|
||||
before(async () => {
|
||||
await PageObjects.header.clickDiscover();
|
||||
await PageObjects.discover.clickNewSearchButton();
|
||||
await dashboardVisualizations.createSavedSearch({ name: searchName, fields: ['bytes'] });
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.header.clickDashboard();
|
||||
|
||||
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
|
||||
if (inViewMode) await PageObjects.dashboard.switchToEditMode();
|
||||
await dashboardAddPanel.addSavedSearch(searchName);
|
||||
});
|
||||
|
||||
it('should be one panel on dashboard', async () => {
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.be(1);
|
||||
});
|
||||
|
||||
it('opens a saved search when edit link is clicked', async () => {
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const queryName = await PageObjects.discover.getCurrentQueryName();
|
||||
expect(queryName).to.be(searchName);
|
||||
});
|
||||
|
||||
it('deletes the saved search when delete link is clicked', async () => {
|
||||
await PageObjects.header.clickDashboard();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.removePanel();
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on an expanded panel', function () {
|
||||
before('reset dashboard', async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
await browser.get(currentUrl.toString(), false);
|
||||
});
|
||||
|
||||
before('and add one panel and save to put dashboard in "view" mode', async () => {
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||
});
|
||||
|
||||
before('expand panel to "full screen"', async () => {
|
||||
await dashboardPanelActions.clickExpandPanelToggle();
|
||||
});
|
||||
|
||||
it('context menu actions are hidden in view mode', async function () {
|
||||
await dashboardPanelActions.expectMissingEditPanelAction();
|
||||
await dashboardPanelActions.expectMissingDuplicatePanelAction();
|
||||
await dashboardPanelActions.expectMissingReplacePanelAction();
|
||||
await dashboardPanelActions.expectMissingRemovePanelAction();
|
||||
});
|
||||
|
||||
describe('in edit mode', () => {
|
||||
it('switch to edit mode', async function () {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
});
|
||||
|
||||
it('some context menu actions should be present', async function () {
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsClonePanelAction();
|
||||
await dashboardPanelActions.expectExistsReplacePanelAction();
|
||||
});
|
||||
|
||||
it('"remove panel" action should not be present', async function () {
|
||||
await dashboardPanelActions.expectMissingRemovePanelAction();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,293 +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 expect from '@kbn/expect';
|
||||
|
||||
import {
|
||||
PIE_CHART_VIS_NAME,
|
||||
AREA_CHART_VIS_NAME,
|
||||
LINE_CHART_VIS_NAME,
|
||||
} from '../../page_objects/dashboard_page';
|
||||
import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants';
|
||||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const browser = getService('browser');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const dashboardReplacePanel = getService('dashboardReplacePanel');
|
||||
const dashboardVisualizations = getService('dashboardVisualizations');
|
||||
const renderable = getService('renderable');
|
||||
const PageObjects = getPageObjects([
|
||||
'dashboard',
|
||||
'header',
|
||||
'visualize',
|
||||
'discover',
|
||||
'timePicker',
|
||||
]);
|
||||
const dashboardName = 'Dashboard Panel Controls Test';
|
||||
|
||||
describe('dashboard panel controls', function viewEditModeTests() {
|
||||
before(async function () {
|
||||
await PageObjects.dashboard.initTests();
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
|
||||
describe('visualization object replace flyout', () => {
|
||||
let intialDimensions;
|
||||
before(async () => {
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setHistoricalDataRange();
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
await dashboardAddPanel.addVisualization(LINE_CHART_VIS_NAME);
|
||||
intialDimensions = await PageObjects.dashboard.getPanelDimensions();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
|
||||
it('replaces old panel with selected panel', async () => {
|
||||
await dashboardPanelActions.replacePanelByTitle(PIE_CHART_VIS_NAME);
|
||||
await dashboardReplacePanel.replaceEmbeddable(AREA_CHART_VIS_NAME);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles.length).to.be(2);
|
||||
expect(panelTitles[0]).to.be(AREA_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
it('replaces selected visualization with old dimensions', async () => {
|
||||
const newDimensions = await PageObjects.dashboard.getPanelDimensions();
|
||||
expect(intialDimensions[0]).to.eql(newDimensions[0]);
|
||||
});
|
||||
|
||||
it('replaced panel persisted correctly when dashboard is hard refreshed', async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
await browser.get(currentUrl, true);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles.length).to.be(2);
|
||||
expect(panelTitles[0]).to.be(AREA_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
it('replaced panel with saved search', async () => {
|
||||
const replacedSearch = 'replaced saved search';
|
||||
await dashboardVisualizations.createSavedSearch({
|
||||
name: replacedSearch,
|
||||
fields: ['bytes', 'agent'],
|
||||
});
|
||||
await PageObjects.header.clickDashboard();
|
||||
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
|
||||
if (inViewMode) {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
}
|
||||
await dashboardPanelActions.replacePanelByTitle(AREA_CHART_VIS_NAME);
|
||||
await dashboardReplacePanel.replaceEmbeddable(replacedSearch, 'search');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles.length).to.be(2);
|
||||
expect(panelTitles[0]).to.be(replacedSearch);
|
||||
});
|
||||
});
|
||||
|
||||
describe('panel cloning', function () {
|
||||
before(async () => {
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setHistoricalDataRange();
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
|
||||
it('clones a panel', async () => {
|
||||
const initialPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
await dashboardPanelActions.clonePanelByTitle(PIE_CHART_VIS_NAME);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const postPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(postPanelTitles.length).to.equal(initialPanelTitles.length + 1);
|
||||
});
|
||||
|
||||
it('appends a clone title tag', async () => {
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles[1]).to.equal(PIE_CHART_VIS_NAME + ' (copy)');
|
||||
});
|
||||
|
||||
it('retains original panel dimensions', async () => {
|
||||
const panelDimensions = await PageObjects.dashboard.getPanelDimensions();
|
||||
expect(panelDimensions[0]).to.eql(panelDimensions[1]);
|
||||
});
|
||||
|
||||
it('gives a correct title to the clone of a clone', async () => {
|
||||
const initialPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
const clonedPanelName = initialPanelTitles[initialPanelTitles.length - 1];
|
||||
await dashboardPanelActions.clonePanelByTitle(clonedPanelName);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const postPanelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(postPanelTitles.length).to.equal(initialPanelTitles.length + 1);
|
||||
expect(postPanelTitles[postPanelTitles.length - 1]).to.equal(
|
||||
PIE_CHART_VIS_NAME + ' (copy 1)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('panel edit controls', function () {
|
||||
before(async () => {
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setHistoricalDataRange();
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
it('are hidden in view mode', async function () {
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.expectMissingEditPanelAction();
|
||||
await dashboardPanelActions.expectMissingRemovePanelAction();
|
||||
});
|
||||
|
||||
it('are shown in edit mode', async function () {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
|
||||
const isContextMenuIconVisible = await dashboardPanelActions.isContextMenuIconVisible();
|
||||
expect(isContextMenuIconVisible).to.equal(true);
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsReplacePanelAction();
|
||||
await dashboardPanelActions.expectExistsDuplicatePanelAction();
|
||||
await dashboardPanelActions.expectExistsRemovePanelAction();
|
||||
});
|
||||
|
||||
// Based off an actual bug encountered in a PR where a hard refresh in edit mode did not show the edit mode
|
||||
// controls.
|
||||
it('are shown in edit mode after a hard refresh', async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
// the second parameter of true will include the timestamp in the url and trigger a hard refresh.
|
||||
await browser.get(currentUrl.toString(), true);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsReplacePanelAction();
|
||||
await dashboardPanelActions.expectExistsDuplicatePanelAction();
|
||||
await dashboardPanelActions.expectExistsRemovePanelAction();
|
||||
|
||||
// Get rid of the timestamp in the url.
|
||||
await browser.get(currentUrl.toString(), false);
|
||||
});
|
||||
|
||||
describe('on an expanded panel', function () {
|
||||
it('are hidden in view mode', async function () {
|
||||
await renderable.waitForRender();
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickExpandPanelToggle();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.expectMissingEditPanelAction();
|
||||
await dashboardPanelActions.expectMissingReplacePanelAction();
|
||||
await dashboardPanelActions.expectMissingDuplicatePanelAction();
|
||||
await dashboardPanelActions.expectMissingRemovePanelAction();
|
||||
});
|
||||
|
||||
it('in edit mode hides remove icons ', async function () {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsReplacePanelAction();
|
||||
await dashboardPanelActions.expectExistsDuplicatePanelAction();
|
||||
await dashboardPanelActions.expectMissingRemovePanelAction();
|
||||
await dashboardPanelActions.clickExpandPanelToggle();
|
||||
});
|
||||
});
|
||||
|
||||
describe('visualization object edit menu', () => {
|
||||
it('opens a visualization when edit link is clicked', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
expect(currentUrl).to.contain(VisualizeConstants.EDIT_PATH);
|
||||
});
|
||||
|
||||
it('deletes the visualization when delete link is clicked', async () => {
|
||||
await PageObjects.header.clickDashboard();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.removePanel();
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saved search object edit menu', () => {
|
||||
const searchName = 'my search';
|
||||
before(async () => {
|
||||
await PageObjects.header.clickDiscover();
|
||||
await PageObjects.discover.clickNewSearchButton();
|
||||
await dashboardVisualizations.createSavedSearch({ name: searchName, fields: ['bytes'] });
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.header.clickDashboard();
|
||||
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
|
||||
if (inViewMode) {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
}
|
||||
await dashboardAddPanel.addSavedSearch(searchName);
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.be(1);
|
||||
});
|
||||
|
||||
it('opens a saved search when edit link is clicked', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const queryName = await PageObjects.discover.getCurrentQueryName();
|
||||
expect(queryName).to.be(searchName);
|
||||
});
|
||||
|
||||
it('deletes the saved search when delete link is clicked', async () => {
|
||||
await PageObjects.header.clickDashboard();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.removePanel();
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Panel expand should also be shown in view mode, but only on mouse hover.
|
||||
describe('panel expand control', function () {
|
||||
it('shown in edit mode', async function () {
|
||||
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.expectExistsToggleExpandAction();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
100
test/functional/apps/dashboard/panel_replacing.ts
Normal file
100
test/functional/apps/dashboard/panel_replacing.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 '@kbn/expect';
|
||||
import {
|
||||
PIE_CHART_VIS_NAME,
|
||||
AREA_CHART_VIS_NAME,
|
||||
LINE_CHART_VIS_NAME,
|
||||
} from '../../page_objects/dashboard_page';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const dashboardReplacePanel = getService('dashboardReplacePanel');
|
||||
const dashboardVisualizations = getService('dashboardVisualizations');
|
||||
const PageObjects = getPageObjects([
|
||||
'dashboard',
|
||||
'header',
|
||||
'visualize',
|
||||
'discover',
|
||||
'timePicker',
|
||||
]);
|
||||
|
||||
describe('replace dashboard panels', function viewEditModeTests() {
|
||||
let intialDimensions: undefined | Array<{ width: number; height: number }>;
|
||||
|
||||
before(async function () {
|
||||
await PageObjects.dashboard.initTests();
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setHistoricalDataRange();
|
||||
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
|
||||
await dashboardAddPanel.addVisualization(LINE_CHART_VIS_NAME);
|
||||
intialDimensions = await PageObjects.dashboard.getPanelDimensions();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
|
||||
it('replaces old panel with selected panel', async () => {
|
||||
await dashboardPanelActions.replacePanelByTitle(PIE_CHART_VIS_NAME);
|
||||
await dashboardReplacePanel.replaceEmbeddable(AREA_CHART_VIS_NAME);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles.length).to.be(2);
|
||||
expect(panelTitles[0]).to.be(AREA_CHART_VIS_NAME);
|
||||
const newDimensions = await PageObjects.dashboard.getPanelDimensions();
|
||||
expect(intialDimensions![0]).to.eql(newDimensions[0]);
|
||||
});
|
||||
|
||||
it('replaced panel persisted correctly when dashboard is hard refreshed', async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
await browser.get(currentUrl, true);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles.length).to.be(2);
|
||||
expect(panelTitles[0]).to.be(AREA_CHART_VIS_NAME);
|
||||
});
|
||||
|
||||
it('replaced panel with saved search', async () => {
|
||||
const replacedSearch = 'replaced saved search';
|
||||
await dashboardVisualizations.createSavedSearch({
|
||||
name: replacedSearch,
|
||||
fields: ['bytes', 'agent'],
|
||||
});
|
||||
await PageObjects.header.clickDashboard();
|
||||
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
|
||||
if (inViewMode) {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
}
|
||||
await dashboardPanelActions.replacePanelByTitle(AREA_CHART_VIS_NAME);
|
||||
await dashboardReplacePanel.replaceEmbeddable(replacedSearch, 'search');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const panelTitles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(panelTitles.length).to.be(2);
|
||||
expect(panelTitles[0]).to.be(replacedSearch);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -59,12 +59,32 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
|
|||
|
||||
async openContextMenu(parent?: WebElementWrapper) {
|
||||
log.debug(`openContextMenu(${parent}`);
|
||||
if (await testSubjects.exists('embeddablePanelContextMenuOpen')) return;
|
||||
await this.toggleContextMenu(parent);
|
||||
await this.expectContextMenuToBeOpen();
|
||||
}
|
||||
|
||||
async hasContextMenuMoreItem() {
|
||||
return await testSubjects.exists('embeddablePanelMore-mainMenu');
|
||||
}
|
||||
|
||||
async clickContextMenuMoreItem() {
|
||||
const hasMoreSubPanel = await testSubjects.exists('embeddablePanelMore-mainMenu');
|
||||
if (hasMoreSubPanel) {
|
||||
await testSubjects.click('embeddablePanelMore-mainMenu');
|
||||
}
|
||||
}
|
||||
|
||||
async openContextMenuMorePanel(parent?: WebElementWrapper) {
|
||||
await this.openContextMenu(parent);
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
|
||||
async clickEdit() {
|
||||
log.debug('clickEdit');
|
||||
await this.openContextMenu();
|
||||
const isActionVisible = await testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
if (!isActionVisible) await this.clickContextMenuMoreItem();
|
||||
await testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.common.waitForTopNavToBeVisible();
|
||||
|
@ -82,18 +102,28 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
|
|||
}
|
||||
|
||||
async clickExpandPanelToggle() {
|
||||
log.debug(`clickExpandPanelToggle`);
|
||||
this.openContextMenu();
|
||||
const isActionVisible = await testSubjects.exists(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
if (!isActionVisible) await this.clickContextMenuMoreItem();
|
||||
await testSubjects.click(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async removePanel() {
|
||||
log.debug('removePanel');
|
||||
await this.openContextMenu();
|
||||
const isActionVisible = await testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
if (!isActionVisible) await this.clickContextMenuMoreItem();
|
||||
const isPanelActionVisible = await testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
if (!isPanelActionVisible) await this.clickContextMenuMoreItem();
|
||||
await testSubjects.click(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async removePanelByTitle(title: string) {
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.openContextMenu(header);
|
||||
const isActionVisible = await testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
if (!isActionVisible) await this.clickContextMenuMoreItem();
|
||||
await testSubjects.click(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
|
@ -110,6 +140,10 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
|
|||
} else {
|
||||
await this.openContextMenu();
|
||||
}
|
||||
const actionExists = await testSubjects.exists(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
if (!actionExists) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
await testSubjects.click(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
|
@ -131,52 +165,78 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
|
|||
|
||||
async openInspector(parent: WebElementWrapper) {
|
||||
await this.openContextMenu(parent);
|
||||
const exists = await testSubjects.exists(OPEN_INSPECTOR_TEST_SUBJ);
|
||||
if (!exists) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectExistsRemovePanelAction() {
|
||||
log.debug('expectExistsRemovePanelAction');
|
||||
await testSubjects.existOrFail(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectMissingRemovePanelAction() {
|
||||
log.debug('expectMissingRemovePanelAction');
|
||||
await testSubjects.missingOrFail(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
async expectExistsPanelAction(testSubject: string) {
|
||||
log.debug('expectExistsPanelAction', testSubject);
|
||||
await this.openContextMenu();
|
||||
if (await testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return;
|
||||
if (await this.hasContextMenuMoreItem()) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
await testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ);
|
||||
await this.toggleContextMenu();
|
||||
}
|
||||
|
||||
async expectMissingPanelAction(testSubject: string) {
|
||||
log.debug('expectMissingPanelAction', testSubject);
|
||||
await this.openContextMenu();
|
||||
await testSubjects.missingOrFail(testSubject);
|
||||
if (await this.hasContextMenuMoreItem()) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
await testSubjects.missingOrFail(testSubject);
|
||||
}
|
||||
await this.toggleContextMenu();
|
||||
}
|
||||
|
||||
async expectExistsEditPanelAction() {
|
||||
log.debug('expectExistsEditPanelAction');
|
||||
await testSubjects.existOrFail(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectExistsReplacePanelAction() {
|
||||
log.debug('expectExistsReplacePanelAction');
|
||||
await testSubjects.existOrFail(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectExistsPanelAction(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectExistsDuplicatePanelAction() {
|
||||
log.debug('expectExistsDuplicatePanelAction');
|
||||
await testSubjects.existOrFail(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
async expectExistsClonePanelAction() {
|
||||
log.debug('expectExistsClonePanelAction');
|
||||
await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectMissingEditPanelAction() {
|
||||
log.debug('expectMissingEditPanelAction');
|
||||
await testSubjects.missingOrFail(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectMissingReplacePanelAction() {
|
||||
log.debug('expectMissingReplacePanelAction');
|
||||
await testSubjects.missingOrFail(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectMissingPanelAction(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectMissingDuplicatePanelAction() {
|
||||
log.debug('expectMissingDuplicatePanelAction');
|
||||
await testSubjects.missingOrFail(REPLACE_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectMissingPanelAction(CLONE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectMissingRemovePanelAction() {
|
||||
log.debug('expectMissingRemovePanelAction');
|
||||
await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectExistsToggleExpandAction() {
|
||||
log.debug('expectExistsToggleExpandAction');
|
||||
await testSubjects.existOrFail(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async getPanelHeading(title: string) {
|
||||
|
|
|
@ -73,7 +73,7 @@ export function DashboardReplacePanelProvider({ getService }: FtrProviderContext
|
|||
return this.replaceEmbeddable(vizName, 'visualization');
|
||||
}
|
||||
|
||||
async replaceEmbeddable(embeddableName: string, embeddableType: string) {
|
||||
async replaceEmbeddable(embeddableName: string, embeddableType?: string) {
|
||||
log.debug(
|
||||
`DashboardReplacePanel.replaceEmbeddable, name: ${embeddableName}, type: ${embeddableType}`
|
||||
);
|
||||
|
|
|
@ -8,7 +8,10 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public';
|
||||
import {
|
||||
isEnhancedEmbeddable,
|
||||
embeddableEnhancedContextMenuDrilldownGrouping,
|
||||
} from '../../../../../../embeddable_enhanced/public';
|
||||
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { StartDependencies } from '../../../../plugin';
|
||||
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
|
@ -24,6 +27,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
|
|||
public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
|
||||
public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
|
||||
public order = 12;
|
||||
public grouping = embeddableEnhancedContextMenuDrilldownGrouping;
|
||||
|
||||
constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {}
|
||||
|
||||
|
|
|
@ -13,7 +13,10 @@ import {
|
|||
import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { txtDisplayName } from './i18n';
|
||||
import { MenuItem } from './menu_item';
|
||||
import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public';
|
||||
import {
|
||||
isEnhancedEmbeddable,
|
||||
embeddableEnhancedContextMenuDrilldownGrouping,
|
||||
} from '../../../../../../embeddable_enhanced/public';
|
||||
import { StartDependencies } from '../../../../plugin';
|
||||
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { ensureNestedTriggers } from '../drilldown_shared';
|
||||
|
@ -28,6 +31,7 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
|
|||
public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN;
|
||||
public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN;
|
||||
public order = 10;
|
||||
public grouping = embeddableEnhancedContextMenuDrilldownGrouping;
|
||||
|
||||
constructor(protected readonly params: FlyoutEditDrilldownParams) {}
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IEmbeddable } from '../../../../../src/plugins/embeddable/public';
|
||||
import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export const contextMenuDrilldownGrouping: PresentableGrouping<{
|
||||
embeddable?: IEmbeddable;
|
||||
}> = [
|
||||
{
|
||||
id: 'drilldowns',
|
||||
getDisplayName: () => 'Drilldowns',
|
||||
getIconType: () => 'symlink',
|
||||
order: 25,
|
||||
},
|
||||
];
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export * from './panel_notifications_action';
|
||||
export * from './drilldown_grouping';
|
||||
|
|
|
@ -20,3 +20,4 @@ export function plugin(context: PluginInitializerContext) {
|
|||
|
||||
export { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types';
|
||||
export { isEnhancedEmbeddable } from './embeddables';
|
||||
export { contextMenuDrilldownGrouping as embeddableEnhancedContextMenuDrilldownGrouping } from './actions';
|
||||
|
|
|
@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
after('clean-up custom time range on panel', async () => {
|
||||
await common.navigateToApp('dashboard');
|
||||
await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
await panelActions.openContextMenu();
|
||||
await panelActions.openContextMenuMorePanel();
|
||||
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
|
||||
await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton();
|
||||
await dashboard.saveDashboard('Dashboard with Pie Chart');
|
||||
|
@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
|
||||
await panelActions.openContextMenu();
|
||||
await panelActions.openContextMenuMorePanel();
|
||||
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
|
||||
await panelActionsTimeRange.clickToggleQuickMenuButton();
|
||||
await panelActionsTimeRange.clickCommonlyUsedTimeRange('Last_90 days');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue