mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
parent
74195d549d
commit
9359c634df
23 changed files with 88 additions and 405 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,7 +40,6 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
|
|||
onOpenInspector: jest.fn(),
|
||||
onFieldEdited: jest.fn(),
|
||||
isPlainRecord: false,
|
||||
persistDataView: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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(`
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
};
|
|
@ -107,7 +107,7 @@ export async function getSharingData(
|
|||
|
||||
searchSource.setField('fields', fields);
|
||||
}
|
||||
return searchSource.getSerializedFields(true);
|
||||
return searchSource.getSerializedFields(true, false);
|
||||
},
|
||||
columns,
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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設定を表示",
|
||||
|
|
|
@ -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 设置",
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue