Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Private Image Sharing: Shared with me tab ([#13500](https://github.com/linode/manager/pull/13500))
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { formatDate } from 'src/utilities/formatDate';

import { TABLE_CELL_BASE_STYLE } from './constants';
import { getRegionListItem } from './utilities';

import type {
IMAGE_SELECT_TABLE_LINODE_CREATE_PENDO_IDS,
Expand Down Expand Up @@ -76,20 +77,12 @@ export const ImageSelectTableRow = (props: Props) => {
return 'β€”';
};

const getRegionListItem = (imageRegion: ImageRegion) => {
const matchingRegion = regions.find((r) => r.id === imageRegion.region);

return matchingRegion
? `${matchingRegion.label} (${imageRegion.region})`
: imageRegion.region;
};

const FormattedRegionList = () => (
<StyledFormattedRegionList>
{imageRegions.map((region: ImageRegion, idx) => {
return (
<ListItem disablePadding key={`${region.region}-${idx}`}>
{getRegionListItem(region)}
{getRegionListItem(regions, region)}
</ListItem>
);
})}
Expand Down
31 changes: 30 additions & 1 deletion packages/manager/src/components/ImageSelect/utilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { regionFactory } from '@linode/utilities';
import { Settings } from 'luxon';

import { imageFactory } from 'src/factories';

import { isImageDeprecated, isImageTooFarPastEOL } from './utilities';
import {
getRegionListItem,
isImageDeprecated,
isImageTooFarPastEOL,
} from './utilities';

describe('isImageTooFarPastEOL', () => {
it('should return false if the image does not have an `eol`', () => {
Expand Down Expand Up @@ -80,3 +85,27 @@ describe('isImageDeprecated', () => {
expect(isImageDeprecated(image)).toBe(false);
});
});

describe('getRegionListItem', () => {
it('should return a formatted string with label and region id when a matching region is found', () => {
const region = regionFactory.build({ id: 'us-east', label: 'Newark, NJ' });
const imageRegion = { region: 'us-east', status: 'available' as const };

expect(getRegionListItem([region], imageRegion)).toBe(
'Newark, NJ (us-east)'
);
});

it('should return only the region id when no matching region is found', () => {
const region = regionFactory.build({ id: 'us-west', label: 'Fremont, CA' });
const imageRegion = { region: 'us-east', status: 'available' as const };

expect(getRegionListItem([region], imageRegion)).toBe('us-east');
});

it('should return only the region id when the regions array is empty', () => {
const imageRegion = { region: 'ap-south', status: 'available' as const };

expect(getRegionListItem([], imageRegion)).toBe('ap-south');
});
});
16 changes: 15 additions & 1 deletion packages/manager/src/components/ImageSelect/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DateTime } from 'luxon';
import { MAX_MONTHS_EOL_FILTER } from 'src/constants';

import type { ImageSelectVariant } from './ImageSelect';
import type { Image, RegionSite } from '@linode/api-v4';
import type { Image, ImageRegion, Region, RegionSite } from '@linode/api-v4';
import type { DisableItemOption } from '@linode/ui';

/**
Expand Down Expand Up @@ -118,3 +118,17 @@ export const getDisabledImages = (options: DisabledImageOptions) => {

return {};
};

/**
* Accepts an array of regions of the Region type and an ImageRegion and returns a string for the matching region to be displayed in the UI
*/
export const getRegionListItem = (
Comment thread
dwiley-akamai marked this conversation as resolved.
regions: Region[],
imageRegion: ImageRegion
) => {
const matchingRegion = regions.find((r) => r.id === imageRegion.region);

return matchingRegion
? `${matchingRegion.label} (${imageRegion.region})`
: imageRegion.region;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { getRestrictedResourceText } from 'src/features/Account/utils';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';

import type { SHARED_WITH_ME_IMAGES_TAB_PENDO_IDS } from '../constants';
import type { Event, Image } from '@linode/api-v4';
import type { Action } from 'src/components/ActionMenu/ActionMenu';

Expand All @@ -20,10 +21,12 @@
event?: Event;
handlers: Handlers;
image: Image;
isSharedImageRow?: boolean;
pendoIDs?: typeof SHARED_WITH_ME_IMAGES_TAB_PENDO_IDS;
}

export const ImagesActionMenu = (props: Props) => {
const { handlers, image } = props;
const { handlers, image, isSharedImageRow, pendoIDs } = props;

const { id, status } = image;

Expand All @@ -46,6 +49,46 @@
const isDisabled = status && status !== 'available';
const isAvailable = !isDisabled;

Comment thread
dwiley-akamai marked this conversation as resolved.
const deployAction = {
disabled: !linodeAccountPermissions.create_linode || isDisabled,
onClick: () => onDeploy?.(id),
pendoId: isSharedImageRow
? pendoIDs?.actionMenu.deployNewLinode
: undefined,
title: 'Deploy to New Linode',
tooltip: !linodeAccountPermissions.create_linode
? getRestrictedResourceText({
action: 'create',
isSingular: false,
resourceType: 'Linodes',
})
: isDisabled
? 'Image is not yet available for use.'

Check warning on line 66 in packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":66,"column":13,"nodeType":"Literal","endLine":66,"endColumn":50}
: undefined,
};

const rebuildAction = {
disabled: isDisabled,
onClick: () => onRebuild?.(image),
pendoId: isSharedImageRow
? pendoIDs?.actionMenu.rebuildLinode
: undefined,
title: 'Rebuild an Existing Linode',
tooltip: isDisabled ? 'Image is not yet available for use.' : undefined,
};

if (isSharedImageRow) {
return [
{
title: 'View Image Details',
onClick: () => null,
pendoId: pendoIDs?.actionMenu.viewImageDetails,
},
{ ...deployAction },
{ ...rebuildAction },
];
}

return [
{
disabled: !imagePermissions.update_image || isDisabled,
Expand Down Expand Up @@ -77,26 +120,8 @@
},
]
: []),
{
disabled: !linodeAccountPermissions.create_linode || isDisabled,
onClick: () => onDeploy?.(id),
title: 'Deploy to New Linode',
tooltip: !linodeAccountPermissions.create_linode
? getRestrictedResourceText({
action: 'create',
isSingular: false,
resourceType: 'Linodes',
})
: isDisabled
? 'Image is not yet available for use.'
: undefined,
},
{
disabled: isDisabled,
onClick: () => onRebuild?.(image),
title: 'Rebuild an Existing Linode',
tooltip: isDisabled ? 'Image is not yet available for use.' : undefined,
},
{ ...deployAction },
{ ...rebuildAction },
{
disabled: !imagePermissions.delete_image,
onClick: () => onDelete?.(image),
Expand All @@ -121,6 +146,8 @@
onDelete,
imagePermissions,
linodeAccountPermissions,
pendoIDs,
isSharedImageRow,
]);

return (
Expand Down
Loading
Loading