Skip to content

Commit

Permalink
[ColorScheme] Update from avatar menu (#161214)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored Jul 12, 2023
1 parent d166193 commit 3a434bf
Show file tree
Hide file tree
Showing 29 changed files with 1,126 additions and 201 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ pageLoadAssetSize:
screenshotMode: 17856
screenshotting: 22870
searchprofiler: 67080
security: 65433
security: 81771
securitySolution: 66738
securitySolutionEss: 16573
securitySolutionServerless: 40000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('maybeAddCloudLinks', () => {
chrome: coreMock.createStart().chrome,
cloud: { ...cloudMock.createStart(), isCloudEnabled: false },
docLinks: coreMock.createStart().docLinks,
uiSettingsClient: coreMock.createStart().uiSettings,
});
// Since there's a promise, let's wait for the next tick
await new Promise((resolve) => process.nextTick(resolve));
Expand All @@ -30,12 +31,13 @@ describe('maybeAddCloudLinks', () => {
security.authc.getCurrentUser.mockResolvedValue(
securityMock.createMockAuthenticatedUser({ elastic_cloud_user: true })
);
const { chrome, docLinks } = coreMock.createStart();
const { chrome, docLinks, uiSettings } = coreMock.createStart();
maybeAddCloudLinks({
security,
chrome,
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
docLinks,
uiSettingsClient: uiSettings,
});
// Since there's a promise, let's wait for the next tick
await new Promise((resolve) => process.nextTick(resolve));
Expand Down Expand Up @@ -73,6 +75,79 @@ describe('maybeAddCloudLinks', () => {
"label": "Organization",
"order": 300,
},
Object {
"content": <ThemDarkModeToggle
security={
Object {
"authc": Object {
"areAPIKeysEnabled": [MockFunction],
"getCurrentUser": [MockFunction] {
"calls": Array [
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
},
},
"hooks": Object {
"useUpdateUserProfile": [MockFunction],
},
"navControlService": Object {
"addUserMenuLinks": [MockFunction] {
"calls": Array [
[Circular],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"getUserMenuLinks$": [MockFunction],
},
"uiApi": Object {
"components": Object {
"getChangePassword": [MockFunction],
"getPersonalInfo": [MockFunction],
},
},
"userProfiles": Object {
"bulkGet": [MockFunction],
"getCurrent": [MockFunction],
"suggest": [MockFunction],
"update": [MockFunction],
"userProfile$": Observable {
"_subscribe": [Function],
},
},
}
}
uiSettingsClient={
Object {
"get": [MockFunction],
"get$": [MockFunction],
"getAll": [MockFunction],
"getUpdate$": [MockFunction],
"getUpdateErrors$": [MockFunction],
"isCustom": [MockFunction],
"isDeclared": [MockFunction],
"isDefault": [MockFunction],
"isOverridden": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
}
}
/>,
"href": "",
"iconType": "",
"label": "",
"order": 400,
},
],
]
`);
Expand Down Expand Up @@ -101,12 +176,13 @@ describe('maybeAddCloudLinks', () => {
it('when cloud enabled and it fails to fetch the user, it sets the links', async () => {
const security = securityMock.createStart();
security.authc.getCurrentUser.mockRejectedValue(new Error('Something went terribly wrong'));
const { chrome, docLinks } = coreMock.createStart();
const { chrome, docLinks, uiSettings } = coreMock.createStart();
maybeAddCloudLinks({
security,
chrome,
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
docLinks,
uiSettingsClient: uiSettings,
});
// Since there's a promise, let's wait for the next tick
await new Promise((resolve) => process.nextTick(resolve));
Expand Down Expand Up @@ -144,6 +220,79 @@ describe('maybeAddCloudLinks', () => {
"label": "Organization",
"order": 300,
},
Object {
"content": <ThemDarkModeToggle
security={
Object {
"authc": Object {
"areAPIKeysEnabled": [MockFunction],
"getCurrentUser": [MockFunction] {
"calls": Array [
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
},
},
"hooks": Object {
"useUpdateUserProfile": [MockFunction],
},
"navControlService": Object {
"addUserMenuLinks": [MockFunction] {
"calls": Array [
[Circular],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"getUserMenuLinks$": [MockFunction],
},
"uiApi": Object {
"components": Object {
"getChangePassword": [MockFunction],
"getPersonalInfo": [MockFunction],
},
},
"userProfiles": Object {
"bulkGet": [MockFunction],
"getCurrent": [MockFunction],
"suggest": [MockFunction],
"update": [MockFunction],
"userProfile$": Observable {
"_subscribe": [Function],
},
},
}
}
uiSettingsClient={
Object {
"get": [MockFunction],
"get$": [MockFunction],
"getAll": [MockFunction],
"getUpdate$": [MockFunction],
"getUpdateErrors$": [MockFunction],
"isCustom": [MockFunction],
"isDeclared": [MockFunction],
"isDefault": [MockFunction],
"isOverridden": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
}
}
/>,
"href": "",
"iconType": "",
"label": "",
"order": 400,
},
],
]
`);
Expand Down Expand Up @@ -173,12 +322,13 @@ describe('maybeAddCloudLinks', () => {
security.authc.getCurrentUser.mockResolvedValue(
securityMock.createMockAuthenticatedUser({ elastic_cloud_user: false })
);
const { chrome, docLinks } = coreMock.createStart();
const { chrome, docLinks, uiSettings } = coreMock.createStart();
maybeAddCloudLinks({
security,
chrome,
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
docLinks,
uiSettingsClient: uiSettings,
});
// Since there's a promise, let's wait for the next tick
await new Promise((resolve) => process.nextTick(resolve));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { ChromeStart } from '@kbn/core/public';
import type { SecurityPluginStart } from '@kbn/security-plugin/public';

import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { createUserMenuLinks } from './user_menu_links';
import { createHelpMenuLinks } from './help_menu_links';
Expand All @@ -21,13 +21,15 @@ export interface MaybeAddCloudLinksDeps {
chrome: ChromeStart;
cloud: CloudStart;
docLinks: DocLinksStart;
uiSettingsClient: IUiSettingsClient;
}

export function maybeAddCloudLinks({
security,
chrome,
cloud,
docLinks,
uiSettingsClient,
}: MaybeAddCloudLinksDeps): void {
const userObservable = defer(() => security.authc.getCurrentUser()).pipe(
// Check if user is a cloud user.
Expand All @@ -45,7 +47,7 @@ export function maybeAddCloudLinks({
href: cloud.deploymentUrl,
});
}
const userMenuLinks = createUserMenuLinks(cloud);
const userMenuLinks = createUserMenuLinks({ cloud, security, uiSettingsClient });
security.navControlService.addUserMenuLinks(userMenuLinks);
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';

interface Deps {
uiSettingsClient: IUiSettingsClient;
security: SecurityPluginStart;
}

export const useThemeDarkmodeToggle = ({ uiSettingsClient, security }: Deps) => {
const [isDarkModeOn, setIsDarkModeOn] = useState(false);
// If a value is set in kibana.yml (uiSettings.overrides.theme:darkMode)
// we don't allow the user to change the theme color.
const valueSetInKibanaConfig = uiSettingsClient.isOverridden('theme:darkMode');

const { userProfileData, isLoading, update } = security.hooks.useUpdateUserProfile({
notificationSuccess: {
title: i18n.translate('xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle', {
defaultMessage: 'Color theme updated',
}),
pageReloadText: i18n.translate(
'xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText',
{
defaultMessage: 'Reload the page to see the changes',
}
),
},
pageReloadChecker: (prev, next) => {
return prev?.userSettings?.darkMode !== next.userSettings?.darkMode;
},
});

const { userSettings: { darkMode: colorScheme } = { darkMode: undefined } } =
userProfileData ?? {};

const toggle = useCallback(
(on: boolean) => {
if (isLoading) {
return;
}
update({
userSettings: {
darkMode: on ? 'dark' : 'light',
},
});
},
[isLoading, update]
);

useEffect(() => {
let updatedValue = false;

if (typeof colorScheme !== 'string') {
// User profile does not have yet any preference -> default to space dark mode value
updatedValue = uiSettingsClient.get('theme:darkMode') ?? false;
} else {
updatedValue = colorScheme === 'dark';
}

setIsDarkModeOn(updatedValue);
}, [colorScheme, uiSettingsClient]);

return {
isVisible: valueSetInKibanaConfig ? false : Boolean(userProfileData),
toggle,
isDarkModeOn,
colorScheme,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { coreMock } from '@kbn/core/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';

import { ThemDarkModeToggle } from './theme_darkmode_toggle';

describe('ThemDarkModeToggle', () => {
const mockUseUpdateUserProfile = jest.fn();
const mockGetSpaceDarkModeValue = jest.fn();

it('renders correctly and toggles dark mode', () => {
const security = {
...securityMock.createStart(),
hooks: { useUpdateUserProfile: mockUseUpdateUserProfile },
};
const { uiSettings } = coreMock.createStart();

const mockUpdate = jest.fn();
mockUseUpdateUserProfile.mockReturnValue({
userProfileData: { userSettings: { darkMode: 'light' } },
isLoading: false,
update: mockUpdate,
});

mockGetSpaceDarkModeValue.mockReturnValue(false);

const { getByTestId, rerender } = render(
<ThemDarkModeToggle security={security} uiSettingsClient={uiSettings} />
);

const toggleSwitch = getByTestId('darkModeToggleSwitch');
fireEvent.click(toggleSwitch);
expect(mockUpdate).toHaveBeenCalledWith({ userSettings: { darkMode: 'dark' } });

// Now we want to simulate toggling back to light
mockUseUpdateUserProfile.mockReturnValue({
userProfileData: { userSettings: { darkMode: 'dark' } },
isLoading: false,
update: mockUpdate,
});

// Rerender the component to apply the new props
rerender(<ThemDarkModeToggle security={security} uiSettingsClient={uiSettings} />);

fireEvent.click(toggleSwitch);
expect(mockUpdate).toHaveBeenLastCalledWith({ userSettings: { darkMode: 'light' } });
});
});
Loading

0 comments on commit 3a434bf

Please sign in to comment.