Skip to content

Commit

Permalink
Update user profile from appearance modal
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga committed Dec 11, 2024
1 parent 7a09dfa commit 61b244b
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ export const useUpdateUserProfile = ({
<D extends Partial<UserProfileData>>(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]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -69,41 +67,17 @@ const colorModeOptions: Array<Value<ColorMode>> = [
},
];

const contrastOptions: Array<Value<Contrast>> = [
{
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;
}

export const AppearanceModal: FC<Props> = ({ closeModal, uiSettingsClient }) => {
const modalTitleId = useGeneratedHtmlId();
const [colorMode, setColorMode] = useState<ColorMode>('system');
const [contrast, setContrast] = useState<Contrast>('system');

// const { isVisible, toggle, isDarkModeOn, colorScheme } = useAppearance({
// uiSettingsClient,
// });
const { onChange, colorMode, isLoading } = useAppearance({
uiSettingsClient,
});

return (
<EuiModal aria-labelledby={modalTitleId} onClose={closeModal} style={{ width: 600 }}>
Expand All @@ -122,18 +96,20 @@ export const AppearanceModal: FC<Props> = ({ closeModal, uiSettingsClient }) =>
})}
values={colorModeOptions}
selectedValue={colorMode}
onChange={setColorMode}
onChange={(id) => {
onChange({ colorMode: id }, false);
}}
ariaLabel={i18n.translate(
'xpack.cloudLinks.userMenuLinks.appearanceModalColorModeAriaLabel',
{
defaultMessage: 'Appearance color mode',
}
)}
/>
<EuiSpacer />

{colorMode === 'space_default' && (
<>
<EuiSpacer />
<EuiCallOut
title={i18n.translate(
'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultTitle',
Expand All @@ -157,25 +133,6 @@ export const AppearanceModal: FC<Props> = ({ closeModal, uiSettingsClient }) =>
<EuiSpacer />
</>
)}

<ValuesGroup<Contrast>
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',
}
)}
/>
<EuiSpacer />
</EuiModalBody>

<EuiModalFooter>
Expand All @@ -186,10 +143,12 @@ export const AppearanceModal: FC<Props> = ({ closeModal, uiSettingsClient }) =>
</EuiButtonEmpty>

<EuiButton
onClick={() => {
// console.log('close');
onClick={async () => {
await onChange({ colorMode }, true);
closeModal();
}}
fill
isLoading={isLoading}
>
{i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalSaveBtnLabel', {
defaultMessage: 'Save changes',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,18 +26,16 @@ interface Props {
export const AppearanceSelector = ({ security, core, closePopover }: Props) => {
return (
<UserProfilesKibanaProvider core={core} security={security} toMountPoint={toMountPoint}>
<AppearanceSelectorUI core={core} closePopover={closePopover} />
<AppearanceSelectorUI core={core} security={security} closePopover={closePopover} />
</UserProfilesKibanaProvider>
);
};

function AppearanceSelectorUI({
core,
closePopover,
}: {
core: CoreStart;
closePopover: () => void;
}) {
function AppearanceSelectorUI({ security, core, closePopover }: Props) {
const { isVisible } = useAppearance({
uiSettingsClient: core.uiSettings,
});

const modalRef = useRef<OverlayRef | null>(null);

const closeModal = () => {
Expand All @@ -47,28 +46,32 @@ function AppearanceSelectorUI({
const openModal = () => {
modalRef.current = core.overlays.openModal(
toMountPoint(
<AppearanceModal closeModal={closeModal} uiSettingsClient={core.uiSettings} />,
<UserProfilesKibanaProvider core={core} security={security} toMountPoint={toMountPoint}>
<AppearanceModal closeModal={closeModal} uiSettingsClient={core.uiSettings} />
</UserProfilesKibanaProvider>,
core
),
{ 'data-test-subj': 'appearanceModal', maxWidth: 600 }
);
};

if (!isVisible) {
return null;
}

return (
<>
<EuiContextMenuItem
icon="brush"
size="s"
onClick={() => {
openModal();
closePopover();
}}
data-test-subj="appearanceSelector"
>
{i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceLinkText', {
defaultMessage: 'Appearance',
})}
</EuiContextMenuItem>
</>
<EuiContextMenuItem
icon="brush"
size="s"
onClick={() => {
openModal();
closePopover();
}}
data-test-subj="appearanceSelector"
>
{i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceLinkText', {
defaultMessage: 'Appearance',
})}
</EuiContextMenuItem>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand All @@ -42,39 +54,49 @@ 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<ColorMode>(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,
},
});
},
[isLoading, update]
);

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,
};
};

0 comments on commit 61b244b

Please sign in to comment.