From 7f7c024f61d40ba9f432a33baba7266cb7a7836d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 6 Dec 2024 19:08:22 +0100 Subject: [PATCH 01/32] Use Appearance selector instead of ThemeDarkModeToggle --- .../src/nav_control/nav_control_service.ts | 2 +- .../appearance_modal.tsx | 57 ++++++++++++ ....test.tsx => appearance_selector.test.tsx} | 10 +- .../appearance_selector.tsx | 73 +++++++++++++++ .../theme_darkmode_toggle.tsx | 91 ------------------- ...arkmode_hook.ts => use_appearance_hook.ts} | 2 +- .../maybe_add_cloud_links/user_menu_links.tsx | 6 +- .../nav_control/nav_control_component.tsx | 17 +++- 8 files changed, 155 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_modal.tsx rename x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/{theme_darkmode_toggle.test.tsx => appearance_selector.test.tsx} (84%) create mode 100644 x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.tsx delete mode 100644 x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx rename x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/{theme_darkmode_hook.ts => use_appearance_hook.ts} (96%) diff --git a/x-pack/packages/security/plugin_types_public/src/nav_control/nav_control_service.ts b/x-pack/packages/security/plugin_types_public/src/nav_control/nav_control_service.ts index 39982a753127c..98670b6364af8 100644 --- a/x-pack/packages/security/plugin_types_public/src/nav_control/nav_control_service.ts +++ b/x-pack/packages/security/plugin_types_public/src/nav_control/nav_control_service.ts @@ -16,7 +16,7 @@ export interface UserMenuLink { order?: number; setAsProfile?: boolean; /** Render a custom ReactNode instead of the default */ - content?: ReactNode; + content?: ReactNode | ((args: { closePopover: () => void }) => ReactNode); } export interface SecurityNavControlServiceStart { diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_modal.tsx new file mode 100644 index 0000000000000..a2910d837c02c --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_modal.tsx @@ -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, { FC } from 'react'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +// import { useAppearance } from './use_appearance_hook'; + +interface Props { + closeModal: () => void; + uiSettingsClient: IUiSettingsClient; +} + +export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => { + const modalTitleId = useGeneratedHtmlId(); + + // const { isVisible, toggle, isDarkModeOn, colorScheme } = useAppearance({ + // uiSettingsClient, + // }); + + return ( + + + Modal title + + + + This modal has the following setup: + +

Content comes here!

+
+ + + { + // console.log('close'); + }} + fill + > + Close + + +
+ ); +}; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.test.tsx similarity index 84% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.test.tsx index 6b06cd64b9e23..da4200e9070f0 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.test.tsx @@ -11,7 +11,7 @@ import '@testing-library/jest-dom'; import { coreMock } from '@kbn/core/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; -import { ThemeDarkModeToggle } from './theme_darkmode_toggle'; +import { AppearanceSelector } from './appearance_selector'; const mockUseUpdateUserProfile = jest.fn(); @@ -23,7 +23,9 @@ jest.mock('@kbn/user-profile-components', () => { }; }); -describe('ThemeDarkModeToggle', () => { +describe('AppearanceSelector', () => { + const closePopover = jest.fn(); + it('renders correctly and toggles dark mode', () => { const security = securityMock.createStart(); const core = coreMock.createStart(); @@ -36,7 +38,7 @@ describe('ThemeDarkModeToggle', () => { }); const { getByTestId, rerender } = render( - + ); const toggleSwitch = getByTestId('darkModeToggleSwitch'); @@ -51,7 +53,7 @@ describe('ThemeDarkModeToggle', () => { }); // Rerender the component to apply the new props - rerender(); + rerender(); fireEvent.click(toggleSwitch); expect(mockUpdate).toHaveBeenLastCalledWith({ userSettings: { darkMode: 'light' } }); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.tsx new file mode 100644 index 0000000000000..5ea942c6e7b0f --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.tsx @@ -0,0 +1,73 @@ +/* + * 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, { useRef } from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import { UserProfilesKibanaProvider } from '@kbn/user-profile-components'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { OverlayRef } from '@kbn/core-mount-utils-browser'; + +import { AppearanceModal } from './appearance_modal'; + +interface Props { + security: SecurityPluginStart; + core: CoreStart; + closePopover: () => void; +} + +export const AppearanceSelector = ({ security, core, closePopover }: Props) => { + return ( + + + + ); +}; + +function AppearanceSelectorUI({ + core, + closePopover, +}: { + core: CoreStart; + closePopover: () => void; +}) { + const modalRef = useRef(null); + + const closeModal = () => { + modalRef.current?.close(); + modalRef.current = null; + }; + + const openModal = () => { + modalRef.current = core.overlays.openModal( + toMountPoint( + , + core + ) + ); + }; + + return ( + <> + { + openModal(); + closePopover(); + }} + data-test-subj="appearanceSelector" + > + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceLinkText', { + defaultMessage: 'Appearance', + })} + + + ); +} diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx deleted file mode 100644 index 731dc6768c48f..0000000000000 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { - EuiContextMenuItem, - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, - useEuiTheme, - useGeneratedHtmlId, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { SecurityPluginStart } from '@kbn/security-plugin/public'; -import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { UserProfilesKibanaProvider } from '@kbn/user-profile-components'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; -import { toMountPoint } from '@kbn/react-kibana-mount'; - -import { useThemeDarkmodeToggle } from './theme_darkmode_hook'; - -interface Props { - security: SecurityPluginStart; - core: CoreStart; -} - -export const ThemeDarkModeToggle = ({ security, core }: Props) => { - return ( - - - - ); -}; - -function ThemeDarkModeToggleUi({ uiSettingsClient }: { uiSettingsClient: IUiSettingsClient }) { - const toggleTextSwitchId = useGeneratedHtmlId({ prefix: 'toggleTextSwitch' }); - const { euiTheme } = useEuiTheme(); - - const { isVisible, toggle, isDarkModeOn, colorScheme } = useThemeDarkmodeToggle({ - uiSettingsClient, - }); - - if (!isVisible) { - return null; - } - - return ( - - - { - const on = colorScheme === 'light' ? true : false; - toggle(on); - }} - data-test-subj="darkModeToggle" - > - {i18n.translate('xpack.cloudLinks.userMenuLinks.darkModeToggle', { - defaultMessage: 'Dark mode', - })} - - - - { - toggle(e.target.checked); - }} - aria-describedby={toggleTextSwitchId} - data-test-subj="darkModeToggleSwitch" - compressed - /> - - - ); -} diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/use_appearance_hook.ts similarity index 96% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/use_appearance_hook.ts index 0e062b693a24f..a7e39596fdc8c 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/use_appearance_hook.ts @@ -15,7 +15,7 @@ interface Deps { uiSettingsClient: IUiSettingsClient; } -export const useThemeDarkmodeToggle = ({ uiSettingsClient }: Deps) => { +export const useAppearance = ({ uiSettingsClient }: Deps) => { const [isDarkModeOn, setIsDarkModeOn] = useState(false); const isMounted = useMountedState(); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx index 16ffa32360f25..f43fc3c942b5d 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { SecurityPluginStart, UserMenuLink } from '@kbn/security-plugin/public'; import type { CoreStart } from '@kbn/core/public'; -import { ThemeDarkModeToggle } from './theme_darkmode_toggle'; +import { AppearanceSelector } from './appearance_selector'; export const createUserMenuLinks = ({ core, @@ -60,7 +60,9 @@ export const createUserMenuLinks = ({ } userMenuLinks.push({ - content: , + content: ({ closePopover }) => ( + + ), order: 400, label: '', iconType: '', diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 431ceaefb1bfc..346897d0dde44 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -29,19 +29,26 @@ import { UserAvatar, type UserProfileAvatarData } from '@kbn/user-profile-compon import { getUserDisplayName, isUserAnonymous } from '../../common/model'; import { useCurrentUser, useUserProfile } from '../components'; -type ContextMenuItem = Omit & { content?: ReactNode }; +type ContextMenuItem = Omit & { + content?: ReactNode | ((args: { closePopover: () => void }) => ReactNode); +}; interface ContextMenuProps { items: ContextMenuItem[]; + closePopover: () => void; } -const ContextMenuContent = ({ items }: ContextMenuProps) => { +const ContextMenuContent = ({ items, closePopover }: ContextMenuProps) => { return ( <> {items.map((item, i) => { if (item.content) { - return {item.content}; + return ( + + {typeof item.content === 'function' ? item.content({ closePopover }) : item.content} + + ); } return ( = ({ { id: 0, title: displayName, - content: , + content: ( + setIsPopoverOpen(false)} /> + ), }, ]} data-test-subj="userMenu" From 290d9d51506e7e9af49acd23e39401405380d039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 6 Dec 2024 19:09:58 +0100 Subject: [PATCH 02/32] Move to folder --- .../{ => appearance_selector}/appearance_modal.tsx | 0 .../appearance_selector.test.tsx | 0 .../{ => appearance_selector}/appearance_selector.tsx | 0 .../maybe_add_cloud_links/appearance_selector/index.ts | 8 ++++++++ .../{ => appearance_selector}/use_appearance_hook.ts | 0 5 files changed, 8 insertions(+) rename x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/{ => appearance_selector}/appearance_modal.tsx (100%) rename x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/{ => appearance_selector}/appearance_selector.test.tsx (100%) rename x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/{ => appearance_selector}/appearance_selector.tsx (100%) create mode 100644 x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/index.ts rename x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/{ => appearance_selector}/use_appearance_hook.ts (100%) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx similarity index 100% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_modal.tsx rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.test.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx similarity index 100% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.test.tsx rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx similarity index 100% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector.tsx rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/index.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/index.ts new file mode 100644 index 0000000000000..cad2bbd3d6ae4 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { AppearanceSelector } from './appearance_selector'; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/use_appearance_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts similarity index 100% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/use_appearance_hook.ts rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts From 496af3c985b75d7e0ce0210b76c157c232e9978f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 6 Dec 2024 21:22:21 +0000 Subject: [PATCH 03/32] WIP design --- .../appearance_selector/appearance_modal.tsx | 107 +++++++++++++++++- .../appearance_selector.tsx | 5 +- .../appearance_selector/values_group.tsx | 100 ++++++++++++++++ 3 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index a2910d837c02c..17ceb2da670ed 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC, useState } from 'react'; import { EuiButton, EuiModal, @@ -14,10 +14,72 @@ import { EuiModalHeaderTitle, EuiSpacer, useGeneratedHtmlId, + EuiButtonEmpty, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { type Value, ValuesGroup } from './values_group'; // import { useAppearance } from './use_appearance_hook'; +type ColorMode = 'system' | 'light' | 'dark' | 'space'; +type Contrast = 'system' | 'normal' | 'high'; + +const systemLabel = i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSystemLabel', { + defaultMessage: 'System', +}); + +const colorModeOptions: Array> = [ + { + id: 'system', + label: systemLabel, + icon: 'desktop', + }, + { + id: 'light', + label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalLightLabel', { + defaultMessage: 'Light', + }), + icon: 'sun', + }, + { + id: 'dark', + label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalDarkLabel', { + defaultMessage: 'Dark', + }), + icon: 'moon', + }, + { + id: 'space', + label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSpaceDefaultLabel', { + defaultMessage: 'Space default', + }), + icon: 'spaces', + }, +]; + +const contrastOptions: Array> = [ + { + id: 'system', + label: systemLabel, + icon: 'desktop', + }, + { + id: 'normal', + label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalNormalLabel', { + defaultMessage: 'Normal', + }), + icon: 'crosshairs', + }, + { + id: 'high', + label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalHighLabel', { + defaultMessage: 'High', + }), + icon: 'crosshairs', + }, +]; + interface Props { closeModal: () => void; uiSettingsClient: IUiSettingsClient; @@ -25,31 +87,64 @@ interface Props { export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => { const modalTitleId = useGeneratedHtmlId(); + const [colorMode, setColorMode] = useState('system'); + const [contrast, setContrast] = useState('system'); // const { isVisible, toggle, isDarkModeOn, colorScheme } = useAppearance({ // uiSettingsClient, // }); return ( - + - Modal title + + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalTitle', { + defaultMessage: 'Appearance', + })} + - This modal has the following setup: + + title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeLabel', { + defaultMessage: 'Color mode', + })} + values={colorModeOptions} + selectedValue={colorMode} + onChange={setColorMode} + /> + + + + title={i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalInterfaceContrastLabel', + { + defaultMessage: 'Interface contrast', + } + )} + values={contrastOptions} + selectedValue={contrast} + onChange={setContrast} + /> -

Content comes here!

+ + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalDiscardBtnLabel', { + defaultMessage: 'Discard', + })} + + { // console.log('close'); }} fill > - Close + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSaveBtnLabel', { + defaultMessage: 'Save changes', + })}
diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx index 5ea942c6e7b0f..b2d9fa8f1a12f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx @@ -49,14 +49,15 @@ function AppearanceSelectorUI({ toMountPoint( , core - ) + ), + { 'data-test-subj': 'appearanceModal', maxWidth: 600 } ); }; return ( <> { openModal(); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx new file mode 100644 index 0000000000000..d158fa57e4dc9 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx @@ -0,0 +1,100 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + type EuiThemeComputed, + useEuiTheme, +} from '@elastic/eui'; +import classNames from 'classnames'; + +import { css } from '@emotion/react'; + +export interface Value { + label: string; + id: T; + icon: string; +} + +const getStyles = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => ({ + title: css` + font-weight: 600; + `, + group: css` + padding-top: ${euiTheme.size.s}; + `, + item: css` + border: 1px solid transparent; + border-radius: ${euiTheme.border.radius.medium}; + padding: ${euiTheme.size.m}; + min-width: 100px; + + &.valueItem--selected { + background-color: ${euiTheme.colors.backgroundLightPrimary}; + } + + &:hover { + border-color: ${euiTheme.colors.backgroundLightPrimary}; + } + `, +}); + +interface Props { + title: string; + values: Array>; + selectedValue: T; + onChange: (id: T) => void; +} + +export function ValuesGroup({ + title, + values, + onChange, + selectedValue, +}: Props) { + const { euiTheme } = useEuiTheme(); + const styles = getStyles({ euiTheme }); + + return ( + <> + + {title} + + + {values.map(({ id, label, icon }) => ( + + ))} + + + ); +} From fa32871d7e873bb1576118931289b5bfa80dbcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 6 Dec 2024 21:48:34 +0000 Subject: [PATCH 04/32] Use EuiKeyPadMenuItem --- .../appearance_selector/appearance_modal.tsx | 10 ++++ .../appearance_selector/values_group.tsx | 48 +++++++++---------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 17ceb2da670ed..0475e30f8427b 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -55,6 +55,16 @@ const colorModeOptions: Array> = [ defaultMessage: 'Space default', }), icon: 'spaces', + betaBadgeLabel: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalBetaBadgeLabel', { + defaultMessage: 'Deprecated', + }), + betaBadgeTooltipContent: i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalBetaBadgeTooltip', + { + defaultMessage: 'Space default settings will be deprecated in 10.0.', + } + ), + betaBadgeIconType: 'warning', }, ]; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx index d158fa57e4dc9..b8ce3c18f657f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx @@ -13,8 +13,8 @@ import { EuiText, type EuiThemeComputed, useEuiTheme, + EuiKeyPadMenuItem, } from '@elastic/eui'; -import classNames from 'classnames'; import { css } from '@emotion/react'; @@ -22,6 +22,9 @@ export interface Value { label: string; id: T; icon: string; + betaBadgeLabel?: string; + betaBadgeTooltipContent?: string; + betaBadgeIconType?: string; } const getStyles = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => ({ @@ -68,32 +71,27 @@ export function ValuesGroup({ {title} + - {values.map(({ id, label, icon }) => ( - - ))} + ) + )} ); From 6dad327b464c8ece32e48b6c2cd5759c17814661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 6 Dec 2024 21:56:05 +0000 Subject: [PATCH 05/32] Add deprecation callout --- .../appearance_selector/appearance_modal.tsx | 27 +++++++++++++++++++ .../appearance_selector/values_group.tsx | 14 ---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 0475e30f8427b..1c257486988ff 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -15,6 +15,7 @@ import { EuiSpacer, useGeneratedHtmlId, EuiButtonEmpty, + EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -125,6 +126,32 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => /> + {colorMode === 'space' && ( + <> + +

+ {i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultDescr', + { + defaultMessage: + 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.', + } + )} +

+
+ + + )} + title={i18n.translate( 'xpack.cloudLinks.userMenuLinks.appearanceModalInterfaceContrastLabel', diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx index b8ce3c18f657f..da8f4b5267cf7 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx @@ -34,20 +34,6 @@ const getStyles = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => ({ group: css` padding-top: ${euiTheme.size.s}; `, - item: css` - border: 1px solid transparent; - border-radius: ${euiTheme.border.radius.medium}; - padding: ${euiTheme.size.m}; - min-width: 100px; - - &.valueItem--selected { - background-color: ${euiTheme.colors.backgroundLightPrimary}; - } - - &:hover { - border-color: ${euiTheme.colors.backgroundLightPrimary}; - } - `, }); interface Props { From 65214ef58862b07945439f77d059e789a44ab223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 9 Dec 2024 12:32:04 +0000 Subject: [PATCH 06/32] Update user profile with system option --- .../kbn-user-profile-components/src/types.ts | 2 +- .../user_profile/user_profile.tsx | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/kbn-user-profile-components/src/types.ts b/packages/kbn-user-profile-components/src/types.ts index ff74061e0ef39..7d55f73fa25a6 100644 --- a/packages/kbn-user-profile-components/src/types.ts +++ b/packages/kbn-user-profile-components/src/types.ts @@ -27,7 +27,7 @@ export interface UserProfileAvatarData { imageUrl?: string | null; } -export type DarkModeValue = '' | 'dark' | 'light'; +export type DarkModeValue = 'system' | 'dark' | 'light' | 'space_default'; /** * User settings stored in the data object of the User Profile diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx index 6d2fd7344850d..b0a3c8fdf4045 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx @@ -11,6 +11,7 @@ import { EuiButton, EuiButtonEmpty, EuiButtonGroup, + EuiCallOut, EuiColorPicker, EuiDescribedFormGroup, EuiDescriptionList, @@ -229,7 +230,7 @@ const UserSettingsEditor: FunctionComponent = ({ = ({ ), }} + css={css` + inline-size: 420px; // Allow for 4 items to fit in a row instead of the default 3 + `} > {themeItem({ - id: '', - label: i18n.translate('xpack.security.accountManagement.userProfile.defaultModeButton', { - defaultMessage: 'Space default', + id: 'system', + label: i18n.translate('xpack.security.accountManagement.userProfile.systemModeButton', { + defaultMessage: 'System', }), - icon: 'spaces', + icon: 'desktop', })} {themeItem({ id: 'light', @@ -282,6 +286,13 @@ const UserSettingsEditor: FunctionComponent = ({ }), icon: 'moon', })} + {themeItem({ + id: 'space_default', + label: i18n.translate('xpack.security.accountManagement.userProfile.defaultModeButton', { + defaultMessage: 'Space default', + }), + icon: 'spaces', + })} ); return themeOverridden ? ( @@ -301,6 +312,32 @@ const UserSettingsEditor: FunctionComponent = ({ ); }; + const deprecatedWarning = idSelected === 'space_default' && ( + <> + + +

+ {i18n.translate( + 'xpack.security.accountManagement.userProfile.deprecatedSpaceDefaultDescr', + { + defaultMessage: + 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.', + } + )} +

+
+ + ); + return ( = ({ } > - {themeMenu(isThemeOverridden)} + <> + {themeMenu(isThemeOverridden)} + {deprecatedWarning} + ); @@ -911,7 +951,7 @@ export function useUserProfileForm({ user, data }: UserProfileProps) { imageUrl: data.avatar?.imageUrl || '', }, userSettings: { - darkMode: data.userSettings?.darkMode || '', + darkMode: data.userSettings?.darkMode || 'space_default', }, } : undefined, From 9a992da10b067a6d9a9929797170d4c46a4e982e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 9 Dec 2024 12:36:35 +0000 Subject: [PATCH 07/32] Use EuiKeyPadMenu instead of flex groups --- .../appearance_selector/appearance_modal.tsx | 22 ++++-- .../appearance_selector/values_group.tsx | 77 ++++++++----------- 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 1c257486988ff..e16cc3c5a84d5 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -23,7 +23,7 @@ import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { type Value, ValuesGroup } from './values_group'; // import { useAppearance } from './use_appearance_hook'; -type ColorMode = 'system' | 'light' | 'dark' | 'space'; +type ColorMode = 'system' | 'light' | 'dark' | 'space_default'; type Contrast = 'system' | 'normal' | 'high'; const systemLabel = i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSystemLabel', { @@ -51,7 +51,7 @@ const colorModeOptions: Array> = [ icon: 'moon', }, { - id: 'space', + id: 'space_default', label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSpaceDefaultLabel', { defaultMessage: 'Space default', }), @@ -117,16 +117,22 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => - title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeLabel', { + title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeTitle', { defaultMessage: 'Color mode', })} values={colorModeOptions} selectedValue={colorMode} onChange={setColorMode} + ariaLabel={i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalColorModeAriaLabel', + { + defaultMessage: 'Appearance color mode', + } + )} /> - {colorMode === 'space' && ( + {colorMode === 'space_default' && ( <> = ({ closeModal, uiSettingsClient }) => title={i18n.translate( - 'xpack.cloudLinks.userMenuLinks.appearanceModalInterfaceContrastLabel', + 'xpack.cloudLinks.userMenuLinks.appearanceModalInterfaceContrastTitle', { defaultMessage: 'Interface contrast', } @@ -162,6 +168,12 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => values={contrastOptions} selectedValue={contrast} onChange={setContrast} + ariaLabel={i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalContrastAriaLabel', + { + defaultMessage: 'Appearance contrast', + } + )} /> diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx index da8f4b5267cf7..faff8ad1d917c 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx @@ -6,16 +6,7 @@ */ import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiText, - type EuiThemeComputed, - useEuiTheme, - EuiKeyPadMenuItem, -} from '@elastic/eui'; - +import { EuiIcon, EuiKeyPadMenuItem, EuiKeyPadMenu } from '@elastic/eui'; import { css } from '@emotion/react'; export interface Value { @@ -27,20 +18,13 @@ export interface Value { betaBadgeIconType?: string; } -const getStyles = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => ({ - title: css` - font-weight: 600; - `, - group: css` - padding-top: ${euiTheme.size.s}; - `, -}); - interface Props { title: string; values: Array>; selectedValue: T; onChange: (id: T) => void; + ariaLabel: string; + // legend: string; } export function ValuesGroup({ @@ -48,37 +32,36 @@ export function ValuesGroup({ values, onChange, selectedValue, + ariaLabel, }: Props) { - const { euiTheme } = useEuiTheme(); - const styles = getStyles({ euiTheme }); - return ( <> - - {title} - - - - {values.map( - ({ id, label, icon, betaBadgeIconType, betaBadgeLabel, betaBadgeTooltipContent }) => ( - - { - onChange(id); - }} - betaBadgeLabel={betaBadgeLabel} - betaBadgeTooltipContent={betaBadgeTooltipContent} - betaBadgeIconType={betaBadgeIconType} - > - - - - ) - )} - + {title}, + }} + css={css` + inline-size: 420px; // Allow for 4 items to fit in a row instead of the default 3 + `} + > + {values.map(({ id, label, icon }) => ( + { + onChange(id); + }} + data-test-subj={`colorModeKeyPadItem${id}`} + > + + + ))} + ); } From 859f7329f05310c3396a14786b77cf9e2701afe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 9 Dec 2024 12:53:12 +0000 Subject: [PATCH 08/32] Update UserSettingsService to return "system" value --- .../src/user_settings_service.test.ts | 34 +++++++++++++++++++ .../src/user_settings_service.ts | 7 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.test.ts b/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.test.ts index 9884300d7239c..02e11a20ca36e 100644 --- a/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.test.ts +++ b/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.test.ts @@ -70,6 +70,23 @@ describe('#setup', () => { }); }); + it('fetches userSettings when client is set and returns `system` when `darkMode` is set to `system`', async () => { + startDeps.userProfile.getCurrent.mockResolvedValue(createUserProfile('system')); + + const { getUserSettingDarkMode } = service.setup(); + service.start(startDeps); + + const kibanaRequest = httpServerMock.createKibanaRequest(); + const darkMode = await getUserSettingDarkMode(kibanaRequest); + + expect(darkMode).toEqual('system'); + expect(startDeps.userProfile.getCurrent).toHaveBeenCalledTimes(1); + expect(startDeps.userProfile.getCurrent).toHaveBeenCalledWith({ + request: kibanaRequest, + dataPath: 'userSettings', + }); + }); + it('fetches userSettings when client is set and returns `undefined` when `darkMode` is set to `` (the default value)', async () => { startDeps.userProfile.getCurrent.mockResolvedValue(createUserProfile('')); @@ -87,6 +104,23 @@ describe('#setup', () => { }); }); + it('fetches userSettings when client is set and returns `undefined` when `darkMode` is set to `space_default`', async () => { + startDeps.userProfile.getCurrent.mockResolvedValue(createUserProfile('space_default')); + + const { getUserSettingDarkMode } = service.setup(); + service.start(startDeps); + + const kibanaRequest = httpServerMock.createKibanaRequest(); + const darkMode = await getUserSettingDarkMode(kibanaRequest); + + expect(darkMode).toEqual(undefined); + expect(startDeps.userProfile.getCurrent).toHaveBeenCalledTimes(1); + expect(startDeps.userProfile.getCurrent).toHaveBeenCalledWith({ + request: kibanaRequest, + dataPath: 'userSettings', + }); + }); + it('does not fetch userSettings when client is not set, returns `undefined`, and logs a debug statement', async () => { const { getUserSettingDarkMode } = service.setup(); diff --git a/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts b/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts index ab6eb501e9643..29a30e41ab824 100644 --- a/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts +++ b/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts @@ -67,8 +67,11 @@ export class UserSettingsService { const getUserSettingDarkMode = ( userSettings: Record ): DarkModeValue | undefined => { - if (userSettings?.darkMode) { - return userSettings.darkMode.toUpperCase() === 'DARK'; + if (userSettings.darkMode) { + const { darkMode } = userSettings; + if (darkMode === 'space_default') return undefined; + + return darkMode.toUpperCase() === 'SYSTEM' ? 'system' : darkMode.toUpperCase() === 'DARK'; } return undefined; }; From 47355a5167c2c358fb6b0ecf2501d1977b1ebcdb Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:06:33 +0000 Subject: [PATCH 09/32] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json index b7759fb7f1c5e..a9b5b4025acb5 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json +++ b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json @@ -25,6 +25,7 @@ "@kbn/share-plugin", "@kbn/cloud", "@kbn/react-kibana-mount", + "@kbn/core-mount-utils-browser", ], "exclude": [ "target/**/*", From 97f4001483f3b14ad915cea6f31f3925c7a411ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 9 Dec 2024 18:10:04 +0000 Subject: [PATCH 10/32] Fix i18n issues --- .../plugins/private/translations/translations/fr-FR.json | 3 --- .../plugins/private/translations/translations/ja-JP.json | 3 --- .../plugins/private/translations/translations/zh-CN.json | 3 --- 3 files changed, 9 deletions(-) diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 503e24e6b9540..f6dc7638b52bd 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -14185,9 +14185,6 @@ "xpack.cloudLinks.userMenuLinks.billingLinkText": "Facturation", "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText": "Recharger la page pour afficher les modifications", "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle": "Thème de couleurs actualisé", - "xpack.cloudLinks.userMenuLinks.darkModeOffLabel": "désactivé", - "xpack.cloudLinks.userMenuLinks.darkModeOnLabel": "le", - "xpack.cloudLinks.userMenuLinks.darkModeToggle": "Mode sombre", "xpack.cloudLinks.userMenuLinks.organizationLinkText": "Organisation", "xpack.cloudLinks.userMenuLinks.profileLinkText": "Profil", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "Créer un modèle de suivi automatique", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 25f2016d88309..1c42fbc7e12b2 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -14055,9 +14055,6 @@ "xpack.cloudLinks.userMenuLinks.billingLinkText": "請求", "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText": "変更を確認するには、ページを再読み込みしてください", "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle": "カラーテーマが更新されました", - "xpack.cloudLinks.userMenuLinks.darkModeOffLabel": "オフ", - "xpack.cloudLinks.userMenuLinks.darkModeOnLabel": "日付", - "xpack.cloudLinks.userMenuLinks.darkModeToggle": "ダークモード", "xpack.cloudLinks.userMenuLinks.organizationLinkText": "組織別", "xpack.cloudLinks.userMenuLinks.profileLinkText": "プロフィール", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "自動フォローパターンを作成", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index ef52b9d34598c..adb566dd7e025 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -13810,9 +13810,6 @@ "xpack.cloudLinks.userMenuLinks.billingLinkText": "帐单", "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText": "重新加载页面以查看更改", "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle": "已更新颜色主题", - "xpack.cloudLinks.userMenuLinks.darkModeOffLabel": "关闭", - "xpack.cloudLinks.userMenuLinks.darkModeOnLabel": "在", - "xpack.cloudLinks.userMenuLinks.darkModeToggle": "深色模式", "xpack.cloudLinks.userMenuLinks.organizationLinkText": "组织", "xpack.cloudLinks.userMenuLinks.profileLinkText": "配置文件", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "创建自动跟随模式", From c6e784fe7ebc41c5952a77d7e257964d645e37be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 9 Dec 2024 18:10:15 +0000 Subject: [PATCH 11/32] Cleanup --- .../maybe_add_cloud_links/appearance_selector/values_group.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx index faff8ad1d917c..30cce1a5d0e68 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/values_group.tsx @@ -24,7 +24,6 @@ interface Props { selectedValue: T; onChange: (id: T) => void; ariaLabel: string; - // legend: string; } export function ValuesGroup({ From 2dfbf8460eab678a53923475c0ea099b4ea200ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 11 Dec 2024 13:29:38 +0000 Subject: [PATCH 12/32] Update user profile from appearance modal --- .../src/hooks/use_update_user_profile.tsx | 7 +- .../appearance_selector/appearance_modal.tsx | 69 ++++--------------- .../appearance_selector.tsx | 51 +++++++------- .../use_appearance_hook.ts | 62 +++++++++++------ 4 files changed, 87 insertions(+), 102 deletions(-) diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx index 57aeec7a51d5a..a777db694e542 100644 --- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx +++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx @@ -125,9 +125,10 @@ export const useUpdateUserProfile = ({ >(updatedData: D) => { userProfileSnapshot.current = merge({}, userProfileData); setIsLoading(true); - return userProfileApiClient - .partialUpdate(updatedData) - .then(() => onUserProfileUpdate(updatedData)); + return userProfileApiClient.partialUpdate(updatedData).then(() => { + onUserProfileUpdate(updatedData); + return updatedData; + }); }, [userProfileApiClient, onUserProfileUpdate, userProfileData] ); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index e16cc3c5a84d5..5f1478e57a5cc 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { type FC, useState } from 'react'; +import React, { type FC } from 'react'; import { EuiButton, EuiModal, @@ -20,11 +20,9 @@ import { import { i18n } from '@kbn/i18n'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import type { DarkModeValue as ColorMode } from '@kbn/user-profile-components'; import { type Value, ValuesGroup } from './values_group'; -// import { useAppearance } from './use_appearance_hook'; - -type ColorMode = 'system' | 'light' | 'dark' | 'space_default'; -type Contrast = 'system' | 'normal' | 'high'; +import { useAppearance } from './use_appearance_hook'; const systemLabel = i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSystemLabel', { defaultMessage: 'System', @@ -69,28 +67,6 @@ const colorModeOptions: Array> = [ }, ]; -const contrastOptions: Array> = [ - { - id: 'system', - label: systemLabel, - icon: 'desktop', - }, - { - id: 'normal', - label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalNormalLabel', { - defaultMessage: 'Normal', - }), - icon: 'crosshairs', - }, - { - id: 'high', - label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalHighLabel', { - defaultMessage: 'High', - }), - icon: 'crosshairs', - }, -]; - interface Props { closeModal: () => void; uiSettingsClient: IUiSettingsClient; @@ -98,12 +74,10 @@ interface Props { export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => { const modalTitleId = useGeneratedHtmlId(); - const [colorMode, setColorMode] = useState('system'); - const [contrast, setContrast] = useState('system'); - // const { isVisible, toggle, isDarkModeOn, colorScheme } = useAppearance({ - // uiSettingsClient, - // }); + const { onChange, colorMode, isLoading } = useAppearance({ + uiSettingsClient, + }); return ( @@ -122,7 +96,9 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => })} values={colorModeOptions} selectedValue={colorMode} - onChange={setColorMode} + onChange={(id) => { + onChange({ colorMode: id }, false); + }} ariaLabel={i18n.translate( 'xpack.cloudLinks.userMenuLinks.appearanceModalColorModeAriaLabel', { @@ -130,10 +106,10 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => } )} /> - {colorMode === 'space_default' && ( <> + = ({ closeModal, uiSettingsClient }) => )} - - - title={i18n.translate( - 'xpack.cloudLinks.userMenuLinks.appearanceModalInterfaceContrastTitle', - { - defaultMessage: 'Interface contrast', - } - )} - values={contrastOptions} - selectedValue={contrast} - onChange={setContrast} - ariaLabel={i18n.translate( - 'xpack.cloudLinks.userMenuLinks.appearanceModalContrastAriaLabel', - { - defaultMessage: 'Appearance contrast', - } - )} - /> - @@ -186,10 +143,12 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => { - // console.log('close'); + onClick={async () => { + await onChange({ colorMode }, true); + closeModal(); }} fill + isLoading={isLoading} > {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSaveBtnLabel', { defaultMessage: 'Save changes', diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx index b2d9fa8f1a12f..e5927e06aadfc 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx @@ -15,6 +15,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import type { OverlayRef } from '@kbn/core-mount-utils-browser'; import { AppearanceModal } from './appearance_modal'; +import { useAppearance } from './use_appearance_hook'; interface Props { security: SecurityPluginStart; @@ -25,18 +26,16 @@ interface Props { export const AppearanceSelector = ({ security, core, closePopover }: Props) => { return ( - + ); }; -function AppearanceSelectorUI({ - core, - closePopover, -}: { - core: CoreStart; - closePopover: () => void; -}) { +function AppearanceSelectorUI({ security, core, closePopover }: Props) { + const { isVisible } = useAppearance({ + uiSettingsClient: core.uiSettings, + }); + const modalRef = useRef(null); const closeModal = () => { @@ -47,28 +46,32 @@ function AppearanceSelectorUI({ const openModal = () => { modalRef.current = core.overlays.openModal( toMountPoint( - , + + + , core ), { 'data-test-subj': 'appearanceModal', maxWidth: 600 } ); }; + if (!isVisible) { + return null; + } + return ( - <> - { - openModal(); - closePopover(); - }} - data-test-subj="appearanceSelector" - > - {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceLinkText', { - defaultMessage: 'Appearance', - })} - - + { + openModal(); + closePopover(); + }} + data-test-subj="appearanceSelector" + > + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceLinkText', { + defaultMessage: 'Appearance', + })} + ); } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts index a7e39596fdc8c..cf66cee03026b 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts @@ -8,28 +8,40 @@ import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { useUpdateUserProfile } from '@kbn/user-profile-components'; -import useMountedState from 'react-use/lib/useMountedState'; +import { + useUpdateUserProfile, + type DarkModeValue as ColorMode, +} from '@kbn/user-profile-components'; + +const parseDarkModeValue = (rawValue: unknown): ColorMode => { + if (rawValue === true || rawValue === 'true' || rawValue === 'enabled') { + return 'dark'; + } + if (rawValue === false || rawValue === 'false' || rawValue === 'disabled') { + return 'light'; + } + if (rawValue === 'system') { + return 'system'; + } + return 'space_default'; +}; interface Deps { uiSettingsClient: IUiSettingsClient; } export const useAppearance = ({ uiSettingsClient }: Deps) => { - const [isDarkModeOn, setIsDarkModeOn] = useState(false); - const isMounted = useMountedState(); - // 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 } = useUpdateUserProfile({ notificationSuccess: { - title: i18n.translate('xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle', { - defaultMessage: 'Color theme updated', + title: i18n.translate('xpack.cloudLinks.userMenuLinks.appearance.successNotificationTitle', { + defaultMessage: 'Appearance settings updated', }), pageReloadText: i18n.translate( - 'xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText', + 'xpack.cloudLinks.userMenuLinks.appearance.successNotificationText', { defaultMessage: 'Reload the page to see the changes', } @@ -42,24 +54,34 @@ export const useAppearance = ({ uiSettingsClient }: Deps) => { const { userSettings: { - darkMode: colorScheme = uiSettingsClient.get('theme:darkMode') === true ? 'dark' : 'light', + darkMode: colorModeUserProfile = parseDarkModeValue(uiSettingsClient.get('theme:darkMode')), } = {}, } = userProfileData ?? { userSettings: {}, }; - const toggle = useCallback( - (on: boolean) => { + const [colorMode, setColorMode] = useState(colorModeUserProfile); + + const onChange = useCallback( + ({ colorMode: updatedColorMode }: { colorMode?: ColorMode }, persist: boolean) => { if (isLoading) { return; } // optimistic update - setIsDarkModeOn(on); + if (updatedColorMode) { + setColorMode(updatedColorMode); + } + + // TODO: here we will update the contrast when available + + if (!persist) { + return; + } - update({ + return update({ userSettings: { - darkMode: on ? 'dark' : 'light', + darkMode: updatedColorMode, }, }); }, @@ -67,14 +89,14 @@ export const useAppearance = ({ uiSettingsClient }: Deps) => { ); useEffect(() => { - if (!isMounted()) return; - setIsDarkModeOn(colorScheme === 'dark'); - }, [isMounted, colorScheme]); + setColorMode(colorModeUserProfile); + }, [colorModeUserProfile]); return { isVisible: valueSetInKibanaConfig ? false : Boolean(userProfileData), - toggle, - isDarkModeOn, - colorScheme, + setColorMode, + colorMode, + onChange, + isLoading, }; }; From b00576192ffb97f0023daf3fc631d6d01c052ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 11 Dec 2024 13:30:19 +0000 Subject: [PATCH 13/32] Update jest test --- .../appearance_selector.test.tsx | 37 ++----------------- .../user_profile/user_profile.test.tsx | 8 ++-- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx index da4200e9070f0..0076b150290a0 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx @@ -13,16 +13,6 @@ import { securityMock } from '@kbn/security-plugin/public/mocks'; import { AppearanceSelector } from './appearance_selector'; -const mockUseUpdateUserProfile = jest.fn(); - -jest.mock('@kbn/user-profile-components', () => { - const original = jest.requireActual('@kbn/user-profile-components'); - return { - ...original, - useUpdateUserProfile: () => mockUseUpdateUserProfile(), - }; -}); - describe('AppearanceSelector', () => { const closePopover = jest.fn(); @@ -30,32 +20,13 @@ describe('AppearanceSelector', () => { const security = securityMock.createStart(); const core = coreMock.createStart(); - const mockUpdate = jest.fn(); - mockUseUpdateUserProfile.mockReturnValue({ - userProfileData: { userSettings: { darkMode: 'light' } }, - isLoading: false, - update: mockUpdate, - }); - - const { getByTestId, rerender } = render( + const { getByTestId } = render( ); - 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(); + const appearanceSelector = getByTestId('appearanceSelector'); + fireEvent.click(appearanceSelector); - fireEvent.click(toggleSwitch); - expect(mockUpdate).toHaveBeenLastCalledWith({ userSettings: { darkMode: 'light' } }); + expect(core.overlays.openModal).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx index 7a20eaf3eff1c..c473a24e6d668 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx @@ -76,7 +76,7 @@ describe('useUserProfileForm', () => { "initials": "fn", }, "userSettings": Object { - "darkMode": "", + "darkMode": "space_default", }, }, "user": Object { @@ -259,7 +259,7 @@ describe('useUserProfileForm', () => { expect(themeMenu).toHaveLength(1); const themeOptions = themeMenu.find('EuiKeyPadMenuItem'); - expect(themeOptions).toHaveLength(3); + expect(themeOptions).toHaveLength(4); themeOptions.forEach((option) => { const menuItemEl = (option.getDOMNode() as unknown as Element[])[1]; expect(menuItemEl.className).not.toContain('disabled'); @@ -343,7 +343,7 @@ describe('useUserProfileForm', () => { expect(themeMenu).toHaveLength(1); const themeOptions = themeMenu.find('EuiKeyPadMenuItem'); - expect(themeOptions).toHaveLength(3); + expect(themeOptions).toHaveLength(4); themeOptions.forEach((option) => { const menuItemEl = (option.getDOMNode() as unknown as Element[])[1]; expect(menuItemEl.className).toContain('disabled'); @@ -379,7 +379,7 @@ describe('useUserProfileForm', () => { expect(themeMenu).toHaveLength(1); const themeOptions = themeMenu.find('EuiKeyPadMenuItem'); - expect(themeOptions).toHaveLength(3); + expect(themeOptions).toHaveLength(4); themeOptions.forEach((option) => { const menuItemEl = (option.getDOMNode() as unknown as Element[])[1]; expect(menuItemEl.className).toContain('disabled'); From 447b229a3919cb545133ad4652915772b3ad6d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 11 Dec 2024 17:41:20 +0000 Subject: [PATCH 14/32] Fix i18n issue --- .../plugins/private/translations/translations/fr-FR.json | 2 -- .../plugins/private/translations/translations/ja-JP.json | 2 -- .../plugins/private/translations/translations/zh-CN.json | 2 -- 3 files changed, 6 deletions(-) diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index f6dc7638b52bd..268ab34566089 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -14183,8 +14183,6 @@ "xpack.cloudLinks.helpMenuLinks.support": "Support technique", "xpack.cloudLinks.setupGuide": "Guides de configuration", "xpack.cloudLinks.userMenuLinks.billingLinkText": "Facturation", - "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText": "Recharger la page pour afficher les modifications", - "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle": "Thème de couleurs actualisé", "xpack.cloudLinks.userMenuLinks.organizationLinkText": "Organisation", "xpack.cloudLinks.userMenuLinks.profileLinkText": "Profil", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "Créer un modèle de suivi automatique", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 1c42fbc7e12b2..350763c5e6743 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -14053,8 +14053,6 @@ "xpack.cloudLinks.helpMenuLinks.support": "サポート", "xpack.cloudLinks.setupGuide": "セットアップガイド", "xpack.cloudLinks.userMenuLinks.billingLinkText": "請求", - "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText": "変更を確認するには、ページを再読み込みしてください", - "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle": "カラーテーマが更新されました", "xpack.cloudLinks.userMenuLinks.organizationLinkText": "組織別", "xpack.cloudLinks.userMenuLinks.profileLinkText": "プロフィール", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "自動フォローパターンを作成", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index adb566dd7e025..791763e2042b1 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -13808,8 +13808,6 @@ "xpack.cloudLinks.helpMenuLinks.support": "支持", "xpack.cloudLinks.setupGuide": "设置指南", "xpack.cloudLinks.userMenuLinks.billingLinkText": "帐单", - "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationText": "重新加载页面以查看更改", - "xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle": "已更新颜色主题", "xpack.cloudLinks.userMenuLinks.organizationLinkText": "组织", "xpack.cloudLinks.userMenuLinks.profileLinkText": "配置文件", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "创建自动跟随模式", From 8a4468b88e63e932f020dd1f3037327829d063f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 12 Dec 2024 15:03:49 +0000 Subject: [PATCH 15/32] Update functional tests --- .../nav_control/nav_control_component.test.tsx | 4 ++++ .../functional/apps/user_profiles/user_profiles.ts | 12 ++++++------ .../functional/page_objects/user_profile_page.ts | 10 +++++----- 3 files changed, 15 insertions(+), 11 deletions(-) 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 08799c1ef910e..aa0a4249a36dd 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 @@ -214,6 +214,7 @@ describe('SecurityNavControl', () => { Array [ Object { "content": { Array [ Object { "content": { Array [ Object { "content": { Array [ Object { "content": { const themeKeyPadMenu = await pageObjects.userProfiles.getThemeKeypadMenu(); expect(themeKeyPadMenu).not.to.be(null); - await pageObjects.userProfiles.changeUserProfileTheme('Dark'); + await pageObjects.userProfiles.changeUserProfileTheme('dark'); const darkModeTag = await pageObjects.userProfiles.getThemeTag(); expect(darkModeTag).to.be('v8dark'); - await pageObjects.userProfiles.changeUserProfileTheme('Light'); + await pageObjects.userProfiles.changeUserProfileTheme('light'); const lightModeTag = await pageObjects.userProfiles.getThemeTag(); expect(lightModeTag).to.be('v8light'); - await pageObjects.userProfiles.changeUserProfileTheme('Space default'); + await pageObjects.userProfiles.changeUserProfileTheme('space_default'); const spaceDefaultModeTag = await pageObjects.userProfiles.getThemeTag(); expect(spaceDefaultModeTag).to.be('v8light'); }); @@ -131,15 +131,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { let spaceDefaultModeTag = await pageObjects.userProfiles.getThemeTag(); expect(spaceDefaultModeTag).to.be('v8dark'); - await pageObjects.userProfiles.changeUserProfileTheme('Light'); + await pageObjects.userProfiles.changeUserProfileTheme('light'); const lightModeTag = await pageObjects.userProfiles.getThemeTag(); expect(lightModeTag).to.be('v8light'); - await pageObjects.userProfiles.changeUserProfileTheme('Dark'); + await pageObjects.userProfiles.changeUserProfileTheme('dark'); const darkModeTag = await pageObjects.userProfiles.getThemeTag(); expect(darkModeTag).to.be('v8dark'); - await pageObjects.userProfiles.changeUserProfileTheme('Space default'); + await pageObjects.userProfiles.changeUserProfileTheme('space_default'); spaceDefaultModeTag = await pageObjects.userProfiles.getThemeTag(); expect(spaceDefaultModeTag).to.be('v8dark'); diff --git a/x-pack/test/functional/page_objects/user_profile_page.ts b/x-pack/test/functional/page_objects/user_profile_page.ts index d777d1c2ffcda..f9513997ac4db 100644 --- a/x-pack/test/functional/page_objects/user_profile_page.ts +++ b/x-pack/test/functional/page_objects/user_profile_page.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { DarkModeValue as ColorMode } from '@kbn/user-profile-components'; import { FtrProviderContext } from '../ftr_provider_context'; export function UserProfilePageProvider({ getService }: FtrProviderContext) { @@ -26,9 +27,8 @@ export function UserProfilePageProvider({ getService }: FtrProviderContext) { return await testSubjects.find('windowReloadButton'); }; - const getThemeKeypadButton = async (option: string) => { - option = option[0].toUpperCase() + option.substring(1).toLowerCase(); - return await testSubjects.find(`themeKeyPadItem${option}`); + const getThemeKeypadButton = async (colorMode: ColorMode) => { + return await testSubjects.find(`themeKeyPadItem${colorMode}`); }; const saveUserProfileChanges = async (): Promise => { @@ -40,8 +40,8 @@ export function UserProfilePageProvider({ getService }: FtrProviderContext) { }); }; - const changeUserProfileTheme = async (theme: string): Promise => { - const themeModeButton = await getThemeKeypadButton(theme); + const changeUserProfileTheme = async (colorMode: ColorMode): Promise => { + const themeModeButton = await getThemeKeypadButton(colorMode); expect(themeModeButton).not.to.be(null); await themeModeButton.click(); From 30a86ea7f72bb5c69d91a333e33a907f1e0c7b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 12 Dec 2024 15:32:48 +0000 Subject: [PATCH 16/32] Fix functional test --- x-pack/test/functional_cloud/tests/cloud_links.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional_cloud/tests/cloud_links.ts b/x-pack/test/functional_cloud/tests/cloud_links.ts index 1534c10002f90..e6450519085a9 100644 --- a/x-pack/test/functional_cloud/tests/cloud_links.ts +++ b/x-pack/test/functional_cloud/tests/cloud_links.ts @@ -146,10 +146,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(cloudLink).to.not.be(null); }); - it('Shows the theme darkMode toggle', async () => { - await PageObjects.common.clickAndValidate('userMenuButton', 'darkModeToggle'); - const darkModeSwitch = await find.byCssSelector('[data-test-subj="darkModeToggleSwitch"]'); - expect(darkModeSwitch).to.not.be(null); + it('Shows the appearance buton', async () => { + await PageObjects.common.clickAndValidate('userMenuButton', 'appearanceSelector'); }); }); }); From 4f4ce2843a5a0746f13433a6ee3a6bbe8552b17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 13 Dec 2024 12:57:31 +0000 Subject: [PATCH 17/32] Add functional tests --- .../appearance_selector/appearance_modal.tsx | 3 +- .../functional_cloud/tests/cloud_links.ts | 117 +++++++++++++++++- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 5f1478e57a5cc..73543b2a4dd42 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -136,13 +136,14 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => - + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalDiscardBtnLabel', { defaultMessage: 'Discard', })} { await onChange({ colorMode }, true); closeModal(); diff --git a/x-pack/test/functional_cloud/tests/cloud_links.ts b/x-pack/test/functional_cloud/tests/cloud_links.ts index e6450519085a9..2719bd1744c7f 100644 --- a/x-pack/test/functional_cloud/tests/cloud_links.ts +++ b/x-pack/test/functional_cloud/tests/cloud_links.ts @@ -6,11 +6,14 @@ */ import expect from '@kbn/expect'; +import type { DarkModeValue as ColorMode } from '@kbn/user-profile-components'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); - const PageObjects = getPageObjects(['common', 'header']); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'header', 'userProfiles', 'settings']); + const testSubjects = getService('testSubjects'); describe('Cloud Links integration', function () { before(async () => { @@ -150,5 +153,117 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.clickAndValidate('userMenuButton', 'appearanceSelector'); }); }); + + describe('Appearance selector modal', () => { + const openAppearanceSelectorModal = async () => { + // Check if the user menu is open + await find.byCssSelector('[data-test-subj="userMenu"]', 1000).catch(async () => { + await testSubjects.click('userMenuButton'); + }); + await testSubjects.click('appearanceSelector'); + const appearanceModal = await find.byCssSelector( + '[data-test-subj="appearanceModal"]', + 1000 + ); + expect(appearanceModal).to.not.be(null); + }; + + const refreshPage = async () => { + await browser.refresh(); + await testSubjects.exists('globalLoadingIndicator-hidden'); + }; + + const changeColorMode = async (colorMode: ColorMode) => { + await openAppearanceSelectorModal(); + await testSubjects.click(`colorModeKeyPadItem${colorMode}`); + await testSubjects.click('appearanceModalSaveButton'); + await testSubjects.missingOrFail('appearanceModal'); + }; + + after(async () => { + await changeColorMode('space_default'); + + await PageObjects.common.navigateToUrl('management', 'kibana/settings', { + basePath: '', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + + // Reset the space default dark mode to "disabled" + await PageObjects.settings.setAdvancedSettingsSelect('theme:darkMode', 'disabled'); + { + const advancedSetting = await PageObjects.settings.getAdvancedSettings('theme:darkMode'); + expect(advancedSetting).to.be('disabled'); + } + + await refreshPage(); + }); + + it('has 4 color mode options to chose from', async () => { + await openAppearanceSelectorModal(); + const colorModes: ColorMode[] = ['light', 'dark', 'system', 'space_default']; + for (const colorMode of colorModes) { + const themeModeButton = await testSubjects.find(`colorModeKeyPadItem${colorMode}`, 1000); + expect(themeModeButton).to.not.be(null); + } + await testSubjects.click('appearanceModalDiscardButton'); + }); + + it('can change the color mode to dark', async () => { + await changeColorMode('dark'); + await refreshPage(); + const colorModeTag = await PageObjects.userProfiles.getThemeTag(); + expect(colorModeTag).to.be('v8dark'); + }); + + it('can change the color mode to light', async () => { + await changeColorMode('light'); + await refreshPage(); + const colorModeTag = await PageObjects.userProfiles.getThemeTag(); + expect(colorModeTag).to.be('v8light'); + }); + + it('can change the color mode to space_default', async () => { + // Let's make sure we are in light mode before changing to space_default + await changeColorMode('light'); + + { + await refreshPage(); + const colorModeTag = await PageObjects.userProfiles.getThemeTag(); + expect(colorModeTag).to.be('v8light'); + } + + // Change the space default dark mode to "enabled" + await PageObjects.common.navigateToUrl('management', 'kibana/settings', { + basePath: '', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + + await PageObjects.settings.setAdvancedSettingsSelect('theme:darkMode', 'enabled'); + { + const advancedSetting = await PageObjects.settings.getAdvancedSettings('theme:darkMode'); + expect(advancedSetting).to.be('enabled'); + } + + // Make sure we are still in light mode as per the User profile + // even after setting the space default to "dark" + { + await refreshPage(); + const colorModeTag = await PageObjects.userProfiles.getThemeTag(); + expect(colorModeTag).to.be('v8light'); + } + + await changeColorMode('space_default'); + + { + await refreshPage(); + const colorModeTag = await PageObjects.userProfiles.getThemeTag(); + expect(colorModeTag).to.be('v8dark'); // We are now in dark mode + } + }); + }); }); } From e3b64b4780d49eb6022dad563c920c0035786b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 16 Dec 2024 10:56:42 +0000 Subject: [PATCH 18/32] Fix default value in modal --- .../use_appearance_hook.ts | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts index cf66cee03026b..6ccc23f5240f0 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts @@ -13,19 +13,6 @@ import { type DarkModeValue as ColorMode, } from '@kbn/user-profile-components'; -const parseDarkModeValue = (rawValue: unknown): ColorMode => { - if (rawValue === true || rawValue === 'true' || rawValue === 'enabled') { - return 'dark'; - } - if (rawValue === false || rawValue === 'false' || rawValue === 'disabled') { - return 'light'; - } - if (rawValue === 'system') { - return 'system'; - } - return 'space_default'; -}; - interface Deps { uiSettingsClient: IUiSettingsClient; } @@ -52,13 +39,10 @@ export const useAppearance = ({ uiSettingsClient }: Deps) => { }, }); - const { - userSettings: { - darkMode: colorModeUserProfile = parseDarkModeValue(uiSettingsClient.get('theme:darkMode')), - } = {}, - } = userProfileData ?? { - userSettings: {}, - }; + const { userSettings: { darkMode: colorModeUserProfile = 'space_default' } = {} } = + userProfileData ?? { + userSettings: {}, + }; const [colorMode, setColorMode] = useState(colorModeUserProfile); From 04a200e58523482887d838b5cfbc718d1ee13930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 16 Dec 2024 11:22:02 +0000 Subject: [PATCH 19/32] Don't render space default on serverless --- .../cloud_links/public/index.ts | 5 +++-- .../appearance_selector/appearance_modal.tsx | 9 +++++++-- .../appearance_selector.test.tsx | 7 ++++++- .../appearance_selector.tsx | 18 ++++++++++++++---- .../maybe_add_cloud_links.test.ts | 4 ++++ .../maybe_add_cloud_links.ts | 9 ++++++++- .../maybe_add_cloud_links/user_menu_links.tsx | 9 ++++++++- .../cloud_links/public/plugin.test.ts | 2 +- .../cloud_links/public/plugin.tsx | 9 +++++++++ 9 files changed, 60 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/index.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/index.ts index edb43cd0405ca..fcb0f22fff804 100755 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/index.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/index.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { PluginInitializerContext } from '@kbn/core/public'; import { CloudLinksPlugin } from './plugin'; -export function plugin() { - return new CloudLinksPlugin(); +export function plugin(context: PluginInitializerContext) { + return new CloudLinksPlugin(context); } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 73543b2a4dd42..31193fa752645 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -70,9 +70,10 @@ const colorModeOptions: Array> = [ interface Props { closeModal: () => void; uiSettingsClient: IUiSettingsClient; + isServerless: boolean; } -export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => { +export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isServerless }) => { const modalTitleId = useGeneratedHtmlId(); const { onChange, colorMode, isLoading } = useAppearance({ @@ -94,7 +95,11 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient }) => title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeTitle', { defaultMessage: 'Color mode', })} - values={colorModeOptions} + values={ + isServerless + ? colorModeOptions.filter(({ id }) => id !== 'space_default') + : colorModeOptions + } selectedValue={colorMode} onChange={(id) => { onChange({ colorMode: id }, false); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx index 0076b150290a0..5fdd762184a6b 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx @@ -21,7 +21,12 @@ describe('AppearanceSelector', () => { const core = coreMock.createStart(); const { getByTestId } = render( - + ); const appearanceSelector = getByTestId('appearanceSelector'); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx index e5927e06aadfc..e7f882d2c6b89 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx @@ -21,17 +21,23 @@ interface Props { security: SecurityPluginStart; core: CoreStart; closePopover: () => void; + isServerless: boolean; } -export const AppearanceSelector = ({ security, core, closePopover }: Props) => { +export const AppearanceSelector = ({ security, core, closePopover, isServerless }: Props) => { return ( - + ); }; -function AppearanceSelectorUI({ security, core, closePopover }: Props) { +function AppearanceSelectorUI({ security, core, closePopover, isServerless }: Props) { const { isVisible } = useAppearance({ uiSettingsClient: core.uiSettings, }); @@ -47,7 +53,11 @@ function AppearanceSelectorUI({ security, core, closePopover }: Props) { modalRef.current = core.overlays.openModal( toMountPoint( - + , core ), diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts index 5fc4a9549682f..67ad63f2b375b 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts @@ -21,6 +21,7 @@ describe('maybeAddCloudLinks', () => { security, share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: false }, + isServerless: false, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); @@ -39,6 +40,7 @@ describe('maybeAddCloudLinks', () => { core, share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, + isServerless: false, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); @@ -113,6 +115,7 @@ describe('maybeAddCloudLinks', () => { core, share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, + isServerless: false, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); @@ -188,6 +191,7 @@ describe('maybeAddCloudLinks', () => { core, share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, + isServerless: false, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts index 8bd2bd0ff1cf7..1a56a22c653aa 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts @@ -20,9 +20,15 @@ export interface MaybeAddCloudLinksDeps { security: SecurityPluginStart; cloud: CloudStart; share: SharePluginStart; + isServerless: boolean; } -export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinksDeps): void { +export function maybeAddCloudLinks({ + core, + security, + cloud, + isServerless, +}: MaybeAddCloudLinksDeps): void { const userObservable = defer(() => security.authc.getCurrentUser()).pipe( // Check if user is a cloud user. map((user) => user.elastic_cloud_user), @@ -43,6 +49,7 @@ export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinks core, cloud, security, + isServerless, }); security.navControlService.addUserMenuLinks(userMenuLinks); }) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx index f43fc3c942b5d..a168956cd1c2d 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx @@ -16,10 +16,12 @@ export const createUserMenuLinks = ({ core, cloud, security, + isServerless, }: { core: CoreStart; cloud: CloudStart; security: SecurityPluginStart; + isServerless: boolean; }): UserMenuLink[] => { const { profileUrl, billingUrl, organizationUrl } = cloud; @@ -61,7 +63,12 @@ export const createUserMenuLinks = ({ userMenuLinks.push({ content: ({ closePopover }) => ( - + ), order: 400, label: '', diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts index d930d024d2484..5f692320dc1e8 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts @@ -17,7 +17,7 @@ describe('Cloud Links Plugin - public', () => { let plugin: CloudLinksPlugin; beforeEach(() => { - plugin = new CloudLinksPlugin(); + plugin = new CloudLinksPlugin(coreMock.createPluginInitializerContext()); }); afterEach(() => { diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx index 9f385500b13e8..c03e15c96776e 100755 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx @@ -8,11 +8,13 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import type { PluginInitializerContext } from '@kbn/core-plugins-browser'; import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import * as connectionDetails from '@kbn/cloud/connection_details'; +import type { BuildFlavor } from '@kbn/config'; import { maybeAddCloudLinks } from './maybe_add_cloud_links'; interface CloudLinksDepsSetup { @@ -30,6 +32,12 @@ interface CloudLinksDepsStart { export class CloudLinksPlugin implements Plugin { + public offering: BuildFlavor; + + constructor(initializerContext: PluginInitializerContext) { + this.offering = initializerContext.env.packageInfo.buildFlavor; + } + public setup({ analytics }: CoreSetup) { analytics.registerEventType({ eventType: 'connection_details_learn_more_clicked', @@ -115,6 +123,7 @@ export class CloudLinksPlugin security, cloud, share, + isServerless: this.offering === 'serverless', }); } } From 6e47aad86bf96f666fdeef8d28fe3e7bd571586f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:32:24 +0000 Subject: [PATCH 20/32] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json index a9b5b4025acb5..ed8239bd32938 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json +++ b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json @@ -26,6 +26,8 @@ "@kbn/cloud", "@kbn/react-kibana-mount", "@kbn/core-mount-utils-browser", + "@kbn/core-plugins-browser", + "@kbn/config", ], "exclude": [ "target/**/*", From 3ca836b0e83d8cf9867e8629fadb5c2a1e57f1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 16 Dec 2024 16:19:29 +0000 Subject: [PATCH 21/32] Fix modal width in serverless --- .../appearance_selector/appearance_modal.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 31193fa752645..401627195e75f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -81,7 +81,18 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer }); return ( - + {i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalTitle', { From c3144dc7e05677a6c8fc6c88a77a753e915b5fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 18 Dec 2024 10:45:38 +0000 Subject: [PATCH 22/32] Fix default value in serverless --- .../appearance_selector/appearance_modal.tsx | 1 + .../appearance_selector/appearance_selector.tsx | 1 + .../appearance_selector/use_appearance_hook.ts | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 401627195e75f..4b2c4dbf65f54 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -78,6 +78,7 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer const { onChange, colorMode, isLoading } = useAppearance({ uiSettingsClient, + defaultColorMode: isServerless ? 'system' : 'space_default', }); return ( diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx index e7f882d2c6b89..60eb3f0114443 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx @@ -40,6 +40,7 @@ export const AppearanceSelector = ({ security, core, closePopover, isServerless function AppearanceSelectorUI({ security, core, closePopover, isServerless }: Props) { const { isVisible } = useAppearance({ uiSettingsClient: core.uiSettings, + defaultColorMode: 'space_default', }); const modalRef = useRef(null); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts index 6ccc23f5240f0..9cd004bcdc69f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts @@ -15,9 +15,10 @@ import { interface Deps { uiSettingsClient: IUiSettingsClient; + defaultColorMode: ColorMode; } -export const useAppearance = ({ uiSettingsClient }: Deps) => { +export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { // 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'); @@ -39,7 +40,7 @@ export const useAppearance = ({ uiSettingsClient }: Deps) => { }, }); - const { userSettings: { darkMode: colorModeUserProfile = 'space_default' } = {} } = + const { userSettings: { darkMode: colorModeUserProfile = defaultColorMode } = {} } = userProfileData ?? { userSettings: {}, }; From 82d06bd52b727119d7d2851e622938881bfab1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 18 Dec 2024 10:48:41 +0000 Subject: [PATCH 23/32] Update warning message --- .../appearance_selector/appearance_modal.tsx | 2 +- .../public/account_management/user_profile/user_profile.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 4b2c4dbf65f54..6f4e961b670dc 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -131,7 +131,7 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer title={i18n.translate( 'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultTitle', { - defaultMessage: 'Space default settings will be deprecated in 10.0', + defaultMessage: 'Space default settings will be removed in a future version', } )} color="warning" diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx index c6f418658b9b1..e1739613dbb22 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx @@ -319,7 +319,7 @@ const UserSettingsEditor: FunctionComponent = ({ title={i18n.translate( 'xpack.security.accountManagement.userProfile.deprecatedSpaceDefaultTitle', { - defaultMessage: 'Space default settings will be deprecated in 10.0', + defaultMessage: 'Space default settings will be removed in a future version', } )} color="warning" From 09a6c68e2533e55169ace7807f42bdf85e1d381e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 18 Dec 2024 11:06:44 +0000 Subject: [PATCH 24/32] Address CR changes --- .../src/user_settings_service.ts | 4 ++++ x-pack/test/functional_cloud/tests/cloud_links.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts b/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts index 29a30e41ab824..9e8cdfbd04584 100644 --- a/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts +++ b/packages/core/user-settings/core-user-settings-server-internal/src/user_settings_service.ts @@ -64,6 +64,10 @@ export class UserSettingsService { } } +/** + * Extracts the dark mode setting from the user settings. + * Returning "undefined" means that we will use the space default settings. + */ const getUserSettingDarkMode = ( userSettings: Record ): DarkModeValue | undefined => { diff --git a/x-pack/test/functional_cloud/tests/cloud_links.ts b/x-pack/test/functional_cloud/tests/cloud_links.ts index 2719bd1744c7f..1549db7e9d0bd 100644 --- a/x-pack/test/functional_cloud/tests/cloud_links.ts +++ b/x-pack/test/functional_cloud/tests/cloud_links.ts @@ -149,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(cloudLink).to.not.be(null); }); - it('Shows the appearance buton', async () => { + it('Shows the appearance button', async () => { await PageObjects.common.clickAndValidate('userMenuButton', 'appearanceSelector'); }); }); From 6799d7137d1a25186ae5c6777c6e5cce9ae3a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 18 Dec 2024 11:16:32 +0000 Subject: [PATCH 25/32] Update i18n id --- .../public/account_management/user_profile/user_profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx index e1739613dbb22..aa66c80953929 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx @@ -327,7 +327,7 @@ const UserSettingsEditor: FunctionComponent = ({ >

{i18n.translate( - 'xpack.security.accountManagement.userProfile.deprecatedSpaceDefaultDescr', + 'xpack.security.accountManagement.userProfile.deprecatedSpaceDefaultDescription', { defaultMessage: 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.', From dc6273e3da63d82ec9a1bb02a3342c88672bd071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 19 Dec 2024 13:01:28 +0000 Subject: [PATCH 26/32] Reduce width --- .../appearance_selector/appearance_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index 6f4e961b670dc..f795b9f3386c0 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -91,7 +91,7 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer : // When not in serverless, we have the "Space default" as an option. // which renders a warning callout. We don't want the modal to scale up when // the callout is rendered, so we set a fixed width. - { width: 600 } + { width: 580 } } > From 9bd3876d876cce3820f159b7519276e84203ac47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 19 Dec 2024 16:12:59 +0000 Subject: [PATCH 27/32] Show toast message whenever the system color mode changes --- .../src/chrome_service.tsx | 21 +++ .../src/handle_system_colormode_change.tsx | 150 ++++++++++++++++++ .../src/core_system.ts | 4 + .../core-theme-browser-internal/index.ts | 1 + 4 files changed, 176 insertions(+) create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.tsx diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx index 511100fff6d40..e8eb19482da7a 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx @@ -14,6 +14,10 @@ import { mergeMap, map, takeUntil, filter } from 'rxjs'; import { parse } from 'url'; import { setEuiDevProviderWarning } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; +import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import type { UserProfileService } from '@kbn/core-user-profile-browser'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import type { CoreContext } from '@kbn/core-base-browser-internal'; import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal'; @@ -54,6 +58,7 @@ import { Header, LoadingIndicator, ProjectHeader } from './ui'; import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; import type { InternalChromeStart } from './types'; import { HeaderTopBanner } from './ui/header/header_top_banner'; +import { handleSystemColorModeChange } from './handle_system_colormode_change'; const IS_LOCKED_KEY = 'core.chrome.isLocked'; const IS_SIDENAV_COLLAPSED_KEY = 'core.chrome.isSideNavCollapsed'; @@ -76,6 +81,10 @@ export interface StartDeps { injectedMetadata: InternalInjectedMetadataStart; notifications: NotificationsStart; customBranding: CustomBrandingStart; + i18n: I18nStart; + theme: ThemeServiceStart; + userProfile: UserProfileService; + uiSettings: IUiSettingsClient; } /** @internal */ @@ -238,9 +247,21 @@ export class ChromeService { injectedMetadata, notifications, customBranding, + i18n: i18nService, + theme, + userProfile, + uiSettings, }: StartDeps): Promise { this.initVisibility(application); this.handleEuiFullScreenChanges(); + + handleSystemColorModeChange({ + notifications, + coreStart: { i18n: i18nService, theme, userProfile }, + stop$: this.stop$, + http, + uiSettings, + }); // commented out until https://github.com/elastic/kibana/issues/201805 can be fixed // this.handleEuiDevProviderWarning(notifications); diff --git a/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.tsx b/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.tsx new file mode 100644 index 0000000000000..01504fdfb1570 --- /dev/null +++ b/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.tsx @@ -0,0 +1,150 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { NotificationsStart } from '@kbn/core-notifications-browser'; +import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import type { UserProfileService } from '@kbn/core-user-profile-browser'; +import { take, type Observable } from 'rxjs'; +import { browsersSupportsSystemTheme } from '@kbn/core-theme-browser-internal'; +import type { InternalHttpStart } from '@kbn/core-http-browser-internal'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; + +const doSyncWithSystem = ( + userSettings: { darkMode?: string } = { darkMode: 'space_default' }, + uiSettingsDarkModeValue: string = 'disabled' +): boolean => { + const { darkMode: userProfileDarkModeValue = 'space_default' } = userSettings; + + if (userProfileDarkModeValue.toUpperCase() === 'SYSTEM') { + return true; + } + + if ( + userProfileDarkModeValue.toUpperCase() === 'SPACE_DEFAULT' && + uiSettingsDarkModeValue.toUpperCase() === 'SYSTEM' + ) { + return true; + } + + return false; +}; + +const isUnauthenticated = (http: InternalHttpStart) => { + const { anonymousPaths } = http; + return anonymousPaths.isAnonymous(window.location.pathname); +}; + +const doHandle = async ({ + http, + coreStart, + uiSettings, +}: { + http: InternalHttpStart; + uiSettings: IUiSettingsClient; + coreStart: { + i18n: I18nStart; + theme: ThemeServiceStart; + userProfile: UserProfileService; + }; +}) => { + if (!browsersSupportsSystemTheme()) return false; + if (isUnauthenticated(http)) return false; + + const userProfile = await coreStart.userProfile.getCurrent<{ + userSettings: { darkMode?: string }; + }>({ + dataPath: 'userSettings', + }); + const { userSettings } = userProfile.data; + + if (!doSyncWithSystem(userSettings, uiSettings.get('theme:darkMode'))) return false; + + return true; +}; + +export async function handleSystemColorModeChange({ + notifications, + uiSettings, + coreStart, + stop$, + http, +}: { + notifications: NotificationsStart; + http: InternalHttpStart; + uiSettings: IUiSettingsClient; + coreStart: { + i18n: I18nStart; + theme: ThemeServiceStart; + userProfile: UserProfileService; + }; + stop$: Observable; +}) { + if (!(await doHandle({ http, uiSettings, coreStart }))) { + return; + } + + let currentDarkModeValue: boolean | undefined; + const matchMedia = window.matchMedia('(prefers-color-scheme: dark)'); + + const onDarkModeChange = ({ matches: isDarkMode }: { matches: boolean }) => { + if (currentDarkModeValue === undefined) { + // The current value can only be set on page reload as that's the moment when + // we actually apply set the dark/light color mode of the page. + currentDarkModeValue = isDarkMode; + } else if (currentDarkModeValue !== isDarkMode) { + notifications.toasts.addSuccess( + { + title: i18n.translate('core.ui.chrome.appearanceChange.successNotificationTitle', { + defaultMessage: 'System color mode updated', + }), + text: toMountPoint( + <> +

+ {i18n.translate('core.ui.chrome.appearanceChange.successNotificationText', { + defaultMessage: 'Reload the page to see the changes', + })} +

+ + + window.location.reload()} + data-test-subj="windowReloadButton" + > + {i18n.translate( + 'core.ui.chrome.appearanceChange.requiresPageReloadButtonLabel', + { + defaultMessage: 'Reload page', + } + )} + + + + , + coreStart + ), + }, + { toastLifeTimeMs: Infinity } // leave it on until discard or page reload + ); + } + }; + + onDarkModeChange(matchMedia); + + matchMedia.addEventListener('change', onDarkModeChange); + + stop$.pipe(take(1)).subscribe(() => { + matchMedia.removeEventListener('change', onDarkModeChange); + }); +} diff --git a/packages/core/root/core-root-browser-internal/src/core_system.ts b/packages/core/root/core-root-browser-internal/src/core_system.ts index 042017368168c..59ba94d01d8d4 100644 --- a/packages/core/root/core-root-browser-internal/src/core_system.ts +++ b/packages/core/root/core-root-browser-internal/src/core_system.ts @@ -360,6 +360,10 @@ export class CoreSystem { injectedMetadata, notifications, customBranding, + i18n, + theme, + userProfile, + uiSettings, }); const deprecations = this.deprecations.start({ http }); diff --git a/packages/core/theme/core-theme-browser-internal/index.ts b/packages/core/theme/core-theme-browser-internal/index.ts index 7f77db8564896..e938ed2eeb5b8 100644 --- a/packages/core/theme/core-theme-browser-internal/index.ts +++ b/packages/core/theme/core-theme-browser-internal/index.ts @@ -10,3 +10,4 @@ export { ThemeService } from './src/theme_service'; export { CoreThemeProvider } from './src/core_theme_provider'; export type { ThemeServiceSetupDeps } from './src/theme_service'; +export { browsersSupportsSystemTheme } from './src/system_theme'; From 6b43d4ef0d3c38b2464de258c9eb98e3c5b2d3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 20 Dec 2024 13:01:10 +0100 Subject: [PATCH 28/32] Add jest tests --- .../handle_system_colormode_change.test.ts | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts diff --git a/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts new file mode 100644 index 0000000000000..91ab2885e3bb6 --- /dev/null +++ b/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts @@ -0,0 +1,258 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { coreMock, httpServiceMock, uiSettingsServiceMock } from '@kbn/core/public/mocks'; +import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; +import { handleSystemColorModeChange } from './handle_system_colormode_change'; +import { ReplaySubject } from 'rxjs'; +import type { GetUserProfileResponse } from '@kbn/core-user-profile-browser'; +import { UserProfileData } from '@kbn/core-user-profile-common'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; + +const mockbrowsersSupportsSystemTheme = jest.fn(); + +jest.mock('@kbn/core-theme-browser-internal', () => { + const original = jest.requireActual('@kbn/core-theme-browser-internal'); + + return { + ...original, + browsersSupportsSystemTheme: () => mockbrowsersSupportsSystemTheme(), + }; +}); + +describe('handleSystemColorModeChange', () => { + const originalMatchMedia = window.matchMedia; + + afterAll(() => { + window.matchMedia = originalMatchMedia; + }); + + const getDeps = () => { + const coreStart = coreMock.createStart(); + const notifications = notificationServiceMock.createStartContract(); + const http = httpServiceMock.createStartContract(); + const uiSettings = uiSettingsServiceMock.createStartContract(); + const stop$ = new ReplaySubject(1); + + return { + coreStart, + notifications, + http, + uiSettings, + stop$, + }; + }; + + const mockMatchMedia = (matches: boolean = false, addEventListenerMock = jest.fn()) => { + const removeEventListenerMock = jest.fn(); + window.matchMedia = jest.fn().mockImplementation(() => { + return { + matches, + addEventListener: addEventListenerMock, + removeEventListener: removeEventListenerMock, + }; + }); + + return { addEventListenerMock, removeEventListenerMock }; + }; + + const mockUserProfileResponse = ( + darkMode: 'dark' | 'light' | 'system' | 'space_default' + ): GetUserProfileResponse => + ({ + data: { + userSettings: { + darkMode, + }, + }, + } as any); + + const mockUiSettingsDarkMode = ( + uiSettings: jest.Mocked, + darkMode: 'dark' | 'light' | 'system' + ) => { + uiSettings.get.mockImplementation((key) => { + if (key === 'theme:darkMode') { + return darkMode; + } + + return 'foo'; + }); + }; + + describe('doHandle guard', () => { + it('does not handle if the system color mode is not supported', () => { + const { addEventListenerMock } = mockMatchMedia(); + expect(addEventListenerMock).not.toHaveBeenCalled(); + mockbrowsersSupportsSystemTheme.mockReturnValue(false); + + handleSystemColorModeChange({} as any); + + expect(addEventListenerMock).not.toHaveBeenCalled(); + }); + + it('does not handle on unauthenticated routes', () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const { addEventListenerMock } = mockMatchMedia(); + expect(addEventListenerMock).not.toHaveBeenCalled(); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(true); + + handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + + expect(addEventListenerMock).not.toHaveBeenCalled(); + }); + + it('does not handle if user profile darkmode is not "system"', () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const { addEventListenerMock } = mockMatchMedia(); + expect(addEventListenerMock).not.toHaveBeenCalled(); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue({ + data: { + userSettings: { + darkMode: 'light', + }, + }, + } as any); + + handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + + expect(addEventListenerMock).not.toHaveBeenCalled(); + }); + + it('does not handle if user profile darkmode is "space_default" but the uiSettings darkmode is not "system"', () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const { addEventListenerMock } = mockMatchMedia(); + expect(addEventListenerMock).not.toHaveBeenCalled(); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue({ + data: { + userSettings: { + darkMode: 'space_default', + }, + }, + } as any); + + uiSettings.get.mockImplementation((key) => { + if (key === 'theme:darkMode') { + return 'light'; + } + + return 'foo'; + }); + + handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + + expect(addEventListenerMock).not.toHaveBeenCalled(); + }); + + it('does handle if user profile darkmode is "system"', async () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const { addEventListenerMock } = mockMatchMedia(false); + expect(addEventListenerMock).not.toHaveBeenCalled(); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue(mockUserProfileResponse('system')); + + await handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + + expect(addEventListenerMock).toHaveBeenCalled(); + }); + + it('does handle if user profile darkmode is "space_default" and uiSetting darkmode is "system"', async () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const { addEventListenerMock } = mockMatchMedia(false); + expect(addEventListenerMock).not.toHaveBeenCalled(); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue(mockUserProfileResponse('space_default')); + mockUiSettingsDarkMode(uiSettings, 'system'); + + await handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + + expect(addEventListenerMock).toHaveBeenCalled(); + }); + }); + + describe('onDarkModeChange()', () => { + it('does show a toast when the system color mode changes', async () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const currentDarkMode = false; // The system is currently in light mode + const addEventListenerMock = jest + .fn() + .mockImplementation((type: string, cb: (evt: MediaQueryListEvent) => any) => { + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + expect(type).toBe('change'); + cb({ matches: true } as any); // The system changed to dark mode + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.any(Function), + title: 'System color mode updated', + }), + { toastLifeTimeMs: Infinity } + ); + }); + mockMatchMedia(currentDarkMode, addEventListenerMock); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue(mockUserProfileResponse('system')); + + await handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + expect(addEventListenerMock).toHaveBeenCalled(); + }); + + it('does **not** show a toast when the system color mode changes to the current darkmode value', async () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const currentDarkMode = true; // The system is currently in dark mode + const addEventListenerMock = jest + .fn() + .mockImplementation((type: string, cb: (evt: MediaQueryListEvent) => any) => { + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + expect(type).toBe('change'); + cb({ matches: true } as any); // The system changed to dark mode + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); + mockMatchMedia(currentDarkMode, addEventListenerMock); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue(mockUserProfileResponse('system')); + + await handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + expect(addEventListenerMock).toHaveBeenCalled(); + }); + + it('stops listening to changes on stop$ change', async () => { + const { coreStart, notifications, http, uiSettings, stop$ } = getDeps(); + const currentDarkMode = false; // The system is currently in light mode + const { addEventListenerMock, removeEventListenerMock } = mockMatchMedia(currentDarkMode); + + mockbrowsersSupportsSystemTheme.mockReturnValue(true); + http.anonymousPaths.isAnonymous.mockReturnValue(false); + coreStart.userProfile.getCurrent.mockResolvedValue(mockUserProfileResponse('system')); + + await handleSystemColorModeChange({ coreStart, notifications, http, uiSettings, stop$ }); + expect(addEventListenerMock).toHaveBeenCalled(); + expect(removeEventListenerMock).not.toHaveBeenCalled(); + + stop$.next(); + + expect(removeEventListenerMock).toHaveBeenCalled(); + }); + }); +}); From dc1aba4ff3a0586267eff97a60aaac7638c69465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 20 Dec 2024 13:55:33 +0100 Subject: [PATCH 29/32] Don't update the profile if colorMode value did not change --- .../src/hooks/use_update_user_profile.tsx | 5 ++++- packages/kbn-user-profile-components/src/types.ts | 1 + .../appearance_selector/appearance_modal.tsx | 6 ++++-- .../appearance_selector/use_appearance_hook.ts | 14 +++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx index a777db694e542..72b1bdadb3393 100644 --- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx +++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx @@ -55,7 +55,7 @@ export const useUpdateUserProfile = ({ pageReloadChecker, }: Props = {}) => { const { userProfileApiClient, notifySuccess } = useUserProfiles(); - const { userProfile$, enabled$ } = userProfileApiClient; + const { userProfile$, enabled$, userProfileLoaded$ } = userProfileApiClient; const { enabled: notificationSuccessEnabled = true, title: notificationTitle = i18nTexts.notificationSuccess.title, @@ -64,6 +64,7 @@ export const useUpdateUserProfile = ({ const [isLoading, setIsLoading] = useState(false); const userProfileData = useObservable(userProfile$); const userProfileEnabled = useObservable(enabled$); + const userProfileLoaded = useObservable(userProfileLoaded$, false); // Keep a snapshot before updating the user profile so we can compare previous and updated values const userProfileSnapshot = useRef(); const isMounted = useRef(false); @@ -151,6 +152,8 @@ export const useUpdateUserProfile = ({ isLoading, /** Flag to indicate if user profile is enabled */ userProfileEnabled, + /** Flag to indicate if the user profile has been loaded */ + userProfileLoaded, }; }; diff --git a/packages/kbn-user-profile-components/src/types.ts b/packages/kbn-user-profile-components/src/types.ts index 7d55f73fa25a6..54b77e63e55f0 100644 --- a/packages/kbn-user-profile-components/src/types.ts +++ b/packages/kbn-user-profile-components/src/types.ts @@ -46,5 +46,6 @@ export interface UserProfileData { export interface UserProfileAPIClient { userProfile$: Observable; enabled$: Observable; + userProfileLoaded$: Observable; partialUpdate: >(data: D) => Promise; } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index f795b9f3386c0..b29f15a26c8c3 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -76,7 +76,7 @@ interface Props { export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isServerless }) => { const modalTitleId = useGeneratedHtmlId(); - const { onChange, colorMode, isLoading } = useAppearance({ + const { onChange, colorMode, isLoading, initialColorModeValue } = useAppearance({ uiSettingsClient, defaultColorMode: isServerless ? 'system' : 'space_default', }); @@ -162,7 +162,9 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer { - await onChange({ colorMode }, true); + if (colorMode !== initialColorModeValue) { + await onChange({ colorMode }, true); + } closeModal(); }} fill diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts index 9cd004bcdc69f..797a8dd39e3d0 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts @@ -23,7 +23,7 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { // we don't allow the user to change the theme color. const valueSetInKibanaConfig = uiSettingsClient.isOverridden('theme:darkMode'); - const { userProfileData, isLoading, update } = useUpdateUserProfile({ + const { userProfileData, isLoading, update, userProfileLoaded } = useUpdateUserProfile({ notificationSuccess: { title: i18n.translate('xpack.cloudLinks.userMenuLinks.appearance.successNotificationTitle', { defaultMessage: 'Appearance settings updated', @@ -46,6 +46,8 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { }; const [colorMode, setColorMode] = useState(colorModeUserProfile); + const [initialColorModeValue, setInitialColorModeValue] = + useState(colorModeUserProfile); const onChange = useCallback( ({ colorMode: updatedColorMode }: { colorMode?: ColorMode }, persist: boolean) => { @@ -77,11 +79,21 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { setColorMode(colorModeUserProfile); }, [colorModeUserProfile]); + useEffect(() => { + if (userProfileLoaded) { + const storedValue = userProfileData?.userSettings?.darkMode; + if (storedValue) { + setInitialColorModeValue(storedValue); + } + } + }, [userProfileData, userProfileLoaded]); + return { isVisible: valueSetInKibanaConfig ? false : Boolean(userProfileData), setColorMode, colorMode, onChange, isLoading, + initialColorModeValue, }; }; From 5b8cde6e6b77a1236c097186346aecdf01c81bb5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:29:51 +0000 Subject: [PATCH 30/32] [CI] Auto-commit changed files from 'node scripts/notice' --- .../chrome/core-chrome-browser-internal/tsconfig.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json index ca9e5d5576ad9..2139f983d9b90 100644 --- a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json +++ b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json @@ -56,6 +56,14 @@ "@kbn/react-kibana-context-render", "@kbn/recently-accessed", "@kbn/core-user-profile-browser-mocks", + "@kbn/core-i18n-browser", + "@kbn/core-theme-browser", + "@kbn/core-user-profile-browser", + "@kbn/core-ui-settings-browser", + "@kbn/core", + "@kbn/core-user-profile-common", + "@kbn/react-kibana-mount", + "@kbn/core-theme-browser-internal", ], "exclude": [ "target/**/*", From 39aba89d99591bc4e60d78c83e0f4512d2d495d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Sat, 21 Dec 2024 12:27:03 +0100 Subject: [PATCH 31/32] Fix TS circular dependency --- .../src/handle_system_colormode_change.test.ts | 12 ++++++++++-- .../core-chrome-browser-internal/tsconfig.json | 3 +-- .../react/kibana_mount/mount_point_portal.test.tsx | 2 +- packages/react/kibana_mount/mount_point_portal.tsx | 2 +- packages/react/kibana_mount/to_mount_point.test.tsx | 2 +- packages/react/kibana_mount/to_mount_point.tsx | 2 +- packages/react/kibana_mount/tsconfig.json | 3 ++- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts index 91ab2885e3bb6..0b190d57d4e1d 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/handle_system_colormode_change.test.ts @@ -7,7 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock, httpServiceMock, uiSettingsServiceMock } from '@kbn/core/public/mocks'; +import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks'; +import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks'; +import { themeServiceMock } from '@kbn/core-theme-browser-mocks'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { handleSystemColorModeChange } from './handle_system_colormode_change'; import { ReplaySubject } from 'rxjs'; @@ -34,7 +38,11 @@ describe('handleSystemColorModeChange', () => { }); const getDeps = () => { - const coreStart = coreMock.createStart(); + const coreStart = { + i18n: i18nServiceMock.createStartContract(), + theme: themeServiceMock.createStartContract(), + userProfile: userProfileServiceMock.createStart(), + }; const notifications = notificationServiceMock.createStartContract(); const http = httpServiceMock.createStartContract(); const uiSettings = uiSettingsServiceMock.createStartContract(); diff --git a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json index 2139f983d9b90..6960b8961561e 100644 --- a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json +++ b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json @@ -60,10 +60,9 @@ "@kbn/core-theme-browser", "@kbn/core-user-profile-browser", "@kbn/core-ui-settings-browser", - "@kbn/core", "@kbn/core-user-profile-common", "@kbn/react-kibana-mount", - "@kbn/core-theme-browser-internal", + "@kbn/core-theme-browser-internal" ], "exclude": [ "target/**/*", diff --git a/packages/react/kibana_mount/mount_point_portal.test.tsx b/packages/react/kibana_mount/mount_point_portal.test.tsx index 405aa5254ad92..b30920c914e4b 100644 --- a/packages/react/kibana_mount/mount_point_portal.test.tsx +++ b/packages/react/kibana_mount/mount_point_portal.test.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { MountPoint, UnmountCallback } from '@kbn/core/public'; +import type { MountPoint, UnmountCallback } from '@kbn/core-mount-utils-browser'; import { MountPointPortal } from './mount_point_portal'; import { act } from 'react-dom/test-utils'; diff --git a/packages/react/kibana_mount/mount_point_portal.tsx b/packages/react/kibana_mount/mount_point_portal.tsx index 5017b57747e48..590862d3d20cd 100644 --- a/packages/react/kibana_mount/mount_point_portal.tsx +++ b/packages/react/kibana_mount/mount_point_portal.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useRef, useEffect, useState, Component, FC, PropsWithChildren } from 'react'; import ReactDOM from 'react-dom'; -import type { MountPoint } from '@kbn/core/public'; +import type { MountPoint } from '@kbn/core-mount-utils-browser'; import { useIfMounted } from './utils'; export interface MountPointPortalProps { diff --git a/packages/react/kibana_mount/to_mount_point.test.tsx b/packages/react/kibana_mount/to_mount_point.test.tsx index 5dafefa8453ef..7587f253096c8 100644 --- a/packages/react/kibana_mount/to_mount_point.test.tsx +++ b/packages/react/kibana_mount/to_mount_point.test.tsx @@ -12,7 +12,7 @@ import { act } from 'react-dom/test-utils'; import { of, BehaviorSubject } from 'rxjs'; import { useEuiTheme } from '@elastic/eui'; import type { UseEuiTheme } from '@elastic/eui'; -import type { CoreTheme } from '@kbn/core/public'; +import type { CoreTheme } from '@kbn/core-theme-browser'; import { toMountPoint } from './to_mount_point'; import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks'; diff --git a/packages/react/kibana_mount/to_mount_point.tsx b/packages/react/kibana_mount/to_mount_point.tsx index 8968decee726a..449e3ed974cde 100644 --- a/packages/react/kibana_mount/to_mount_point.tsx +++ b/packages/react/kibana_mount/to_mount_point.tsx @@ -9,7 +9,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import type { MountPoint } from '@kbn/core/public'; +import type { MountPoint } from '@kbn/core-mount-utils-browser'; import { KibanaRenderContextProvider, KibanaRenderContextProviderProps, diff --git a/packages/react/kibana_mount/tsconfig.json b/packages/react/kibana_mount/tsconfig.json index 8294fad813c28..15dc55d36b052 100644 --- a/packages/react/kibana_mount/tsconfig.json +++ b/packages/react/kibana_mount/tsconfig.json @@ -16,11 +16,12 @@ "target/**/*" ], "kbn_references": [ - "@kbn/core", "@kbn/i18n", "@kbn/core-i18n-browser-mocks", "@kbn/react-kibana-context-render", "@kbn/core-analytics-browser-mocks", "@kbn/core-user-profile-browser-mocks", + "@kbn/core-mount-utils-browser", + "@kbn/core-theme-browser", ] } From 583b0696d8503442de3f39b803ec428a81c1023e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Sun, 22 Dec 2024 11:19:10 +0100 Subject: [PATCH 32/32] Update ChromeService jest teset --- .../src/chrome_service.test.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx index ade55365409cb..893910d1e4e47 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx @@ -30,6 +30,14 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { findTestSubject } from '@kbn/test-jest-helpers'; import { ChromeService } from './chrome_service'; +const mockhandleSystemColorModeChange = jest.fn(); + +jest.mock('./handle_system_colormode_change', () => { + return { + handleSystemColorModeChange: (...args: any[]) => mockhandleSystemColorModeChange(...args), + }; +}); + class FakeApp implements App { public title: string; public mount = () => () => {}; @@ -205,6 +213,29 @@ describe('start', () => { expect(startDeps.notifications.toasts.addWarning).not.toBeCalled(); }); + it('calls handleSystemColorModeChange() with the correct parameters', async () => { + mockhandleSystemColorModeChange.mockReset(); + await start(); + expect(mockhandleSystemColorModeChange).toHaveBeenCalledTimes(1); + + const [firstCallArg] = mockhandleSystemColorModeChange.mock.calls[0]; + expect(Object.keys(firstCallArg).sort()).toEqual([ + 'coreStart', + 'http', + 'notifications', + 'stop$', + 'uiSettings', + ]); + + expect(mockhandleSystemColorModeChange).toHaveBeenCalledWith({ + http: expect.any(Object), + coreStart: expect.any(Object), + uiSettings: expect.any(Object), + notifications: expect.any(Object), + stop$: expect.any(Object), + }); + }); + describe('getHeaderComponent', () => { it('returns a renderable React component', async () => { const { chrome } = await start();