mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Share Modal] Reinstate switch to support generating public urls for embed when supported (#207383)
## Summary Closes https://github.com/elastic/kibana/issues/194105 This PR aims to resolve a regression with the share embed option, prior to 8.14.0 there was a functionality that provided the ability for users to get an embed link that would allow public access to the object of the share (i.e. dashboards, visualisations) if they had the right configuration ([see here](https://www.elastic.co/guide/en/kibana/8.13/kibana-authentication.html#anonymous-access-and-embedding) on how to). ## How to test - Attempt to get an embed link from for example the dashboard, the user shouldn't not be present with an option to create a url with public access. - Now configure anonymous login in your `kibana.dev.yml`, like so; ``` xpack.security.authc.providers: basic.basic1: order: 0 anonymous.anonymous1: order: 1 credentials: username: "elastic" password: "changeme" ``` - On doing this, you should be presented with the option to create a public URL using the toggle switch similar to the image below, select this option. <img width="602" alt="Screenshot 2025-01-20 at 15 07 03" src="https://github.com/user-attachments/assets/2af9082b-c44c-4cd0-89ae-de423bc7d18d" /> - Click copy code button - Next, we'll create a dummy html document to verify the code copied works, in your terminal simply run; ```bash touch embed.html echo "paste embed code content here" >> embed.html npx --package=serve@latest -y serve ``` - On running the commands above, we can try out the embed by opening up the URL at `http://localhost:3000/embed` - Ideally if all the steps were followed we should have a page that loads up the object which the share URL was generated from. https://github.com/user-attachments/assets/c5c873a4-5417-4bcf-b0cb-132d9073992f <!-- ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... -->
This commit is contained in:
parent
fd7c7591da
commit
7b2631a21d
11 changed files with 257 additions and 147 deletions
|
@ -245,6 +245,7 @@ export function ShowShareModal({
|
|||
: shareModalStrings.getDraftShareWarning('embed')}
|
||||
</EuiCallOut>
|
||||
),
|
||||
computeAnonymousCapabilities: showPublicUrlSwitch,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -266,7 +267,6 @@ export function ShowShareModal({
|
|||
component: EmbedUrlParamExtension,
|
||||
},
|
||||
],
|
||||
showPublicUrlSwitch,
|
||||
snapshotShareWarning: Boolean(unsavedDashboardState?.panels)
|
||||
? shareModalStrings.getSnapshotShareWarning()
|
||||
: undefined,
|
||||
|
|
|
@ -7,46 +7,81 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { EmbedContent } from './embed_content';
|
||||
import React from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import type { BrowserUrlService } from '../../../types';
|
||||
import { urlServiceTestSetup } from '../../../../common/url_service/__tests__/setup';
|
||||
|
||||
let urlService: BrowserUrlService;
|
||||
|
||||
// @ts-expect-error there is a type error because we override the shortUrls implementation
|
||||
// eslint-disable-next-line prefer-const
|
||||
({ service: urlService } = urlServiceTestSetup());
|
||||
|
||||
const defaultProps: Pick<
|
||||
ComponentProps<typeof EmbedContent>,
|
||||
'allowShortUrl' | 'isDirty' | 'shareableUrl' | 'urlService' | 'objectType'
|
||||
> = {
|
||||
isDirty: false,
|
||||
objectType: 'dashboard',
|
||||
shareableUrl: '/home#/',
|
||||
urlService,
|
||||
allowShortUrl: false,
|
||||
};
|
||||
|
||||
const renderComponent = (props: ComponentProps<typeof EmbedContent>) => {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<EmbedContent {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Share modal embed content tab', () => {
|
||||
describe('share url embedded', () => {
|
||||
let component: ReactWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
component = mountWithIntl(
|
||||
<EmbedContent isDirty={false} objectType="dashboard" shareableUrl="/home#/" />
|
||||
);
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(document, 'execCommand', {
|
||||
value: jest.fn(() => true),
|
||||
});
|
||||
});
|
||||
|
||||
it('works for simple url', async () => {
|
||||
component.setProps({ shareableUrl: 'http://localhost:5601/app/home#/' });
|
||||
component.update();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const shareUrl = component
|
||||
.find('button[data-test-subj="copyEmbedUrlButton"]')
|
||||
.prop('data-share-url');
|
||||
expect(shareUrl).toBe(
|
||||
'<iframe src="http://localhost:5601/app/home#/?embed=true&_g=" height="600" width="800"></iframe>'
|
||||
);
|
||||
renderComponent({ ...defaultProps, shareableUrl: 'http://localhost:5601/app/home#/' });
|
||||
|
||||
const copyButton = screen.getByTestId('copyEmbedUrlButton');
|
||||
|
||||
await user.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(copyButton.getAttribute('data-share-url')).toBe(
|
||||
'<iframe src="http://localhost:5601/app/home#/?embed=true&_g=" height="600" width="800"></iframe>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('works if the url has a query string', async () => {
|
||||
component.setProps({
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
shareableUrl:
|
||||
'http://localhost:5601/app/dashboards#/create?_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A60000)%2Ctime%3A(from%3Anow-15m%2Cto%3Anow))',
|
||||
});
|
||||
component.update();
|
||||
|
||||
const shareUrl = component
|
||||
.find('button[data-test-subj="copyEmbedUrlButton"]')
|
||||
.prop('data-share-url');
|
||||
expect(shareUrl).toBe(
|
||||
'<iframe src="http://localhost:5601/app/dashboards#/create?embed=true&_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A60000)%2Ctime%3A(from%3Anow-15m%2Cto%3Anow))" height="600" width="800"></iframe>'
|
||||
);
|
||||
const copyButton = screen.getByTestId('copyEmbedUrlButton');
|
||||
|
||||
await user.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(copyButton.getAttribute('data-share-url')).toBe(
|
||||
'<iframe src="http://localhost:5601/app/dashboards#/create?embed=true&_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A60000)%2Ctime%3A(from%3Anow-15m%2Cto%3Anow))" height="600" width="800"></iframe>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,11 +15,15 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiFlexItem,
|
||||
EuiCopy,
|
||||
EuiSwitch,
|
||||
type EuiSwitchEvent,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
copyToClipboard,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { format as formatUrl, parse as parseUrl } from 'url';
|
||||
import { AnonymousAccessState } from '../../../../common';
|
||||
|
||||
|
@ -33,6 +37,9 @@ type EmbedProps = Pick<
|
|||
| 'embedUrlParamExtensions'
|
||||
| 'objectType'
|
||||
| 'isDirty'
|
||||
| 'allowShortUrl'
|
||||
| 'anonymousAccess'
|
||||
| 'urlService'
|
||||
> & {
|
||||
objectConfig?: ShareContextObjectTypeConfig;
|
||||
};
|
||||
|
@ -43,27 +50,55 @@ interface UrlParams {
|
|||
};
|
||||
}
|
||||
|
||||
export enum ExportUrlAsType {
|
||||
EXPORT_URL_AS_SAVED_OBJECT = 'savedObject',
|
||||
EXPORT_URL_AS_SNAPSHOT = 'snapshot',
|
||||
}
|
||||
|
||||
export const EmbedContent = ({
|
||||
embedUrlParamExtensions: urlParamExtensions,
|
||||
shareableUrlForSavedObject,
|
||||
shareableUrl,
|
||||
shareableUrlLocatorParams,
|
||||
objectType,
|
||||
objectConfig = {},
|
||||
isDirty,
|
||||
allowShortUrl,
|
||||
urlService,
|
||||
anonymousAccess,
|
||||
}: EmbedProps) => {
|
||||
const isMounted = useMountedState();
|
||||
const [urlParams, setUrlParams] = useState<UrlParams | undefined>(undefined);
|
||||
const [useShortUrl] = useState<boolean>(true);
|
||||
const [exportUrlAs] = useState<ExportUrlAsType>(ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT);
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const [shortUrlCache, setShortUrlCache] = useState<string | undefined>(undefined);
|
||||
const [anonymousAccessParameters] = useState<AnonymousAccessState['accessURLParameters']>(null);
|
||||
const [usePublicUrl] = useState<boolean>(false);
|
||||
const urlParamsRef = useRef<UrlParams | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [snapshotUrl, setSnapshotUrl] = useState<string>('');
|
||||
const [isTextCopied, setTextCopied] = useState(false);
|
||||
const urlToCopy = useRef<string | undefined>(undefined);
|
||||
const [anonymousAccessParameters, setAnonymousAccessParameters] =
|
||||
useState<AnonymousAccessState['accessURLParameters']>(null);
|
||||
const [usePublicUrl, setUsePublicUrl] = useState<boolean>(false);
|
||||
const [showPublicUrlSwitch, setShowPublicUrlSwitch] = useState(false);
|
||||
const copiedTextToolTipCleanupIdRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const { draftModeCallOut: DraftModeCallout, computeAnonymousCapabilities } = objectConfig;
|
||||
|
||||
useEffect(() => {
|
||||
if (computeAnonymousCapabilities && anonymousAccess) {
|
||||
const resolveAnonymousAccessClaims = async () => {
|
||||
try {
|
||||
const [state, capabilities] = await Promise.all([
|
||||
anonymousAccess.getState(),
|
||||
anonymousAccess.getCapabilities(),
|
||||
]);
|
||||
|
||||
if (state?.isEnabled) {
|
||||
setAnonymousAccessParameters(state?.accessURLParameters);
|
||||
|
||||
if (capabilities) {
|
||||
setShowPublicUrlSwitch(computeAnonymousCapabilities?.(capabilities));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
resolveAnonymousAccessClaims();
|
||||
}
|
||||
}, [anonymousAccess, computeAnonymousCapabilities]);
|
||||
|
||||
const makeUrlEmbeddable = useCallback((tempUrl: string): string => {
|
||||
const embedParam = '?embed=true';
|
||||
|
@ -76,55 +111,43 @@ export const EmbedContent = ({
|
|||
return `${tempUrl}${embedParam}`;
|
||||
}, []);
|
||||
|
||||
const getUrlParamExtensions = useCallback(
|
||||
(tempUrl: string): string => {
|
||||
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;
|
||||
}, tempUrl)
|
||||
: tempUrl;
|
||||
},
|
||||
[urlParams]
|
||||
);
|
||||
const getUrlParamExtensions = useCallback((tempUrl: string): string => {
|
||||
const urlWithUpdatedParams = urlParamsRef.current
|
||||
? Object.keys(urlParamsRef.current).reduce((urlAccumulator, key) => {
|
||||
const urlParam = urlParamsRef.current?.[key];
|
||||
return urlParam
|
||||
? Object.keys(urlParam).reduce((queryAccumulator, queryParam) => {
|
||||
const isQueryParamEnabled = urlParam[queryParam];
|
||||
return isQueryParamEnabled
|
||||
? queryAccumulator + `&${queryParam}=true`
|
||||
: queryAccumulator;
|
||||
}, urlAccumulator)
|
||||
: urlAccumulator;
|
||||
}, tempUrl)
|
||||
: tempUrl;
|
||||
|
||||
return urlWithUpdatedParams;
|
||||
}, []);
|
||||
|
||||
const updateUrlParams = useCallback(
|
||||
(tempUrl: string) => {
|
||||
tempUrl = makeUrlEmbeddable(tempUrl);
|
||||
tempUrl = urlParams ? getUrlParamExtensions(tempUrl) : tempUrl;
|
||||
return tempUrl;
|
||||
},
|
||||
[makeUrlEmbeddable, getUrlParamExtensions, urlParams]
|
||||
(url: string) => getUrlParamExtensions(makeUrlEmbeddable(url)),
|
||||
[makeUrlEmbeddable, getUrlParamExtensions]
|
||||
);
|
||||
|
||||
const getSnapshotUrl = useCallback(
|
||||
(forSavedObject?: boolean) => {
|
||||
let tempUrl = '';
|
||||
if (forSavedObject && shareableUrlForSavedObject) {
|
||||
tempUrl = shareableUrlForSavedObject;
|
||||
}
|
||||
if (!tempUrl) {
|
||||
tempUrl = shareableUrl || window.location.href;
|
||||
}
|
||||
return updateUrlParams(tempUrl);
|
||||
},
|
||||
[shareableUrl, shareableUrlForSavedObject, updateUrlParams]
|
||||
useEffect(
|
||||
() => setSnapshotUrl(updateUrlParams(shareableUrl || window.location.href)),
|
||||
[shareableUrl, updateUrlParams]
|
||||
);
|
||||
|
||||
const getSavedObjectUrl = useCallback(() => {
|
||||
const tempUrl = getSnapshotUrl(true);
|
||||
const tempUrl = shareableUrlForSavedObject
|
||||
? updateUrlParams(shareableUrlForSavedObject)
|
||||
: snapshotUrl;
|
||||
|
||||
const parsedUrl = parseUrl(tempUrl);
|
||||
|
||||
if (!parsedUrl || !parsedUrl.hash) {
|
||||
return;
|
||||
return tempUrl;
|
||||
}
|
||||
|
||||
// Get the application route, after the hash, and remove the #.
|
||||
|
@ -146,7 +169,18 @@ export const EmbedContent = ({
|
|||
});
|
||||
|
||||
return updateUrlParams(formattedUrl);
|
||||
}, [getSnapshotUrl, updateUrlParams]);
|
||||
}, [shareableUrlForSavedObject, snapshotUrl, updateUrlParams]);
|
||||
|
||||
const createShortUrl = useCallback(async () => {
|
||||
const shortUrlService = urlService.shortUrls.get(null);
|
||||
|
||||
if (shareableUrlLocatorParams) {
|
||||
const shortUrl = await shortUrlService.createWithLocator(shareableUrlLocatorParams);
|
||||
return shortUrl.locator.getUrl(shortUrl.params, { absolute: true });
|
||||
} else {
|
||||
return (await shortUrlService.createFromLongUrl(snapshotUrl)).url;
|
||||
}
|
||||
}, [shareableUrlLocatorParams, snapshotUrl, urlService.shortUrls]);
|
||||
|
||||
const addUrlAnonymousAccessParameters = useCallback(
|
||||
(tempUrl: string): string => {
|
||||
|
@ -165,53 +199,41 @@ export const EmbedContent = ({
|
|||
[anonymousAccessParameters, usePublicUrl]
|
||||
);
|
||||
|
||||
const makeIframeTag = (tempUrl: string) => {
|
||||
if (!tempUrl) {
|
||||
return;
|
||||
}
|
||||
const getEmbedLink = useCallback(async () => {
|
||||
const embedUrl = addUrlAnonymousAccessParameters(
|
||||
!isDirty ? getSavedObjectUrl() : allowShortUrl ? await createShortUrl() : snapshotUrl
|
||||
);
|
||||
|
||||
return `<iframe src="${tempUrl}" height="600" width="800"></iframe>`;
|
||||
};
|
||||
|
||||
const setUrlHelper = useCallback(() => {
|
||||
let tempUrl: string | undefined;
|
||||
|
||||
if (exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) {
|
||||
tempUrl = getSavedObjectUrl();
|
||||
} else if (useShortUrl && shortUrlCache) {
|
||||
tempUrl = shortUrlCache;
|
||||
} else {
|
||||
tempUrl = getSnapshotUrl();
|
||||
}
|
||||
|
||||
if (tempUrl) {
|
||||
tempUrl = addUrlAnonymousAccessParameters(tempUrl!);
|
||||
}
|
||||
|
||||
tempUrl = makeIframeTag(tempUrl!);
|
||||
|
||||
setUrl(tempUrl!);
|
||||
return `<iframe src="${embedUrl}" height="600" width="800"></iframe>`;
|
||||
}, [
|
||||
addUrlAnonymousAccessParameters,
|
||||
exportUrlAs,
|
||||
allowShortUrl,
|
||||
createShortUrl,
|
||||
getSavedObjectUrl,
|
||||
getSnapshotUrl,
|
||||
shortUrlCache,
|
||||
useShortUrl,
|
||||
isDirty,
|
||||
snapshotUrl,
|
||||
]);
|
||||
|
||||
const resetUrl = useCallback(() => {
|
||||
if (isMounted()) {
|
||||
setShortUrlCache(undefined);
|
||||
setUrlHelper();
|
||||
}
|
||||
}, [isMounted, setUrlHelper]);
|
||||
const copyUrlHelper = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
useEffect(() => {
|
||||
setUrlHelper();
|
||||
getUrlParamExtensions(url);
|
||||
window.addEventListener('hashchange', resetUrl, false);
|
||||
}, [getUrlParamExtensions, resetUrl, setUrlHelper, url]);
|
||||
urlToCopy.current = await getEmbedLink();
|
||||
|
||||
copyToClipboard(urlToCopy.current!);
|
||||
setTextCopied(() => {
|
||||
if (copiedTextToolTipCleanupIdRef.current) {
|
||||
clearTimeout(copiedTextToolTipCleanupIdRef.current);
|
||||
}
|
||||
|
||||
// set up timer to revert copied state to false after specified duration
|
||||
copiedTextToolTipCleanupIdRef.current = setTimeout(() => setTextCopied(false), 1000);
|
||||
|
||||
// set copied state to true for now
|
||||
return true;
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
}, [getEmbedLink]);
|
||||
|
||||
const renderUrlParamExtensions = () => {
|
||||
if (!urlParamExtensions) {
|
||||
|
@ -221,8 +243,7 @@ export const EmbedContent = ({
|
|||
const setParamValue =
|
||||
(paramName: string) =>
|
||||
(values: { [queryParam: string]: boolean } = {}): void => {
|
||||
setUrlParams({ ...urlParams, [paramName]: { ...values } });
|
||||
setUrlHelper();
|
||||
urlParamsRef.current = { ...urlParamsRef.current, [paramName]: { ...values } };
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -236,6 +257,38 @@ export const EmbedContent = ({
|
|||
);
|
||||
};
|
||||
|
||||
const renderPublicUrlOptionsSwitch = () => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="share.embed.publicUrlOptionsSwitch.label"
|
||||
defaultMessage="Allow public access"
|
||||
/>
|
||||
}
|
||||
checked={usePublicUrl}
|
||||
onChange={(e: EuiSwitchEvent) => setUsePublicUrl(e.target.checked)}
|
||||
data-test-subj="embedPublicUrlOptionsSwitch"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="share.embed.publicUrlOptionsSwitch.tooltip"
|
||||
defaultMessage="Enabling public access generates a sharable URL that allows anonymous access without a login prompt."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const helpText =
|
||||
objectType === 'dashboard' ? (
|
||||
<FormattedMessage
|
||||
|
@ -250,8 +303,6 @@ export const EmbedContent = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const { draftModeCallOut: DraftModeCallout } = objectConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiForm>
|
||||
|
@ -267,22 +318,30 @@ export const EmbedContent = ({
|
|||
<EuiSpacer />
|
||||
</EuiForm>
|
||||
<EuiFlexGroup justifyContent="flexEnd" responsive={false}>
|
||||
<React.Fragment>
|
||||
{showPublicUrlSwitch ? renderPublicUrlOptionsSwitch() : null}
|
||||
</React.Fragment>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCopy textToCopy={url}>
|
||||
{(copy) => (
|
||||
<EuiButton
|
||||
data-test-subj="copyEmbedUrlButton"
|
||||
onClick={copy}
|
||||
data-share-url={url}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="share.link.copyEmbedCodeButton"
|
||||
defaultMessage="Copy embed code"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiCopy>
|
||||
<EuiToolTip
|
||||
content={
|
||||
isTextCopied
|
||||
? i18n.translate('share.embed.copied', { defaultMessage: 'Link copied' })
|
||||
: null
|
||||
}
|
||||
>
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="copyEmbedUrlButton"
|
||||
onClick={copyUrlHelper}
|
||||
data-share-url={urlToCopy.current}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="share.embed.copyEmbedCodeButton"
|
||||
defaultMessage="Copy embed code"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
|
|
|
@ -23,6 +23,10 @@ const EmbedTabContent: NonNullable<IEmbedTab['content']> = ({ state, dispatch })
|
|||
objectType,
|
||||
objectTypeMeta,
|
||||
isDirty,
|
||||
allowShortUrl,
|
||||
anonymousAccess,
|
||||
urlService,
|
||||
shareableUrlLocatorParams,
|
||||
} = useShareTabsContext()!;
|
||||
|
||||
return (
|
||||
|
@ -34,6 +38,10 @@ const EmbedTabContent: NonNullable<IEmbedTab['content']> = ({ state, dispatch })
|
|||
objectType,
|
||||
objectConfig: objectTypeMeta?.config?.embed,
|
||||
isDirty,
|
||||
anonymousAccess,
|
||||
allowShortUrl,
|
||||
urlService,
|
||||
shareableUrlLocatorParams,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('LinkContent', () => {
|
|||
|
||||
let urlService: BrowserUrlService;
|
||||
|
||||
// @ts-expect-error there is a type because we override the shortUrls implementation
|
||||
// @ts-expect-error there is a type error because we override the shortUrls implementation
|
||||
// eslint-disable-next-line prefer-const
|
||||
({ service: urlService } = urlServiceTestSetup({
|
||||
shortUrls: ({ locators }) =>
|
||||
|
@ -102,7 +102,7 @@ describe('LinkContent', () => {
|
|||
|
||||
await user.click(copyButton);
|
||||
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(copyButton.getAttribute('data-share-url')).toBe(shareableUrl);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -104,7 +104,7 @@ export const LinkContent = ({
|
|||
copyToClipboard(urlToCopy.current);
|
||||
setTextCopied(() => {
|
||||
if (copiedTextToolTipCleanupIdRef.current) {
|
||||
clearInterval(copiedTextToolTipCleanupIdRef.current);
|
||||
clearTimeout(copiedTextToolTipCleanupIdRef.current);
|
||||
}
|
||||
|
||||
// set up timer to revert copied state to false after specified duration
|
||||
|
|
|
@ -23,6 +23,7 @@ export type BrowserUrlService = UrlService<
|
|||
|
||||
export interface ShareContextObjectTypeConfig {
|
||||
draftModeCallOut?: ReactNode;
|
||||
computeAnonymousCapabilities?: (anonymousUserCapabilities: Capabilities) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,6 +72,9 @@ export interface ShareContext {
|
|||
sharingData: { [key: string]: unknown };
|
||||
isDirty: boolean;
|
||||
onClose: () => void;
|
||||
/**
|
||||
* @deprecated use computeAnonymousCapabilities defined on objectTypeMeta config
|
||||
*/
|
||||
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
|
||||
disabledShareUrl?: boolean;
|
||||
toasts: ToastsSetup;
|
||||
|
|
|
@ -398,6 +398,11 @@ export const getTopNavConfig = (
|
|||
title: i18n.translate('visualizations.share.shareModal.title', {
|
||||
defaultMessage: 'Share this visualization',
|
||||
}),
|
||||
config: {
|
||||
embed: {
|
||||
computeAnonymousCapabilities: showPublicUrlSwitch,
|
||||
},
|
||||
},
|
||||
},
|
||||
sharingData: {
|
||||
title:
|
||||
|
@ -413,7 +418,6 @@ export const getTopNavConfig = (
|
|||
},
|
||||
},
|
||||
isDirty: hasUnappliedChanges || hasUnsavedChanges,
|
||||
showPublicUrlSwitch,
|
||||
toasts: toastNotifications,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7511,11 +7511,11 @@
|
|||
"share.contextMenu.permalinksTab": "Liens",
|
||||
"share.contextMenuTitle": "Partager ce {objectType}",
|
||||
"share.dashboard.link.description": "Partagez un lien direct avec cette recherche.",
|
||||
"share.embed.copyEmbedCodeButton": "Copier le code intégré",
|
||||
"share.embed.dashboard.helpText": "Intégrez ce tableau de bord dans une autre page web. Sélectionnez les éléments à inclure dans la vue intégrable.",
|
||||
"share.embed.helpText": "Intégrez ce {objectType} dans une autre page web.",
|
||||
"share.fileType": "Type de fichier",
|
||||
"share.link.copied": "Texte copié",
|
||||
"share.link.copyEmbedCodeButton": "Copier le code intégré",
|
||||
"share.link.copyLinkButton": "Copier le lien",
|
||||
"share.link.helpText": "Partager un lien direct vers ce {objectType}.",
|
||||
"share.modalContent.copyUrlButtonLabel": "Copier l'URL Post",
|
||||
|
|
|
@ -7388,11 +7388,11 @@
|
|||
"share.contextMenu.permalinksTab": "リンク",
|
||||
"share.contextMenuTitle": "この {objectType} を共有",
|
||||
"share.dashboard.link.description": "この検索への直接リンクを共有します。",
|
||||
"share.embed.copyEmbedCodeButton": "埋め込みコードをコピー",
|
||||
"share.embed.dashboard.helpText": "このダッシュボードを別のWebページに埋め込みます。埋め込み可能なビューに含める項目を選択します。",
|
||||
"share.embed.helpText": "この{objectType}を別のWebページに埋め込みます。",
|
||||
"share.fileType": "ファイルタイプ",
|
||||
"share.link.copied": "テキストがコピーされました",
|
||||
"share.link.copyEmbedCodeButton": "埋め込みコードをコピー",
|
||||
"share.link.copyLinkButton": "リンクをコピー",
|
||||
"share.link.helpText": "この{objectType}への直接リンクを共有します。",
|
||||
"share.modalContent.copyUrlButtonLabel": "POST URLをコピー",
|
||||
|
|
|
@ -7277,11 +7277,11 @@
|
|||
"share.contextMenu.permalinksTab": "链接",
|
||||
"share.contextMenuTitle": "共享此 {objectType}",
|
||||
"share.dashboard.link.description": "共享指向此搜索的直接链接。",
|
||||
"share.embed.copyEmbedCodeButton": "复制嵌入代码",
|
||||
"share.embed.dashboard.helpText": "将此仪表板嵌入到其他网页。选择要在可嵌入视图中包括哪些项目。",
|
||||
"share.embed.helpText": "将此 {objectType} 嵌入到其他网页。",
|
||||
"share.fileType": "文件类型",
|
||||
"share.link.copied": "文本已复制",
|
||||
"share.link.copyEmbedCodeButton": "复制嵌入代码",
|
||||
"share.link.copyLinkButton": "复制链接",
|
||||
"share.link.helpText": "共享指向此 {objectType} 的直接链接。",
|
||||
"share.modalContent.copyUrlButtonLabel": "复制 Post URL",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue