Skip to content

Fixes Save Modal promise chain onSave#233933

Merged
nickofthyme merged 10 commits intoelastic:mainfrom
nickofthyme:fix-save-modal-chain
Sep 8, 2025
Merged

Fixes Save Modal promise chain onSave#233933
nickofthyme merged 10 commits intoelastic:mainfrom
nickofthyme:fix-save-modal-chain

Conversation

@nickofthyme
Copy link
Contributor

@nickofthyme nickofthyme commented Sep 3, 2025

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 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.

export function showSaveModal(
saveModal: React.ReactElement<MinimalSaveModalProps>,
Wrapper?: FC<PropsWithChildren<unknown>>
) {

Here's a rough example...

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

  • Unit or functional tests were updated or added to match the most common scenarios
  • 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.
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines
  • Review the backport guidelines and apply applicable backport:* labels.

Release Notes

Fixes issue with save modal allowing duplicate saves of dashboard, visualizations and others.

@nickofthyme nickofthyme added release_note:fix Team:Visualizations Team label for Lens, elastic-charts, Graph, legacy editors (TSVB, Visualize, Timelion) t// labels Sep 3, 2025
@nickofthyme nickofthyme marked this pull request as ready for review September 4, 2025 15:24
@nickofthyme nickofthyme requested review from a team as code owners September 4, 2025 15:24
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-visualizations (Team:Visualizations)

Copy link
Contributor

@davismcphee davismcphee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🤔

@botelastic botelastic bot added the Team:actionable-obs Formerly "obs-ux-management", responsible for SLO, o11y alerting, significant events, & synthetics. label Sep 4, 2025
@elasticmachine
Copy link
Contributor

Pinging @elastic/obs-ux-management-team (Team:obs-ux-management)

@elasticmachine
Copy link
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] FTR Configs #58 / InfraOps App feature controls infrastructure security global infrastructure all privileges infrastructure landing page with data shows Wafflemap

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
presentationUtil 104 105 +1

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
savedObjects 86 89 +3

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
aiops 517.9KB 517.9KB +15.0B
dashboard 623.0KB 623.1KB +28.0B
discover 1.1MB 1.1MB +12.0B
graph 371.0KB 371.0KB +23.0B
lens 1.5MB 1.5MB +57.0B
links 100.7KB 100.7KB +14.0B
maps 3.1MB 3.1MB +14.0B
ml 5.4MB 5.4MB +15.0B
presentationUtil 67.2KB 67.4KB +201.0B
slo 974.2KB 974.2KB +10.0B
synthetics 1.0MB 1.0MB +5.0B
visualizations 351.5KB 351.5KB +18.0B
total +412.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
presentationUtil 8.2KB 8.3KB +125.0B
savedObjects 10.5KB 10.6KB +91.0B
total +216.0B
Unknown metric groups

API count

id before after diff
presentationUtil 106 108 +2
savedObjects 88 94 +6
total +8

async chunk count

id before after diff
presentationUtil 9 10 +1

ESLint disabled line counts

id before after diff
presentationUtil 10 11 +1

References to deprecated APIs

id before after diff
dashboard 5 13 +8
discover 5 9 +4
graph 44 48 +4
lens 4 6 +2
links 1 5 +4
maps 20 22 +2
presentationUtil 0 2 +2
visualizations 8 14 +6
total +32

Total ESLint disabled count

id before after diff
presentationUtil 10 11 +1

History

@nickofthyme nickofthyme merged commit 8415158 into elastic:main Sep 8, 2025
12 checks passed
@nickofthyme nickofthyme deleted the fix-save-modal-chain branch September 8, 2025 21:31
@kibanamachine
Copy link
Contributor

Starting backport for target branches: 8.19, 9.1

https://github.com/elastic/kibana/actions/runs/17564712467

@kibanamachine
Copy link
Contributor

💔 All backports failed

Status Branch Result
8.19 Backport failed because of merge conflicts
9.1 Backport failed because of merge conflicts

Manual backport

To create the backport manually run:

node scripts/backport --pr 233933

Questions ?

Please refer to the Backport tool documentation

@nickofthyme nickofthyme restored the fix-save-modal-chain branch September 9, 2025 18:10
nickofthyme added a commit to nickofthyme/kibana that referenced this pull request Sep 9, 2025
## 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
@nickofthyme
Copy link
Contributor Author

💚 All backports created successfully

Status Branch Result
9.1
8.19

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

nickofthyme added a commit to nickofthyme/kibana that referenced this pull request Sep 9, 2025
## 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
eleonoramicozzi pushed a commit to eleonoramicozzi/kibana that referenced this pull request Sep 10, 2025
## 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>
@kibanamachine kibanamachine added the backport missing Added to PRs automatically when the are determined to be missing a backport. label Sep 10, 2025
@kibanamachine
Copy link
Contributor

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.
cc: @nickofthyme

2 similar comments
@kibanamachine
Copy link
Contributor

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.
cc: @nickofthyme

@kibanamachine
Copy link
Contributor

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.
cc: @nickofthyme

nickofthyme added a commit that referenced this pull request Sep 15, 2025
# 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>
KodeRad pushed a commit to KodeRad/kibana that referenced this pull request Sep 15, 2025
## 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>
nickofthyme added a commit that referenced this pull request Sep 15, 2025
# 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>
@kibanamachine kibanamachine removed the backport missing Added to PRs automatically when the are determined to be missing a backport. label Sep 15, 2025
CAWilson94 pushed a commit to CAWilson94/kibana that referenced this pull request Sep 24, 2025
## 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>
niros1 pushed a commit that referenced this pull request Sep 30, 2025
## 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>
@nickofthyme nickofthyme deleted the fix-save-modal-chain branch October 2, 2025 14:31
@nickofthyme nickofthyme added backport missing Added to PRs automatically when the are determined to be missing a backport. and removed backport missing Added to PRs automatically when the are determined to be missing a backport. labels Oct 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:version Backport to applied version labels release_note:fix Team:actionable-obs Formerly "obs-ux-management", responsible for SLO, o11y alerting, significant events, & synthetics. Team:Visualizations Team label for Lens, elastic-charts, Graph, legacy editors (TSVB, Visualize, Timelion) t// v8.19.4 v9.1.4 v9.2.0

9 participants