in x-pack/platform/plugins/shared/lens/public/app_plugin/app.tsx [63:548]
export function App({
history,
onAppLeave,
redirectTo,
editorFrame,
initialInput,
incomingState,
redirectToOrigin,
setHeaderActionMenu,
datasourceMap,
visualizationMap,
contextOriginatingApp,
topNavMenuEntryGenerators,
initialContext,
coreStart,
}: LensAppProps) {
const lensAppServices = useKibana<LensAppServices>().services;
const {
data,
dataViews,
uiActions,
uiSettings,
chrome,
inspector: lensInspector,
application,
savedObjectsTagging,
getOriginatingAppName,
spaces,
http,
notifications,
executionContext,
locator,
share,
serverless,
} = lensAppServices;
const saveAndExit = useRef<() => void>();
const dispatch = useLensDispatch();
const dispatchSetState = useCallback(
(state: Partial<LensAppState>) => dispatch(setState(state)),
[dispatch]
);
const {
persistedDoc,
sharingSavedObjectProps,
isLinkedToOriginatingApp,
searchSessionId,
datasourceStates,
isLoading,
isSaveable,
visualization,
annotationGroups,
} = useLensSelector((state) => state.lens);
const activeVisualization = visualization.activeId
? visualizationMap[visualization.activeId]
: undefined;
const selectorDependencies = useMemo(
() => ({
datasourceMap,
visualizationMap,
extractFilterReferences: data.query.filterManager.extract.bind(data.query.filterManager),
}),
[datasourceMap, visualizationMap, data.query.filterManager]
);
const currentDoc = useLensSelector((state) =>
selectSavedObjectFormat(state, selectorDependencies)
);
// Used to show a popover that guides the user towards changing the date range when no data is available.
const [indicateNoData, setIndicateNoData] = useState(false);
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
const [lastKnownDoc, setLastKnownDoc] = useState<LensDocument | undefined>(undefined);
const [initialDocFromContext, setInitialDocFromContext] = useState<LensDocument | undefined>(
undefined
);
const [shouldCloseAndSaveTextBasedQuery, setShouldCloseAndSaveTextBasedQuery] = useState(false);
const savedObjectId = initialInput?.savedObjectId;
const isFromLegacyEditorEmbeddable = isLegacyEditorEmbeddable(initialContext);
const legacyEditorAppName =
initialContext && 'originatingApp' in initialContext
? initialContext.originatingApp
: undefined;
const legacyEditorAppUrl =
initialContext && 'vizEditorOriginatingAppUrl' in initialContext
? initialContext.vizEditorOriginatingAppUrl
: undefined;
const initialContextIsEmbedded = Boolean(legacyEditorAppName);
useEffect(() => {
if (currentDoc) {
setLastKnownDoc(currentDoc);
}
}, [currentDoc]);
const showNoDataPopover = useCallback(() => {
setIndicateNoData(true);
}, [setIndicateNoData]);
useExecutionContext(executionContext, {
type: 'application',
id: savedObjectId || 'new', // TODO: this doesn't consider when lens is saved by value
page: 'editor',
});
useEffect(() => {
if (indicateNoData) {
setIndicateNoData(false);
}
}, [setIndicateNoData, indicateNoData, searchSessionId]);
const getIsByValueMode = useCallback(
() => Boolean(isLinkedToOriginatingApp && !savedObjectId),
[isLinkedToOriginatingApp, savedObjectId]
);
// Wrap the isEqual call to avoid to carry all the static references
// around all the time.
const isLensEqualWrapper = useCallback(
(refDoc: LensDocument | undefined) => {
return isLensEqual(
refDoc,
lastKnownDoc,
data.query.filterManager.inject.bind(data.query.filterManager),
datasourceMap,
visualizationMap,
annotationGroups
);
},
[annotationGroups, data.query.filterManager, datasourceMap, lastKnownDoc, visualizationMap]
);
useEffect(() => {
onAppLeave((actions) => {
if (
application.capabilities.visualize_v2?.save &&
!isLensEqualWrapper(persistedDoc) &&
(isSaveable || persistedDoc)
) {
return actions.confirm(
i18n.translate('xpack.lens.app.unsavedWorkMessage', {
defaultMessage: 'Leave with unsaved changes?',
}),
i18n.translate('xpack.lens.app.unsavedWorkTitle', {
defaultMessage: 'Unsaved changes',
}),
undefined,
i18n.translate('xpack.lens.app.unsavedWorkConfirmBtn', {
defaultMessage: 'Discard changes',
}),
'danger'
);
} else {
return actions.default();
}
});
}, [
onAppLeave,
lastKnownDoc,
isSaveable,
persistedDoc,
application.capabilities.visualize_v2?.save,
data.query.filterManager,
datasourceMap,
visualizationMap,
annotationGroups,
isLensEqualWrapper,
]);
const getLegacyUrlConflictCallout = useCallback(() => {
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
if (spaces && sharingSavedObjectProps?.outcome === 'conflict' && persistedDoc?.savedObjectId) {
// We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a
// callout with a warning for the user, and provide a way for them to navigate to the other object.
const currentObjectId = persistedDoc.savedObjectId;
const otherObjectId = sharingSavedObjectProps?.aliasTargetId!; // This is always defined if outcome === 'conflict'
const otherObjectPath = http.basePath.prepend(
`${getEditPath(otherObjectId)}${history.location.search}`
);
return spaces.ui.components.getLegacyUrlConflict({
objectNoun: i18n.translate('xpack.lens.appName', {
defaultMessage: 'Lens visualization',
}),
currentObjectId,
otherObjectId,
otherObjectPath,
});
}
return null;
}, [persistedDoc, sharingSavedObjectProps, spaces, http, history]);
// Sync Kibana breadcrumbs any time the saved document's title changes
useEffect(() => {
const isByValueMode = getIsByValueMode();
const currentDocTitle = getCurrentTitle(persistedDoc, isByValueMode, initialContext);
setBreadcrumbsTitle(
{ application, chrome, serverless },
{
isByValueMode,
currentDocTitle,
redirectToOrigin,
isFromLegacyEditor: Boolean(isLinkedToOriginatingApp || legacyEditorAppName),
originatingAppName: getOriginatingAppName(),
}
);
}, [
getOriginatingAppName,
redirectToOrigin,
getIsByValueMode,
application,
chrome,
isLinkedToOriginatingApp,
persistedDoc,
isFromLegacyEditorEmbeddable,
legacyEditorAppName,
serverless,
initialContext,
]);
const switchDatasource = useCallback(() => {
if (saveAndExit && saveAndExit.current) {
saveAndExit.current();
}
}, []);
const runSave = useCallback(
async (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
dispatch(applyChanges());
const prevVisState =
persistedDoc?.visualizationType === visualization.activeId
? persistedDoc?.state.visualization
: undefined;
const telemetryEvents = activeVisualization?.getTelemetryEventsOnSave?.(
visualization.state,
prevVisState
);
if (telemetryEvents && telemetryEvents.length) {
trackSaveUiCounterEvents(telemetryEvents);
}
try {
const newState = await runSaveLensVisualization(
{
lastKnownDoc,
savedObjectsTagging,
initialInput,
redirectToOrigin,
persistedDoc,
onAppLeave,
redirectTo,
switchDatasource,
originatingApp: incomingState?.originatingApp,
textBasedLanguageSave: shouldCloseAndSaveTextBasedQuery,
...lensAppServices,
},
saveProps,
options
);
if (newState) {
dispatchSetState(newState);
setIsSaveModalVisible(false);
setShouldCloseAndSaveTextBasedQuery(false);
}
} catch (e) {
// error is handled inside the modal
// so ignoring it here
}
},
[
visualization.activeId,
visualization.state,
activeVisualization,
dispatch,
lastKnownDoc,
savedObjectsTagging,
initialInput,
redirectToOrigin,
persistedDoc,
onAppLeave,
redirectTo,
switchDatasource,
incomingState?.originatingApp,
shouldCloseAndSaveTextBasedQuery,
lensAppServices,
dispatchSetState,
]
);
// keeping the initial doc state created by the context
useEffect(() => {
if (lastKnownDoc && !initialDocFromContext) {
setInitialDocFromContext(lastKnownDoc);
}
}, [lastKnownDoc, initialDocFromContext]);
const {
shouldShowGoBackToVizEditorModal,
goBackToOriginatingApp,
navigateToVizEditor,
closeGoBackToVizEditorModal,
} = useNavigateBackToApp({
application,
onAppLeave,
legacyEditorAppName,
legacyEditorAppUrl,
initialDocFromContext,
persistedDoc,
isLensEqual: isLensEqualWrapper,
});
const indexPatternService = useMemo(
() =>
createIndexPatternService({
dataViews,
uiActions,
core: { http, notifications, uiSettings },
contextDataViewSpec: (initialContext as VisualizeFieldContext | undefined)?.dataViewSpec,
updateIndexPatterns: (newIndexPatternsState, options) => {
dispatch(updateIndexPatterns(newIndexPatternsState));
if (options?.applyImmediately) {
dispatch(applyChanges());
}
},
replaceIndexPattern: (newIndexPattern, oldId, options) => {
dispatch(replaceIndexpattern({ newIndexPattern, oldId }));
if (options?.applyImmediately) {
dispatch(applyChanges());
}
},
}),
[dataViews, uiActions, http, notifications, uiSettings, initialContext, dispatch]
);
const shortUrlService = useShortUrlService(locator, share);
const isManaged = useLensSelector(selectIsManaged);
const returnToOriginSwitchLabelForContext =
isFromLegacyEditorEmbeddable && !persistedDoc
? i18n.translate('xpack.lens.app.replacePanel', {
defaultMessage: 'Replace panel on {originatingApp}',
values: {
originatingApp: initialContext?.originatingApp,
},
})
: undefined;
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
const framePublicAPI = useLensSelector((state) => selectFramePublicAPI(state, datasourceMap));
const { getUserMessages, addUserMessages } = useApplicationUserMessages({
coreStart,
framePublicAPI,
activeDatasourceId,
datasourceState:
activeDatasourceId && datasourceStates[activeDatasourceId]
? datasourceStates[activeDatasourceId]
: null,
datasource:
activeDatasourceId && datasourceMap[activeDatasourceId]
? datasourceMap[activeDatasourceId]
: null,
dispatch,
visualization: activeVisualization,
visualizationType: visualization.activeId,
visualizationState: visualization,
});
return (
<>
<div
data-test-subj="lnsApp"
className="lnsApp"
role="main"
css={css`
flex: 1 1 auto;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
`}
>
<LensTopNavMenu
initialInput={initialInput}
redirectToOrigin={redirectToOrigin}
getIsByValueMode={getIsByValueMode}
onAppLeave={onAppLeave}
runSave={runSave}
setIsSaveModalVisible={setIsSaveModalVisible}
setHeaderActionMenu={setHeaderActionMenu}
indicateNoData={indicateNoData}
datasourceMap={datasourceMap}
visualizationMap={visualizationMap}
title={persistedDoc?.title}
lensInspector={lensInspector}
currentDoc={currentDoc}
isCurrentStateDirty={!isLensEqualWrapper(persistedDoc)}
goBackToOriginatingApp={goBackToOriginatingApp}
contextOriginatingApp={contextOriginatingApp}
initialContextIsEmbedded={initialContextIsEmbedded}
topNavMenuEntryGenerators={topNavMenuEntryGenerators}
initialContext={initialContext}
indexPatternService={indexPatternService}
getUserMessages={getUserMessages}
shortUrlService={shortUrlService}
startServices={coreStart}
/>
{getLegacyUrlConflictCallout()}
{(!isLoading || persistedDoc) && (
<MemoizedEditorFrameWrapper
editorFrame={editorFrame}
showNoDataPopover={showNoDataPopover}
lensInspector={lensInspector}
indexPatternService={indexPatternService}
getUserMessages={getUserMessages}
addUserMessages={addUserMessages}
/>
)}
</div>
{isSaveModalVisible && (
<SaveModalContainer
lensServices={lensAppServices}
originatingApp={
isLinkedToOriginatingApp
? incomingState?.originatingApp ?? initialContext?.originatingApp
: undefined
}
isSaveable={isSaveable}
runSave={runSave}
onClose={() => {
setIsSaveModalVisible(false);
}}
getAppNameFromId={() => getOriginatingAppName()}
lastKnownDoc={lastKnownDoc}
onAppLeave={onAppLeave}
persistedDoc={persistedDoc}
initialInput={initialInput}
redirectTo={redirectTo}
redirectToOrigin={redirectToOrigin}
managed={isManaged}
initialContext={initialContext}
returnToOriginSwitchLabel={
returnToOriginSwitchLabelForContext ??
(getIsByValueMode() && initialInput
? i18n.translate('xpack.lens.app.updatePanel', {
defaultMessage: 'Update panel on {originatingAppName}',
values: { originatingAppName: getOriginatingAppName() },
})
: undefined)
}
/>
)}
{shouldShowGoBackToVizEditorModal && (
<EuiConfirmModal
maxWidth={600}
title={i18n.translate('xpack.lens.app.unsavedWorkTitle', {
defaultMessage: 'Unsaved changes',
})}
onCancel={closeGoBackToVizEditorModal}
onConfirm={navigateToVizEditor}
cancelButtonText={i18n.translate('xpack.lens.app.goBackModalCancelBtn', {
defaultMessage: 'Cancel',
})}
confirmButtonText={i18n.translate('xpack.lens.app.unsavedWorkConfirmBtn', {
defaultMessage: 'Discard changes',
})}
buttonColor="danger"
defaultFocusedButton="confirm"
data-test-subj="lnsApp_discardChangesModalOrigin"
>
{i18n.translate('xpack.lens.app.goBackModalMessage', {
defaultMessage:
'Your changes here won’t work with your original {contextOriginatingApp} visualization. Leave with unsaved changes and return to {contextOriginatingApp}?',
values: { contextOriginatingApp },
})}
</EuiConfirmModal>
)}
</>
);
}