Skip to content

Commit

Permalink
[Visualize Editor] Add cancel button when navigating from Dashboard (#…
Browse files Browse the repository at this point in the history
…77608)

* Add cancel button in the visualize editor

* Fixing i18n namespace

* Always show cancel button

* Always show cancel button

* Adding a fucntional test

* Show confirm dialog only if there are unsaved changes

* Show confirm modal only if there are changes

* Add onAppLeave handler and ditch confirmModal

* Fix functional test

* Only use onAppLeave if coming from dashboard/canvas

* Add actions.default to onSave and onSaveAndReturn

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
Maja Grubic and elasticmachine authored Oct 6, 2020
1 parent 68c6aa7 commit 0e89431
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 11 deletions.
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

0 comments on commit 0e89431

Please sign in to comment.