Skip to content

Commit e12261f

Browse files
fabrice-akamaipmakode-akamaidwiley-akamai
authored
upcoming: [UIE 9401] – Implement Share Groups Landing page tabs (#13471)
* Added missing padding around the Managed dashboard card * changed spacing to spacingFunction * Implement share groups landing page tabs * Update image subtab interface * Fix image utils test case * Update packages/manager/src/features/Images/utils.test.tsx Co-authored-by: Purvesh Makode <pmakode@akamai.com> * Added changeset: Add share groups tabs * Update file name and consolidate imports * Rename file for consistency * Update packages/manager/.changeset/pr-13471-upcoming-features-1773085243886.md Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> --------- Co-authored-by: Purvesh Makode <pmakode@akamai.com> Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com>
1 parent f6b2e8d commit e12261f

9 files changed

Lines changed: 270 additions & 27 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Private Image Sharing: Add Share Groups tabs ([#13471](https://github.com/linode/manager/pull/13471))

packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { TabList } from 'src/components/Tabs/TabList';
1010
import { TabPanels } from 'src/components/Tabs/TabPanels';
1111
import { Tabs } from 'src/components/Tabs/Tabs';
1212

13-
import { getImageLibrarySubTabIndex } from '../../../utils';
13+
import { getSubTabIndex } from '../../../utils';
1414
import { DeleteImageDialog } from '../../DeleteImageDialog';
1515
import { EditImageDrawer } from '../../EditImageDrawer';
1616
import { ManageImageReplicasForm } from '../../ImageRegions/ManageImageRegionsForm';
@@ -108,10 +108,7 @@ export const ImageLibraryTabs = () => {
108108
onRebuild: handleRebuild,
109109
};
110110

111-
const subTabIndex = getImageLibrarySubTabIndex(
112-
subTabs,
113-
imageTypeParams?.imageType
114-
);
111+
const subTabIndex = getSubTabIndex(subTabs, imageTypeParams?.imageType);
115112

116113
const onTabChange = (index: number) => {
117114
// - Update the "imageType" param.

packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/imageLibraryTabsConfig.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
MANUAL_IMAGES_PREFERENCE_KEY,
1313
} from '../../../constants';
1414

15-
import type { ImageLibrarySubTab, ImageLibraryType } from '../../../utils';
15+
import type { ImageLibraryType, ImageSubTab } from '../../../utils';
1616
import type { Image } from '@linode/api-v4';
1717
import type { HiddenProps } from '@linode/ui';
1818

@@ -54,7 +54,7 @@ export interface ImageConfig {
5454
type: Image['type'];
5555
}
5656

57-
export const imageLibrarySubTabs: ImageLibrarySubTab[] = [
57+
export const imageLibrarySubTabs: ImageSubTab<ImageLibraryType>[] = [
5858
{ type: 'owned-by-me', title: 'Owned by me' },
5959
{
6060
type: 'shared-with-me',
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { userEvent } from '@testing-library/user-event/dist/cjs/setup/index.js';
2+
import * as React from 'react';
3+
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { ShareGroupsTabs } from './ShareGroupsTabs';
7+
8+
const queryMocks = vi.hoisted(() => ({
9+
useNavigate: vi.fn(),
10+
useParams: vi.fn(),
11+
useLocation: vi.fn(),
12+
}));
13+
14+
vi.mock('@tanstack/react-router', async () => {
15+
const actual = await vi.importActual('@tanstack/react-router');
16+
return {
17+
...actual,
18+
useLocation: queryMocks.useLocation,
19+
useNavigate: queryMocks.useNavigate,
20+
useParams: queryMocks.useParams,
21+
};
22+
});
23+
24+
describe('ShareGroupsTabs', () => {
25+
beforeEach(() => {
26+
vi.clearAllMocks();
27+
const mockNavigate = vi.fn();
28+
queryMocks.useNavigate.mockReturnValue(mockNavigate);
29+
});
30+
31+
it('should render all share groups tabs', async () => {
32+
queryMocks.useParams.mockReturnValue({ shareGroupsType: 'owned-groups' });
33+
34+
const { getByText } = renderWithTheme(<ShareGroupsTabs />, {
35+
initialRoute: '/images/share-groups/owned-groups',
36+
});
37+
38+
expect(getByText('Owned groups')).toBeVisible();
39+
expect(getByText('Joined groups')).toBeVisible();
40+
expect(getByText('My membership requests')).toBeVisible();
41+
});
42+
43+
it('should navigate to owned-groups tab when clicked', async () => {
44+
queryMocks.useParams.mockReturnValue({ shareGroupsType: 'owned-groups' });
45+
const mockNavigate = vi.fn();
46+
queryMocks.useNavigate.mockReturnValue(mockNavigate);
47+
48+
const { getByText } = renderWithTheme(<ShareGroupsTabs />, {
49+
initialRoute: '/images/share-groups/owned-groups',
50+
});
51+
52+
const ownedGroupsTab = getByText('Owned groups', { selector: 'button' });
53+
await userEvent.click(ownedGroupsTab);
54+
55+
expect(mockNavigate).toHaveBeenCalledWith({
56+
to: '/images/share-groups/$shareGroupsType',
57+
params: {
58+
shareGroupsType: 'owned-groups',
59+
},
60+
});
61+
});
62+
63+
it('should navigate to joined-groups tab when clicked', async () => {
64+
queryMocks.useParams.mockReturnValue({ shareGroupsType: 'owned-groups' });
65+
const mockNavigate = vi.fn();
66+
queryMocks.useNavigate.mockReturnValue(mockNavigate);
67+
68+
const { getByText } = renderWithTheme(<ShareGroupsTabs />, {
69+
initialRoute: '/images/share-groups/owned-groups',
70+
});
71+
72+
const joinedGroupsTab = getByText('Joined groups', { selector: 'button' });
73+
await userEvent.click(joinedGroupsTab);
74+
75+
expect(mockNavigate).toHaveBeenCalledWith({
76+
to: '/images/share-groups/$shareGroupsType',
77+
params: {
78+
shareGroupsType: 'joined-groups',
79+
},
80+
});
81+
});
82+
83+
it('should navigate to membership-requests tab when clicked', async () => {
84+
queryMocks.useParams.mockReturnValue({ shareGroupsType: 'owned-groups' });
85+
const mockNavigate = vi.fn();
86+
queryMocks.useNavigate.mockReturnValue(mockNavigate);
87+
88+
const { getByText } = renderWithTheme(<ShareGroupsTabs />, {
89+
initialRoute: '/images/share-groups/owned-groups',
90+
});
91+
92+
const membershipTab = getByText('My membership requests', {
93+
selector: 'button',
94+
});
95+
await userEvent.click(membershipTab);
96+
97+
expect(mockNavigate).toHaveBeenCalledWith({
98+
to: '/images/share-groups/$shareGroupsType',
99+
params: {
100+
shareGroupsType: 'membership-requests',
101+
},
102+
});
103+
});
104+
});
Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,71 @@
1-
import { Notice } from '@linode/ui';
1+
import { BetaChip, Notice, Stack } from '@linode/ui';
2+
import { useNavigate, useParams } from '@tanstack/react-router';
23
import React from 'react';
34

5+
import { SuspenseLoader } from 'src/components/SuspenseLoader';
6+
import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
7+
import { Tab } from 'src/components/Tabs/Tab';
8+
import { TabList } from 'src/components/Tabs/TabList';
9+
import { TabPanels } from 'src/components/Tabs/TabPanels';
10+
import { Tabs } from 'src/components/Tabs/Tabs';
11+
import { getSubTabIndex } from 'src/features/Images/utils';
12+
13+
import { shareGroupsSubTabs as subTabs } from './shareGroupsTabsConfig';
14+
415
export const ShareGroupsTabs = () => {
5-
return <Notice variant="info">Share Groups is coming soon...</Notice>;
16+
const navigate = useNavigate();
17+
18+
const shareGroupsTypeParams = useParams({
19+
from: '/images/share-groups/$shareGroupsType',
20+
shouldThrow: false,
21+
});
22+
23+
const onTabChange = (index: number) => {
24+
navigate({
25+
to: `/images/share-groups/$shareGroupsType`,
26+
params: {
27+
shareGroupsType: subTabs[index].type,
28+
},
29+
});
30+
};
31+
32+
const subTabIndex = getSubTabIndex(
33+
subTabs,
34+
shareGroupsTypeParams?.shareGroupsType
35+
);
36+
37+
return (
38+
<Stack spacing={3}>
39+
<Tabs index={subTabIndex} onChange={onTabChange}>
40+
<TabList>
41+
{subTabs.map((tab) => (
42+
<Tab key={`images-${tab.type}`}>
43+
{tab.title} {tab.isBeta ? <BetaChip /> : null}
44+
</Tab>
45+
))}
46+
</TabList>
47+
<React.Suspense fallback={<SuspenseLoader />}>
48+
<TabPanels>
49+
{subTabs.map((tab, index) => (
50+
<SafeTabPanel index={index} key={`images-${tab.type}-content`}>
51+
{tab.type === 'owned-groups' && (
52+
<Notice variant="info">Owned Groups is coming soon...</Notice>
53+
)}
54+
{tab.type === 'joined-groups' && (
55+
<Notice variant="info">
56+
Joined Groups is coming soon...
57+
</Notice>
58+
)}
59+
{tab.type === 'membership-requests' && (
60+
<Notice variant="info">
61+
Membership Requests is coming soon...
62+
</Notice>
63+
)}
64+
</SafeTabPanel>
65+
))}
66+
</TabPanels>
67+
</React.Suspense>
68+
</Tabs>
69+
</Stack>
70+
);
671
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { ImageSubTab, ShareGroupsType } from 'src/features/Images/utils';
2+
3+
export const shareGroupsSubTabs: ImageSubTab<ShareGroupsType>[] = [
4+
{
5+
type: 'owned-groups',
6+
title: 'Owned groups',
7+
},
8+
{
9+
type: 'joined-groups',
10+
title: 'Joined groups',
11+
},
12+
{
13+
type: 'membership-requests',
14+
title: 'My membership requests',
15+
},
16+
];

packages/manager/src/features/Images/utils.test.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { wrapWithTheme } from 'src/utilities/testHelpers';
77
import {
88
getEventsForImages,
99
getImageLabelForLinode,
10-
getImageLibrarySubTabIndex,
1110
getImageTypeToImageLibraryType,
11+
getSubTabIndex,
1212
useIsPrivateImageSharingEnabled,
1313
} from './utils';
1414

15-
import type { ImageLibrarySubTab } from './utils';
15+
import type { ImageLibraryType, ImageSubTab } from './utils';
1616

1717
describe('getImageLabelForLinode', () => {
1818
it('handles finding an image and getting the label', () => {
@@ -94,30 +94,30 @@ describe('useIsPrivateImageSharingEnabled', () => {
9494
});
9595
});
9696

97-
describe('getImageLibrarySubTabIndex', () => {
98-
const subTabs: ImageLibrarySubTab[] = [
97+
describe('getSubTabIndex', () => {
98+
const subTabs: ImageSubTab<ImageLibraryType>[] = [
9999
{ type: 'owned-by-me', title: 'Owned by me' },
100100
{ type: 'shared-with-me', title: 'Shared with me', isBeta: true },
101101
{ type: 'recovery-images', title: 'Recovery images' },
102102
];
103103

104104
it('returns 0 if selectedTab is undefined', () => {
105-
expect(getImageLibrarySubTabIndex(subTabs, undefined)).toBe(0);
105+
expect(getSubTabIndex(subTabs, undefined)).toBe(0);
106106
});
107107

108108
it('returns the correct index when selectedTab matches a tab key', () => {
109-
expect(getImageLibrarySubTabIndex(subTabs, 'owned-by-me')).toBe(0);
110-
expect(getImageLibrarySubTabIndex(subTabs, 'shared-with-me')).toBe(1);
111-
expect(getImageLibrarySubTabIndex(subTabs, 'recovery-images')).toBe(2);
109+
expect(getSubTabIndex(subTabs, 'owned-by-me')).toBe(0);
110+
expect(getSubTabIndex(subTabs, 'shared-with-me')).toBe(1);
111+
expect(getSubTabIndex(subTabs, 'recovery-images')).toBe(2);
112112
});
113113

114114
it('returns 0 if selectedTab does not exist in subTabs', () => {
115115
// @ts-expect-error intentionally passing an unexpected value
116-
expect(getImageLibrarySubTabIndex(subTabs, 'hey')).toBe(0);
116+
expect(getSubTabIndex(subTabs, 'hey')).toBe(0);
117117
});
118118

119119
it('works with an empty subTabs array', () => {
120-
expect(getImageLibrarySubTabIndex([], 'owned-by-me')).toBe(0);
120+
expect(getSubTabIndex([], 'owned-by-me')).toBe(0);
121121
});
122122
});
123123

packages/manager/src/features/Images/utils.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ export type ImageLibraryType =
1010
| 'recovery-images'
1111
| 'shared-with-me';
1212

13+
export type ShareGroupsType =
14+
| 'joined-groups'
15+
| 'membership-requests'
16+
| 'owned-groups';
17+
1318
/**
14-
* Configuration for image sub-tabs within the Image Library tab.
19+
* Generic configuration for image sub-tabs within the Images feature for Image Library and Share Groups.
1520
*/
16-
export interface ImageLibrarySubTab {
21+
export interface ImageSubTab<T> {
1722
/** Whether this tab represents a beta feature */
1823
isBeta?: boolean;
1924
/** Display title for the tab */
2025
title: string;
2126
/** The type this tab represents */
22-
type: ImageLibraryType;
27+
type: T;
2328
}
2429

2530
export const getImageLabelForLinode = (linode: Linode, images: Image[]) => {
@@ -82,9 +87,9 @@ export const useIsPrivateImageSharingEnabled = () => {
8287
*
8388
* @returns the index of the selected sub-tab
8489
*/
85-
export const getImageLibrarySubTabIndex = (
86-
subTabs: ImageLibrarySubTab[],
87-
selectedTab: ImageLibraryType | undefined
90+
export const getSubTabIndex = (
91+
subTabs: ImageSubTab<ImageLibraryType | ShareGroupsType>[],
92+
selectedTab: ImageLibraryType | ShareGroupsType | undefined
8893
) => {
8994
if (selectedTab === undefined) {
9095
return 0;

0 commit comments

Comments
 (0)