Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { i18n } from '@kbn/i18n';
import { AppMountParameters } from 'kibana/public';
import { ViewMode } from '../../embeddable_plugin';
import { TopNavIds } from './top_nav_ids';
import { NavAction } from '../../types';
Expand All @@ -31,7 +32,8 @@ import { NavAction } from '../../types';
export function getTopNavConfig(
dashboardMode: ViewMode,
actions: { [key: string]: NavAction },
hideWriteControls: boolean
hideWriteControls: boolean,
onAppLeave?: AppMountParameters['onAppLeave']
) {
switch (dashboardMode) {
case ViewMode.VIEW:
Expand Down
11 changes: 8 additions & 3 deletions src/plugins/visualize/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import './app.scss';
import React, { useEffect } from 'react';
import { Route, Switch, useLocation } from 'react-router-dom';

import { AppMountParameters } from 'kibana/public';
import { syncQueryStateWithUrl } from '../../../data/public';
import { useKibana } from '../../../kibana_react/public';
import { VisualizeServices } from './types';
Expand All @@ -32,7 +33,11 @@ import {
} from './components';
import { VisualizeConstants } from './visualize_constants';

export const VisualizeApp = () => {
export interface VisualizeAppProps {
onAppLeave: AppMountParameters['onAppLeave'];
}

export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => {
const {
services: {
data: { query },
Expand All @@ -54,10 +59,10 @@ export const VisualizeApp = () => {
return (
<Switch>
<Route exact path={`${VisualizeConstants.EDIT_BY_VALUE_PATH}`}>
<VisualizeByValueEditor />
<VisualizeByValueEditor onAppLeave={onAppLeave} />
</Route>
<Route path={[VisualizeConstants.CREATE_PATH, `${VisualizeConstants.EDIT_PATH}/:id`]}>
<VisualizeEditor />
<VisualizeEditor onAppLeave={onAppLeave} />
</Route>
<Route
exact
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ import {
} from '../utils';
import { VisualizeServices } from '../types';
import { VisualizeEditorCommon } from './visualize_editor_common';
import { VisualizeAppProps } from '../app';

export const VisualizeByValueEditor = () => {
export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => {
const [originatingApp, setOriginatingApp] = useState<string>();
const { services } = useKibana<VisualizeServices>();
const [eventEmitter] = useState(new EventEmitter());
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(true);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [embeddableId, setEmbeddableId] = useState<string>();
const [valueInput, setValueInput] = useState<VisualizeInput>();

Expand Down Expand Up @@ -100,6 +101,7 @@ export const VisualizeByValueEditor = () => {
setHasUnsavedChanges={setHasUnsavedChanges}
visEditorRef={visEditorRef}
embeddableId={embeddableId}
onAppLeave={onAppLeave}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ import {
} from '../utils';
import { VisualizeServices } from '../types';
import { VisualizeEditorCommon } from './visualize_editor_common';
import { VisualizeAppProps } from '../app';

export const VisualizeEditor = () => {
export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => {
const { id: visualizationIdFromUrl } = useParams<{ id: string }>();
const [originatingApp, setOriginatingApp] = useState<string>();
const { services } = useKibana<VisualizeServices>();
Expand Down Expand Up @@ -91,6 +92,7 @@ export const VisualizeEditor = () => {
visualizationIdFromUrl={visualizationIdFromUrl}
setHasUnsavedChanges={setHasUnsavedChanges}
visEditorRef={visEditorRef}
onAppLeave={onAppLeave}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import './visualize_editor.scss';
import React, { RefObject } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiScreenReaderOnly } from '@elastic/eui';
import { AppMountParameters } from 'kibana/public';
import { VisualizeTopNav } from './visualize_top_nav';
import { ExperimentalVisInfo } from './experimental_vis_info';
import {
Expand All @@ -38,6 +39,7 @@ interface VisualizeEditorCommonProps {
setHasUnsavedChanges: (value: boolean) => void;
hasUnappliedChanges: boolean;
isEmbeddableRendered: boolean;
onAppLeave: AppMountParameters['onAppLeave'];
visEditorRef: RefObject<HTMLDivElement>;
originatingApp?: string;
setOriginatingApp?: (originatingApp: string | undefined) => void;
Expand All @@ -54,6 +56,7 @@ export const VisualizeEditorCommon = ({
setHasUnsavedChanges,
hasUnappliedChanges,
isEmbeddableRendered,
onAppLeave,
originatingApp,
setOriginatingApp,
visualizationIdFromUrl,
Expand All @@ -76,6 +79,7 @@ export const VisualizeEditorCommon = ({
stateContainer={appState}
visualizationIdFromUrl={visualizationIdFromUrl}
embeddableId={embeddableId}
onAppLeave={onAppLeave}
/>
)}
{visInstance?.vis?.type?.stage === 'experimental' && <ExperimentalVisInfo />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';

import { OverlayRef } from 'kibana/public';
import { AppMountParameters, OverlayRef } from 'kibana/public';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../kibana_react/public';
import {
VisualizeServices,
Expand All @@ -43,6 +45,7 @@ interface VisualizeTopNavProps {
stateContainer: VisualizeAppStateContainer;
visualizationIdFromUrl?: string;
embeddableId?: string;
onAppLeave: AppMountParameters['onAppLeave'];
}

const TopNav = ({
Expand All @@ -58,10 +61,11 @@ const TopNav = ({
stateContainer,
visualizationIdFromUrl,
embeddableId,
onAppLeave,
}: VisualizeTopNavProps) => {
const { services } = useKibana<VisualizeServices>();
const { TopNavMenu } = services.navigation.ui;
const { setHeaderActionMenu } = services;
const { setHeaderActionMenu, visualizeCapabilities } = services;
const { embeddableHandler, vis } = visInstance;
const [inspectorSession, setInspectorSession] = useState<OverlayRef>();
const openInspector = useCallback(() => {
Expand Down Expand Up @@ -93,6 +97,7 @@ const TopNav = ({
visualizationIdFromUrl,
stateTransfer,
embeddableId,
onAppLeave,
},
services
);
Expand All @@ -111,6 +116,7 @@ const TopNav = ({
services,
embeddableId,
stateTransfer,
onAppLeave,
]);
const [indexPattern, setIndexPattern] = useState(vis.data.indexPattern);
const showDatePicker = () => {
Expand All @@ -131,6 +137,33 @@ const TopNav = ({
};
}, [inspectorSession]);

useEffect(() => {
onAppLeave((actions) => {
// Confirm when the user has made any changes to an existing visualizations
// or when the user has configured something without saving
if (
((originatingApp && originatingApp === 'dashboards') || originatingApp === 'canvas') &&
(hasUnappliedChanges || hasUnsavedChanges)
) {
return actions.confirm(
i18n.translate('visualize.confirmModal.confirmTextDescription', {
defaultMessage: 'Leave Visualize editor with unsaved changes?',
}),
i18n.translate('visualize.confirmModal.title', {
defaultMessage: 'Unsaved changes',
})
);
}
return actions.default();
});
}, [
onAppLeave,
hasUnappliedChanges,
hasUnsavedChanges,
visualizeCapabilities.save,
originatingApp,
]);

useEffect(() => {
if (!vis.data.indexPattern) {
services.data.indexPatterns.getDefault().then((index) => {
Expand Down
7 changes: 5 additions & 2 deletions src/plugins/visualize/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import { VisualizeApp } from './app';
import { VisualizeServices } from './types';
import { addHelpMenuToAppChrome, addBadgeToAppChrome } from './utils';

export const renderApp = ({ element }: AppMountParameters, services: VisualizeServices) => {
export const renderApp = (
{ element, onAppLeave }: AppMountParameters,
services: VisualizeServices
) => {
// add help link to visualize docs into app chrome menu
addHelpMenuToAppChrome(services.chrome, services.docLinks);
// add readonly badge if saving restricted
Expand All @@ -39,7 +42,7 @@ export const renderApp = ({ element }: AppMountParameters, services: VisualizeSe
<Router history={services.history}>
<KibanaContextProvider services={services}>
<services.i18n.Context>
<VisualizeApp />
<VisualizeApp onAppLeave={onAppLeave} />
</services.i18n.Context>
</KibanaContextProvider>
</Router>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';

import { TopNavMenuData } from 'src/plugins/navigation/public';
import { AppMountParameters } from 'kibana/public';
import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public';
import {
showSaveModal,
Expand Down Expand Up @@ -51,6 +52,7 @@ interface TopNavConfigParams {
visualizationIdFromUrl?: string;
stateTransfer: EmbeddableStateTransfer;
embeddableId?: string;
onAppLeave: AppMountParameters['onAppLeave'];
}

export const getTopNavConfig = (
Expand All @@ -66,6 +68,7 @@ export const getTopNavConfig = (
visualizationIdFromUrl,
stateTransfer,
embeddableId,
onAppLeave,
}: TopNavConfigParams,
{
application,
Expand Down Expand Up @@ -174,6 +177,12 @@ export const getTopNavConfig = (
stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state });
};

const navigateToOriginatingApp = () => {
if (originatingApp) {
application.navigateToApp(originatingApp);
}
};

const topNavMenu: TopNavMenuData[] = [
{
id: 'inspector',
Expand Down Expand Up @@ -225,6 +234,31 @@ export const getTopNavConfig = (
// disable the Share button if no action specified
disableButton: !share || !!embeddableId,
},
...(originatingApp === 'dashboards' || originatingApp === 'canvas'
? [
{
id: 'cancel',
label: i18n.translate('visualize.topNavMenu.cancelButtonLabel', {
defaultMessage: 'Cancel',
}),
emphasize: false,
description: i18n.translate('visualize.topNavMenu.cancelButtonAriaLabel', {
defaultMessage: 'Return to the last app without saving changes',
}),
testId: 'visualizeCancelAndReturnButton',
tooltip() {
if (hasUnappliedChanges || hasUnsavedChanges) {
return i18n.translate('visualize.topNavMenu.cancelAndReturnButtonTooltip', {
defaultMessage: 'Discard your changes before finishing',
});
}
},
run: async () => {
return navigateToOriginatingApp();
},
},
]
: []),
...(visualizeCapabilities.save && !embeddableId
? [
{
Expand Down Expand Up @@ -297,6 +331,9 @@ export const getTopNavConfig = (
/>
);
const isSaveAsButton = anchorElement.classList.contains('saveAsButton');
onAppLeave((actions) => {
return actions.default();
});
if (
originatingApp === 'dashboards' &&
dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables &&
Expand Down Expand Up @@ -342,6 +379,9 @@ export const getTopNavConfig = (
confirmOverwrite: false,
returnToOrigin: true,
};
onAppLeave((actions) => {
return actions.default();
});
if (
originatingApp === 'dashboards' &&
dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables &&
Expand Down
Loading