Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8680aae
Added missing padding around the Managed dashboard card
fabrice-akamai Sep 26, 2025
d53e3f6
changed spacing to spacingFunction
fabrice-akamai Sep 26, 2025
ef5485f
Merge branch 'develop' of https://github.com/fabrice-akamai/manager i…
fabrice-akamai Mar 2, 2026
2a8188d
Merge branch 'develop' of https://github.com/fabrice-akamai/manager i…
fabrice-akamai Mar 3, 2026
2db02f3
Merge branch 'develop' of https://github.com/fabrice-akamai/manager i…
fabrice-akamai Mar 10, 2026
a8323a1
Merge branch 'develop' of https://github.com/fabrice-akamai/manager i…
fabrice-akamai Mar 19, 2026
77ec16e
Merge branch 'develop' of https://github.com/fabrice-akamai/manager i…
fabrice-akamai Mar 25, 2026
4a803c8
Merge branch 'develop' of https://github.com/fabrice-akamai/manager i…
fabrice-akamai Mar 27, 2026
408d904
Implement basic sharegroup create form
fabrice-akamai Mar 31, 2026
1befb73
Added changeset: Implement the basic share group create page
fabrice-akamai Mar 31, 2026
2c3f4dc
update unit test
fabrice-akamai Mar 31, 2026
66a034c
Merge branch 'develop' into UIE-9410-create-share-group-page
fabrice-akamai Mar 31, 2026
007e94d
Update packages/manager/.changeset/pr-13550-upcoming-features-1774974…
fabrice-akamai Mar 31, 2026
2dbdb37
Update the createShareGroup query
fabrice-akamai Mar 31, 2026
664d8c3
Merge branch 'UIE-9410-create-share-group-page' of https://github.com…
fabrice-akamai Mar 31, 2026
aabb130
Update description cell to truncate the text overflow and use tooltips
fabrice-akamai Apr 1, 2026
f099e81
Update table design and tooltip appearance
fabrice-akamai Apr 2, 2026
e23162c
Update table cell styling
fabrice-akamai Apr 2, 2026
15f2a0e
Update the share groups table columns and add tooltips
fabrice-akamai Apr 6, 2026
1f3b935
Update description column width
fabrice-akamai Apr 6, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Private Image Sharing: Implement basic structure of Share Group Create page ([#13550](https://github.com/linode/manager/pull/13550))
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ export const StyledImageTableContainer = styled(Box, {
'& cds-table-row:last-child:not([rowborder])': {
borderBottom: `1px solid ${theme.tokens.component.Table.Row.Border}`,
},

'& cds-table-header-cell, & cds-table-cell': {
boxSizing: 'border-box',
},
}));
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { usePreferences, useProfile } from '@linode/queries';
import { Hidden, LinkButton } from '@linode/ui';
import { Hidden, LinkButton, Tooltip } from '@linode/ui';
import { truncateEnd } from '@linode/utilities';
import { TableCell, TableRow } from 'akamai-cds-react-components/Table';
import React from 'react';

Expand Down Expand Up @@ -42,26 +43,46 @@ export const ShareGroupRow = (props: Props) => {
data-qa-sharegroup-row={id}
key={id}
rowborder={!isTableStripingEnabled}
style={{ padding: 0 }}
zebra={isTableStripingEnabled}
>
<TableCell data-pendo-id={`Images Groups Owned-Group name`}>
<LinkButton onClick={() => {}}>{label}</LinkButton>
</TableCell>
<TableCell>{description}</TableCell>
<TableCell>{members_count}</TableCell>
<Tooltip title={label.length > 32 ? label : ''}>
<TableCell
className="group-column"
data-pendo-id={`Images Groups Owned-Group name`}
>
<LinkButton
onClick={() => {}}
sx={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
display: 'block',
}}
>
{truncateEnd(label, 32)}
</LinkButton>
</TableCell>
</Tooltip>
<Tooltip title={description.length > 50 ? description : ''}>
<TableCell className="description-column">
{truncateEnd(description, 50)}
</TableCell>
</Tooltip>
<TableCell className="membersCount-column">{members_count}</TableCell>
<Hidden smDown>
<TableCell>{images_count}</TableCell>
<TableCell className="imagesCount-column">{images_count}</TableCell>
</Hidden>
<Hidden lgDown>
<TableCell style={{ whiteSpace: 'nowrap' }}>
<TableCell className="created-column">
{created &&
formatDate(created, {
timezone: profile?.timezone,
})}
</TableCell>
</Hidden>
<Hidden lgDown>
<TableCell style={{ whiteSpace: 'nowrap' }}>
<TableCell className="updated-column">
{updated !== null
? formatDate(updated, { timezone: profile?.timezone })
: '–'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const StyledActionMenuWrapper = styled(TableCell, {
justifyContent: 'flex-end',
display: 'flex',
alignItems: 'center',
maxWidth: 40,
maxWidth: '5%',
'& button': {
padding: 0,
color: theme.tokens.alias.Content.Icon.Primary.Default,
Expand All @@ -18,3 +18,61 @@ export const StyledActionMenuWrapper = styled(TableCell, {
color: theme.tokens.alias.Content.Icon.Primary.Hover,
},
}));

const TABLE_CELL_BASE_STYLES: React.CSSProperties = {
boxSizing: 'border-box',
};

export const StyledShareGroupsTableContainer = styled('div', {
label: 'StyledShareGroupsTable',
})(({ theme }) => ({
'& .group-column': {
minWidth: '20%',
...TABLE_CELL_BASE_STYLES,
[theme.breakpoints.down('sm')]: {
minWidth: '30%',
},
},
'& .description-column': {
minWidth: '25%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
display: 'block',
...TABLE_CELL_BASE_STYLES,
[theme.breakpoints.down('lg')]: {
minWidth: '40%',
},
[theme.breakpoints.down('sm')]: {
minWidth: '40%',
},
},
'& .membersCount-column': {
minWidth: '11%',
...TABLE_CELL_BASE_STYLES,
[theme.breakpoints.down('lg')]: {
minWidth: '15%',
},
},
'& .imagesCount-column': {
minWidth: '9%',
...TABLE_CELL_BASE_STYLES,
[theme.breakpoints.down('lg')]: {
minWidth: '15%',
},
},
'& .created-column': {
minWidth: '15%',
...TABLE_CELL_BASE_STYLES,
whiteSpace: 'nowrap',
},
'& .updated-column': {
minWidth: '15%',
...TABLE_CELL_BASE_STYLES,
whiteSpace: 'nowrap',
},
'& .action-column': {
maxWidth: '5%',
...TABLE_CELL_BASE_STYLES,
},
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { imageSharegroupFactory } from '@linode/utilities';
import { userEvent } from '@testing-library/user-event';
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { ShareGroupsCreate } from './ShareGroupsCreate';

const queryMocks = vi.hoisted(() => ({
useCreateShareGroupMutation: vi.fn().mockReturnValue({}),
useNavigate: vi.fn().mockReturnValue(vi.fn()),
}));

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useCreateShareGroupMutation: queryMocks.useCreateShareGroupMutation,
};
});

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
...actual,
useNavigate: queryMocks.useNavigate,
};
});

describe('ShareGroupsCreate', () => {
const shareGroupLabel = 'My Share Group';
const shareGroupDescription = 'Test Description';

let mockNavigate: ReturnType<typeof vi.fn>;
let mockMutateAsync: ReturnType<typeof vi.fn>;

beforeEach(() => {
mockNavigate = vi.fn();
mockMutateAsync = vi.fn();

queryMocks.useNavigate.mockReturnValue(mockNavigate);
queryMocks.useCreateShareGroupMutation.mockReturnValue({
mutateAsync: mockMutateAsync,
isLoading: false,
error: null,
});
});

afterEach(() => {
vi.clearAllMocks();
});

it('should render the form with all fields, titles, and buttons in their default state', () => {
const { getByRole, getByText } = renderWithTheme(<ShareGroupsCreate />);

expect(getByText('Share group details')).toBeVisible();
expect(getByText('Images')).toBeVisible();
expect(getByText('Selected images (0)')).toBeVisible();

expect(
getByText(
'Add a name and description for your share group. These details are visible to all group members.'
)
).toBeVisible();

const labelField = getByRole('textbox', { name: /Label/i });
expect(labelField).toBeVisible();
expect(labelField).toHaveValue('');

const descriptionField = getByRole('textbox', { name: /Description/i });
expect(descriptionField).toBeVisible();
expect(descriptionField).toHaveValue('');

const submitButton = getByRole('button', { name: /Create Share Group/i });
expect(submitButton).toBeVisible();
expect(submitButton).toBeEnabled();
});

it('should submit the form with valid data', async () => {
const shareGroup = imageSharegroupFactory.build();

mockMutateAsync.mockResolvedValue(shareGroup);

const { getByRole } = renderWithTheme(<ShareGroupsCreate />);

const labelField = getByRole('textbox', { name: /Label/i });
const descriptionField = getByRole('textbox', { name: /Description/i });
const submitButton = getByRole('button', { name: /Create Share Group/i });

await userEvent.type(labelField, shareGroupLabel);
await userEvent.type(descriptionField, shareGroupDescription);
await userEvent.click(submitButton);

expect(mockMutateAsync).toHaveBeenCalledWith({
label: shareGroupLabel,
description: shareGroupDescription,
});

expect(mockNavigate).toHaveBeenCalledWith({
search: expect.any(Function),
to: '/images/share-groups',
});
});

it('should submit the form with only label (description is optional)', async () => {
const shareGroup = imageSharegroupFactory.build();

mockMutateAsync.mockResolvedValue(shareGroup);

const { getByRole } = renderWithTheme(<ShareGroupsCreate />);

const labelField = getByRole('textbox', { name: /Label/i });
const submitButton = getByRole('button', { name: /Create Share Group/i });

await userEvent.type(labelField, shareGroupLabel);
await userEvent.click(submitButton);

expect(mockMutateAsync).toHaveBeenCalledWith({
label: shareGroupLabel,
});
});

it('should display field-specific errors from API', async () => {
const apiError = [
{
field: 'label',
reason: 'Label must be unique',
},
];

mockMutateAsync.mockRejectedValue(apiError);

const { getByRole, getByText } = renderWithTheme(<ShareGroupsCreate />);

const labelField = getByRole('textbox', { name: /Label/i });
const submitButton = getByRole('button', { name: /Create Share Group/i });

await userEvent.type(labelField, 'Duplicate Label');
await userEvent.click(submitButton);

expect(getByText('Label must be unique')).toBeVisible();
});
});
Loading
Loading