Skip to content

Commit

Permalink
[Ingest Pipelines] Add unsaved changes prompt (#183699)
Browse files Browse the repository at this point in the history
  • Loading branch information
sabarasaba committed Jun 7, 2024
1 parent 1ec9412 commit 1e197cf
Show file tree
Hide file tree
Showing 23 changed files with 421 additions and 6 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ examples/unified_field_list_examples @elastic/kibana-data-discovery
src/plugins/unified_histogram @elastic/kibana-data-discovery
src/plugins/unified_search @elastic/kibana-visualizations
packages/kbn-unsaved-changes-badge @elastic/kibana-data-discovery
packages/kbn-unsaved-changes-prompt @elastic/kibana-management
x-pack/plugins/upgrade_assistant @elastic/kibana-management
x-pack/plugins/observability_solution/uptime @elastic/obs-ux-infra_services-team
x-pack/plugins/drilldowns/url_drilldown @elastic/appex-sharedux
Expand Down
1 change: 1 addition & 0 deletions .i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
"unifiedHistogram": "src/plugins/unified_histogram",
"unifiedDataTable": "packages/kbn-unified-data-table",
"unsavedChangesBadge": "packages/kbn-unsaved-changes-badge",
"unsavedChangesPrompt": "packages/kbn-unsaved-changes-prompt",
"managedContentBadge": "packages/kbn-managed-content-badge"
},
"translations": []
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@
"@kbn/unified-histogram-plugin": "link:src/plugins/unified_histogram",
"@kbn/unified-search-plugin": "link:src/plugins/unified_search",
"@kbn/unsaved-changes-badge": "link:packages/kbn-unsaved-changes-badge",
"@kbn/unsaved-changes-prompt": "link:packages/kbn-unsaved-changes-prompt",
"@kbn/upgrade-assistant-plugin": "link:x-pack/plugins/upgrade_assistant",
"@kbn/uptime-plugin": "link:x-pack/plugins/observability_solution/uptime",
"@kbn/url-drilldown-plugin": "link:x-pack/plugins/drilldowns/url_drilldown",
Expand Down
30 changes: 30 additions & 0 deletions packages/kbn-unsaved-changes-prompt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# @kbn/unsaved-changes-prompt

The useUnsavedChangesPrompt function is a custom React hook that prompts users with
a confirmation dialog when they try to leave a page with unsaved changes. It blocks
navigation and shows a dialog using the provided openConfirm function. If the user
confirms, it navigates away; otherwise, it cancels the navigation, ensuring unsaved
changes are not lost.


```typescript
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';

export const SampleForm = ({ servicesForUnsavedChangesPrompt }) => {
const { form } = useForm();
const isFormDirty = useFormIsModified({ form });

useUnsavedChangesPrompt({
hasUnsavedChanges: isFormDirty,
...servicesForUnsavedChangesPrompt,
});

return (
<>
<Form form={form}>
....
</Form>
</>
);
};
```
9 changes: 9 additions & 0 deletions packages/kbn-unsaved-changes-prompt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export { useUnsavedChangesPrompt } from './src/unsaved_changes_prompt';
13 changes: 13 additions & 0 deletions packages/kbn-unsaved-changes-prompt/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-unsaved-changes-prompt'],
};
5 changes: 5 additions & 0 deletions packages/kbn-unsaved-changes-prompt/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/unsaved-changes-prompt",
"owner": "@elastic/kibana-management"
}
6 changes: 6 additions & 0 deletions packages/kbn-unsaved-changes-prompt/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/unsaved-changes-prompt",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export { useUnsavedChangesPrompt } from './unsaved_changes_prompt';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { createMemoryHistory } from 'history';
import { renderHook, act } from '@testing-library/react-hooks';

import { coreMock } from '@kbn/core/public/mocks';
import { CoreScopedHistory } from '@kbn/core/public';

import { useUnsavedChangesPrompt } from './unsaved_changes_prompt';

const basePath = '/mock';
const memoryHistory = createMemoryHistory({ initialEntries: [basePath] });
const history = new CoreScopedHistory(memoryHistory, basePath);
const coreStart = coreMock.createStart();
const navigateToUrl = jest.fn().mockImplementation(async (url) => {
history.push(url);
});

describe('useUnsavedChangesPrompt', () => {
it('should not block if not edited', () => {
renderHook(() =>
useUnsavedChangesPrompt({
hasUnsavedChanges: false,
http: coreStart.http,
openConfirm: coreStart.overlays.openConfirm,
history,
navigateToUrl,
})
);

act(() => history.push('/test'));

expect(history.location.pathname).toBe('/test');
expect(history.location.search).toBe('');
expect(coreStart.overlays.openConfirm).not.toBeCalled();
});

it('should block if edited', async () => {
coreStart.overlays.openConfirm.mockResolvedValue(true);

renderHook(() =>
useUnsavedChangesPrompt({
hasUnsavedChanges: true,
http: coreStart.http,
openConfirm: coreStart.overlays.openConfirm,
history,
navigateToUrl,
})
);

act(() => history.push('/test'));

// needed because we have an async useEffect
await act(() => new Promise((resolve) => resolve()));

expect(navigateToUrl).toBeCalledWith('/mock/test', expect.anything());
expect(coreStart.overlays.openConfirm).toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 { useEffect } from 'react';
import { i18n } from '@kbn/i18n';

import { ApplicationStart, ScopedHistory, OverlayStart, HttpStart } from '@kbn/core/public';

const DEFAULT_BODY_TEXT = i18n.translate('unsavedChangesPrompt.defaultModalText', {
defaultMessage: `The data will be lost if you leave this page without saving the changes.`,
});

const DEFAULT_TITLE_TEXT = i18n.translate('unsavedChangesPrompt.defaultModalTitle', {
defaultMessage: 'Discard unsaved changes?',
});

const DEFAULT_CANCEL_BUTTON = i18n.translate('unsavedChangesPrompt.defaultModalCancel', {
defaultMessage: 'Keep editing',
});

const DEFAULT_CONFIRM_BUTTON = i18n.translate('unsavedChangesPrompt.defaultModalConfirm', {
defaultMessage: 'Leave page',
});

interface Props {
hasUnsavedChanges: boolean;
http: HttpStart;
openConfirm: OverlayStart['openConfirm'];
history: ScopedHistory;
navigateToUrl: ApplicationStart['navigateToUrl'];
titleText?: string;
messageText?: string;
cancelButtonText?: string;
confirmButtonText?: string;
}

export const useUnsavedChangesPrompt = ({
hasUnsavedChanges,
openConfirm,
history,
http,
navigateToUrl,
// Provide overrides for confirm dialog
messageText = DEFAULT_BODY_TEXT,
titleText = DEFAULT_TITLE_TEXT,
confirmButtonText = DEFAULT_CONFIRM_BUTTON,
cancelButtonText = DEFAULT_CANCEL_BUTTON,
}: Props) => {
useEffect(() => {
if (!hasUnsavedChanges) {
return;
}

const unblock = history.block((state) => {
async function confirmAsync() {
const confirmResponse = await openConfirm(messageText, {
title: titleText,
cancelButtonText,
confirmButtonText,
'data-test-subj': 'navigationBlockConfirmModal',
});

if (confirmResponse) {
// Compute the URL we want to redirect to
const url = http.basePath.prepend(state.pathname) + state.hash + state.search;
// Unload history block
unblock();
// Navigate away
navigateToUrl(url, {
state: state.state,
});
}
}

confirmAsync();
return false;
});

return unblock;
}, [
history,
hasUnsavedChanges,
openConfirm,
navigateToUrl,
http.basePath,
titleText,
cancelButtonText,
confirmButtonText,
messageText,
]);
};
14 changes: 14 additions & 0 deletions packages/kbn-unsaved-changes-prompt/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["*.ts", "src/**/*"],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
"@kbn/core"
]
}
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,8 @@
"@kbn/unified-search-plugin/*": ["src/plugins/unified_search/*"],
"@kbn/unsaved-changes-badge": ["packages/kbn-unsaved-changes-badge"],
"@kbn/unsaved-changes-badge/*": ["packages/kbn-unsaved-changes-badge/*"],
"@kbn/unsaved-changes-prompt": ["packages/kbn-unsaved-changes-prompt"],
"@kbn/unsaved-changes-prompt/*": ["packages/kbn-unsaved-changes-prompt/*"],
"@kbn/upgrade-assistant-plugin": ["x-pack/plugins/upgrade_assistant"],
"@kbn/upgrade-assistant-plugin/*": ["x-pack/plugins/upgrade_assistant/*"],
"@kbn/uptime-plugin": ["x-pack/plugins/observability_solution/uptime"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
scopedHistoryMock,
uiSettingsServiceMock,
applicationServiceMock,
overlayServiceMock,
} from '@kbn/core/public/mocks';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';

import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';

Expand Down Expand Up @@ -66,6 +68,8 @@ const appServices = {
share: {
url: new MockUrlService(),
},
overlays: overlayServiceMock.createStartContract(),
http: httpServiceMock.createStartContract({ basePath: '/mock' }),
};

export const setupEnvironment = () => {
Expand Down
Loading

0 comments on commit 1e197cf

Please sign in to comment.