Skip to content

Commit

Permalink
refactor: [M3-8981] - User Preferences optimization with selector pat…
Browse files Browse the repository at this point in the history
…tern (Part 1) (#11386)

* initial refactor

* fix type-safety issues

* Update packages/manager/src/queries/profile/preferences.ts

Co-authored-by: Purvesh Makode <pmakode@akamai.com>

* fix re-render at maincontent level @mjac0bs

* Update packages/manager/src/queries/profile/preferences.ts

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
Co-authored-by: Purvesh Makode <pmakode@akamai.com>
Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 12, 2024
1 parent 3eba173 commit abf05ca
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 61 deletions.
10 changes: 6 additions & 4 deletions packages/manager/src/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { sessionExpirationContext } from './context/sessionExpirationContext';
import { switchAccountSessionContext } from './context/switchAccountSessionContext';
import { useIsACLPEnabled } from './features/CloudPulse/Utils/utils';
import { useIsDatabasesEnabled } from './features/Databases/utilities';
import { useIsIAMEnabled } from './features/IAM/Shared/utilities';
import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils';
import { useGlobalErrors } from './hooks/useGlobalErrors';
import { useAccountSettings } from './queries/account/settings';
Expand All @@ -39,7 +40,6 @@ import { migrationRouter } from './routes';

import type { Theme } from '@mui/material/styles';
import type { AnyRouter } from '@tanstack/react-router';
import { useIsIAMEnabled } from './features/IAM/Shared/utilities';

const useStyles = makeStyles()((theme: Theme) => ({
activationWrapper: {
Expand Down Expand Up @@ -205,7 +205,9 @@ const IAM = React.lazy(() =>

export const MainContent = () => {
const { classes, cx } = useStyles();
const { data: preferences } = usePreferences();
const { data: isDesktopSidebarOpenPreference } = usePreferences(
(preferences) => preferences?.desktop_sidebar_open
);
const { mutateAsync: updatePreferences } = useMutatePreferences();
const queryClient = useQueryClient();

Expand Down Expand Up @@ -287,11 +289,11 @@ export const MainContent = () => {
return <MaintenanceScreen />;
}

const desktopMenuIsOpen = preferences?.desktop_sidebar_open ?? false;
const desktopMenuIsOpen = isDesktopSidebarOpenPreference ?? false;

const desktopMenuToggle = () => {
updatePreferences({
desktop_sidebar_open: !preferences?.desktop_sidebar_open,
desktop_sidebar_open: !isDesktopSidebarOpenPreference,
});
};

Expand Down
8 changes: 5 additions & 3 deletions packages/manager/src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import { useStyles } from './Root.styles';

export const Root = () => {
const { classes, cx } = useStyles();
const { data: preferences } = usePreferences();
const { data: isDesktopSidebarOpenPreference } = usePreferences(
(preferences) => preferences?.desktop_sidebar_open
);
const { mutateAsync: updatePreferences } = useMutatePreferences();

const globalErrors = useGlobalErrors();
Expand All @@ -57,11 +59,11 @@ export const Root = () => {
const { data: profile } = useProfile();
const username = profile?.username || '';

const desktopMenuIsOpen = preferences?.desktop_sidebar_open ?? false;
const desktopMenuIsOpen = isDesktopSidebarOpenPreference ?? false;

const desktopMenuToggle = () => {
updatePreferences({
desktop_sidebar_open: !preferences?.desktop_sidebar_open,
desktop_sidebar_open: !isDesktopSidebarOpenPreference,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,30 @@ import {
usePreferences,
} from 'src/queries/profile/preferences';

export interface PreferenceToggleProps<T> {
preference: T;
togglePreference: () => T;
}
import type { ManagerPreferences } from 'src/types/ManagerPreferences';

interface RenderChildrenProps<T> {
preference: T;
togglePreference: () => T;
preference: NonNullable<T>;
togglePreference: () => NonNullable<T>;
}

type RenderChildren<T> = (props: RenderChildrenProps<T>) => JSX.Element;

interface Props<T> {
children: RenderChildren<T>;
initialSetCallbackFn?: (value: T) => void;
preferenceKey: string;
preferenceOptions: [T, T];
toggleCallbackFn?: (value: T) => void;
value?: T;
interface Props<Key extends keyof ManagerPreferences> {
children: RenderChildren<ManagerPreferences[Key]>;
initialSetCallbackFn?: (value: ManagerPreferences[Key]) => void;
preferenceKey: Key;
preferenceOptions: [ManagerPreferences[Key], ManagerPreferences[Key]];
toggleCallbackFn?: (value: ManagerPreferences[Key]) => void;
value?: ManagerPreferences[Key];
}

export const PreferenceToggle = <T,>(props: Props<T>) => {
/**
* @deprecated There are more simple ways to use preferences. Look into using `usePreferences` directly.
*/
export const PreferenceToggle = <Key extends keyof ManagerPreferences>(
props: Props<Key>
) => {
const {
children,
preferenceKey,
Expand All @@ -38,7 +40,7 @@ export const PreferenceToggle = <T,>(props: Props<T>) => {
const { mutateAsync: updateUserPreferences } = useMutatePreferences();

const togglePreference = () => {
let newPreferenceToSet: T;
let newPreferenceToSet: ManagerPreferences[Key];

if (preferences?.[preferenceKey] === undefined) {
// Because we default to preferenceOptions[0], toggling with no preference should pick preferenceOptions[1]
Expand All @@ -58,11 +60,11 @@ export const PreferenceToggle = <T,>(props: Props<T>) => {
toggleCallbackFn(newPreferenceToSet);
}

return newPreferenceToSet;
return newPreferenceToSet!;
};

return children({
preference: value ?? preferences?.[preferenceKey] ?? preferenceOptions[0],
preference: value ?? preferences?.[preferenceKey] ?? preferenceOptions[0]!,
togglePreference,
});
};
13 changes: 6 additions & 7 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ export const PrimaryNav = (props: PrimaryNavProps) => {

const { isIAMEnabled, isIAMBeta } = useIsIAMEnabled();

const { data: preferences } = usePreferences();
const { data: collapsedSideNavPreference } = usePreferences(
(preferences) => preferences?.collapsedSideNavProductFamilies
);

const collapsedAccordions = collapsedSideNavPreference ?? [];

const { mutateAsync: updatePreferences } = useMutatePreferences();

const productFamilyLinkGroups: ProductFamilyLinkGroup<
Expand Down Expand Up @@ -253,10 +258,6 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
]
);

const [collapsedAccordions, setCollapsedAccordions] = React.useState<
number[]
>(preferences?.collapsedSideNavProductFamilies ?? []);

const accordionClicked = (index: number) => {
let updatedCollapsedAccordions;
if (collapsedAccordions.includes(index)) {
Expand All @@ -266,13 +267,11 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
updatePreferences({
collapsedSideNavProductFamilies: updatedCollapsedAccordions,
});
setCollapsedAccordions(updatedCollapsedAccordions);
} else {
updatedCollapsedAccordions = [...collapsedAccordions, index];
updatePreferences({
collapsedSideNavProductFamilies: updatedCollapsedAccordions,
});
setCollapsedAccordions(updatedCollapsedAccordions);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ export const LinodeResize = (props: Props) => {
);

const { data: types } = useAllTypes(open);
const { data: preferences } = usePreferences(open);

const { data: typeToConfirmPreference } = usePreferences(
(preferences) => preferences?.type_to_confirm,
open
);

const { enqueueSnackbar } = useSnackbar();
const [confirmationText, setConfirmationText] = React.useState('');
const [resizeError, setResizeError] = React.useState<string>('');
Expand Down Expand Up @@ -162,8 +167,7 @@ export const LinodeResize = (props: Props) => {
const tableDisabled = hostMaintenance || isLinodesGrantReadOnly;

const submitButtonDisabled =
preferences?.type_to_confirm !== false &&
confirmationText !== linode?.label;
typeToConfirmPreference !== false && confirmationText !== linode?.label;

const type = types?.find((t) => t.id === linode?.type);

Expand Down Expand Up @@ -323,7 +327,7 @@ export const LinodeResize = (props: Props) => {
title="Confirm"
typographyStyle={{ marginBottom: 8 }}
value={confirmationText}
visible={preferences?.type_to_confirm}
visible={typeToConfirmPreference}
/>
</Box>
<Box display="flex" justifyContent="flex-end">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import type { ExtendedStatus } from './utils';
import type { Config } from '@linode/api-v4/lib/linodes/types';
import type { APIError } from '@linode/api-v4/lib/types';
import type { RouteComponentProps } from 'react-router-dom';
import type { PreferenceToggleProps } from 'src/components/PreferenceToggle/PreferenceToggle';
import type { WithFeatureFlagProps } from 'src/containers/flags.container';
import type { WithProfileProps } from 'src/containers/profile.container';
import type { DialogType } from 'src/features/Linodes/types';
Expand Down Expand Up @@ -308,17 +307,17 @@ class ListLinodes extends React.Component<CombinedProps, State> {
)}
<DocumentTitleSegment segment="Linodes" />
<ProductInformationBanner bannerLocation="Linodes" />
<PreferenceToggle<boolean>
<PreferenceToggle
preferenceKey="linodes_group_by_tag"
preferenceOptions={[false, true]}
toggleCallbackFn={sendGroupByAnalytic}
>
{({
preference: linodesAreGrouped,
togglePreference: toggleGroupLinodes,
}: PreferenceToggleProps<boolean>) => {
}) => {
return (
<PreferenceToggle<'grid' | 'list'>
<PreferenceToggle
preferenceKey="linodes_view_style"
preferenceOptions={['list', 'grid']}
toggleCallbackFn={this.changeView}
Expand All @@ -331,7 +330,7 @@ class ListLinodes extends React.Component<CombinedProps, State> {
{({
preference: linodeViewPreference,
togglePreference: toggleLinodeView,
}: PreferenceToggleProps<'grid' | 'list'>) => {
}) => {
return (
<React.Fragment>
<React.Fragment>
Expand Down
35 changes: 19 additions & 16 deletions packages/manager/src/queries/profile/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
import { updateUserPreferences } from '@linode/api-v4';
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';

import { ManagerPreferences } from 'src/types/ManagerPreferences';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

import { queryPresets } from '../base';
import { profileQueries } from './profile';

import type { APIError } from '@linode/api-v4';
import type { QueryClient } from '@tanstack/react-query';
import type { ManagerPreferences } from 'src/types/ManagerPreferences';

export const usePreferences = (enabled = true) =>
useQuery<ManagerPreferences, APIError[]>({
// Reference for this pattern: https://tkdodo.eu/blog/react-query-data-transformations#3-using-the-select-option
export const usePreferences = <TData = ManagerPreferences>(
select?: (data: ManagerPreferences | undefined) => TData,
enabled = true
) =>
useQuery({
...profileQueries.preferences,
...queryPresets.oneTimeFetch,
enabled,
select,
});

export const useMutatePreferences = (replace = false) => {
const { data: preferences } = usePreferences(!replace);
const queryClient = useQueryClient();

return useMutation<
ManagerPreferences,
APIError[],
Partial<ManagerPreferences>
>({
mutationFn: (data) =>
updateUserPreferences({
...(!replace && preferences !== undefined ? preferences : {}),
...data,
}),
async mutationFn(data) {
if (replace) {
return updateUserPreferences(data);
}
const existingPreferences = await queryClient.ensureQueryData<ManagerPreferences>(
profileQueries.preferences
);
return updateUserPreferences({ ...existingPreferences, ...data });
},
onMutate: (data) => updatePreferenceData(data, replace, queryClient),
});
};
Expand Down
6 changes: 4 additions & 2 deletions packages/manager/src/types/ManagerPreferences.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UserPreferences } from '@linode/api-v4';
import type { AclpConfig } from '@linode/api-v4';
import type { Order } from 'src/hooks/useOrder';
import type { ThemeChoice } from 'src/utilities/theme';

Expand All @@ -18,9 +18,11 @@ export interface DismissedNotification {
label?: string;
}

export interface ManagerPreferences extends UserPreferences {
export interface ManagerPreferences {
aclpPreference?: AclpConfig; // Why is this type in @linode/api-v4?
avatarColor?: string;
backups_cta_dismissed?: boolean;
collapsedSideNavProductFamilies?: number[];
desktop_sidebar_open?: boolean;
dismissed_notifications?: Record<string, DismissedNotification>;
domains_group_by_tag?: boolean;
Expand Down
12 changes: 9 additions & 3 deletions packages/manager/src/utilities/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ export const getThemeFromPreferenceValue = (
};

export const useColorMode = () => {
// Make sure we are authenticated before we fetch preferences.
const isAuthenticated = !!useAuthentication().token;
const { data: preferences } = usePreferences(isAuthenticated);

const { data: themePreference } = usePreferences(
(preferences) => preferences?.theme,
// Make sure we are authenticated before we fetch preferences.
// If we don't, we get an authentication loop.
isAuthenticated
);

const isSystemInDarkMode = useMediaQuery('(prefers-color-scheme: dark)');

const colorMode = getThemeFromPreferenceValue(
preferences?.theme,
themePreference,
isSystemInDarkMode
);

Expand Down

0 comments on commit abf05ca

Please sign in to comment.