Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13056-fixed-1762431727811.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Race condition with Preferences overrides in PrimaryNav ([#13056](https://github.com/linode/manager/pull/13056))
125 changes: 64 additions & 61 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import PrimaryNav from './PrimaryNav';

import type { ManagerPreferences } from '@linode/utilities';
import type { Flags } from 'src/featureFlags';

const props = {
Expand All @@ -28,6 +27,8 @@
isIAMEnabled: false,
})),
usePreferences: vi.fn().mockReturnValue({}),
useAccount: vi.fn().mockReturnValue({}),
useAccountSettings: vi.fn().mockReturnValue({}),
}));

vi.mock('src/features/IAM/hooks/useIsIAMEnabled', () => ({
Expand All @@ -39,28 +40,42 @@
return {
...actual,
usePreferences: queryMocks.usePreferences,
useAccount: queryMocks.useAccount,
useAccountSettings: queryMocks.useAccountSettings,
};
});

describe('PrimaryNav', () => {
const preference: ManagerPreferences['collapsedSideNavProductFamilies'] = [];
beforeEach(() => {
queryMocks.usePreferences.mockReturnValue({
data: {
collapsedSideNavProductFamilies: [],
},
isLoading: false,
error: null,
});
});

it('only contains a "Managed" menu link if the user has Managed services.', async () => {
server.use(
http.get('*/account/maintenance', () => {
return HttpResponse.json({ managed: false });
})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ™ƒ how was that even working it's mocking an endpoint that doesn't exist

);
queryMocks.useAccountSettings.mockReturnValue({
data: {
managed: false,
},
isLoading: false,
error: null,
});

const { findByTestId, getByTestId, queryByTestId, rerender } =
renderWithTheme(<PrimaryNav {...props} />, { queryClient });
expect(queryByTestId(queryString)).not.toBeInTheDocument();

server.use(
http.get('*/account/maintenance', () => {
return HttpResponse.json({ managed: true });
})
);
queryMocks.useAccountSettings.mockReturnValue({
data: {
managed: true,
},
isLoading: false,
error: null,
});

rerender(wrapWithTheme(<PrimaryNav {...props} />, { queryClient }));

Expand All @@ -70,6 +85,13 @@
});

it('should have aria-current attribute for accessible links', () => {
queryMocks.useAccountSettings.mockReturnValue({
data: {
managed: true,
},
isLoading: false,
error: null,
});
const { getByTestId } = renderWithTheme(<PrimaryNav {...props} />, {
queryClient,
});
Expand All @@ -78,19 +100,15 @@
});

it('should show Databases menu item if the user has the account capability V1', async () => {
queryMocks.usePreferences.mockReturnValue({
data: preference,
});

const account = accountFactory.build({
capabilities: ['Managed Databases'],
});

server.use(
http.get('*/account', () => {
return HttpResponse.json(account);
})
);
queryMocks.useAccount.mockReturnValue({
data: account,
isLoading: false,
error: null,
});

const flags: Partial<Flags> = {
dbaasV2: {
Expand All @@ -99,33 +117,26 @@
},
};

const { findByTestId, queryByTestId } = renderWithTheme(
<PrimaryNav {...props} />,
{
flags,
}
);
renderWithTheme(<PrimaryNav {...props} />, {
flags,
});

const databaseNavItem = await findByTestId('menu-item-Databases');
const databaseNavItem = screen.getByTestId('menu-item-Databases');

Check warning on line 124 in packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 4 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 4 times.","line":124,"column":48,"nodeType":"Literal","endLine":124,"endColumn":69}
Comment thread
abailly-akamai marked this conversation as resolved.

expect(databaseNavItem).toBeVisible();
expect(queryByTestId('betaChip')).toBeNull();
expect(screen.queryByTestId('betaChip')).toBeNull();
});

it('should show Databases menu item if the user has the account capability V2 Beta', async () => {
queryMocks.usePreferences.mockReturnValue({
data: preference,
});

const account = accountFactory.build({
capabilities: ['Managed Databases Beta'],
});

server.use(
http.get('*/account', () => {
return HttpResponse.json(account);
})
);
queryMocks.useAccount.mockReturnValue({
data: account,
isLoading: false,
error: null,
});

const flags: Partial<Flags> = {
dbaasV2: {
Expand All @@ -146,19 +157,15 @@
});

it('should show Databases menu item if the user has the account capability V2', async () => {
queryMocks.usePreferences.mockReturnValue({
data: preference,
});

const account = accountFactory.build({
capabilities: ['Managed Databases'],
});

server.use(
http.get('*/account', () => {
return HttpResponse.json(account);
})
);
queryMocks.useAccount.mockReturnValue({
data: account,
isLoading: false,
error: null,
});

const flags: Partial<Flags> = {
dbaasV2: {
Expand All @@ -181,19 +188,15 @@
});

it('should show Databases menu item if the user has the account capability V2', async () => {
queryMocks.usePreferences.mockReturnValue({
data: preference,
});

const account = accountFactory.build({
capabilities: ['Managed Databases Beta'],
});

server.use(
http.get('*/account', () => {
return HttpResponse.json(account);
})
);
queryMocks.useAccount.mockReturnValue({
data: account,
isLoading: false,
error: null,
});

const flags: Partial<Flags> = {
dbaasV2: {
Expand All @@ -216,11 +219,11 @@
capabilities: ['Akamai Cloud Pulse'],
});

server.use(
http.get('*/account', () => {
return HttpResponse.json(account);
})
);
queryMocks.useAccount.mockReturnValue({
data: account,
isLoading: false,
error: null,
});

const flags = {
aclp: {
Expand Down
54 changes: 32 additions & 22 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
const location = useLocation();

const { data: accountSettings } = useAccountSettings();

const isManaged = accountSettings?.managed ?? false;

const { isACLPEnabled } = useIsACLPEnabled();
Expand All @@ -114,16 +115,18 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled();

const {
data: collapsedSideNavPreference,
data: preferences,
error: preferencesError,
isLoading: preferencesLoading,
} = usePreferences(
(preferences) => preferences?.collapsedSideNavProductFamilies
);
} = usePreferences();

const collapsedAccordions = collapsedSideNavPreference ?? [
1, 2, 3, 4, 5, 6, 7,
]; // by default, we collapse all categories if no preference is set;
const collapsedSideNavPreference =
preferences?.collapsedSideNavProductFamilies;

const collapsedAccordions = React.useMemo(
() => collapsedSideNavPreference ?? [1, 2, 3, 4, 5, 6, 7], // by default, we collapse all categories if no preference is set;
[collapsedSideNavPreference]
);

const { mutateAsync: updatePreferences } = useMutatePreferences();

Expand Down Expand Up @@ -337,22 +340,22 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
]
);

const accordionClicked = (index: number) => {
let updatedCollapsedAccordions: number[] = [1, 2, 3, 4, 5, 6, 7];
if (collapsedAccordions.includes(index)) {
updatedCollapsedAccordions = collapsedAccordions.filter(
(accIndex) => accIndex !== index
);
updatePreferences({
collapsedSideNavProductFamilies: updatedCollapsedAccordions,
});
} else {
updatedCollapsedAccordions = [...collapsedAccordions, index];
const accordionClicked = React.useCallback(
(index: number) => {
let updatedCollapsedAccordions: number[];
Comment thread
abailly-akamai marked this conversation as resolved.
if (collapsedAccordions.includes(index)) {
updatedCollapsedAccordions = collapsedAccordions.filter(
(accIndex) => accIndex !== index
);
} else {
updatedCollapsedAccordions = [...collapsedAccordions, index];
}
updatePreferences({
collapsedSideNavProductFamilies: updatedCollapsedAccordions,
});
}
};
},
[collapsedAccordions, updatePreferences]
);

const checkOverflow = React.useCallback(() => {
if (navItemsRef.current && primaryNavRef.current) {
Expand Down Expand Up @@ -404,10 +407,17 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
// When a user lands on a page and does not have any preference set,
// we want to expand the accordion that contains the active link for convenience and discoverability
React.useEffect(() => {
// Wait for preferences to load or if there's an error
if (preferencesLoading || preferencesError) {
return;
}

// Wait for preferences data to be available (not just the field, but the whole object)
if (!preferences) {
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual intended fix


// If user has already set collapsedSideNavProductFamilies preference, don't override it
if (collapsedSideNavPreference) {
return;
}
Expand All @@ -424,13 +434,13 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
if (activeGroupIndex !== -1) {
accordionClicked(activeGroupIndex);
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
accordionClicked,
location.pathname,
location.search,
productFamilyLinkGroups,
collapsedSideNavPreference,
preferences,
preferencesLoading,
preferencesError,
]);
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/dev-tools/DevTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const DevTools = (props: Props) => {
};

const handleGoToPreferences = () => {
window.location.assign('/profile/settings?preferenceEditor=true');
window.location.assign('/profile/preferences?preferenceEditor=true');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

link was broken due to recent nav changes

};

React.useEffect(() => {
Expand Down