mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Expose anonymous access through a switch in sharing menu (#86965)
* feat: 🎸 add "Public URL" switch * feat: 🎸 add url subtitle * feat: 🎸 add public URL toggle state * feat: 🎸 allow to dynamically enable anonymous access switch * feat: 🎸 add anon access url parameters to share url * fix: 🐛 correctly add params to url * fix: 🐛 correctly add anon access to saved object URL * fix: 🐛 don't generate anon access urls twice * feat: 🎸 add ability to check anonymous user capabilities * feat: 🎸 add capability checks to Discover and Visualize apps * refactor: 💡 use early return * test: 💍 use security_oss mocks * feat: 🎸 add anon access url params to short url * test: 💍 fix jest snapshots * perf: ⚡️ make capabilities check synchronous * style: 💄 add stylistic review changes * perf: ⚡️ don't fetch anon user capabilities if anon not enabled * fix: 🐛 in discover app check if discover exists in capabilities * test: 💍 add tests for discover sharing check * test: 💍 add tests for showPublicUrlSwitch checks * feat: 🎸 make visualize capabilities props required * style: 💄 remove unused import * feat: 🎸 improve tooltip copy
This commit is contained in:
parent
c73000f644
commit
10e6354d77
17 changed files with 584 additions and 146 deletions
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { showPublicUrlSwitch } from './show_share_modal';
|
||||
|
||||
describe('showPublicUrlSwitch', () => {
|
||||
test('returns false if "dashboard" app is not available', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if "dashboard" app is not accessible', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
dashboard: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true if "dashboard" app is not available an accessible', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
dashboard: {
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { EuiCheckboxGroup } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ReactElement, useState } from 'react';
|
||||
|
@ -27,6 +28,14 @@ interface ShowShareModalProps {
|
|||
dashboardStateManager: DashboardStateManager;
|
||||
}
|
||||
|
||||
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
|
||||
if (!anonymousUserCapabilities.dashboard) return false;
|
||||
|
||||
const dashboard = (anonymousUserCapabilities.dashboard as unknown) as DashboardCapabilities;
|
||||
|
||||
return !!dashboard.show;
|
||||
};
|
||||
|
||||
export function ShowShareModal({
|
||||
share,
|
||||
anchorElement,
|
||||
|
@ -113,5 +122,6 @@ export function ShowShareModal({
|
|||
component: EmbedUrlParamExtension,
|
||||
},
|
||||
],
|
||||
showPublicUrlSwitch,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { showOpenSearchPanel } from './show_open_search_panel';
|
||||
import { getSharingData } from '../../helpers/get_sharing_data';
|
||||
import { getSharingData, showPublicUrlSwitch } from '../../helpers/get_sharing_data';
|
||||
import { unhashUrl } from '../../../../../kibana_utils/public';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { Adapters } from '../../../../../inspector/common/adapters';
|
||||
|
@ -108,6 +108,7 @@ export const getTopNavLinks = ({
|
|||
title: savedSearch.title,
|
||||
},
|
||||
isDirty: !savedSearch.id || state.isAppStateDirty(),
|
||||
showPublicUrlSwitch,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getSharingData } from './get_sharing_data';
|
||||
import { Capabilities } from 'kibana/public';
|
||||
import { getSharingData, showPublicUrlSwitch } from './get_sharing_data';
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks';
|
||||
import { indexPatternMock } from '../../__mocks__/index_pattern';
|
||||
|
@ -68,3 +69,44 @@ describe('getSharingData', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showPublicUrlSwitch', () => {
|
||||
test('returns false if "discover" app is not available', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if "discover" app is not accessible', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
discover: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true if "discover" app is not available an accessible', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
discover: {
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import { Capabilities, IUiSettingsClient } from 'kibana/public';
|
||||
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
|
||||
import { getSortForSearchSource } from '../angular/doc_table';
|
||||
import { SearchSource } from '../../../../data/common';
|
||||
|
@ -76,3 +76,19 @@ export async function getSharingData(
|
|||
indexPatternId: index.id,
|
||||
};
|
||||
}
|
||||
|
||||
export interface DiscoverCapabilities {
|
||||
createShortUrl?: boolean;
|
||||
save?: boolean;
|
||||
saveQuery?: boolean;
|
||||
show?: boolean;
|
||||
storeSearchSession?: boolean;
|
||||
}
|
||||
|
||||
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
|
||||
if (!anonymousUserCapabilities.discover) return false;
|
||||
|
||||
const discover = (anonymousUserCapabilities.discover as unknown) as DiscoverCapabilities;
|
||||
|
||||
return !!discover.show;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
|
||||
import { InsecureClusterServiceStart } from './insecure_cluster_service';
|
||||
import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock';
|
||||
import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
|
||||
|
||||
|
@ -18,7 +19,11 @@ export const mockSecurityOssPlugin = {
|
|||
},
|
||||
createStart: () => {
|
||||
return {
|
||||
insecureCluster: mockInsecureClusterService.createStart(),
|
||||
insecureCluster: mockInsecureClusterService.createStart() as jest.Mocked<InsecureClusterServiceStart>,
|
||||
anonymousAccess: {
|
||||
getAccessURLParameters: jest.fn().mockResolvedValue(null),
|
||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
} as DeeplyMockedKeys<SecurityOssPluginStart>;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": ["kibanaUtils"]
|
||||
"requiredBundles": ["kibanaUtils"],
|
||||
"optionalPlugins": ["securityOss"]
|
||||
}
|
||||
|
|
|
@ -115,49 +115,68 @@ exports[`share url panel content render 1`] = `
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
data-test-subj="createShortUrl"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="URL"
|
||||
id="share.urlPanel.urlGroupTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
data-test-subj="createShortUrl"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={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>
|
||||
<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>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
|
@ -277,49 +296,68 @@ exports[`share url panel content should enable saved object export option when o
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
data-test-subj="createShortUrl"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="URL"
|
||||
id="share.urlPanel.urlGroupTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
data-test-subj="createShortUrl"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={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>
|
||||
<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>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
|
@ -438,6 +476,25 @@ exports[`share url panel content should hide short url section when allowShortUr
|
|||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="URL"
|
||||
id="share.urlPanel.urlGroupTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
|
@ -569,49 +626,68 @@ exports[`should show url param extensions 1`] = `
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
data-test-subj="createShortUrl"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="URL"
|
||||
id="share.urlPanel.urlGroupTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
data-test-subj="createShortUrl"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={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>
|
||||
<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>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
|
|
|
@ -13,9 +13,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
|
||||
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import type { Capabilities } from 'src/core/public';
|
||||
|
||||
import { UrlPanelContent } from './url_panel_content';
|
||||
import { ShareMenuItem, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
|
||||
import type { SecurityOssPluginStart } from '../../../security_oss/public';
|
||||
|
||||
interface Props {
|
||||
allowEmbed: boolean;
|
||||
|
@ -29,6 +31,8 @@ interface Props {
|
|||
basePath: string;
|
||||
post: HttpStart['post'];
|
||||
embedUrlParamExtensions?: UrlParamExtension[];
|
||||
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
|
||||
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
|
||||
}
|
||||
|
||||
export class ShareContextMenu extends Component<Props> {
|
||||
|
@ -62,6 +66,8 @@ export class ShareContextMenu extends Component<Props> {
|
|||
basePath={this.props.basePath}
|
||||
post={this.props.post}
|
||||
shareableUrl={this.props.shareableUrl}
|
||||
anonymousAccess={this.props.anonymousAccess}
|
||||
showPublicUrlSwitch={this.props.showPublicUrlSwitch}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
@ -91,6 +97,8 @@ export class ShareContextMenu extends Component<Props> {
|
|||
post={this.props.post}
|
||||
shareableUrl={this.props.shareableUrl}
|
||||
urlParamExtensions={this.props.embedUrlParamExtensions}
|
||||
anonymousAccess={this.props.anonymousAccess}
|
||||
showPublicUrlSwitch={this.props.showPublicUrlSwitch}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -28,9 +28,11 @@ import { format as formatUrl, parse as parseUrl } from 'url';
|
|||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Capabilities } from 'src/core/public';
|
||||
|
||||
import { shortenUrl } from '../lib/url_shortener';
|
||||
import { UrlParamExtension } from '../types';
|
||||
import type { SecurityOssPluginStart } from '../../../security_oss/public';
|
||||
|
||||
interface Props {
|
||||
allowShortUrl: boolean;
|
||||
|
@ -41,6 +43,8 @@ interface Props {
|
|||
basePath: string;
|
||||
post: HttpStart['post'];
|
||||
urlParamExtensions?: UrlParamExtension[];
|
||||
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
|
||||
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
|
||||
}
|
||||
|
||||
export enum ExportUrlAsType {
|
||||
|
@ -57,10 +61,13 @@ interface UrlParams {
|
|||
interface State {
|
||||
exportUrlAs: ExportUrlAsType;
|
||||
useShortUrl: boolean;
|
||||
usePublicUrl: boolean;
|
||||
isCreatingShortUrl: boolean;
|
||||
url?: string;
|
||||
shortUrlErrorMsg?: string;
|
||||
urlParams?: UrlParams;
|
||||
anonymousAccessParameters: Record<string, string> | null;
|
||||
showPublicUrlSwitch: boolean;
|
||||
}
|
||||
|
||||
export class UrlPanelContent extends Component<Props, State> {
|
||||
|
@ -75,8 +82,11 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
this.state = {
|
||||
exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT,
|
||||
useShortUrl: false,
|
||||
usePublicUrl: false,
|
||||
isCreatingShortUrl: false,
|
||||
url: '',
|
||||
anonymousAccessParameters: null,
|
||||
showPublicUrlSwitch: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -91,6 +101,41 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
this.setUrl();
|
||||
|
||||
window.addEventListener('hashchange', this.resetUrl, false);
|
||||
|
||||
if (this.props.anonymousAccess) {
|
||||
(async () => {
|
||||
const anonymousAccessParameters = await this.props.anonymousAccess!.getAccessURLParameters();
|
||||
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!anonymousAccessParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
let showPublicUrlSwitch: boolean = false;
|
||||
|
||||
if (this.props.showPublicUrlSwitch) {
|
||||
const anonymousUserCapabilities = await this.props.anonymousAccess!.getCapabilities();
|
||||
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showPublicUrlSwitch = this.props.showPublicUrlSwitch!(anonymousUserCapabilities);
|
||||
} catch {
|
||||
showPublicUrlSwitch = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
anonymousAccessParameters,
|
||||
showPublicUrlSwitch,
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -99,7 +144,16 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareUrlForm">
|
||||
{this.renderExportAsRadioGroup()}
|
||||
{this.renderUrlParamExtensions()}
|
||||
{this.renderShortUrlSwitch()}
|
||||
|
||||
<EuiFormRow
|
||||
label={<FormattedMessage id="share.urlPanel.urlGroupTitle" defaultMessage="URL" />}
|
||||
>
|
||||
<>
|
||||
<EuiSpacer size={'s'} />
|
||||
{this.renderShortUrlSwitch()}
|
||||
{this.renderPublicUrlSwitch()}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
|
@ -150,10 +204,10 @@ 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;
|
||||
url = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url;
|
||||
url = this.state.urlParams ? this.getUrlParamExtensions(url) : url;
|
||||
|
||||
return extendUrl;
|
||||
return url;
|
||||
};
|
||||
|
||||
private getSavedObjectUrl = () => {
|
||||
|
@ -206,6 +260,20 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
return `${url}${embedParam}`;
|
||||
};
|
||||
|
||||
private addUrlAnonymousAccessParameters = (url: string): string => {
|
||||
if (!this.state.anonymousAccessParameters || !this.state.usePublicUrl) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
for (const [name, value] of Object.entries(this.state.anonymousAccessParameters)) {
|
||||
parsedUrl.searchParams.set(name, value);
|
||||
}
|
||||
|
||||
return parsedUrl.toString();
|
||||
};
|
||||
|
||||
private getUrlParamExtensions = (url: string): string => {
|
||||
const { urlParams } = this.state;
|
||||
return urlParams
|
||||
|
@ -232,7 +300,8 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private setUrl = () => {
|
||||
let url;
|
||||
let url: string | undefined;
|
||||
|
||||
if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) {
|
||||
url = this.getSavedObjectUrl();
|
||||
} else if (this.state.useShortUrl) {
|
||||
|
@ -241,6 +310,10 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
url = this.getSnapshotUrl();
|
||||
}
|
||||
|
||||
if (url) {
|
||||
url = this.addUrlAnonymousAccessParameters(url);
|
||||
}
|
||||
|
||||
if (this.props.isEmbedded) {
|
||||
url = this.makeIframeTag(url);
|
||||
}
|
||||
|
@ -269,6 +342,14 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
this.createShortUrl();
|
||||
};
|
||||
|
||||
private handlePublicUrlChange = () => {
|
||||
this.setState(({ usePublicUrl }) => {
|
||||
return {
|
||||
usePublicUrl: !usePublicUrl,
|
||||
};
|
||||
}, this.setUrl);
|
||||
};
|
||||
|
||||
private createShortUrl = async () => {
|
||||
this.setState({
|
||||
isCreatingShortUrl: true,
|
||||
|
@ -280,33 +361,38 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
basePath: this.props.basePath,
|
||||
post: this.props.post,
|
||||
});
|
||||
if (this.mounted) {
|
||||
this.shortUrlCache = shortUrl;
|
||||
this.setState(
|
||||
{
|
||||
isCreatingShortUrl: false,
|
||||
useShortUrl: true,
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shortUrlCache = shortUrl;
|
||||
this.setState(
|
||||
{
|
||||
isCreatingShortUrl: false,
|
||||
useShortUrl: true,
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
} catch (fetchError) {
|
||||
if (this.mounted) {
|
||||
this.shortUrlCache = undefined;
|
||||
this.setState(
|
||||
{
|
||||
useShortUrl: false,
|
||||
isCreatingShortUrl: false,
|
||||
shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', {
|
||||
defaultMessage: 'Unable to create short URL. Error: {errorMessage}',
|
||||
values: {
|
||||
errorMessage: fetchError.message,
|
||||
},
|
||||
}),
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shortUrlCache = undefined;
|
||||
this.setState(
|
||||
{
|
||||
useShortUrl: false,
|
||||
isCreatingShortUrl: false,
|
||||
shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', {
|
||||
defaultMessage: 'Unable to create short URL. Error: {errorMessage}',
|
||||
values: {
|
||||
errorMessage: fetchError.message,
|
||||
},
|
||||
}),
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -421,6 +507,36 @@ export class UrlPanelContent extends Component<Props, State> {
|
|||
);
|
||||
};
|
||||
|
||||
private renderPublicUrlSwitch = () => {
|
||||
if (!this.state.anonymousAccessParameters || !this.state.showPublicUrlSwitch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const switchLabel = (
|
||||
<FormattedMessage id="share.urlPanel.publicUrlLabel" defaultMessage="Public URL" />
|
||||
);
|
||||
const switchComponent = (
|
||||
<EuiSwitch
|
||||
label={switchLabel}
|
||||
checked={this.state.usePublicUrl}
|
||||
onChange={this.handlePublicUrlChange}
|
||||
data-test-subj="usePublicUrl"
|
||||
/>
|
||||
);
|
||||
const tipContent = (
|
||||
<FormattedMessage
|
||||
id="share.urlPanel.publicUrlHelpText"
|
||||
defaultMessage="Use public URL to share with anyone. It enables one-step anonymous access by removing the login prompt."
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow data-test-subj="createPublicUrl">
|
||||
{this.renderWithIconTip(switchComponent, tipContent)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
private renderUrlParamExtensions = (): ReactElement | void => {
|
||||
if (!this.props.urlParamExtensions) {
|
||||
return;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { registryMock, managerMock } from './plugin.test.mocks';
|
|||
import { SharePlugin } from './plugin';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { coreMock } from '../../../core/public/mocks';
|
||||
import { mockSecurityOssPlugin } from '../../security_oss/public/mocks';
|
||||
|
||||
describe('SharePlugin', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -21,14 +22,20 @@ describe('SharePlugin', () => {
|
|||
describe('setup', () => {
|
||||
test('wires up and returns registry', async () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const setup = await new SharePlugin().setup(coreSetup);
|
||||
const plugins = {
|
||||
securityOss: mockSecurityOssPlugin.createSetup(),
|
||||
};
|
||||
const setup = await new SharePlugin().setup(coreSetup, plugins);
|
||||
expect(registryMock.setup).toHaveBeenCalledWith();
|
||||
expect(setup.register).toBeDefined();
|
||||
});
|
||||
|
||||
test('registers redirect app', async () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
await new SharePlugin().setup(coreSetup);
|
||||
const plugins = {
|
||||
securityOss: mockSecurityOssPlugin.createSetup(),
|
||||
};
|
||||
await new SharePlugin().setup(coreSetup, plugins);
|
||||
expect(coreSetup.application.register).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'short_url_redirect',
|
||||
|
@ -40,13 +47,22 @@ describe('SharePlugin', () => {
|
|||
describe('start', () => {
|
||||
test('wires up and returns show function, but not registry', async () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const pluginsSetup = {
|
||||
securityOss: mockSecurityOssPlugin.createSetup(),
|
||||
};
|
||||
const service = new SharePlugin();
|
||||
await service.setup(coreSetup);
|
||||
const start = await service.start({} as CoreStart);
|
||||
await service.setup(coreSetup, pluginsSetup);
|
||||
const pluginsStart = {
|
||||
securityOss: mockSecurityOssPlugin.createStart(),
|
||||
};
|
||||
const start = await service.start({} as CoreStart, pluginsStart);
|
||||
expect(registryMock.start).toHaveBeenCalled();
|
||||
expect(managerMock.start).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ getShareMenuItems: expect.any(Function) })
|
||||
expect.objectContaining({
|
||||
getShareMenuItems: expect.any(Function),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
expect(start.toggleShareContextMenu).toBeDefined();
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import './index.scss';
|
|||
|
||||
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { ShareMenuManager, ShareMenuManagerStart } from './services';
|
||||
import type { SecurityOssPluginSetup, SecurityOssPluginStart } from '../../security_oss/public';
|
||||
import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services';
|
||||
import { createShortUrlRedirectApp } from './services/short_url_redirect_app';
|
||||
import {
|
||||
|
@ -18,12 +19,20 @@ import {
|
|||
UrlGeneratorsStart,
|
||||
} from './url_generators/url_generator_service';
|
||||
|
||||
export interface ShareSetupDependencies {
|
||||
securityOss?: SecurityOssPluginSetup;
|
||||
}
|
||||
|
||||
export interface ShareStartDependencies {
|
||||
securityOss?: SecurityOssPluginStart;
|
||||
}
|
||||
|
||||
export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
||||
private readonly shareMenuRegistry = new ShareMenuRegistry();
|
||||
private readonly shareContextMenu = new ShareMenuManager();
|
||||
private readonly urlGeneratorsService = new UrlGeneratorsService();
|
||||
|
||||
public setup(core: CoreSetup): SharePluginSetup {
|
||||
public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup {
|
||||
core.application.register(createShortUrlRedirectApp(core, window.location));
|
||||
return {
|
||||
...this.shareMenuRegistry.setup(),
|
||||
|
@ -31,9 +40,13 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
|||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart): SharePluginStart {
|
||||
public start(core: CoreStart, plugins: ShareStartDependencies): SharePluginStart {
|
||||
return {
|
||||
...this.shareContextMenu.start(core, this.shareMenuRegistry.start()),
|
||||
...this.shareContextMenu.start(
|
||||
core,
|
||||
this.shareMenuRegistry.start(),
|
||||
plugins.securityOss?.anonymousAccess
|
||||
),
|
||||
urlGenerators: this.urlGeneratorsService.start(core),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -15,13 +15,18 @@ import { CoreStart, HttpStart } from 'kibana/public';
|
|||
import { ShareContextMenu } from '../components/share_context_menu';
|
||||
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
|
||||
import { ShareMenuRegistryStart } from './share_menu_registry';
|
||||
import type { SecurityOssPluginStart } from '../../../security_oss/public';
|
||||
|
||||
export class ShareMenuManager {
|
||||
private isOpen = false;
|
||||
|
||||
private container = document.createElement('div');
|
||||
|
||||
start(core: CoreStart, shareRegistry: ShareMenuRegistryStart) {
|
||||
start(
|
||||
core: CoreStart,
|
||||
shareRegistry: ShareMenuRegistryStart,
|
||||
anonymousAccess?: SecurityOssPluginStart['anonymousAccess']
|
||||
) {
|
||||
return {
|
||||
/**
|
||||
* Collects share menu items from registered providers and mounts the share context menu under
|
||||
|
@ -35,6 +40,7 @@ export class ShareMenuManager {
|
|||
menuItems,
|
||||
post: core.http.post,
|
||||
basePath: core.http.basePath.get(),
|
||||
anonymousAccess,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -57,10 +63,13 @@ export class ShareMenuManager {
|
|||
post,
|
||||
basePath,
|
||||
embedUrlParamExtensions,
|
||||
anonymousAccess,
|
||||
showPublicUrlSwitch,
|
||||
}: ShowShareMenuOptions & {
|
||||
menuItems: ShareMenuItem[];
|
||||
post: HttpStart['post'];
|
||||
basePath: string;
|
||||
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
|
||||
}) {
|
||||
if (this.isOpen) {
|
||||
this.onClose();
|
||||
|
@ -92,6 +101,8 @@ export class ShareMenuManager {
|
|||
post={post}
|
||||
basePath={basePath}
|
||||
embedUrlParamExtensions={embedUrlParamExtensions}
|
||||
anonymousAccess={anonymousAccess}
|
||||
showPublicUrlSwitch={showPublicUrlSwitch}
|
||||
/>
|
||||
</EuiWrappingPopover>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { ComponentType } from 'react';
|
||||
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
|
||||
import { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/components/context_menu/context_menu';
|
||||
import type { Capabilities } from 'src/core/public';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -35,6 +36,7 @@ export interface ShareContext {
|
|||
sharingData: { [key: string]: unknown };
|
||||
isDirty: boolean;
|
||||
onClose: () => void;
|
||||
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"include": ["common/**/*", "public/**/*", "server/**/*"],
|
||||
"references": [
|
||||
{ "path": "../../core/tsconfig.json" },
|
||||
{ "path": "../../plugins/kibana_utils/tsconfig.json" }
|
||||
{ "path": "../kibana_utils/tsconfig.json" },
|
||||
{ "path": "../security_oss/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { showPublicUrlSwitch } from './get_top_nav_config';
|
||||
|
||||
describe('showPublicUrlSwitch', () => {
|
||||
test('returns false if "visualize" app is not available', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if "visualize" app is not accessible', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
visualize: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true if "visualize" app is not available an accessible', () => {
|
||||
const anonymousUserCapabilities: Capabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
visualize: {
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
const result = showPublicUrlSwitch(anonymousUserCapabilities);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { TopNavMenuData } from 'src/plugins/navigation/public';
|
||||
import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public';
|
||||
import {
|
||||
|
@ -30,6 +31,14 @@ import { VisualizeConstants } from '../visualize_constants';
|
|||
import { getEditBreadcrumbs } from './breadcrumbs';
|
||||
import { EmbeddableStateTransfer } from '../../../../embeddable/public';
|
||||
|
||||
interface VisualizeCapabilities {
|
||||
createShortUrl: boolean;
|
||||
delete: boolean;
|
||||
save: boolean;
|
||||
saveQuery: boolean;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
interface TopNavConfigParams {
|
||||
hasUnsavedChanges: boolean;
|
||||
setHasUnsavedChanges: (value: boolean) => void;
|
||||
|
@ -45,6 +54,14 @@ interface TopNavConfigParams {
|
|||
embeddableId?: string;
|
||||
}
|
||||
|
||||
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
|
||||
if (!anonymousUserCapabilities.visualize) return false;
|
||||
|
||||
const visualize = (anonymousUserCapabilities.visualize as unknown) as VisualizeCapabilities;
|
||||
|
||||
return !!visualize.show;
|
||||
};
|
||||
|
||||
export const getTopNavConfig = (
|
||||
{
|
||||
hasUnsavedChanges,
|
||||
|
@ -243,6 +260,7 @@ export const getTopNavConfig = (
|
|||
title: savedVis?.title,
|
||||
},
|
||||
isDirty: hasUnappliedChanges || hasUnsavedChanges,
|
||||
showPublicUrlSwitch,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue