Fixes Save Modal promise chain onSave#233933
Conversation
|
Pinging @elastic/kibana-visualizations (Team:Visualizations) |
davismcphee
left a comment
There was a problem hiding this comment.
Code-only review, Data Discovery changes LGTM 👍 Thanks for fixing it! ...Although I'm not sure how I feel about my secret trick to saving multiple SOs being prevented now 🤔
...atform/plugins/shared/discover/public/application/main/components/top_nav/on_save_search.tsx
Outdated
Show resolved
Hide resolved
|
Pinging @elastic/obs-ux-management-team (Team:obs-ux-management) |
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]Module Count
Public APIs missing comments
Async chunks
Page load bundle
Unknown metric groupsAPI count
async chunk count
ESLint disabled line counts
References to deprecated APIs
Total ESLint disabled count
History
|
|
Starting backport for target branches: 8.19, 9.1 |
💔 All backports failed
Manual backportTo create the backport manually run: Questions ?Please refer to the Backport tool documentation |
## Summary This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances. ### Demo #### Before https://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4 #### After https://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531 Fixes elastic#233906 ## Details When attempting to save any SO using the [`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95), the logic was attempting to `await` the call to `onSave` and block any additional calls. This was working correctly to set the `isLoading` state, but the `onSave` callback is not expecting a `Promise` to be returned so it has no affect and the `onSave` is immediately resolved, unsetting the loading state and thus allowing multiple calls to save. So the issue is that `onSave` should expect `() => Promise<void>` not `() => void`. A crude fix to this could be to throttle the calls to `onSave` and preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem. So the chain of `onSave` is broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from the `onSave` prop all the way up to the root consumer, which is typically calling an async function. Further complicating things.... there is this dreadful, I mean deprecated, `showSaveModal` function that wraps a modal component (almost like a HOC) and calls `props.onSave` of the passed component to determine the success state of the call to `onSave`. https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37 Here's a rough example... ```tsx import { showSaveModal } from "@kbn/saved-objects-plugin/public"; export function openSaveModal() { // ... showSaveModal( <SavedObjectSaveModal ... onSave={async () => { const response = await doTheSave(); if(response.success) { return { id: response.id }; } else { return { error: 'failed to save' } } }} /> ); } ``` The strange thing is that the `SavedObjectSaveModal.onSave` doesn't need the `SaveResult` to be returned and these modal components are used in various places but only some are passed to `showSaveModal`. So I cleaned up this code as well to expect the `SaveResult` when using `showSaveModal` and `void` otherwise. ### Checklist - [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 - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ## Release Notes Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others. --------- Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co> (cherry picked from commit 8415158) # Conflicts: # src/platform/plugins/private/links/public/content_management/save_to_library.tsx # src/platform/plugins/shared/dashboard/public/dashboard_actions/library_add_action.tsx # src/platform/plugins/shared/discover/public/application/main/components/top_nav/save_discover_session/save_modal.tsx # src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx # src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx # src/platform/plugins/shared/visualizations/public/legacy/embeddable/attribute_service.tsx # src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_top_nav_config.tsx # x-pack/platform/plugins/private/graph/public/components/save_modal.tsx # x-pack/platform/plugins/shared/maps/public/routes/map_page/top_nav_config.tsx
💚 All backports created successfully
Note: Successful backport PRs will be merged automatically after passing CI. Questions ?Please refer to the Backport tool documentation |
## Summary This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances. ### Demo #### Before https://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4 #### After https://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531 Fixes elastic#233906 ## Details When attempting to save any SO using the [`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95), the logic was attempting to `await` the call to `onSave` and block any additional calls. This was working correctly to set the `isLoading` state, but the `onSave` callback is not expecting a `Promise` to be returned so it has no affect and the `onSave` is immediately resolved, unsetting the loading state and thus allowing multiple calls to save. So the issue is that `onSave` should expect `() => Promise<void>` not `() => void`. A crude fix to this could be to throttle the calls to `onSave` and preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem. So the chain of `onSave` is broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from the `onSave` prop all the way up to the root consumer, which is typically calling an async function. Further complicating things.... there is this dreadful, I mean deprecated, `showSaveModal` function that wraps a modal component (almost like a HOC) and calls `props.onSave` of the passed component to determine the success state of the call to `onSave`. https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37 Here's a rough example... ```tsx import { showSaveModal } from "@kbn/saved-objects-plugin/public"; export function openSaveModal() { // ... showSaveModal( <SavedObjectSaveModal ... onSave={async () => { const response = await doTheSave(); if(response.success) { return { id: response.id }; } else { return { error: 'failed to save' } } }} /> ); } ``` The strange thing is that the `SavedObjectSaveModal.onSave` doesn't need the `SaveResult` to be returned and these modal components are used in various places but only some are passed to `showSaveModal`. So I cleaned up this code as well to expect the `SaveResult` when using `showSaveModal` and `void` otherwise. ### Checklist - [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 - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ## Release Notes Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others. --------- Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co> (cherry picked from commit 8415158) # Conflicts: # src/platform/plugins/private/links/public/content_management/save_to_library.tsx # src/platform/plugins/shared/dashboard/public/dashboard_actions/library_add_action.tsx # src/platform/plugins/shared/discover/public/application/main/components/top_nav/save_discover_session/save_modal.tsx # src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.test.tsx # src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx # src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx # src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx # src/platform/plugins/shared/visualizations/public/legacy/embeddable/attribute_service.tsx # src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_top_nav_config.tsx # x-pack/platform/plugins/private/graph/public/components/save_modal.tsx # x-pack/platform/plugins/shared/maps/public/routes/map_page/top_nav_config.tsx
## Summary This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances. ### Demo #### Before https://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4 #### After https://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531 Fixes elastic#233906 ## Details When attempting to save any SO using the [`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95), the logic was attempting to `await` the call to `onSave` and block any additional calls. This was working correctly to set the `isLoading` state, but the `onSave` callback is not expecting a `Promise` to be returned so it has no affect and the `onSave` is immediately resolved, unsetting the loading state and thus allowing multiple calls to save. So the issue is that `onSave` should expect `() => Promise<void>` not `() => void`. A crude fix to this could be to throttle the calls to `onSave` and preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem. So the chain of `onSave` is broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from the `onSave` prop all the way up to the root consumer, which is typically calling an async function. Further complicating things.... there is this dreadful, I mean deprecated, `showSaveModal` function that wraps a modal component (almost like a HOC) and calls `props.onSave` of the passed component to determine the success state of the call to `onSave`. https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37 Here's a rough example... ```tsx import { showSaveModal } from "@kbn/saved-objects-plugin/public"; export function openSaveModal() { // ... showSaveModal( <SavedObjectSaveModal ... onSave={async () => { const response = await doTheSave(); if(response.success) { return { id: response.id }; } else { return { error: 'failed to save' } } }} /> ); } ``` The strange thing is that the `SavedObjectSaveModal.onSave` doesn't need the `SaveResult` to be returned and these modal components are used in various places but only some are passed to `showSaveModal`. So I cleaned up this code as well to expect the `SaveResult` when using `showSaveModal` and `void` otherwise. ### Checklist - [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 - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ## Release Notes Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others. --------- Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
|
Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync. |
2 similar comments
|
Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync. |
|
Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync. |
# Backport This will backport the following commits from `main` to `9.1`: - [Fix Save Modal promise chain `onSave` (#233933)](#233933) <!--- Backport version: 10.0.2 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Nick Partridge","email":"nicholas.partridge@elastic.co"},"sourceCommit":{"committedDate":"2025-09-08T21:31:34Z","message":"Fix Save Modal promise chain `onSave` (#233933)\n\n## Summary\n\nThis PR fixes an issue in which the SO save modal in Lens, Dashboard and\nothers, allowed the user to spam the save button, resulting in multiple\nsaved instances.\n\n### Demo\n\n#### Before\n\n\nhttps://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4\n\n#### After\n\n\nhttps://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531\n\n\nFixes #233906\n\n## Details\n\nWhen attempting to save any SO using the\n[`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95),\nthe logic was attempting to `await` the call to `onSave` and block any\nadditional calls. This was working correctly to set the `isLoading`\nstate, but the `onSave` callback is not expecting a `Promise` to be\nreturned so it has no affect and the `onSave` is immediately resolved,\nunsetting the loading state and thus allowing multiple calls to save.\n\nSo the issue is that `onSave` should expect `() => Promise<void>` not\n`() => void`. A crude fix to this could be to throttle the calls to\n`onSave` and preventing and future clicks until the save is eventually\ncomplete. But if the save takes longer or fails this will enable the\nsave button and allow clicking again, creating other possible issues and\nnot solving the root of the problem.\n\nSo the chain of `onSave` is broken from calling it here to the eventual\nfunction up the call stack that is performing the save. This involves\nupdating the chain of promised from the `onSave` prop all the way up to\nthe root consumer, which is typically calling an async function.\n\nFurther complicating things.... there is this dreadful, I mean\ndeprecated, `showSaveModal` function that wraps a modal component\n(almost like a HOC) and calls `props.onSave` of the passed component to\ndetermine the success state of the call to `onSave`.\n\n\nhttps://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37\n\nHere's a rough example...\n\n```tsx\nimport { showSaveModal } from \"@kbn/saved-objects-plugin/public\";\n\nexport function openSaveModal() {\n // ...\n\n showSaveModal(\n <SavedObjectSaveModal\n ...\n onSave={async () => {\n const response = await doTheSave();\n\n if(response.success) {\n return { id: response.id };\n } else {\n return { error: 'failed to save' }\n }\n }}\n />\n );\n}\n```\n\nThe strange thing is that the `SavedObjectSaveModal.onSave` doesn't need\nthe `SaveResult` to be returned and these modal components are used in\nvarious places but only some are passed to `showSaveModal`.\n\nSo I cleaned up this code as well to expect the `SaveResult` when using\n`showSaveModal` and `void` otherwise.\n\n\n### Checklist\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n## Release Notes\n\nFixes issue with save modal allowing duplicate saves of dashboard,\nvisualizations and others.\n\n---------\n\nCo-authored-by: Marco Vettorello <marco.vettorello@elastic.co>\nCo-authored-by: Davis McPhee <davis.mcphee@elastic.co>","sha":"8415158b5d487e7dfa58ebbdb5021df160ab4279","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Visualizations","Team:obs-ux-management","backport:version","v9.2.0","v9.1.4","v8.19.4"],"title":"Fixes Save Modal promise chain `onSave`","number":233933,"url":"https://github.com/elastic/kibana/pull/233933","mergeCommit":{"message":"Fix Save Modal promise chain `onSave` (#233933)\n\n## Summary\n\nThis PR fixes an issue in which the SO save modal in Lens, Dashboard and\nothers, allowed the user to spam the save button, resulting in multiple\nsaved instances.\n\n### Demo\n\n#### Before\n\n\nhttps://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4\n\n#### After\n\n\nhttps://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531\n\n\nFixes #233906\n\n## Details\n\nWhen attempting to save any SO using the\n[`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95),\nthe logic was attempting to `await` the call to `onSave` and block any\nadditional calls. This was working correctly to set the `isLoading`\nstate, but the `onSave` callback is not expecting a `Promise` to be\nreturned so it has no affect and the `onSave` is immediately resolved,\nunsetting the loading state and thus allowing multiple calls to save.\n\nSo the issue is that `onSave` should expect `() => Promise<void>` not\n`() => void`. A crude fix to this could be to throttle the calls to\n`onSave` and preventing and future clicks until the save is eventually\ncomplete. But if the save takes longer or fails this will enable the\nsave button and allow clicking again, creating other possible issues and\nnot solving the root of the problem.\n\nSo the chain of `onSave` is broken from calling it here to the eventual\nfunction up the call stack that is performing the save. This involves\nupdating the chain of promised from the `onSave` prop all the way up to\nthe root consumer, which is typically calling an async function.\n\nFurther complicating things.... there is this dreadful, I mean\ndeprecated, `showSaveModal` function that wraps a modal component\n(almost like a HOC) and calls `props.onSave` of the passed component to\ndetermine the success state of the call to `onSave`.\n\n\nhttps://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37\n\nHere's a rough example...\n\n```tsx\nimport { showSaveModal } from \"@kbn/saved-objects-plugin/public\";\n\nexport function openSaveModal() {\n // ...\n\n showSaveModal(\n <SavedObjectSaveModal\n ...\n onSave={async () => {\n const response = await doTheSave();\n\n if(response.success) {\n return { id: response.id };\n } else {\n return { error: 'failed to save' }\n }\n }}\n />\n );\n}\n```\n\nThe strange thing is that the `SavedObjectSaveModal.onSave` doesn't need\nthe `SaveResult` to be returned and these modal components are used in\nvarious places but only some are passed to `showSaveModal`.\n\nSo I cleaned up this code as well to expect the `SaveResult` when using\n`showSaveModal` and `void` otherwise.\n\n\n### Checklist\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n## Release Notes\n\nFixes issue with save modal allowing duplicate saves of dashboard,\nvisualizations and others.\n\n---------\n\nCo-authored-by: Marco Vettorello <marco.vettorello@elastic.co>\nCo-authored-by: Davis McPhee <davis.mcphee@elastic.co>","sha":"8415158b5d487e7dfa58ebbdb5021df160ab4279"}},"sourceBranch":"main","suggestedTargetBranches":["9.1","8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/233933","number":233933,"mergeCommit":{"message":"Fix Save Modal promise chain `onSave` (#233933)\n\n## Summary\n\nThis PR fixes an issue in which the SO save modal in Lens, Dashboard and\nothers, allowed the user to spam the save button, resulting in multiple\nsaved instances.\n\n### Demo\n\n#### Before\n\n\nhttps://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4\n\n#### After\n\n\nhttps://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531\n\n\nFixes #233906\n\n## Details\n\nWhen attempting to save any SO using the\n[`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95),\nthe logic was attempting to `await` the call to `onSave` and block any\nadditional calls. This was working correctly to set the `isLoading`\nstate, but the `onSave` callback is not expecting a `Promise` to be\nreturned so it has no affect and the `onSave` is immediately resolved,\nunsetting the loading state and thus allowing multiple calls to save.\n\nSo the issue is that `onSave` should expect `() => Promise<void>` not\n`() => void`. A crude fix to this could be to throttle the calls to\n`onSave` and preventing and future clicks until the save is eventually\ncomplete. But if the save takes longer or fails this will enable the\nsave button and allow clicking again, creating other possible issues and\nnot solving the root of the problem.\n\nSo the chain of `onSave` is broken from calling it here to the eventual\nfunction up the call stack that is performing the save. This involves\nupdating the chain of promised from the `onSave` prop all the way up to\nthe root consumer, which is typically calling an async function.\n\nFurther complicating things.... there is this dreadful, I mean\ndeprecated, `showSaveModal` function that wraps a modal component\n(almost like a HOC) and calls `props.onSave` of the passed component to\ndetermine the success state of the call to `onSave`.\n\n\nhttps://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37\n\nHere's a rough example...\n\n```tsx\nimport { showSaveModal } from \"@kbn/saved-objects-plugin/public\";\n\nexport function openSaveModal() {\n // ...\n\n showSaveModal(\n <SavedObjectSaveModal\n ...\n onSave={async () => {\n const response = await doTheSave();\n\n if(response.success) {\n return { id: response.id };\n } else {\n return { error: 'failed to save' }\n }\n }}\n />\n );\n}\n```\n\nThe strange thing is that the `SavedObjectSaveModal.onSave` doesn't need\nthe `SaveResult` to be returned and these modal components are used in\nvarious places but only some are passed to `showSaveModal`.\n\nSo I cleaned up this code as well to expect the `SaveResult` when using\n`showSaveModal` and `void` otherwise.\n\n\n### Checklist\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n## Release Notes\n\nFixes issue with save modal allowing duplicate saves of dashboard,\nvisualizations and others.\n\n---------\n\nCo-authored-by: Marco Vettorello <marco.vettorello@elastic.co>\nCo-authored-by: Davis McPhee <davis.mcphee@elastic.co>","sha":"8415158b5d487e7dfa58ebbdb5021df160ab4279"}},{"branch":"9.1","label":"v9.1.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
## Summary This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances. ### Demo #### Before https://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4 #### After https://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531 Fixes elastic#233906 ## Details When attempting to save any SO using the [`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95), the logic was attempting to `await` the call to `onSave` and block any additional calls. This was working correctly to set the `isLoading` state, but the `onSave` callback is not expecting a `Promise` to be returned so it has no affect and the `onSave` is immediately resolved, unsetting the loading state and thus allowing multiple calls to save. So the issue is that `onSave` should expect `() => Promise<void>` not `() => void`. A crude fix to this could be to throttle the calls to `onSave` and preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem. So the chain of `onSave` is broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from the `onSave` prop all the way up to the root consumer, which is typically calling an async function. Further complicating things.... there is this dreadful, I mean deprecated, `showSaveModal` function that wraps a modal component (almost like a HOC) and calls `props.onSave` of the passed component to determine the success state of the call to `onSave`. https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37 Here's a rough example... ```tsx import { showSaveModal } from "@kbn/saved-objects-plugin/public"; export function openSaveModal() { // ... showSaveModal( <SavedObjectSaveModal ... onSave={async () => { const response = await doTheSave(); if(response.success) { return { id: response.id }; } else { return { error: 'failed to save' } } }} /> ); } ``` The strange thing is that the `SavedObjectSaveModal.onSave` doesn't need the `SaveResult` to be returned and these modal components are used in various places but only some are passed to `showSaveModal`. So I cleaned up this code as well to expect the `SaveResult` when using `showSaveModal` and `void` otherwise. ### Checklist - [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 - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ## Release Notes Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others. --------- Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
# Backport This will backport the following commits from `main` to `8.19`: - [Fix Save Modal promise chain `onSave` (#233933)](#233933) <!--- Backport version: 10.0.2 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Nick Partridge","email":"nicholas.partridge@elastic.co"},"sourceCommit":{"committedDate":"2025-09-08T21:31:34Z","message":"Fix Save Modal promise chain `onSave` (#233933)\n\n## Summary\n\nThis PR fixes an issue in which the SO save modal in Lens, Dashboard and\nothers, allowed the user to spam the save button, resulting in multiple\nsaved instances.\n\n### Demo\n\n#### Before\n\n\nhttps://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4\n\n#### After\n\n\nhttps://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531\n\n\nFixes #233906\n\n## Details\n\nWhen attempting to save any SO using the\n[`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95),\nthe logic was attempting to `await` the call to `onSave` and block any\nadditional calls. This was working correctly to set the `isLoading`\nstate, but the `onSave` callback is not expecting a `Promise` to be\nreturned so it has no affect and the `onSave` is immediately resolved,\nunsetting the loading state and thus allowing multiple calls to save.\n\nSo the issue is that `onSave` should expect `() => Promise<void>` not\n`() => void`. A crude fix to this could be to throttle the calls to\n`onSave` and preventing and future clicks until the save is eventually\ncomplete. But if the save takes longer or fails this will enable the\nsave button and allow clicking again, creating other possible issues and\nnot solving the root of the problem.\n\nSo the chain of `onSave` is broken from calling it here to the eventual\nfunction up the call stack that is performing the save. This involves\nupdating the chain of promised from the `onSave` prop all the way up to\nthe root consumer, which is typically calling an async function.\n\nFurther complicating things.... there is this dreadful, I mean\ndeprecated, `showSaveModal` function that wraps a modal component\n(almost like a HOC) and calls `props.onSave` of the passed component to\ndetermine the success state of the call to `onSave`.\n\n\nhttps://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37\n\nHere's a rough example...\n\n```tsx\nimport { showSaveModal } from \"@kbn/saved-objects-plugin/public\";\n\nexport function openSaveModal() {\n // ...\n\n showSaveModal(\n <SavedObjectSaveModal\n ...\n onSave={async () => {\n const response = await doTheSave();\n\n if(response.success) {\n return { id: response.id };\n } else {\n return { error: 'failed to save' }\n }\n }}\n />\n );\n}\n```\n\nThe strange thing is that the `SavedObjectSaveModal.onSave` doesn't need\nthe `SaveResult` to be returned and these modal components are used in\nvarious places but only some are passed to `showSaveModal`.\n\nSo I cleaned up this code as well to expect the `SaveResult` when using\n`showSaveModal` and `void` otherwise.\n\n\n### Checklist\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n## Release Notes\n\nFixes issue with save modal allowing duplicate saves of dashboard,\nvisualizations and others.\n\n---------\n\nCo-authored-by: Marco Vettorello <marco.vettorello@elastic.co>\nCo-authored-by: Davis McPhee <davis.mcphee@elastic.co>","sha":"8415158b5d487e7dfa58ebbdb5021df160ab4279","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Visualizations","Team:obs-ux-management","backport:version","v9.2.0","v9.1.4","v8.19.4"],"title":"Fixes Save Modal promise chain `onSave`","number":233933,"url":"https://github.com/elastic/kibana/pull/233933","mergeCommit":{"message":"Fix Save Modal promise chain `onSave` (#233933)\n\n## Summary\n\nThis PR fixes an issue in which the SO save modal in Lens, Dashboard and\nothers, allowed the user to spam the save button, resulting in multiple\nsaved instances.\n\n### Demo\n\n#### Before\n\n\nhttps://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4\n\n#### After\n\n\nhttps://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531\n\n\nFixes #233906\n\n## Details\n\nWhen attempting to save any SO using the\n[`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95),\nthe logic was attempting to `await` the call to `onSave` and block any\nadditional calls. This was working correctly to set the `isLoading`\nstate, but the `onSave` callback is not expecting a `Promise` to be\nreturned so it has no affect and the `onSave` is immediately resolved,\nunsetting the loading state and thus allowing multiple calls to save.\n\nSo the issue is that `onSave` should expect `() => Promise<void>` not\n`() => void`. A crude fix to this could be to throttle the calls to\n`onSave` and preventing and future clicks until the save is eventually\ncomplete. But if the save takes longer or fails this will enable the\nsave button and allow clicking again, creating other possible issues and\nnot solving the root of the problem.\n\nSo the chain of `onSave` is broken from calling it here to the eventual\nfunction up the call stack that is performing the save. This involves\nupdating the chain of promised from the `onSave` prop all the way up to\nthe root consumer, which is typically calling an async function.\n\nFurther complicating things.... there is this dreadful, I mean\ndeprecated, `showSaveModal` function that wraps a modal component\n(almost like a HOC) and calls `props.onSave` of the passed component to\ndetermine the success state of the call to `onSave`.\n\n\nhttps://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37\n\nHere's a rough example...\n\n```tsx\nimport { showSaveModal } from \"@kbn/saved-objects-plugin/public\";\n\nexport function openSaveModal() {\n // ...\n\n showSaveModal(\n <SavedObjectSaveModal\n ...\n onSave={async () => {\n const response = await doTheSave();\n\n if(response.success) {\n return { id: response.id };\n } else {\n return { error: 'failed to save' }\n }\n }}\n />\n );\n}\n```\n\nThe strange thing is that the `SavedObjectSaveModal.onSave` doesn't need\nthe `SaveResult` to be returned and these modal components are used in\nvarious places but only some are passed to `showSaveModal`.\n\nSo I cleaned up this code as well to expect the `SaveResult` when using\n`showSaveModal` and `void` otherwise.\n\n\n### Checklist\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n## Release Notes\n\nFixes issue with save modal allowing duplicate saves of dashboard,\nvisualizations and others.\n\n---------\n\nCo-authored-by: Marco Vettorello <marco.vettorello@elastic.co>\nCo-authored-by: Davis McPhee <davis.mcphee@elastic.co>","sha":"8415158b5d487e7dfa58ebbdb5021df160ab4279"}},"sourceBranch":"main","suggestedTargetBranches":["9.1","8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/233933","number":233933,"mergeCommit":{"message":"Fix Save Modal promise chain `onSave` (#233933)\n\n## Summary\n\nThis PR fixes an issue in which the SO save modal in Lens, Dashboard and\nothers, allowed the user to spam the save button, resulting in multiple\nsaved instances.\n\n### Demo\n\n#### Before\n\n\nhttps://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4\n\n#### After\n\n\nhttps://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531\n\n\nFixes #233906\n\n## Details\n\nWhen attempting to save any SO using the\n[`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95),\nthe logic was attempting to `await` the call to `onSave` and block any\nadditional calls. This was working correctly to set the `isLoading`\nstate, but the `onSave` callback is not expecting a `Promise` to be\nreturned so it has no affect and the `onSave` is immediately resolved,\nunsetting the loading state and thus allowing multiple calls to save.\n\nSo the issue is that `onSave` should expect `() => Promise<void>` not\n`() => void`. A crude fix to this could be to throttle the calls to\n`onSave` and preventing and future clicks until the save is eventually\ncomplete. But if the save takes longer or fails this will enable the\nsave button and allow clicking again, creating other possible issues and\nnot solving the root of the problem.\n\nSo the chain of `onSave` is broken from calling it here to the eventual\nfunction up the call stack that is performing the save. This involves\nupdating the chain of promised from the `onSave` prop all the way up to\nthe root consumer, which is typically calling an async function.\n\nFurther complicating things.... there is this dreadful, I mean\ndeprecated, `showSaveModal` function that wraps a modal component\n(almost like a HOC) and calls `props.onSave` of the passed component to\ndetermine the success state of the call to `onSave`.\n\n\nhttps://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37\n\nHere's a rough example...\n\n```tsx\nimport { showSaveModal } from \"@kbn/saved-objects-plugin/public\";\n\nexport function openSaveModal() {\n // ...\n\n showSaveModal(\n <SavedObjectSaveModal\n ...\n onSave={async () => {\n const response = await doTheSave();\n\n if(response.success) {\n return { id: response.id };\n } else {\n return { error: 'failed to save' }\n }\n }}\n />\n );\n}\n```\n\nThe strange thing is that the `SavedObjectSaveModal.onSave` doesn't need\nthe `SaveResult` to be returned and these modal components are used in\nvarious places but only some are passed to `showSaveModal`.\n\nSo I cleaned up this code as well to expect the `SaveResult` when using\n`showSaveModal` and `void` otherwise.\n\n\n### Checklist\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n## Release Notes\n\nFixes issue with save modal allowing duplicate saves of dashboard,\nvisualizations and others.\n\n---------\n\nCo-authored-by: Marco Vettorello <marco.vettorello@elastic.co>\nCo-authored-by: Davis McPhee <davis.mcphee@elastic.co>","sha":"8415158b5d487e7dfa58ebbdb5021df160ab4279"}},{"branch":"9.1","label":"v9.1.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co>
## Summary This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances. ### Demo #### Before https://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4 #### After https://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531 Fixes elastic#233906 ## Details When attempting to save any SO using the [`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95), the logic was attempting to `await` the call to `onSave` and block any additional calls. This was working correctly to set the `isLoading` state, but the `onSave` callback is not expecting a `Promise` to be returned so it has no affect and the `onSave` is immediately resolved, unsetting the loading state and thus allowing multiple calls to save. So the issue is that `onSave` should expect `() => Promise<void>` not `() => void`. A crude fix to this could be to throttle the calls to `onSave` and preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem. So the chain of `onSave` is broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from the `onSave` prop all the way up to the root consumer, which is typically calling an async function. Further complicating things.... there is this dreadful, I mean deprecated, `showSaveModal` function that wraps a modal component (almost like a HOC) and calls `props.onSave` of the passed component to determine the success state of the call to `onSave`. https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37 Here's a rough example... ```tsx import { showSaveModal } from "@kbn/saved-objects-plugin/public"; export function openSaveModal() { // ... showSaveModal( <SavedObjectSaveModal ... onSave={async () => { const response = await doTheSave(); if(response.success) { return { id: response.id }; } else { return { error: 'failed to save' } } }} /> ); } ``` The strange thing is that the `SavedObjectSaveModal.onSave` doesn't need the `SaveResult` to be returned and these modal components are used in various places but only some are passed to `showSaveModal`. So I cleaned up this code as well to expect the `SaveResult` when using `showSaveModal` and `void` otherwise. ### Checklist - [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 - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ## Release Notes Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others. --------- Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
## Summary This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances. ### Demo #### Before https://github.com/user-attachments/assets/70ddc6c1-b99d-4758-8333-fe9ab47474a4 #### After https://github.com/user-attachments/assets/84f016dd-05d9-441c-864c-b06160c9d531 Fixes #233906 ## Details When attempting to save any SO using the [`SavedObjectSaveModal`](https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/saved_object_save_modal.tsx#L95), the logic was attempting to `await` the call to `onSave` and block any additional calls. This was working correctly to set the `isLoading` state, but the `onSave` callback is not expecting a `Promise` to be returned so it has no affect and the `onSave` is immediately resolved, unsetting the loading state and thus allowing multiple calls to save. So the issue is that `onSave` should expect `() => Promise<void>` not `() => void`. A crude fix to this could be to throttle the calls to `onSave` and preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem. So the chain of `onSave` is broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from the `onSave` prop all the way up to the root consumer, which is typically calling an async function. Further complicating things.... there is this dreadful, I mean deprecated, `showSaveModal` function that wraps a modal component (almost like a HOC) and calls `props.onSave` of the passed component to determine the success state of the call to `onSave`. https://github.com/elastic/kibana/blob/07cc9e9c1a271c37f42c84f57a1600596070fd9c/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx#L34-L37 Here's a rough example... ```tsx import { showSaveModal } from "@kbn/saved-objects-plugin/public"; export function openSaveModal() { // ... showSaveModal( <SavedObjectSaveModal ... onSave={async () => { const response = await doTheSave(); if(response.success) { return { id: response.id }; } else { return { error: 'failed to save' } } }} /> ); } ``` The strange thing is that the `SavedObjectSaveModal.onSave` doesn't need the `SaveResult` to be returned and these modal components are used in various places but only some are passed to `showSaveModal`. So I cleaned up this code as well to expect the `SaveResult` when using `showSaveModal` and `void` otherwise. ### Checklist - [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 - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ## Release Notes Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others. --------- Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
Summary
This PR fixes an issue in which the SO save modal in Lens, Dashboard and others, allowed the user to spam the save button, resulting in multiple saved instances.
Demo
Before
Zight.Recording.2025-09-03.at.12.08.19.PM.mp4
After
Zight.Recording.2025-09-03.at.03.54.03.PM.mp4
Fixes #233906
Details
When attempting to save any SO using the
SavedObjectSaveModal, the logic was attempting toawaitthe call toonSaveand block any additional calls. This was working correctly to set theisLoadingstate, but theonSavecallback is not expecting aPromiseto be returned so it has no affect and theonSaveis immediately resolved, unsetting the loading state and thus allowing multiple calls to save.So the issue is that
onSaveshould expect() => Promise<void>not() => void. A crude fix to this could be to throttle the calls toonSaveand preventing and future clicks until the save is eventually complete. But if the save takes longer or fails this will enable the save button and allow clicking again, creating other possible issues and not solving the root of the problem.So the chain of
onSaveis broken from calling it here to the eventual function up the call stack that is performing the save. This involves updating the chain of promised from theonSaveprop all the way up to the root consumer, which is typically calling an async function.Further complicating things.... there is this dreadful, I mean deprecated,
showSaveModalfunction that wraps a modal component (almost like a HOC) and callsprops.onSaveof the passed component to determine the success state of the call toonSave.kibana/src/platform/plugins/shared/saved_objects/public/save_modal/show_saved_object_save_modal.tsx
Lines 34 to 37 in 07cc9e9
Here's a rough example...
The strange thing is that the
SavedObjectSaveModal.onSavedoesn't need theSaveResultto be returned and these modal components are used in various places but only some are passed toshowSaveModal.So I cleaned up this code as well to expect the
SaveResultwhen usingshowSaveModalandvoidotherwise.Checklist
release_note:breakinglabel should be applied in these situations.release_note:*label is applied per the guidelinesbackport:*labels.Release Notes
Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others.