- {i18n.translate(
- 'xpack.security.accountManagement.userProfile.requiresPageReloadToastDescription',
- {
- defaultMessage:
- 'One or more settings require you to reload the page to take effect.',
- }
- )}
-
- window.location.reload()}
- data-test-subj="windowReloadButton"
- >
- {i18n.translate(
- 'xpack.security.accountManagement.userProfile.requiresPageReloadToastButtonLabel',
- {
- defaultMessage: 'Reload page',
- }
- )}
-
-
-
- ),
- };
- }
-
- services.notifications.toasts.addSuccess(successToastInput, successToastOptions);
+ showSuccessNotification({ isRefreshRequired });
},
initialValues,
enableReinitialize: true,
diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.test.ts b/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.test.ts
index 4fdb482a2cbae..0c275db2c1a90 100644
--- a/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.test.ts
+++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.test.ts
@@ -14,6 +14,7 @@ describe('UserProfileAPIClient', () => {
let apiClient: UserProfileAPIClient;
beforeEach(() => {
coreStart = coreMock.createStart();
+ coreStart.http.get.mockResolvedValue(undefined);
coreStart.http.post.mockResolvedValue(undefined);
apiClient = new UserProfileAPIClient(coreStart.http);
diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts b/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts
index a2fc60f7b4d75..4b992f616ca14 100644
--- a/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts
+++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts
@@ -5,12 +5,14 @@
* 2.0.
*/
+import { merge } from 'lodash';
import type { Observable } from 'rxjs';
-import { Subject } from 'rxjs';
+import { BehaviorSubject, Subject } from 'rxjs';
import type { HttpStart } from '@kbn/core/public';
-import type { GetUserProfileResponse, UserProfile, UserProfileData } from '../../../common';
+import type { GetUserProfileResponse, UserProfile } from '../../../common';
+import type { UserProfileData } from './user_profile';
/**
* Parameters for the get user profile for the current user API.
@@ -70,6 +72,11 @@ export class UserProfileAPIClient {
public readonly dataUpdates$: Observable =
this.internalDataUpdates$.asObservable();
+ private readonly _userProfile$ = new BehaviorSubject(null);
+
+ /** Observable of the current user profile data */
+ public readonly userProfile$ = this._userProfile$.asObservable();
+
constructor(private readonly http: HttpStart) {}
/**
@@ -80,9 +87,16 @@ export class UserProfileAPIClient {
* optional "dataPath" parameter can be used to return personal data for this user.
*/
public getCurrent(params?: UserProfileGetCurrentParams) {
- return this.http.get>('/internal/security/user_profile', {
- query: { dataPath: params?.dataPath },
- });
+ return this.http
+ .get>('/internal/security/user_profile', {
+ query: { dataPath: params?.dataPath },
+ })
+ .then((response) => {
+ const data = response?.data ?? {};
+ const updated = merge(this._userProfile$.getValue(), data);
+ this._userProfile$.next(updated);
+ return response;
+ });
}
/**
@@ -126,10 +140,19 @@ export class UserProfileAPIClient {
* @param data Application data to be written (merged with existing data).
*/
public update(data: D) {
+ // Optimistic update the user profile Observable.
+ const previous = this._userProfile$.getValue();
+ this._userProfile$.next(data);
+
return this.http
.post('/internal/security/user_profile/_data', { body: JSON.stringify(data) })
.then(() => {
this.internalDataUpdates$.next(data);
+ })
+ .catch((err) => {
+ // Revert the user profile data to the previous state.
+ this._userProfile$.next(previous);
+ return Promise.reject(err);
});
}
}
diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts
index 209bc5ff576b6..b51bb3d25092e 100644
--- a/x-pack/plugins/security/public/index.ts
+++ b/x-pack/plugins/security/public/index.ts
@@ -24,6 +24,7 @@ export type {
UserProfileBulkGetParams,
UserProfileGetCurrentParams,
UserProfileSuggestParams,
+ UpdateUserProfileHook,
} from './account_management';
export type { AuthenticationServiceStart, AuthenticationServiceSetup } from './authentication';
diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts
index e776d55f17d06..f0081307ef33f 100644
--- a/x-pack/plugins/security/public/mocks.ts
+++ b/x-pack/plugins/security/public/mocks.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { of } from 'rxjs';
+
import { licenseMock } from '../common/licensing/index.mock';
import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock';
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
@@ -22,8 +24,17 @@ function createStartMock() {
return {
authc: authenticationMock.createStart(),
navControlService: navControlServiceMock.createStart(),
- userProfiles: { getCurrent: jest.fn(), bulkGet: jest.fn(), suggest: jest.fn() },
+ userProfiles: {
+ getCurrent: jest.fn(),
+ bulkGet: jest.fn(),
+ suggest: jest.fn(),
+ update: jest.fn(),
+ userProfile$: of({}),
+ },
uiApi: getUiApiMock.createStart(),
+ hooks: {
+ useUpdateUserProfile: jest.fn(),
+ },
};
}
diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx
index 2936f705b75a2..a0f9df820c91e 100644
--- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx
+++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx
@@ -166,6 +166,8 @@ describe('SecurityNavControl', () => {
});
it('should render additional user menu links registered by other plugins and should render the default Edit Profile link as the first link when no custom profile link is provided', async () => {
+ const DummyComponent = () =>