Use locators service for Discover shared URLs (#154947)

## Summary

Resolves https://github.com/elastic/kibana/issues/148886.
Resolves https://github.com/elastic/kibana/issues/142525.
Resolves https://github.com/elastic/kibana/issues/156275.

Uses the URL locators service for generating the URL when clicking
"Share" in Discover. This enables ad-hoc data views in shared URLs. As a
result, we no longer show a prompt to save the ad-hoc data view before
sharing, which means we can get rid of some no-longer used hooks.

### Checklist

- [ ] 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/packages/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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] 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 renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Lukas Olson 2023-05-17 14:26:53 -07:00 committed by GitHub
parent 74195d549d
commit 9359c634df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 88 additions and 405 deletions

View file

@ -122,7 +122,6 @@ async function mountComponent(
state: { columns: [], query, hideChart: false, interval: 'auto' },
stateContainer,
setExpandedDoc: jest.fn(),
persistDataView: jest.fn(),
updateDataViewList: jest.fn(),
};
stateContainer.searchSessionManager = createSearchSessionMock(session).searchSessionManager;

View file

@ -22,7 +22,7 @@ import { METRIC_TYPE } from '@kbn/analytics';
import classNames from 'classnames';
import { generateFilters } from '@kbn/data-plugin/public';
import { DragContext } from '@kbn/dom-drag-drop';
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
import { DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
import { useSavedSearchInitial } from '../../services/discover_state_provider';
import { DiscoverStateContainer } from '../../services/discover_state';
import { VIEW_MODE } from '../../../../../common/constants';
@ -59,14 +59,9 @@ const TopNavMemoized = React.memo(DiscoverTopNav);
export interface DiscoverLayoutProps {
navigateTo: (url: string) => void;
stateContainer: DiscoverStateContainer;
persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
}
export function DiscoverLayout({
navigateTo,
stateContainer,
persistDataView,
}: DiscoverLayoutProps) {
export function DiscoverLayout({ navigateTo, stateContainer }: DiscoverLayoutProps) {
const {
trackUiMetric,
capabilities,
@ -284,7 +279,6 @@ export function DiscoverLayout({
isPlainRecord={isPlainRecord}
textBasedLanguageModeErrors={textBasedLanguageModeErrors}
onFieldEdited={onFieldEdited}
persistDataView={persistDataView}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
<SavedSearchURLConflictCallout

View file

@ -40,7 +40,6 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
onOpenInspector: jest.fn(),
onFieldEdited: jest.fn(),
isPlainRecord: false,
persistDataView: jest.fn(),
};
}

View file

@ -31,7 +31,6 @@ export type DiscoverTopNavProps = Pick<DiscoverLayoutProps, 'navigateTo'> & {
isPlainRecord: boolean;
textBasedLanguageModeErrors?: Error;
onFieldEdited: () => Promise<void>;
persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
};
export const DiscoverTopNav = ({
@ -44,7 +43,6 @@ export const DiscoverTopNav = ({
isPlainRecord,
textBasedLanguageModeErrors,
onFieldEdited,
persistDataView,
}: DiscoverTopNavProps) => {
const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews);
const dataView = useInternalStateSelector((state) => state.dataView!);
@ -120,18 +118,8 @@ export const DiscoverTopNav = ({
onOpenInspector,
isPlainRecord,
adHocDataViews,
persistDataView,
}),
[
dataView,
navigateTo,
services,
stateContainer,
onOpenInspector,
isPlainRecord,
adHocDataViews,
persistDataView,
]
[dataView, navigateTo, services, stateContainer, onOpenInspector, isPlainRecord, adHocDataViews]
);
const onEditDataView = async (editedDataView: DataView) => {

View file

@ -32,7 +32,6 @@ test('getTopNavLinks result', () => {
services,
state,
isPlainRecord: false,
persistDataView: jest.fn(),
adHocDataViews: [],
});
expect(topNavLinks).toMatchInlineSnapshot(`
@ -93,7 +92,6 @@ test('getTopNavLinks result for sql mode', () => {
services,
state,
isPlainRecord: true,
persistDataView: jest.fn(),
adHocDataViews: [],
});
expect(topNavLinks).toMatchInlineSnapshot(`

View file

@ -8,8 +8,8 @@
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { unhashUrl } from '@kbn/kibana-utils-plugin/public';
import type { TopNavMenuData } from '@kbn/navigation-plugin/public';
import type { DiscoverAppLocatorParams } from '../../../../../common';
import { showOpenSearchPanel } from './show_open_search_panel';
import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data';
import { DiscoverServices } from '../../../../build_services';
@ -28,7 +28,6 @@ export const getTopNavLinks = ({
state,
onOpenInspector,
isPlainRecord,
persistDataView,
adHocDataViews,
}: {
dataView: DataView;
@ -38,7 +37,6 @@ export const getTopNavLinks = ({
onOpenInspector: () => void;
isPlainRecord: boolean;
adHocDataViews: DataView[];
persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
}): TopNavMenuData[] => {
const options = {
id: 'options',
@ -143,17 +141,7 @@ export const getTopNavLinks = ({
}),
testId: 'shareTopNavButton',
run: async (anchorElement: HTMLElement) => {
if (!services.share) {
return;
}
// this prompts the user to save the dataview if adhoc dataview is detected
// for text based languages we don't want this check
if (!isPlainRecord) {
const updatedDataView = await persistDataView(dataView);
if (!updatedDataView) {
return;
}
}
if (!services.share) return;
const savedSearch = state.savedSearchState.getState();
const sharingData = await getSharingData(
savedSearch.searchSource,
@ -162,11 +150,46 @@ export const getTopNavLinks = ({
isPlainRecord
);
const { locator } = services;
const appState = state.appState.getState();
const { timefilter } = services.data.query.timefilter;
const timeRange = timefilter.getTime();
const refreshInterval = timefilter.getRefreshInterval();
const { grid, ...otherState } = appState;
const filters = services.filterManager.getFilters();
// Share -> Get links -> Snapshot
const params: DiscoverAppLocatorParams = {
...otherState,
...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}),
...(dataView?.isPersisted()
? { dataViewId: dataView?.id }
: { dataViewSpec: dataView?.toSpec() }),
filters,
timeRange,
refreshInterval,
};
const relativeUrl = locator.getRedirectUrl(params);
// This logic is duplicated from `relativeToAbsolute` (for bundle size reasons). Ultimately, this should be
// replaced when https://github.com/elastic/kibana/issues/153323 is implemented.
const link = document.createElement('a');
link.setAttribute('href', relativeUrl);
const shareableUrl = link.href;
// Share -> Get links -> Saved object
const shareableUrlForSavedObject = await locator.getUrl(
{ savedSearchId: savedSearch.id },
{ absolute: true }
);
services.share.toggleShareContextMenu({
anchorElement,
allowEmbed: false,
allowShortUrl: !!services.capabilities.discover.createShortUrl,
shareableUrl: unhashUrl(window.location.href),
shareableUrl,
shareableUrlForSavedObject,
shareableUrlLocatorParams: { locator, params },
objectId: savedSearch.id,
objectType: 'search',
sharingData: {

View file

@ -52,10 +52,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
/**
* Adhoc data views functionality
*/
const { persistDataView } = useAdHocDataViews({
stateContainer,
services,
});
useAdHocDataViews({ stateContainer, services });
/**
* State changes (data view, columns), when a text base query result is returned
@ -97,11 +94,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
return (
<RootDragDropProvider>
<DiscoverLayoutMemoized
navigateTo={navigateTo}
stateContainer={stateContainer}
persistDataView={persistDataView}
/>
<DiscoverLayoutMemoized navigateTo={navigateTo} stateContainer={stateContainer} />
</RootDragDropProvider>
);
}

View file

@ -1,97 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { renderHook } from '@testing-library/react-hooks';
import { useAdHocDataViews } from './use_adhoc_data_views';
import * as persistencePromptModule from '../../../hooks/use_confirm_persistence_prompt';
import { urlTrackerMock } from '../../../__mocks__/url_tracker.mock';
import { setUrlTracker } from '../../../kibana_services';
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
import { DiscoverMainProvider } from '../services/discover_state_provider';
import { discoverServiceMock } from '../../../__mocks__/services';
jest.mock('../../../hooks/use_confirm_persistence_prompt', () => {
const createdDataView = {
id: 'updated-mock-id',
};
const mocks = {
openConfirmSavePrompt: jest.fn(() => Promise.resolve(createdDataView)),
updateSavedSearch: jest.fn(() => Promise.resolve({})),
};
return {
useConfirmPersistencePrompt: () => mocks,
mocks,
};
});
jest.mock('../../../kibana_services', () => {
const actual = jest.requireActual('../../../kibana_services');
return {
...actual,
getUiActions: jest.fn(() => ({
getTrigger: jest.fn(() => {}),
getAction: jest.fn(() => ({ execute: jest.fn() })),
})),
};
});
setUrlTracker(urlTrackerMock);
interface ConfirmPromptMocks {
openConfirmSavePrompt: jest.Mock;
updateSavedSearch: jest.Mock;
}
const persistencePromptMocks = (
persistencePromptModule as unknown as {
useConfirmPersistencePrompt: () => ConfirmPromptMocks;
mocks: ConfirmPromptMocks;
}
).mocks;
const mockDataView = {
id: 'mock-id',
title: 'mock-title',
timeFieldName: 'mock-time-field-name',
isPersisted: () => false,
getName: () => 'mock-data-view',
toSpec: () => ({}),
isTimeBased: () => true,
} as DataView;
describe('useAdHocDataViews', () => {
it('should save data view with new id and update saved search', async () => {
const stateContainer = getDiscoverStateMock({
isTimeBased: true,
});
stateContainer.actions.setDataView(mockDataView);
const hook = renderHook(
() =>
useAdHocDataViews({
stateContainer,
services: discoverServiceMock,
}),
{
wrapper: ({ children }: { children: React.ReactElement }) => (
<DiscoverMainProvider value={stateContainer}>{children}</DiscoverMainProvider>
),
}
);
const savedDataView = await hook.result.current.persistDataView();
expect(persistencePromptMocks.openConfirmSavePrompt).toHaveBeenCalledWith(mockDataView);
const updateSavedSearchCall = persistencePromptMocks.updateSavedSearch.mock.calls[0];
expect(updateSavedSearchCall[0].dataView.id).toEqual('updated-mock-id');
expect(savedDataView!.id).toEqual('updated-mock-id');
});
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { useCallback, useEffect } from 'react';
import { useEffect } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import { DiscoverServices } from '../../../build_services';
import { useSavedSearch } from '../services/discover_state_provider';
@ -14,12 +14,10 @@ import { isTextBasedQuery } from '../utils/is_text_based_query';
import { useAppStateSelector } from '../services/discover_app_state_container';
import { useInternalStateSelector } from '../services/discover_internal_state_container';
import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../../../constants';
import { useConfirmPersistencePrompt } from '../../../hooks/use_confirm_persistence_prompt';
import { DiscoverStateContainer } from '../services/discover_state';
import { useFiltersValidation } from './use_filters_validation';
export const useAdHocDataViews = ({
stateContainer,
services,
}: {
stateContainer: DiscoverStateContainer;
@ -41,27 +39,4 @@ export const useAdHocDataViews = ({
* Takes care of checking data view id references in filters
*/
useFiltersValidation({ savedSearch, filterManager, toastNotifications });
const { openConfirmSavePrompt, updateSavedSearch } = useConfirmPersistencePrompt(stateContainer);
const persistDataView = useCallback(async () => {
const currentDataView = stateContainer.internalState.getState().dataView;
if (!currentDataView || currentDataView.isPersisted()) {
return currentDataView;
}
const createdDataView = await openConfirmSavePrompt(currentDataView);
if (!createdDataView) {
return; // persistance cancelled
}
if (savedSearch.id) {
// update saved search with saved data view
const currentState = stateContainer.appState.getState();
await updateSavedSearch({ savedSearch, dataView: createdDataView, state: currentState });
}
return createdDataView;
}, [stateContainer, openConfirmSavePrompt, updateSavedSearch, savedSearch]);
return { persistDataView };
};

View file

@ -1,79 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { renderHook } from '@testing-library/react-hooks';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { discoverServiceMock as mockDiscoverServices } from '../__mocks__/services';
import { useConfirmPersistencePrompt } from './use_confirm_persistence_prompt';
import { getDiscoverStateMock } from '../__mocks__/discover_state.mock';
import { setUiActions } from '../kibana_services';
jest.mock('./show_confirm_panel', () => {
return {
showConfirmPanel: ({ onConfirm }: { onConfirm: () => void }) => onConfirm(),
};
});
const mockDataView = {
id: 'mock-id',
title: 'mock-title',
timeFieldName: 'mock-time-field-name',
isPersisted: () => false,
getName: () => 'mock-data-view',
toSpec: () => ({}),
} as DataView;
setUiActions({
getTrigger: jest.fn(() => {}),
getAction: jest.fn(() => ({ execute: jest.fn() })),
} as unknown as UiActionsStart);
describe('useConfirmPersistencePrompt', () => {
it('should save data view correctly', async () => {
mockDiscoverServices.dataViews.createAndSave = jest.fn().mockResolvedValue(mockDataView);
const hook = renderHook(
(d: DataView) => useConfirmPersistencePrompt(getDiscoverStateMock({})),
{
initialProps: mockDataView,
wrapper: ({ children }) => (
<KibanaContextProvider services={mockDiscoverServices}>{children}</KibanaContextProvider>
),
}
);
const result = await hook.result.current.openConfirmSavePrompt(mockDataView);
expect(mockDiscoverServices.dataViews.createAndSave).toHaveBeenCalled();
expect(result).toEqual(mockDataView);
});
it('should show error toast if creation failed', async () => {
mockDiscoverServices.dataViews.createAndSave = jest
.fn()
.mockRejectedValue(new Error('failed to save'));
const hook = renderHook(
(d: DataView) => useConfirmPersistencePrompt(getDiscoverStateMock({})),
{
initialProps: mockDataView,
wrapper: ({ children }) => (
<KibanaContextProvider services={mockDiscoverServices}>{children}</KibanaContextProvider>
),
}
);
try {
await hook.result.current.openConfirmSavePrompt(mockDataView);
} catch (e) {
expect(mockDiscoverServices.toastNotifications.addDanger).toHaveBeenCalled();
expect(e.message).toEqual('failed to save');
}
});
});

View file

@ -1,102 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { useDiscoverServices } from './use_discover_services';
import { showConfirmPanel } from './show_confirm_panel';
import { DiscoverStateContainer } from '../application/main/services/discover_state';
export const useConfirmPersistencePrompt = (stateContainer: DiscoverStateContainer) => {
const services = useDiscoverServices();
const persistDataView: (adHocDataView: DataView) => Promise<DataView> = useCallback(
async (adHocDataView) => {
try {
const persistedDataView = await stateContainer.actions.persistAdHocDataView(adHocDataView);
const message = i18n.translate('discover.dataViewPersist.message', {
defaultMessage: "Saved '{dataViewName}'",
values: { dataViewName: persistedDataView.getName() },
});
services.toastNotifications.addSuccess(message);
return persistedDataView;
} catch (error) {
services.toastNotifications.addDanger({
title: i18n.translate('discover.dataViewPersistError.title', {
defaultMessage: 'Unable to create data view',
}),
text: error.message,
});
throw new Error(error);
}
},
[services.toastNotifications, stateContainer]
);
const openConfirmSavePrompt: (dataView: DataView) => Promise<DataView | undefined> = useCallback(
async (dataView) => {
return new Promise((resolve) =>
showConfirmPanel({
onConfirm: () =>
persistDataView(dataView)
.then((createdDataView) => resolve(createdDataView))
.catch(() => resolve(undefined)),
onCancel: () => resolve(undefined),
})
);
},
[persistDataView]
);
const onUpdateSuccess = useCallback(
(savedSearch: SavedSearch) => {
services.toastNotifications.addSuccess({
title: i18n.translate('discover.notifications.updateSavedSearchTitle', {
defaultMessage: `Search '{savedSearchTitle}' updated with saved data view`,
values: {
savedSearchTitle: savedSearch.title,
},
}),
'data-test-subj': 'updateSearchSuccess',
});
},
[services.toastNotifications]
);
const onUpdateError = useCallback(
(error: Error, savedSearch: SavedSearch) => {
services.toastNotifications.addDanger({
title: i18n.translate('discover.notifications.notUpdatedSavedSearchTitle', {
defaultMessage: `Search '{savedSearchTitle}' was not updated with savedDataView.`,
values: {
savedSearchTitle: savedSearch.title,
},
}),
text: error.message,
});
},
[services.toastNotifications]
);
const updateSavedSearch = useCallback(
async ({ savedSearch }) => {
try {
await stateContainer.savedSearchState.persist(savedSearch);
onUpdateSuccess(savedSearch);
} catch (e) {
onUpdateError(e, savedSearch);
}
},
[onUpdateError, onUpdateSuccess, stateContainer.savedSearchState]
);
return { openConfirmSavePrompt, updateSavedSearch };
};

View file

@ -107,7 +107,7 @@ export async function getSharingData(
searchSource.setField('fields', fields);
}
return searchSource.getSerializedFields(true);
return searchSource.getSerializedFields(true, false);
},
columns,
};

View file

@ -14,6 +14,7 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
import type { Capabilities } from '@kbn/core/public';
import type { LocatorPublic } from '../../common';
import { UrlPanelContent } from './url_panel_content';
import { ShareMenuItem, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
import { AnonymousAccessServiceContract } from '../../common/anonymous_access';
@ -26,6 +27,10 @@ export interface ShareContextMenuProps {
objectType: string;
shareableUrl?: string;
shareableUrlForSavedObject?: string;
shareableUrlLocatorParams?: {
locator: LocatorPublic<any>;
params: any;
};
shareMenuItems: ShareMenuItem[];
sharingData: any;
onClose: () => void;
@ -68,6 +73,7 @@ export class ShareContextMenu extends Component<ShareContextMenuProps> {
objectType={this.props.objectType}
shareableUrl={this.props.shareableUrl}
shareableUrlForSavedObject={this.props.shareableUrlForSavedObject}
shareableUrlLocatorParams={this.props.shareableUrlLocatorParams}
anonymousAccess={this.props.anonymousAccess}
showPublicUrlSwitch={this.props.showPublicUrlSwitch}
urlService={this.props.urlService}
@ -102,6 +108,7 @@ export class ShareContextMenu extends Component<ShareContextMenuProps> {
objectType={this.props.objectType}
shareableUrl={this.props.shareableUrl}
shareableUrlForSavedObject={this.props.shareableUrlForSavedObject}
shareableUrlLocatorParams={this.props.shareableUrlLocatorParams}
urlParamExtensions={this.props.embedUrlParamExtensions}
anonymousAccess={this.props.anonymousAccess}
showPublicUrlSwitch={this.props.showPublicUrlSwitch}

View file

@ -28,6 +28,7 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { Capabilities } from '@kbn/core/public';
import type { LocatorPublic } from '../../common';
import { UrlParamExtension } from '../types';
import {
AnonymousAccessServiceContract,
@ -42,6 +43,10 @@ export interface UrlPanelContentProps {
objectType: string;
shareableUrl?: string;
shareableUrlForSavedObject?: string;
shareableUrlLocatorParams?: {
locator: LocatorPublic<any>;
params: any;
};
urlParamExtensions?: UrlParamExtension[];
anonymousAccess?: AnonymousAccessServiceContract;
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
@ -351,16 +356,23 @@ export class UrlPanelContent extends Component<UrlPanelContentProps, State> {
});
try {
const snapshotUrl = this.getSnapshotUrl();
const shortUrl = await this.props.urlService.shortUrls
.get(null)
.createFromLongUrl(snapshotUrl);
const { shareableUrlLocatorParams } = this.props;
if (shareableUrlLocatorParams) {
const shortUrls = this.props.urlService.shortUrls.get(null);
const shortUrl = await shortUrls.createWithLocator(shareableUrlLocatorParams);
this.shortUrlCache = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true });
} else {
const snapshotUrl = this.getSnapshotUrl();
const shortUrl = await this.props.urlService.shortUrls
.get(null)
.createFromLongUrl(snapshotUrl);
this.shortUrlCache = shortUrl.url;
}
if (!this.mounted) {
return;
}
this.shortUrlCache = shortUrl.url;
this.setState(
{
isCreatingShortUrl: false,

View file

@ -70,6 +70,7 @@ export class ShareMenuManager {
menuItems,
shareableUrl,
shareableUrlForSavedObject,
shareableUrlLocatorParams,
embedUrlParamExtensions,
theme,
showPublicUrlSwitch,
@ -115,6 +116,7 @@ export class ShareMenuManager {
sharingData={sharingData}
shareableUrl={shareableUrl}
shareableUrlForSavedObject={shareableUrlForSavedObject}
shareableUrlLocatorParams={shareableUrlLocatorParams}
onClose={onClose}
embedUrlParamExtensions={embedUrlParamExtensions}
anonymousAccess={anonymousAccess}

View file

@ -10,7 +10,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 '@kbn/core/public';
import type { UrlService } from '../common/url_service';
import type { UrlService, LocatorPublic } from '../common/url_service';
import type { BrowserShortUrlClientFactoryCreateParams } from './url_service/short_urls/short_url_client_factory';
import type { BrowserShortUrlClient } from './url_service/short_urls/short_url_client';
@ -42,6 +42,10 @@ export interface ShareContext {
*/
shareableUrl: string;
shareableUrlForSavedObject?: string;
shareableUrlLocatorParams?: {
locator: LocatorPublic<any>;
params: any;
};
sharingData: { [key: string]: unknown };
isDirty: boolean;
onClose: () => void;

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
@ -73,19 +74,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('permalink', function () {
it('should allow for copying the snapshot URL', async function () {
const expectedUrl =
baseUrl +
'/app/discover?_t=1453775307251#' +
'/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time' +
":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" +
"-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-" +
"*',interval:auto,query:(language:kuery,query:'')" +
",sort:!(!('@timestamp',desc)))";
const lz =
'N4IgjgrgpgTgniAXKSsGJCANCANgQwDsBzCfYqJEAa2nhAF8cBnAexgBckBtbkAAQ4BLALZRmHfCIAO2EABNxAYxABdVTiWtcEEYWY8N' +
'IIYUUAPKrlbEJ%2BZgAsAtACo5JjrABu%2BXFXwQOVjkAMyFcDxgDRG4jeXxJADUhKAB3AEl5S2tbBxc5YTEAJSIKJFBgmFYRKgAmAAY' +
'ARgBWRzqATkcGtoAVOoA2RABmBsQAFlGAOjrpgC18oIx65taOmsHuhoAOIZHxqdnGHBgoCvF7NMII719kEGvoJD7p6Zxpf2ZKRA4YaAY' +
'GIA%3D';
const actualUrl = await PageObjects.share.getSharedUrl();
// strip the timestamp out of each URL
expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be(
expectedUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')
);
expect(actualUrl).to.contain(`?l=${DISCOVER_APP_LOCATOR}`);
expect(actualUrl).to.contain(`&lz=${lz}`);
});
it('should allow for copying the snapshot URL as a short URL', async function () {
@ -99,12 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should allow for copying the saved object URL', async function () {
const expectedUrl =
baseUrl +
'/app/discover#' +
'/view/ab12e3c0-f231-11e6-9486-733b1ac9221a' +
'?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A60000)' +
"%2Ctime%3A(from%3A'2015-09-19T06%3A31%3A44.000Z'%2C" +
"to%3A'2015-09-23T18%3A31%3A44.000Z'))";
baseUrl + '/app/discover#' + '/view/ab12e3c0-f231-11e6-9486-733b1ac9221a' + '?_g=()';
await PageObjects.discover.loadSavedSearch('A Saved Search');
await PageObjects.share.clickShareTopNavButton();
await PageObjects.share.exportAsSavedObject();

View file

@ -134,18 +134,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(prevDataViewId).not.to.equal(newDataViewId);
});
it('should update data view id when saving data view from hoc one', async () => {
const prevDataViewId = await PageObjects.discover.getCurrentDataViewId();
await testSubjects.click('shareTopNavButton');
await testSubjects.click('confirmModalConfirmButton');
await PageObjects.header.waitUntilLoadingHasFinished();
const newDataViewId = await PageObjects.discover.getCurrentDataViewId();
expect(prevDataViewId).not.to.equal(newDataViewId);
});
it('search results should be different after data view update', async () => {
await PageObjects.discover.createAdHocDataView('logst', true);
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -66,6 +66,7 @@
"@kbn/utility-types",
"@kbn/dev-proc-runner",
"@kbn/enterprise-search-plugin",
"@kbn/core-saved-objects-server"
"@kbn/core-saved-objects-server",
"@kbn/discover-plugin"
]
}

View file

@ -2233,8 +2233,6 @@
"discover.contextViewRoute.errorTitle": "Une erreur s'est produite",
"discover.controlColumnHeader": "Colonne de commande",
"discover.copyToClipboardJSON": "Copier les documents dans le presse-papiers (JSON)",
"discover.dataViewPersist.message": "\"{dataViewName}\" enregistrée",
"discover.dataViewPersistError.title": "Impossible de créer la vue de données",
"discover.discoverBreadcrumbTitle": "Découverte",
"discover.discoverDefaultSearchSessionName": "Découverte",
"discover.discoverDescription": "Explorez vos données de manière interactive en interrogeant et en filtrant des documents bruts.",
@ -2415,9 +2413,7 @@
"discover.notifications.invalidTimeRangeText": "La plage temporelle spécifiée n'est pas valide (de : \"{from}\" à \"{to}\").",
"discover.notifications.invalidTimeRangeTitle": "Plage temporelle non valide",
"discover.notifications.notSavedSearchTitle": "La recherche \"{savedSearchTitle}\" n'a pas été enregistrée.",
"discover.notifications.notUpdatedSavedSearchTitle": "La recherche \"{savedSearchTitle}\" n'a pas été mise à jour avec savedDataView.",
"discover.notifications.savedSearchTitle": "La recherche \"{savedSearchTitle}\" a été enregistrée.",
"discover.notifications.updateSavedSearchTitle": "La recherche \"{savedSearchTitle}\" a été mise à jour avec la vue de données enregistrée",
"discover.openOptionsPopover.classicDiscoverText": "Classique",
"discover.openOptionsPopover.documentExplorerText": "Explorateur de documents",
"discover.openOptionsPopover.gotToSettings": "Afficher les paramètres de Discover",

View file

@ -2233,8 +2233,6 @@
"discover.contextViewRoute.errorTitle": "エラーが発生しました",
"discover.controlColumnHeader": "列の制御",
"discover.copyToClipboardJSON": "ドキュメントをクリップボードにコピーJSON",
"discover.dataViewPersist.message": "'{dataViewName}'が保存されました",
"discover.dataViewPersistError.title": "データビューを作成できません",
"discover.discoverBreadcrumbTitle": "Discover",
"discover.discoverDefaultSearchSessionName": "Discover",
"discover.discoverDescription": "ドキュメントにクエリをかけたりフィルターを適用することで、データをインタラクティブに閲覧できます。",
@ -2415,9 +2413,7 @@
"discover.notifications.invalidTimeRangeText": "指定された時間範囲が無効です。(開始:'{from}'、終了:'{to}'",
"discover.notifications.invalidTimeRangeTitle": "無効な時間範囲",
"discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。",
"discover.notifications.notUpdatedSavedSearchTitle": "検索'{savedSearchTitle}'はsavedDataViewで更新されませんでした。",
"discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。",
"discover.notifications.updateSavedSearchTitle": "検索'{savedSearchTitle}'は保存されたデータビューで更新されました",
"discover.openOptionsPopover.classicDiscoverText": "クラシック",
"discover.openOptionsPopover.documentExplorerText": "ドキュメントエクスプローラー",
"discover.openOptionsPopover.gotToSettings": "Discover設定を表示",

View file

@ -2233,8 +2233,6 @@
"discover.contextViewRoute.errorTitle": "发生错误",
"discover.controlColumnHeader": "控制列",
"discover.copyToClipboardJSON": "将文档复制到剪贴板 (JSON)",
"discover.dataViewPersist.message": "已保存“{dataViewName}”",
"discover.dataViewPersistError.title": "无法创建数据视图",
"discover.discoverBreadcrumbTitle": "Discover",
"discover.discoverDefaultSearchSessionName": "Discover",
"discover.discoverDescription": "通过查询和筛选原始文档来以交互方式浏览您的数据。",
@ -2415,9 +2413,7 @@
"discover.notifications.invalidTimeRangeText": "提供的时间范围无效。(自:“{from}”,至:“{to}”)",
"discover.notifications.invalidTimeRangeTitle": "时间范围无效",
"discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。",
"discover.notifications.notUpdatedSavedSearchTitle": "未使用 savedDataView 更新搜索“{savedSearchTitle}”。",
"discover.notifications.savedSearchTitle": "搜索“{savedSearchTitle}”已保存",
"discover.notifications.updateSavedSearchTitle": "未使用保存的数据视图更新搜索“{savedSearchTitle}”",
"discover.openOptionsPopover.classicDiscoverText": "经典",
"discover.openOptionsPopover.documentExplorerText": "Document Explorer",
"discover.openOptionsPopover.gotToSettings": "查看 Discover 设置",

View file

@ -87,8 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.saveSearch('single-timefilter-search');
// get shared URL value
await PageObjects.share.clickShareTopNavButton();
const sharedURL = await PageObjects.share.getSharedUrl();
const sharedURL = await browser.getCurrentUrl();
// click 'Copy POST URL'
await PageObjects.share.clickShareTopNavButton();