mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
## Summary Fixes: https://github.com/elastic/kibana/issues/149347 This PR replaces deprecated `uiSettings` client with `settings.client` in `useUiSetting` hook. As a result, all consumers of the hook need to provide `settings` service to Kibana context. The majority of this PR is providing the `settings` as a dependency to affected plugins. It would be great if sometime in the future we could get rid of `uiSettings` entirely. `CodeEditor` is one of the components relying on this hook, which caused a lot of the changes in this PR. If you have been tagged for review it means your code is using `useUiSetting` hook directly, or is consuming `CodeEditor` component. I have been focused on updating plugins that had failing functional tests, but would appreciate a manual pass on this as well. xoxo ### Checklist Delete any items that are not applicable to this PR. ~ [ ] 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~ - [X] [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> Co-authored-by: Maja Grubic <maja.grubic@elastic.co> Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
440 lines
15 KiB
TypeScript
440 lines
15 KiB
TypeScript
/*
|
|
* 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; you may not use this file except in compliance with the Elastic License
|
|
* 2.0.
|
|
*/
|
|
|
|
import React, { FC, useCallback, useEffect, useState, useMemo } from 'react';
|
|
import { PreloadedState } from '@reduxjs/toolkit';
|
|
import { AppMountParameters, CoreSetup, CoreStart } from '@kbn/core/public';
|
|
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
|
|
import { HashRouter, RouteComponentProps, Switch } from 'react-router-dom';
|
|
import { Route } from '@kbn/shared-ux-router';
|
|
import { History } from 'history';
|
|
import { render, unmountComponentAtNode } from 'react-dom';
|
|
import { i18n } from '@kbn/i18n';
|
|
import { Provider } from 'react-redux';
|
|
import {
|
|
createKbnUrlStateStorage,
|
|
Storage,
|
|
withNotifyOnErrors,
|
|
} from '@kbn/kibana-utils-plugin/public';
|
|
import {
|
|
AnalyticsNoDataPageKibanaProvider,
|
|
AnalyticsNoDataPage,
|
|
} from '@kbn/shared-ux-page-analytics-no-data';
|
|
|
|
import { ACTION_VISUALIZE_LENS_FIELD, VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
|
import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public';
|
|
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
|
import { EuiLoadingSpinner } from '@elastic/eui';
|
|
import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public';
|
|
|
|
import { App } from './app';
|
|
import { EditorFrameStart, LensTopNavMenuEntryGenerator, VisualizeEditorContext } from '../types';
|
|
import { addHelpMenuToAppChrome } from '../help_menu_util';
|
|
import { LensPluginStartDependencies } from '../plugin';
|
|
import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common/constants';
|
|
import {
|
|
LensEmbeddableInput,
|
|
LensByReferenceInput,
|
|
LensByValueInput,
|
|
} from '../embeddable/embeddable';
|
|
import { LensAttributeService } from '../lens_attribute_service';
|
|
import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types';
|
|
import {
|
|
makeConfigureStore,
|
|
navigateAway,
|
|
LensRootStore,
|
|
loadInitial,
|
|
LensAppState,
|
|
LensState,
|
|
} from '../state_management';
|
|
import { getPreloadedState, setState } from '../state_management/lens_slice';
|
|
import { getLensInspectorService } from '../lens_inspector_service';
|
|
import {
|
|
LensAppLocator,
|
|
LENS_SHARE_STATE_ACTION,
|
|
MainHistoryLocationState,
|
|
} from '../../common/locator/locator';
|
|
import { SavedObjectIndexStore } from '../persistence';
|
|
|
|
function getInitialContext(history: AppMountParameters['history']) {
|
|
const historyLocationState = history.location.state as
|
|
| MainHistoryLocationState
|
|
| HistoryLocationState
|
|
| undefined;
|
|
|
|
if (historyLocationState) {
|
|
if (historyLocationState.type === LENS_SHARE_STATE_ACTION) {
|
|
return {
|
|
contextType: historyLocationState.type,
|
|
initialStateFromLocator: historyLocationState.payload,
|
|
};
|
|
}
|
|
// get state from location, used for navigating from Visualize/Discover to Lens
|
|
if ([ACTION_VISUALIZE_LENS_FIELD, ACTION_CONVERT_TO_LENS].includes(historyLocationState.type)) {
|
|
return {
|
|
contextType: historyLocationState.type,
|
|
initialContext: historyLocationState.payload,
|
|
originatingApp: historyLocationState.originatingApp,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function getLensServices(
|
|
coreStart: CoreStart,
|
|
startDependencies: LensPluginStartDependencies,
|
|
attributeService: LensAttributeService,
|
|
initialContext?: VisualizeFieldContext | VisualizeEditorContext,
|
|
locator?: LensAppLocator
|
|
): Promise<LensAppServices> {
|
|
const {
|
|
data,
|
|
inspector,
|
|
navigation,
|
|
embeddable,
|
|
savedObjectsTagging,
|
|
usageCollection,
|
|
fieldFormats,
|
|
spaces,
|
|
share,
|
|
unifiedSearch,
|
|
} = startDependencies;
|
|
|
|
const storage = new Storage(localStorage);
|
|
const stateTransfer = embeddable?.getStateTransfer();
|
|
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
|
|
|
|
return {
|
|
data,
|
|
storage,
|
|
inspector: getLensInspectorService(inspector),
|
|
navigation,
|
|
fieldFormats,
|
|
stateTransfer,
|
|
usageCollection,
|
|
savedObjectsTagging,
|
|
attributeService,
|
|
executionContext: coreStart.executionContext,
|
|
http: coreStart.http,
|
|
uiActions: startDependencies.uiActions,
|
|
chrome: coreStart.chrome,
|
|
overlays: coreStart.overlays,
|
|
uiSettings: coreStart.uiSettings,
|
|
settings: coreStart.settings,
|
|
application: coreStart.application,
|
|
notifications: coreStart.notifications,
|
|
savedObjectStore: new SavedObjectIndexStore(startDependencies.contentManagement.client),
|
|
presentationUtil: startDependencies.presentationUtil,
|
|
dataViewEditor: startDependencies.dataViewEditor,
|
|
dataViewFieldEditor: startDependencies.dataViewFieldEditor,
|
|
dashboard: startDependencies.dashboard,
|
|
charts: startDependencies.charts,
|
|
getOriginatingAppName: () => {
|
|
const originatingApp =
|
|
embeddableEditorIncomingState?.originatingApp ?? initialContext?.originatingApp;
|
|
return originatingApp ? stateTransfer?.getAppNameFromId(originatingApp) : undefined;
|
|
},
|
|
dataViews: startDependencies.dataViews,
|
|
// Temporarily required until the 'by value' paradigm is default.
|
|
dashboardFeatureFlag: startDependencies.dashboard.dashboardFeatureFlagConfig,
|
|
spaces,
|
|
share,
|
|
unifiedSearch,
|
|
docLinks: coreStart.docLinks,
|
|
locator,
|
|
};
|
|
}
|
|
|
|
export async function mountApp(
|
|
core: CoreSetup<LensPluginStartDependencies, void>,
|
|
params: AppMountParameters,
|
|
mountProps: {
|
|
createEditorFrame: EditorFrameStart['createInstance'];
|
|
attributeService: LensAttributeService;
|
|
getPresentationUtilContext: () => FC;
|
|
topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[];
|
|
locator?: LensAppLocator;
|
|
}
|
|
) {
|
|
const {
|
|
createEditorFrame,
|
|
attributeService,
|
|
getPresentationUtilContext,
|
|
topNavMenuEntryGenerators,
|
|
locator,
|
|
} = mountProps;
|
|
const [[coreStart, startDependencies], instance] = await Promise.all([
|
|
core.getStartServices(),
|
|
createEditorFrame(),
|
|
]);
|
|
|
|
const { contextType, initialContext, initialStateFromLocator, originatingApp } =
|
|
getInitialContext(params.history) || {};
|
|
|
|
const lensServices = await getLensServices(
|
|
coreStart,
|
|
startDependencies,
|
|
attributeService,
|
|
initialContext,
|
|
locator
|
|
);
|
|
|
|
const { stateTransfer, data, savedObjectStore } = lensServices;
|
|
|
|
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
|
|
|
|
addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks);
|
|
if (!lensServices.application.capabilities.visualize.save) {
|
|
coreStart.chrome.setBadge({
|
|
text: i18n.translate('xpack.lens.badge.readOnly.text', {
|
|
defaultMessage: 'Read only',
|
|
}),
|
|
tooltip: i18n.translate('xpack.lens.badge.readOnly.tooltip', {
|
|
defaultMessage: 'Unable to save visualizations to the library',
|
|
}),
|
|
iconType: 'glasses',
|
|
});
|
|
}
|
|
coreStart.chrome.docTitle.change(
|
|
i18n.translate('xpack.lens.pageTitle', { defaultMessage: 'Lens' })
|
|
);
|
|
|
|
const getInitialInput = (id?: string, editByValue?: boolean): LensEmbeddableInput | undefined => {
|
|
if (editByValue) {
|
|
return embeddableEditorIncomingState?.valueInput as LensByValueInput;
|
|
}
|
|
if (id) {
|
|
return { savedObjectId: id } as LensByReferenceInput;
|
|
}
|
|
};
|
|
|
|
const redirectTo = (history: History<unknown>, savedObjectId?: string) => {
|
|
if (!savedObjectId) {
|
|
history.push({ pathname: '/', search: history.location.search });
|
|
} else {
|
|
history.push({
|
|
pathname: `/edit/${savedObjectId}`,
|
|
search: history.location.search,
|
|
});
|
|
}
|
|
};
|
|
|
|
const redirectToOrigin = (props?: RedirectToOriginProps) => {
|
|
const contextOriginatingApp =
|
|
initialContext && 'originatingApp' in initialContext ? initialContext.originatingApp : null;
|
|
const mergedOriginatingApp =
|
|
embeddableEditorIncomingState?.originatingApp ?? contextOriginatingApp;
|
|
if (!mergedOriginatingApp) {
|
|
throw new Error('redirectToOrigin called without an originating app');
|
|
}
|
|
let embeddableId = embeddableEditorIncomingState?.embeddableId;
|
|
if (initialContext && 'embeddableId' in initialContext) {
|
|
embeddableId = initialContext.embeddableId;
|
|
}
|
|
if (stateTransfer && props?.input) {
|
|
const { input, isCopied } = props;
|
|
stateTransfer.navigateToWithEmbeddablePackage(mergedOriginatingApp, {
|
|
path: embeddableEditorIncomingState?.originatingPath,
|
|
state: {
|
|
embeddableId: isCopied ? undefined : embeddableId,
|
|
type: LENS_EMBEDDABLE_TYPE,
|
|
input,
|
|
searchSessionId: data.search.session.getSessionId(),
|
|
},
|
|
});
|
|
} else {
|
|
coreStart.application.navigateToApp(mergedOriginatingApp, {
|
|
path: embeddableEditorIncomingState?.originatingPath,
|
|
});
|
|
}
|
|
};
|
|
|
|
if (contextType === ACTION_VISUALIZE_LENS_FIELD && initialContext?.originatingApp) {
|
|
// remove originatingApp from context when visualizing a field in Lens
|
|
// so Lens does not try to return to the original app on Save
|
|
// see https://github.com/elastic/kibana/issues/128695
|
|
delete initialContext.originatingApp;
|
|
}
|
|
|
|
if (embeddableEditorIncomingState?.searchSessionId) {
|
|
data.search.session.continue(embeddableEditorIncomingState.searchSessionId);
|
|
}
|
|
|
|
const { datasourceMap, visualizationMap } = instance;
|
|
const storeDeps = {
|
|
lensServices,
|
|
datasourceMap,
|
|
visualizationMap,
|
|
embeddableEditorIncomingState,
|
|
initialContext,
|
|
initialStateFromLocator,
|
|
};
|
|
const lensStore: LensRootStore = makeConfigureStore(storeDeps, {
|
|
lens: getPreloadedState(storeDeps) as LensAppState,
|
|
} as unknown as PreloadedState<LensState>);
|
|
|
|
const EditorRenderer = React.memo(
|
|
(props: { id?: string; history: History<unknown>; editByValue?: boolean }) => {
|
|
const [editorState, setEditorState] = useState<'loading' | 'no_data' | 'data'>('loading');
|
|
|
|
useEffect(() => {
|
|
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
|
history: props.history,
|
|
useHash: lensServices.uiSettings.get('state:storeInSessionStorage'),
|
|
...withNotifyOnErrors(lensServices.notifications.toasts),
|
|
});
|
|
const { stop: stopSyncingQueryServiceStateWithUrl } = syncGlobalQueryStateWithUrl(
|
|
data.query,
|
|
kbnUrlStateStorage
|
|
);
|
|
|
|
return () => {
|
|
stopSyncingQueryServiceStateWithUrl();
|
|
};
|
|
}, [props.history]);
|
|
const redirectCallback = useCallback(
|
|
(id?: string) => {
|
|
redirectTo(props.history, id);
|
|
},
|
|
[props.history]
|
|
);
|
|
const initialInput = useMemo(() => {
|
|
return getInitialInput(props.id, props.editByValue);
|
|
}, [props.editByValue, props.id]);
|
|
|
|
const initCallback = useCallback(() => {
|
|
// Clear app-specific filters when navigating to Lens. Necessary because Lens
|
|
// can be loaded without a full page refresh.
|
|
// If the user navigates to Lens from Discover, or comes from a Lens share link we keep the filters
|
|
if (!initialContext) {
|
|
data.query.filterManager.setAppFilters([]);
|
|
}
|
|
// if user comes from a dashboard to convert a legacy viz to a Lens chart
|
|
// we clear up the dashboard filters and query
|
|
if (initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable) {
|
|
data.query.filterManager.setAppFilters([]);
|
|
data.query.queryString.clearQuery();
|
|
}
|
|
lensStore.dispatch(setState(getPreloadedState(storeDeps) as LensAppState));
|
|
lensStore.dispatch(loadInitial({ redirectCallback, initialInput, history: props.history }));
|
|
}, [initialInput, props.history, redirectCallback]);
|
|
useEffect(() => {
|
|
(async () => {
|
|
const hasUserDataView = await data.dataViews.hasData.hasUserDataView().catch(() => false);
|
|
if (!hasUserDataView) {
|
|
setEditorState('no_data');
|
|
return;
|
|
}
|
|
setEditorState('data');
|
|
initCallback();
|
|
})();
|
|
}, [initCallback, initialInput, props.history, redirectCallback]);
|
|
|
|
if (editorState === 'loading') {
|
|
return <EuiLoadingSpinner />;
|
|
}
|
|
|
|
if (editorState === 'no_data') {
|
|
const analyticsServices = {
|
|
coreStart,
|
|
dataViews: data.dataViews,
|
|
dataViewEditor: startDependencies.dataViewEditor,
|
|
};
|
|
return (
|
|
<AnalyticsNoDataPageKibanaProvider {...analyticsServices}>
|
|
<AnalyticsNoDataPage
|
|
onDataViewCreated={() => {
|
|
setEditorState('data');
|
|
initCallback();
|
|
}}
|
|
/>
|
|
</AnalyticsNoDataPageKibanaProvider>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Provider store={lensStore}>
|
|
<App
|
|
incomingState={embeddableEditorIncomingState}
|
|
editorFrame={instance}
|
|
initialInput={initialInput}
|
|
redirectTo={redirectCallback}
|
|
redirectToOrigin={redirectToOrigin}
|
|
onAppLeave={params.onAppLeave}
|
|
setHeaderActionMenu={params.setHeaderActionMenu}
|
|
history={props.history}
|
|
datasourceMap={datasourceMap}
|
|
visualizationMap={visualizationMap}
|
|
initialContext={initialContext}
|
|
contextOriginatingApp={originatingApp}
|
|
topNavMenuEntryGenerators={topNavMenuEntryGenerators}
|
|
theme$={core.theme.theme$}
|
|
coreStart={coreStart}
|
|
savedObjectStore={savedObjectStore}
|
|
/>
|
|
</Provider>
|
|
);
|
|
}
|
|
);
|
|
|
|
const EditorRoute = (
|
|
routeProps: RouteComponentProps<{ id?: string }> & { editByValue?: boolean }
|
|
) => {
|
|
return (
|
|
<EditorRenderer
|
|
id={routeProps.match.params.id}
|
|
history={routeProps.history}
|
|
editByValue={routeProps.editByValue}
|
|
/>
|
|
);
|
|
};
|
|
|
|
function NotFound() {
|
|
return <FormattedMessage id="xpack.lens.app404" defaultMessage="404 Not Found" />;
|
|
}
|
|
// dispatch synthetic hash change event to update hash history objects
|
|
// this is necessary because hash updates triggered by using popState won't trigger this event naturally.
|
|
const unlistenParentHistory = params.history.listen(() => {
|
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
});
|
|
|
|
params.element.classList.add('lnsAppWrapper');
|
|
|
|
const PresentationUtilContext = getPresentationUtilContext();
|
|
|
|
render(
|
|
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
|
|
<I18nProvider>
|
|
<KibanaContextProvider services={lensServices}>
|
|
<PresentationUtilContext>
|
|
<HashRouter>
|
|
<Switch>
|
|
<Route exact path="/edit/:id" component={EditorRoute} />
|
|
<Route
|
|
exact
|
|
path={`/${LENS_EDIT_BY_VALUE}`}
|
|
render={(routeProps) => <EditorRoute {...routeProps} editByValue />}
|
|
/>
|
|
<Route exact path="/" component={EditorRoute} />
|
|
<Route path="/" component={NotFound} />
|
|
</Switch>
|
|
</HashRouter>
|
|
</PresentationUtilContext>
|
|
</KibanaContextProvider>
|
|
</I18nProvider>
|
|
</KibanaThemeProvider>,
|
|
params.element
|
|
);
|
|
return () => {
|
|
data.search.session.clear();
|
|
unmountComponentAtNode(params.element);
|
|
lensServices.inspector.close();
|
|
unlistenParentHistory();
|
|
lensStore.dispatch(navigateAway());
|
|
stateTransfer.clearEditorState?.(APP_ID);
|
|
};
|
|
}
|