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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Improve the Linodes restricted user experience ([#10227](https://github.com/linode/manager/pull/10227))
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import type {

interface ButtonProps {
children: string;
disabled?: boolean;
onClick: () => void;
tooltipText?: string;

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.

These are conditional props to disable the empty state landing page button

}

interface ResourcesSectionProps {
Expand Down
5 changes: 4 additions & 1 deletion packages/manager/src/components/TagCell/TagCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { StyledPlusIcon, StyledTagButton } from '../Button/StyledTagButton';
import { AddTag } from './AddTag';

interface TagCellProps {
disabled?: boolean;
listAllTags: (tags: string[]) => void;
sx?: SxProps;
tags: string[];
Expand All @@ -35,7 +36,7 @@ const checkOverflow = (el: any) => {
};

const TagCell = (props: TagCellProps) => {
const { sx, tags, updateTags } = props;
const { disabled, sx, tags, updateTags } = props;

const [hasOverflow, setOverflow] = React.useState<boolean>(false);
const [addingTag, setAddingTag] = React.useState<boolean>(false);
Expand Down Expand Up @@ -90,6 +91,7 @@ const TagCell = (props: TagCellProps) => {
{tags.map((thisTag) => (
<StyledTag
colorVariant="lightBlue"
disabled={disabled}
key={`tag-item-${thisTag}`}
label={thisTag}
loading={loading}
Expand All @@ -110,6 +112,7 @@ const TagCell = (props: TagCellProps) => {
) : null}
<StyledTagButton
buttonType="outlined"
disabled={disabled}
Comment thread
jaalah-akamai marked this conversation as resolved.
endIcon={<StyledPlusIcon />}
onClick={() => setAddingTag(true)}
title="Add a tag"
Expand Down
4 changes: 2 additions & 2 deletions packages/manager/src/components/TagCell/TagDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export interface TagDrawerProps {
}

const TagDrawer = (props: TagDrawerProps) => {
const { entityLabel, onClose, open, tags, updateTags } = props;
const { entityID, entityLabel, onClose, open, tags, updateTags } = props;

return (
<Drawer onClose={onClose} open={open} title={`Tags (${entityLabel})`}>
<TagsPanel tags={tags} updateTags={updateTags} />
<TagsPanel entityId={entityID} tags={tags} updateTags={updateTags} />
</Drawer>
);
};
Expand Down
15 changes: 10 additions & 5 deletions packages/manager/src/components/TagsPanel/TagsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import { renderWithTheme } from 'src/utilities/testHelpers';

import { TagsPanel } from './TagsPanel';

const tagsPanelProps = {
entityId: 123,
tags: ['Tag1', 'Tag2'],
};

describe('TagsPanel', () => {
it('renders TagsPanel component with existing tags', async () => {
const updateTagsMock = vi.fn(() => Promise.resolve());

const { getByLabelText, getByText } = renderWithTheme(
<TagsPanel tags={['Tag1', 'Tag2']} updateTags={updateTagsMock} />
<TagsPanel {...tagsPanelProps} updateTags={updateTagsMock} />
);

expect(getByText('Tag1')).toBeInTheDocument();
Expand All @@ -30,7 +35,7 @@ describe('TagsPanel', () => {
const updateTagsMock = vi.fn(() => Promise.resolve());

const { getByLabelText, getByText } = renderWithTheme(
<TagsPanel tags={['Tag1', 'Tag2']} updateTags={updateTagsMock} />
<TagsPanel {...tagsPanelProps} updateTags={updateTagsMock} />
);

await userEvent.click(getByText('Add a tag'));
Expand All @@ -51,7 +56,7 @@ describe('TagsPanel', () => {
const updateTagsMock = vi.fn(() => Promise.resolve());

const { getByLabelText, getByText } = renderWithTheme(
<TagsPanel tags={['Tag1', 'Tag2']} updateTags={updateTagsMock} />
<TagsPanel {...tagsPanelProps} updateTags={updateTagsMock} />
);

await userEvent.click(getByText('Add a tag'));
Expand All @@ -75,7 +80,7 @@ describe('TagsPanel', () => {
const updateTagsMock = vi.fn(() => Promise.resolve());

const { getByLabelText, getByText, queryByLabelText } = renderWithTheme(
<TagsPanel tags={['Tag1', 'Tag2']} updateTags={updateTagsMock} />
<TagsPanel {...tagsPanelProps} updateTags={updateTagsMock} />
);

expect(getByText('Tag1')).toBeInTheDocument();
Expand All @@ -93,7 +98,7 @@ describe('TagsPanel', () => {
const updateTagsMock = vi.fn(() => Promise.resolve());

const { getByText, queryByLabelText, queryByText } = renderWithTheme(
<TagsPanel disabled tags={['Tag1', 'Tag2']} updateTags={updateTagsMock} />
<TagsPanel disabled {...tagsPanelProps} updateTags={updateTagsMock} />
);

expect(getByText('Tag1')).toBeInTheDocument();
Expand Down
18 changes: 15 additions & 3 deletions packages/manager/src/components/TagsPanel/TagsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CircleProgress } from 'src/components/CircleProgress';
import Select from 'src/components/EnhancedSelect/Select';
import { Tag } from 'src/components/Tag/Tag';
import { Typography } from 'src/components/Typography';
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
import { useProfile } from 'src/queries/profile';
import { updateTagsSuggestionsData, useTagSuggestions } from 'src/queries/tags';
import { getErrorStringOrDefault } from 'src/utilities/errorUtils';
Expand All @@ -33,6 +34,10 @@ export interface TagsPanelProps {
* If true, the input will be disabled and no tags can be added or removed.
*/
disabled?: boolean;
/**
* The ID of the entity to which the tags belong.
*/
entityId: number;
/**
* The tags to display.
*/
Expand All @@ -45,7 +50,7 @@ export interface TagsPanelProps {

export const TagsPanel = (props: TagsPanelProps) => {
const { classes, cx } = useStyles();
const { disabled, tags, updateTags } = props;
const { disabled, entityId, tags, updateTags } = props;

const queryClient = useQueryClient();

Expand All @@ -61,6 +66,12 @@ export const TagsPanel = (props: TagsPanelProps) => {
isLoading: userTagsLoading,
} = useTagSuggestions(!profile?.restricted);

const isLinodesGrantReadOnly = useIsResourceRestricted({
grantLevel: 'read_only',
grantType: 'linode',
id: entityId,
});

const tagsToSuggest = React.useMemo<Item[] | undefined>(
() =>
userTags
Expand Down Expand Up @@ -165,7 +176,7 @@ export const TagsPanel = (props: TagsPanelProps) => {
className={classes.selectTag}
creatable
createOptionPosition="first"
disabled={disabled}
disabled={disabled || isLinodesGrantReadOnly}
escapeClearsValue
hideLabel
isLoading={userTagsLoading}
Expand All @@ -184,7 +195,7 @@ export const TagsPanel = (props: TagsPanelProps) => {
>
<StyledTagButton
buttonType="outlined"
disabled={disabled}
disabled={disabled || isLinodesGrantReadOnly}
endIcon={<StyledPlusIcon disabled={disabled} />}
onClick={toggleTagInput}
>
Expand All @@ -206,6 +217,7 @@ export const TagsPanel = (props: TagsPanelProps) => {
[classes.tag]: true,
})}
colorVariant="lightBlue"
disabled={disabled || isLinodesGrantReadOnly}
key={`tag-item-${thisTag}`}
label={thisTag}
maxLength={30}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const DomainDetail = () => {
title="Domain Details"
/>
{location.state && location.state.recordError && (
<StyledNotice variant="error" text={location.state.recordError} />
<StyledNotice text={location.state.recordError} variant="error" />
)}
<StyledRootGrid container>
<StyledMainGrid xs={12}>
Expand All @@ -123,7 +123,11 @@ export const DomainDetail = () => {
<StyledTypography data-qa-title variant="h3">
Tags
</StyledTypography>
<TagsPanel tags={domain.tags} updateTags={handleUpdateTags} />
<TagsPanel
entityId={domain.id}
tags={domain.tags}
updateTags={handleUpdateTags}
/>
</StyledPaper>
<StyledDiv>
<DeleteDomain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ export const KubeSummaryPanel = (props: Props) => {
</Button>
</Grid>
<Grid className={classes.tags}>
<TagsPanel tags={cluster.tags} updateTags={handleUpdateTags} />
<TagsPanel
entityId={cluster.id}
tags={cluster.tags}
updateTags={handleUpdateTags}
/>
</Grid>
</Grid>
</Grid>
Expand Down
119 changes: 70 additions & 49 deletions packages/manager/src/features/Linodes/LinodeEntityDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as React from 'react';

import { EntityDetail } from 'src/components/EntityDetail/EntityDetail';
import { Notice } from 'src/components/Notice/Notice';
import { getRestrictedResourceText } from 'src/features/Account/utils';
import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext';
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface';
import { useInProgressEvents } from 'src/queries/events/events';
import { useAllImagesQuery } from 'src/queries/images';
Expand Down Expand Up @@ -64,6 +67,12 @@ export const LinodeEntityDetail = (props: Props) => {
vpcLinodeIsAssignedTo,
} = useVPCConfigInterface(linode.id);

const isLinodesGrantReadOnly = useIsResourceRestricted({
grantLevel: 'read_only',
grantType: 'linode',
id: linode.id,
});

const imageVendor =
images?.find((i) => i.id === linode.image)?.vendor ?? null;

Expand All @@ -83,55 +92,67 @@ export const LinodeEntityDetail = (props: Props) => {
const trimmedIPv6 = linode.ipv6?.replace('/128', '') || null;

return (
<EntityDetail
body={
<LinodeEntityDetailBody
configInterfaceWithVPC={configInterfaceWithVPC}
displayVPCSection={showVPCs}
gbRAM={linode.specs.memory / 1024}
gbStorage={linode.specs.disk / 1024}
ipv4={linode.ipv4}
ipv6={trimmedIPv6}
isVPCOnlyLinode={isVPCOnlyLinode}
linodeId={linode.id}
linodeLabel={linode.label}
numCPUs={linode.specs.vcpus}
numVolumes={numberOfVolumes}
region={linode.region}
vpcLinodeIsAssignedTo={vpcLinodeIsAssignedTo}
/>
}
footer={
<LinodeEntityDetailFooter
linodeCreated={linode.created}
linodeId={linode.id}
linodeLabel={linode.label}
linodePlan={linodePlan}
linodeRegionDisplay={linodeRegionDisplay}
linodeTags={linode.tags}
openTagDrawer={openTagDrawer}
/>
}
header={
<LinodeEntityDetailHeader
backups={linode.backups}
configs={configs}
enableVPCLogic={showVPCs}
handlers={handlers}
image={linode.image ?? 'Unknown Image'}
imageVendor={imageVendor}
isSummaryView={isSummaryView}
linodeId={linode.id}
linodeLabel={linode.label}
linodeRegionDisplay={linodeRegionDisplay}
linodeStatus={linode.status}
openNotificationMenu={notificationContext.openMenu}
progress={progress}
transitionText={transitionText}
type={type ?? null}
variant={variant}
<>
{isLinodesGrantReadOnly && (
<Notice
text={getRestrictedResourceText({
resourceType: 'Linodes',
})}
important
variant="warning"

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.

New page level notice

/>
}
/>
)}
<EntityDetail
body={
<LinodeEntityDetailBody
configInterfaceWithVPC={configInterfaceWithVPC}
displayVPCSection={showVPCs}
gbRAM={linode.specs.memory / 1024}
gbStorage={linode.specs.disk / 1024}
ipv4={linode.ipv4}
ipv6={trimmedIPv6}
isVPCOnlyLinode={isVPCOnlyLinode}
linodeId={linode.id}
linodeLabel={linode.label}
numCPUs={linode.specs.vcpus}
numVolumes={numberOfVolumes}
region={linode.region}
vpcLinodeIsAssignedTo={vpcLinodeIsAssignedTo}
/>
}
footer={
<LinodeEntityDetailFooter
isLinodesGrantReadOnly={isLinodesGrantReadOnly}
linodeCreated={linode.created}
linodeId={linode.id}
linodeLabel={linode.label}
linodePlan={linodePlan}
linodeRegionDisplay={linodeRegionDisplay}
linodeTags={linode.tags}
openTagDrawer={openTagDrawer}
/>
}
header={
<LinodeEntityDetailHeader
backups={linode.backups}
configs={configs}
enableVPCLogic={showVPCs}
handlers={handlers}
image={linode.image ?? 'Unknown Image'}
imageVendor={imageVendor}
isSummaryView={isSummaryView}
linodeId={linode.id}
linodeLabel={linode.label}
linodeRegionDisplay={linodeRegionDisplay}
linodeStatus={linode.status}
openNotificationMenu={notificationContext.openMenu}
progress={progress}
transitionText={transitionText}
type={type ?? null}
variant={variant}
/>
}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Grid from '@mui/material/Unstable_Grid2';
import { useTheme } from '@mui/material/styles';
import Grid from '@mui/material/Unstable_Grid2';
import { useSnackbar } from 'notistack';
import * as React from 'react';

Expand Down Expand Up @@ -34,6 +34,7 @@ export type Props = LinodeEntityDetailProps & {
};

interface FooterProps {
isLinodesGrantReadOnly: boolean;
linodeCreated: string;
linodeId: number;
linodeLabel: string;
Expand All @@ -49,6 +50,7 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
const { data: profile } = useProfile();

const {
isLinodesGrantReadOnly,
linodeCreated,
linodeId,
linodePlan,
Expand Down Expand Up @@ -160,6 +162,7 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
flexDirection: 'row-reverse',
},
}}
disabled={isLinodesGrantReadOnly}
listAllTags={openTagDrawer}
tags={linodeTags}
updateTags={updateTags}
Expand Down
Loading