Add embed mode options in the Share UI (#58435)

This commit is contained in:
Alex Wild 2020-06-08 13:12:03 +01:00 committed by GitHub
parent 00cff3cd7e
commit 38b9a8db8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 564 additions and 71 deletions

View file

@ -64,10 +64,6 @@ input[type='checkbox'],
padding-bottom: $euiSizeS;
}
.globalQueryBar {
padding: 0px $euiSizeS $euiSizeS $euiSizeS;
}
> nav,
> navbar {
z-index: 2 !important;

View file

@ -19,8 +19,9 @@
import _, { uniq } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui';
import React from 'react';
import { EUI_MODAL_CANCEL_BUTTON, EuiCheckboxGroup } from '@elastic/eui';
import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group';
import React, { useState, ReactElement } from 'react';
import ReactDOM from 'react-dom';
import angular from 'angular';
@ -94,6 +95,25 @@ export interface DashboardAppControllerDependencies extends RenderDeps {
navigation: NavigationStart;
}
enum UrlParams {
SHOW_TOP_MENU = 'show-top-menu',
SHOW_QUERY_INPUT = 'show-query-input',
SHOW_TIME_FILTER = 'show-time-filter',
SHOW_FILTER_BAR = 'show-filter-bar',
HIDE_FILTER_BAR = 'hide-filter-bar',
}
interface UrlParamsSelectedMap {
[UrlParams.SHOW_TOP_MENU]: boolean;
[UrlParams.SHOW_QUERY_INPUT]: boolean;
[UrlParams.SHOW_TIME_FILTER]: boolean;
[UrlParams.SHOW_FILTER_BAR]: boolean;
}
interface UrlParamValues extends Omit<UrlParamsSelectedMap, UrlParams.SHOW_FILTER_BAR> {
[UrlParams.HIDE_FILTER_BAR]: boolean;
}
export class DashboardAppController {
// Part of the exposed plugin API - do not remove without careful consideration.
appStatus: {
@ -133,8 +153,16 @@ export class DashboardAppController {
const filterManager = queryService.filterManager;
const queryFilter = filterManager;
const timefilter = queryService.timefilter.timefilter;
let showSearchBar = true;
let showQueryBar = true;
const isEmbeddedExternally = Boolean($routeParams.embed);
// url param rules should only apply when embedded (e.g. url?embed=true)
const shouldForceDisplay = (param: string): boolean =>
isEmbeddedExternally && Boolean($routeParams[param]);
const forceShowTopNavMenu = shouldForceDisplay(UrlParams.SHOW_TOP_MENU);
const forceShowQueryInput = shouldForceDisplay(UrlParams.SHOW_QUERY_INPUT);
const forceShowDatePicker = shouldForceDisplay(UrlParams.SHOW_TIME_FILTER);
const forceHideFilterBar = shouldForceDisplay(UrlParams.HIDE_FILTER_BAR);
let lastReloadRequestTime = 0;
const dash = ($scope.dash = $route.current.locals.dash);
@ -251,9 +279,6 @@ export class DashboardAppController {
}
};
const showFilterBar = () =>
$scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode();
const getEmptyScreenProps = (
shouldShowEditHelp: boolean,
isEmptyInReadOnlyMode: boolean
@ -299,6 +324,7 @@ export class DashboardAppController {
viewMode: dashboardStateManager.getViewMode(),
panels: embeddablesMap,
isFullScreenMode: dashboardStateManager.getFullScreenMode(),
isEmbeddedExternally,
isEmptyState: shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadonlyMode,
useMargins: dashboardStateManager.getUseMargins(),
lastReloadRequestTime,
@ -590,17 +616,33 @@ export class DashboardAppController {
dashboardStateManager.setSavedQueryId(savedQueryId);
};
const shouldShowFilterBar = (forceHide: boolean): boolean =>
!forceHide && ($scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode());
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
(forceShow || $scope.isVisible) && !dashboardStateManager.getFullScreenMode();
const getNavBarProps = () => {
const isFullScreenMode = dashboardStateManager.getFullScreenMode();
const screenTitle = dashboardStateManager.getTitle();
const showTopNavMenu = shouldShowNavBarComponent(forceShowTopNavMenu);
const showQueryInput = shouldShowNavBarComponent(forceShowQueryInput);
const showDatePicker = shouldShowNavBarComponent(forceShowDatePicker);
const showQueryBar = showQueryInput || showDatePicker;
const showFilterBar = shouldShowFilterBar(forceHideFilterBar);
const showSearchBar = showQueryBar || showFilterBar;
return {
appName: 'dashboard',
config: $scope.isVisible ? $scope.topNavMenu : undefined,
config: showTopNavMenu ? $scope.topNavMenu : undefined,
className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined,
screenTitle,
showTopNavMenu,
showSearchBar,
showQueryBar,
showFilterBar: showFilterBar(),
showQueryInput,
showDatePicker,
showFilterBar,
indexPatterns: $scope.indexPatterns,
showSaveQuery: $scope.showSaveQuery,
query: $scope.model.query,
@ -798,7 +840,6 @@ export class DashboardAppController {
} = {};
navActions[TopNavIds.FULL_SCREEN] = () => {
dashboardStateManager.setFullScreenMode(true);
showQueryBar = false;
updateNavBar();
};
navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW);
@ -923,6 +964,80 @@ export class DashboardAppController {
if (share) {
// the share button is only availabale if "share" plugin contract enabled
navActions[TopNavIds.SHARE] = (anchorElement) => {
const EmbedUrlParamExtension = ({
setParamValue,
}: {
setParamValue: (paramUpdate: UrlParamValues) => void;
}): ReactElement => {
const [urlParamsSelectedMap, setUrlParamsSelectedMap] = useState<UrlParamsSelectedMap>({
[UrlParams.SHOW_TOP_MENU]: false,
[UrlParams.SHOW_QUERY_INPUT]: false,
[UrlParams.SHOW_TIME_FILTER]: false,
[UrlParams.SHOW_FILTER_BAR]: true,
});
const checkboxes = [
{
id: UrlParams.SHOW_TOP_MENU,
label: i18n.translate('dashboard.embedUrlParamExtension.topMenu', {
defaultMessage: 'Top menu',
}),
},
{
id: UrlParams.SHOW_QUERY_INPUT,
label: i18n.translate('dashboard.embedUrlParamExtension.query', {
defaultMessage: 'Query',
}),
},
{
id: UrlParams.SHOW_TIME_FILTER,
label: i18n.translate('dashboard.embedUrlParamExtension.timeFilter', {
defaultMessage: 'Time filter',
}),
},
{
id: UrlParams.SHOW_FILTER_BAR,
label: i18n.translate('dashboard.embedUrlParamExtension.filterBar', {
defaultMessage: 'Filter bar',
}),
},
];
const handleChange = (param: string): void => {
const urlParamsSelectedMapUpdate = {
...urlParamsSelectedMap,
[param]: !urlParamsSelectedMap[param as keyof UrlParamsSelectedMap],
};
setUrlParamsSelectedMap(urlParamsSelectedMapUpdate);
const urlParamValues = {
[UrlParams.SHOW_TOP_MENU]: urlParamsSelectedMap[UrlParams.SHOW_TOP_MENU],
[UrlParams.SHOW_QUERY_INPUT]: urlParamsSelectedMap[UrlParams.SHOW_QUERY_INPUT],
[UrlParams.SHOW_TIME_FILTER]: urlParamsSelectedMap[UrlParams.SHOW_TIME_FILTER],
[UrlParams.HIDE_FILTER_BAR]: !urlParamsSelectedMap[UrlParams.SHOW_FILTER_BAR],
[param === UrlParams.SHOW_FILTER_BAR ? UrlParams.HIDE_FILTER_BAR : param]:
param === UrlParams.SHOW_FILTER_BAR
? urlParamsSelectedMap[UrlParams.SHOW_FILTER_BAR]
: !urlParamsSelectedMap[param as keyof UrlParamsSelectedMap],
};
setParamValue(urlParamValues);
};
return (
<EuiCheckboxGroup
options={checkboxes}
idToSelectedMap={(urlParamsSelectedMap as unknown) as EuiCheckboxGroupIdToSelectedMap}
onChange={handleChange}
legend={{
children: i18n.translate('dashboard.embedUrlParamExtension.include', {
defaultMessage: 'Include',
}),
}}
data-test-subj="embedUrlParamExtension"
/>
);
};
share.toggleShareContextMenu({
anchorElement,
allowEmbed: true,
@ -935,6 +1050,12 @@ export class DashboardAppController {
title: dash.title,
},
isDirty: dashboardStateManager.getIsDirty(),
embedUrlParamExtensions: [
{
paramName: 'embed',
component: EmbedUrlParamExtension,
},
],
});
};
}
@ -955,8 +1076,6 @@ export class DashboardAppController {
const visibleSubscription = chrome.getIsVisible$().subscribe((isVisible) => {
$scope.$evalAsync(() => {
$scope.isVisible = isVisible;
showSearchBar = isVisible || showFilterBar();
showQueryBar = !dashboardStateManager.getFullScreenMode() && isVisible;
updateNavBar();
});
});

View file

@ -57,6 +57,7 @@ export interface DashboardContainerInput extends ContainerInput {
useMargins: boolean;
title: string;
description?: string;
isEmbeddedExternally: boolean;
isFullScreenMode: boolean;
panels: {
[panelId: string]: DashboardPanelState<EmbeddableInput & { [k: string]: unknown }>;
@ -105,6 +106,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
super(
{
panels: {},
isEmbeddedExternally: false,
isFullScreenMode: false,
filters: [],
useMargins: true,

View file

@ -65,6 +65,7 @@ export class DashboardContainerFactory
public getDefaultInput(): Partial<DashboardContainerInput> {
return {
panels: {},
isEmbeddedExternally: false,
isFullScreenMode: false,
useMargins: true,
};

View file

@ -36,6 +36,7 @@ interface State {
title: string;
description?: string;
panels: { [key: string]: PanelState };
isEmbeddedExternally?: boolean;
isEmptyState?: boolean;
}
@ -52,6 +53,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
panels,
useMargins,
title,
isEmbeddedExternally,
isEmptyState,
} = this.props.container.getInput();
@ -60,6 +62,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
panels,
useMargins,
title,
isEmbeddedExternally,
isEmptyState,
};
}
@ -72,6 +75,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
useMargins,
title,
description,
isEmbeddedExternally,
isEmptyState,
} = this.props.container.getInput();
if (this.mounted) {
@ -80,6 +84,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
description,
useMargins,
title,
isEmbeddedExternally,
isEmptyState,
});
}
@ -101,12 +106,13 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
private renderEmptyScreen() {
const { renderEmpty } = this.props;
const { isFullScreenMode } = this.state;
const { isEmbeddedExternally, isFullScreenMode } = this.state;
return (
<div className="dshDashboardEmptyScreen">
{isFullScreenMode && (
<this.context.services.ExitFullScreenButton
onExitFullScreenMode={this.onExitFullScreenMode}
toggleChrome={!isEmbeddedExternally}
/>
)}
{renderEmpty && renderEmpty()}
@ -116,7 +122,14 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
private renderContainerScreen() {
const { container, PanelComponent } = this.props;
const { isFullScreenMode, panels, title, description, useMargins } = this.state;
const {
isEmbeddedExternally,
isFullScreenMode,
panels,
title,
description,
useMargins,
} = this.state;
return (
<div
data-shared-items-count={Object.values(panels).length}
@ -128,6 +141,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
{isFullScreenMode && (
<this.context.services.ExitFullScreenButton
onExitFullScreenMode={this.onExitFullScreenMode}
toggleChrome={!isEmbeddedExternally}
/>
)}
<DashboardGrid container={container} PanelComponent={PanelComponent} />

View file

@ -27,6 +27,7 @@ export function getSampleDashboardInput(
id: '123',
filters: [],
useMargins: false,
isEmbeddedExternally: false,
isFullScreenMode: false,
title: 'My Dashboard',
query: {

View file

@ -167,15 +167,26 @@ export class DashboardPlugin
const getStartServices = async () => {
const [coreStart, deps] = await core.getStartServices();
const useHideChrome = () => {
const useHideChrome = ({ toggleChrome } = { toggleChrome: true }) => {
React.useEffect(() => {
coreStart.chrome.setIsVisible(false);
return () => coreStart.chrome.setIsVisible(true);
}, []);
if (toggleChrome) {
coreStart.chrome.setIsVisible(false);
}
return () => {
if (toggleChrome) {
coreStart.chrome.setIsVisible(true);
}
};
}, [toggleChrome]);
};
const ExitFullScreenButton: React.FC<ExitFullScreenButtonProps> = (props) => {
useHideChrome();
const ExitFullScreenButton: React.FC<
ExitFullScreenButtonProps & {
toggleChrome: boolean;
}
> = ({ toggleChrome, ...props }) => {
useHideChrome({ toggleChrome });
return <ExitFullScreenButtonUi {...props} />;
};
return {

View file

@ -3,6 +3,10 @@
padding: 0px $euiSizeS $euiSizeS $euiSizeS;
}
.globalQueryBar:first-child {
padding-top: $euiSizeS;
}
.globalQueryBar:not(:empty) {
padding-bottom: $euiSizeS;
}

View file

@ -94,11 +94,11 @@ export const createTopNavHelper = ({ TopNavMenu }) => (reactDirective) => {
// All modifiers default to true.
// Set to false to hide subcomponents.
'showSearchBar',
'showFilterBar',
'showQueryBar',
'showQueryInput',
'showDatePicker',
'showSaveQuery',
'showDatePicker',
'showFilterBar',
'appName',
'screenTitle',

View file

@ -29,6 +29,7 @@ const dataShim = {
};
describe('TopNavMenu', () => {
const WRAPPER_SELECTOR = '.kbnTopNavMenu__wrapper';
const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem';
const SEARCH_BAR_SELECTOR = 'SearchBar';
const menuItems: TopNavMenuData[] = [
@ -51,18 +52,28 @@ describe('TopNavMenu', () => {
it('Should render nothing when no config is provided', () => {
const component = shallowWithIntl(<TopNavMenu appName={'test'} />);
expect(component.find(WRAPPER_SELECTOR).length).toBe(0);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should not render menu items when config is empty', () => {
const component = shallowWithIntl(<TopNavMenu appName={'test'} config={[]} />);
expect(component.find(WRAPPER_SELECTOR).length).toBe(0);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should render 1 menu item', () => {
const component = shallowWithIntl(<TopNavMenu appName={'test'} config={[menuItems[0]]} />);
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(1);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should render multiple menu items', () => {
const component = shallowWithIntl(<TopNavMenu appName={'test'} config={menuItems} />);
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(menuItems.length);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
@ -71,15 +82,25 @@ describe('TopNavMenu', () => {
const component = shallowWithIntl(
<TopNavMenu appName={'test'} showSearchBar={true} data={dataShim as any} />
);
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
});
it('Should render menu items and search bar', () => {
const component = shallowWithIntl(
<TopNavMenu appName={'test'} config={menuItems} showSearchBar={true} data={dataShim as any} />
);
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(menuItems.length);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
});
it('Should render with a class name', () => {
const component = shallowWithIntl(
<TopNavMenu
appName={'test'}
config={menuItems}
showSearchBar={true}
data={dataShim as any}
className={'myCoolClass'}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import React, { ReactElement } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
@ -29,6 +29,10 @@ import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/pub
export type TopNavMenuProps = StatefulSearchBarProps & {
config?: TopNavMenuData[];
showSearchBar?: boolean;
showQueryBar?: boolean;
showQueryInput?: boolean;
showDatePicker?: boolean;
showFilterBar?: boolean;
data?: DataPublicPluginStart;
className?: string;
};
@ -42,10 +46,15 @@ export type TopNavMenuProps = StatefulSearchBarProps & {
*
**/
export function TopNavMenu(props: TopNavMenuProps) {
export function TopNavMenu(props: TopNavMenuProps): ReactElement | null {
const { config, showSearchBar, ...searchBarProps } = props;
function renderItems() {
if (!config) return;
if ((!config || config.length === 0) && (!showSearchBar || !props.data)) {
return null;
}
function renderItems(): ReactElement[] | null {
if (!config || config.length === 0) return null;
return config.map((menuItem: TopNavMenuData, i: number) => {
return (
<EuiFlexItem
@ -59,9 +68,25 @@ export function TopNavMenu(props: TopNavMenuProps) {
});
}
function renderSearchBar() {
function renderMenu(className: string): ReactElement | null {
if (!config || config.length === 0) return null;
return (
<EuiFlexGroup
data-test-subj="top-nav"
justifyContent="flexStart"
alignItems="center"
gutterSize="none"
className={className}
responsive={false}
>
{renderItems()}
</EuiFlexGroup>
);
}
function renderSearchBar(): ReactElement | null {
// Validate presense of all required fields
if (!showSearchBar || !props.data) return;
if (!showSearchBar || !props.data) return null;
const { SearchBar } = props.data.ui;
return <SearchBar {...searchBarProps} />;
}
@ -70,16 +95,7 @@ export function TopNavMenu(props: TopNavMenuProps) {
const className = classNames('kbnTopNavMenu', props.className);
return (
<span className="kbnTopNavMenu__wrapper">
<EuiFlexGroup
data-test-subj="top-nav"
justifyContent="flexStart"
alignItems="center"
gutterSize="none"
className={className}
responsive={false}
>
{renderItems()}
</EuiFlexGroup>
{renderMenu(className)}
{renderSearchBar()}
</span>
);

View file

@ -44,7 +44,9 @@ exports[`share url panel content render 1`] = `
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
@ -79,7 +81,9 @@ exports[`share url panel content render 1`] = `
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
@ -123,7 +127,9 @@ exports[`share url panel content render 1`] = `
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
@ -200,7 +206,9 @@ exports[`share url panel content should enable saved object export option when o
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
@ -235,7 +243,9 @@ exports[`share url panel content should enable saved object export option when o
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
@ -279,7 +289,9 @@ exports[`share url panel content should enable saved object export option when o
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
@ -356,7 +368,9 @@ exports[`share url panel content should hide short url section when allowShortUr
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
@ -391,7 +405,9 @@ exports[`share url panel content should hide short url section when allowShortUr
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
@ -435,3 +451,178 @@ exports[`share url panel content should hide short url section when allowShortUr
</EuiForm>
</I18nProvider>
`;
exports[`should show url param extensions 1`] = `
<I18nProvider>
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": false,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem
grow={false}
>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
key="testExtension"
labelType="label"
>
<TestExtension
setParamValue={[Function]}
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
</I18nProvider>
`;

View file

@ -26,7 +26,7 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { HttpStart } from 'kibana/public';
import { UrlPanelContent } from './url_panel_content';
import { ShareMenuItem, ShareContextMenuPanelItem } from '../types';
import { ShareMenuItem, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
interface Props {
allowEmbed: boolean;
@ -39,6 +39,7 @@ interface Props {
onClose: () => void;
basePath: string;
post: HttpStart['post'];
embedUrlParamExtensions?: UrlParamExtension[];
}
export class ShareContextMenu extends Component<Props> {
@ -100,6 +101,7 @@ export class ShareContextMenu extends Component<Props> {
basePath={this.props.basePath}
post={this.props.post}
shareableUrl={this.props.shareableUrl}
urlParamExtensions={this.props.embedUrlParamExtensions}
/>
),
};

View file

@ -202,3 +202,13 @@ describe('share url panel content', () => {
});
});
});
test('should show url param extensions', () => {
const TestExtension = () => <div data-test-subj="testExtension" />;
const extensions = [{ paramName: 'testExtension', component: TestExtension }];
const component = shallow(
<UrlPanelContent {...defaultProps} urlParamExtensions={extensions} objectId="id1" />
);
expect(component.find('TestExtension').length).toBe(1);
expect(component).toMatchSnapshot();
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React, { Component } from 'react';
import React, { Component, ReactElement } from 'react';
import {
EuiButton,
@ -41,6 +41,7 @@ import { HttpStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { shortenUrl } from '../lib/url_shortener';
import { UrlParamExtension } from '../types';
interface Props {
allowShortUrl: boolean;
@ -50,6 +51,7 @@ interface Props {
shareableUrl?: string;
basePath: string;
post: HttpStart['post'];
urlParamExtensions?: UrlParamExtension[];
}
export enum ExportUrlAsType {
@ -57,12 +59,19 @@ export enum ExportUrlAsType {
EXPORT_URL_AS_SNAPSHOT = 'snapshot',
}
interface UrlParams {
[extensionName: string]: {
[queryParam: string]: boolean;
};
}
interface State {
exportUrlAs: ExportUrlAsType;
useShortUrl: boolean;
isCreatingShortUrl: boolean;
url?: string;
shortUrlErrorMsg?: string;
urlParams?: UrlParams;
}
export class UrlPanelContent extends Component<Props, State> {
@ -100,7 +109,7 @@ export class UrlPanelContent extends Component<Props, State> {
<I18nProvider>
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareUrlForm">
{this.renderExportAsRadioGroup()}
{this.renderUrlParamExtensions()}
{this.renderShortUrlSwitch()}
<EuiSpacer size="m" />
@ -151,6 +160,13 @@ export class UrlPanelContent extends Component<Props, State> {
}
};
private updateUrlParams = (url: string) => {
const embedUrl = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url;
const extendUrl = this.state.urlParams ? this.getUrlParamExtensions(embedUrl) : embedUrl;
return extendUrl;
};
private getSavedObjectUrl = () => {
if (this.isNotSaved()) {
return;
@ -166,7 +182,7 @@ export class UrlPanelContent extends Component<Props, State> {
// Get the application route, after the hash, and remove the #.
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
let formattedUrl = formatUrl({
const formattedUrl = formatUrl({
protocol: parsedUrl.protocol,
auth: parsedUrl.auth,
host: parsedUrl.host,
@ -180,28 +196,42 @@ export class UrlPanelContent extends Component<Props, State> {
},
}),
});
if (this.props.isEmbedded) {
formattedUrl = this.makeUrlEmbeddable(formattedUrl);
}
return formattedUrl;
return this.updateUrlParams(formattedUrl);
};
private getSnapshotUrl = () => {
let url = this.props.shareableUrl || window.location.href;
if (this.props.isEmbedded) {
url = this.makeUrlEmbeddable(url);
}
return url;
const url = this.props.shareableUrl || window.location.href;
return this.updateUrlParams(url);
};
private makeUrlEmbeddable = (url: string) => {
const embedQueryParam = '?embed=true';
private makeUrlEmbeddable = (url: string): string => {
const embedParam = '?embed=true';
const urlHasQueryString = url.indexOf('?') !== -1;
if (urlHasQueryString) {
return url.replace('?', `${embedQueryParam}&`);
return url.replace('?', `${embedParam}&`);
}
return `${url}${embedQueryParam}`;
return `${url}${embedParam}`;
};
private getUrlParamExtensions = (url: string): string => {
const { urlParams } = this.state;
return urlParams
? Object.keys(urlParams).reduce((urlAccumulator, key) => {
const urlParam = urlParams[key];
return urlParam
? Object.keys(urlParam).reduce((queryAccumulator, queryParam) => {
const isQueryParamEnabled = urlParam[queryParam];
return isQueryParamEnabled
? queryAccumulator + `&${queryParam}=true`
: queryAccumulator;
}, urlAccumulator)
: urlAccumulator;
}, url)
: url;
};
private makeIframeTag = (url?: string) => {
@ -247,6 +277,10 @@ export class UrlPanelContent extends Component<Props, State> {
}
// "Use short URL" is checked but shortUrl has not been generated yet so one needs to be created.
this.createShortUrl();
};
private createShortUrl = async () => {
this.setState({
isCreatingShortUrl: true,
shortUrlErrorMsg: undefined,
@ -262,7 +296,7 @@ export class UrlPanelContent extends Component<Props, State> {
this.setState(
{
isCreatingShortUrl: false,
useShortUrl: isChecked,
useShortUrl: true,
},
this.setUrl
);
@ -321,7 +355,7 @@ export class UrlPanelContent extends Component<Props, State> {
private renderWithIconTip = (child: React.ReactNode, tipContent: React.ReactNode) => {
return (
<EuiFlexGroup gutterSize="none" responsive={false}>
<EuiFlexItem>{child}</EuiFlexItem>
<EuiFlexItem grow={false}>{child}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip content={tipContent} position="bottom" />
</EuiFlexItem>
@ -397,4 +431,34 @@ export class UrlPanelContent extends Component<Props, State> {
</EuiFormRow>
);
};
private renderUrlParamExtensions = (): ReactElement | void => {
if (!this.props.urlParamExtensions) {
return;
}
const setParamValue = (paramName: string) => (
values: { [queryParam: string]: boolean } = {}
): void => {
const stateUpdate = {
urlParams: {
...this.state.urlParams,
[paramName]: {
...values,
},
},
};
this.setState(stateUpdate, this.state.useShortUrl ? this.createShortUrl : this.setUrl);
};
return (
<React.Fragment>
{this.props.urlParamExtensions.map(({ paramName, component: UrlParamComponent }) => (
<EuiFormRow key={paramName}>
<UrlParamComponent setParamValue={setParamValue(paramName)} />
</EuiFormRow>
))}
</React.Fragment>
);
};
}

View file

@ -67,6 +67,7 @@ export class ShareMenuManager {
shareableUrl,
post,
basePath,
embedUrlParamExtensions,
}: ShowShareMenuOptions & {
menuItems: ShareMenuItem[];
post: HttpStart['post'];
@ -102,6 +103,7 @@ export class ShareMenuManager {
onClose={this.onClose}
post={post}
basePath={basePath}
embedUrlParamExtensions={embedUrlParamExtensions}
/>
</EuiWrappingPopover>
</I18nProvider>

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { ComponentType } from 'react';
import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
/**
@ -80,9 +81,19 @@ export interface ShareMenuProvider {
getShareMenuItems: (context: ShareContext) => ShareMenuItem[];
}
interface UrlParamExtensionProps {
setParamValue: (values: {}) => void;
}
export interface UrlParamExtension {
paramName: string;
component: ComponentType<UrlParamExtensionProps>;
}
/** @public */
export interface ShowShareMenuOptions extends Omit<ShareContext, 'onClose'> {
anchorElement: HTMLElement;
allowEmbed: boolean;
allowShortUrl: boolean;
embedUrlParamExtensions?: UrlParamExtension[];
}

View file

@ -20,6 +20,7 @@
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
@ -28,6 +29,13 @@ export default function ({ getService, getPageObjects }) {
const globalNav = getService('globalNav');
describe('embed mode', () => {
const urlParamExtensions = [
'show-top-menu=true',
'show-query-input=true',
'show-time-filter=true',
'hide-filter-bar=true',
];
before(async () => {
await esArchiver.load('dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
@ -54,9 +62,28 @@ export default function ({ getService, getPageObjects }) {
});
});
it('shows or hides elements based on URL params', async () => {
await testSubjects.missingOrFail('top-nav');
await testSubjects.missingOrFail('queryInput');
await testSubjects.missingOrFail('superDatePickerToggleQuickMenuButton');
await testSubjects.existOrFail('showFilterActions');
const currentUrl = await browser.getCurrentUrl();
const newUrl = [currentUrl].concat(urlParamExtensions).join('&');
// Embed parameter only works on a hard refresh.
const useTimeStamp = true;
await browser.get(newUrl.toString(), useTimeStamp);
await testSubjects.existOrFail('top-nav');
await testSubjects.existOrFail('queryInput');
await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton');
await testSubjects.missingOrFail('showFilterActions');
});
after(async function () {
const currentUrl = await browser.getCurrentUrl();
const newUrl = currentUrl.replace('&embed=true', '');
const replaceParams = ['', 'embed=true'].concat(urlParamExtensions).join('&');
const newUrl = currentUrl.replace(replaceParams, '');
// First use the timestamp to cause a hard refresh so the new embed parameter works correctly.
let useTimeStamp = true;
await browser.get(newUrl.toString(), useTimeStamp);

View file

@ -92,6 +92,7 @@ export const dashboardInput: DashboardContainerInput = {
},
},
},
isEmbeddedExternally: false,
isFullScreenMode: false,
filters: [],
useMargins: true,