Skip to content

Commit

Permalink
upcoming: [M3-7612] Placement Group Linodes List (#10123)
Browse files Browse the repository at this point in the history
* Initial commit - save work

* Post rebase fixes

* Formatting and styling

* Cleanup and sorting improvements

* Cleanup and sorting improvements

* Adding unit tests

* Cleanup

* Added changeset: Placement GroupLinode List

* Simplify logic - avoid useEffect

* Feedback
  • Loading branch information
abailly-akamai authored Feb 7, 2024
1 parent 418f4f6 commit 3bb8475
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add Placement Group Linodes List ([#10123](https://github.com/linode/manager/pull/10123))
15 changes: 8 additions & 7 deletions packages/manager/src/components/ErrorState/ErrorState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ const StyledIconContainer = styled('div')({
textAlign: 'center',
});

const ErrorStateRoot = styled(Grid)<Partial<ErrorStateProps>>(
({ theme, ...props }) => ({
marginLeft: 0,
padding: props.compact ? theme.spacing(5) : theme.spacing(10),
width: '100%',
})
);
const ErrorStateRoot = styled(Grid, {
label: 'ErrorStateRoot',
shouldForwardProp: (prop) => prop !== 'compact',
})<Partial<ErrorStateProps>>(({ theme, ...props }) => ({
marginLeft: 0,
padding: props.compact ? theme.spacing(5) : theme.spacing(10),
width: '100%',
}));
2 changes: 2 additions & 0 deletions packages/manager/src/factories/placementGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export const placementGroupFactory = Factory.Sync.makeFactory<PlacementGroup>({
id: Factory.each((id) => id),
label: Factory.each((id) => `pg-${id}`),
linode_ids: Factory.each(() => [
0,
pickRandom([1, 2, 3]),
pickRandom([4, 5, 6]),
pickRandom([7, 8, 9]),
43,
]),
region: Factory.each(() =>
pickRandom(['us-east', 'us-southeast', 'ca-central'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@ describe('PlacementGroupsLanding', () => {
expect(getByText(/my first pg \(Anti-affinity\)/i)).toBeInTheDocument();
expect(getByText(/docs/i)).toBeInTheDocument();
expect(getByRole('tab', { name: 'Summary' })).toBeInTheDocument();
expect(getByRole('tab', { name: 'Linodes (3)' })).toBeInTheDocument();
expect(getByRole('tab', { name: 'Linodes (5)' })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { getErrorStringOrDefault } from 'src/utilities/errorUtils';

import { getPlacementGroupLinodeCount } from '../utils';
import { PlacementGroupsLinodes } from './PlacementGroupsLinodes/PlacementGroupsLinodes';

export const PlacementGroupsDetail = () => {
const flags = useFlags();
Expand Down Expand Up @@ -106,10 +107,11 @@ export const PlacementGroupsDetail = () => {
onChange={(i) => history.push(tabs[i].routeName)}
>
<TabLinkList tabs={tabs} />

<TabPanels>
<SafeTabPanel index={0}>TODO VM_Placement: summary</SafeTabPanel>
<SafeTabPanel index={1}>TODO VM_Placement: linode list</SafeTabPanel>
<SafeTabPanel index={1}>
<PlacementGroupsLinodes placementGroup={placementGroup} />
</SafeTabPanel>
</TabPanels>
</Tabs>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from 'react';

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

import { PLACEMENT_GROUP_LINODES_ERROR_MESSAGE } from '../../constants';
import { PlacementGroupsLinodes } from './PlacementGroupsLinodes';

describe('PlacementGroupsLanding', () => {
it('renders an error state if placement groups are undefined', () => {
const { getByText } = renderWithTheme(
<PlacementGroupsLinodes placementGroup={undefined} />
);

expect(
getByText(PLACEMENT_GROUP_LINODES_ERROR_MESSAGE)
).toBeInTheDocument();
});

it('features the linodes table, a filter field, a create button and a docs link', () => {
const placementGroup = placementGroupFactory.build({
capacity: 2,
linode_ids: [1],
});

const { getByPlaceholderText, getByRole, getByTestId } = renderWithTheme(
<PlacementGroupsLinodes placementGroup={placementGroup} />
);

expect(getByTestId('add-linode-to-placement-group-button')).toHaveAttribute(
'aria-disabled',
'false'
);
expect(getByPlaceholderText('Search Linodes')).toBeInTheDocument();
expect(getByRole('table')).toBeInTheDocument();
});

it('has a disabled create button if the placement group has reached capacity', () => {
const placementGroup = placementGroupFactory.build({
capacity: 1,
linode_ids: [1],
});

const { getByTestId } = renderWithTheme(
<PlacementGroupsLinodes placementGroup={placementGroup} />
);

expect(getByTestId('add-linode-to-placement-group-button')).toHaveAttribute(
'aria-disabled',
'true'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useTheme } from '@mui/material';
import { useMediaQuery } from '@mui/material';
import Grid from '@mui/material/Unstable_Grid2/Grid2';
import * as React from 'react';

import { Box } from 'src/components/Box';
import { Button } from 'src/components/Button/Button';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { Stack } from 'src/components/Stack';
import { Typography } from 'src/components/Typography';
import { useAllLinodesQuery } from 'src/queries/linodes/linodes';

import {
MAX_NUMBER_OF_LINODES_IN_PLACEMENT_GROUP_MESSAGE,
PLACEMENT_GROUP_LINODES_ERROR_MESSAGE,
} from '../../constants';
import { hasPlacementGroupReachedCapacity } from '../../utils';
import { PlacementGroupsLinodesTable } from './PlacementGroupsLinodesTable';

import type { Linode, PlacementGroup } from '@linode/api-v4';

interface Props {
placementGroup: PlacementGroup | undefined;
}

export const PlacementGroupsLinodes = (props: Props) => {
const { placementGroup } = props;
const {
data: placementGroupLinodes,
error: linodesError,
isLoading: linodesLoading,
} = useAllLinodesQuery(
{},
{
'+or': placementGroup?.linode_ids.map((id) => ({
id,
})),
}
);
const theme = useTheme();
const matchesSmDown = useMediaQuery(theme.breakpoints.down('md'));
const [searchText, setSearchText] = React.useState('');

if (!placementGroup) {
return <ErrorState errorText={PLACEMENT_GROUP_LINODES_ERROR_MESSAGE} />;
}

const { capacity } = placementGroup;

const getLinodesList = () => {
if (!placementGroupLinodes) {
return [];
}

if (searchText) {
return placementGroupLinodes.filter((linode: Linode) => {
return linode.label.toLowerCase().includes(searchText.toLowerCase());
});
}

return placementGroupLinodes;
};

return (
<Stack spacing={2}>
<Box sx={{ px: matchesSmDown ? 2 : 0, py: 2 }}>
<Typography>
The following Linodes have been assigned to this Placement Group. A
Linode can only be assigned to a single Placement Group.
</Typography>
<Typography sx={{ mt: 1 }}>
Limit of Linodes for this Placement Group: {capacity}
</Typography>
</Box>

<Grid container justifyContent="space-between">
<Grid flexGrow={1} sm={6} sx={{ mb: 1 }} xs={12}>
<DebouncedSearchTextField
onSearch={(value) => {
setSearchText(value);
}}
debounceTime={250}
hideLabel
label="Search Linodes"
placeholder="Search Linodes"
value={searchText}
/>
</Grid>
<Grid>
<Button
buttonType="primary"
data-testid="add-linode-to-placement-group-button"
disabled={hasPlacementGroupReachedCapacity(placementGroup)}
// onClick={TODO VM_Placement: open assign linode drawer}
tooltipText={MAX_NUMBER_OF_LINODES_IN_PLACEMENT_GROUP_MESSAGE}
>
Add Linode to Placement Group
</Button>
</Grid>
</Grid>
<PlacementGroupsLinodesTable
error={linodesError ?? []}
linodes={getLinodesList() ?? []}
loading={linodesLoading}
/>
{/* TODO VM_Placement: ASSIGN LINODE DRAWER */}
{/* TODO VM_Placement: UNASSIGN LINODE DRAWER */}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react';

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

import { PlacementGroupsLinodesTable } from './PlacementGroupsLinodesTable';

const defaultProps = {
error: [],
linodes: linodeFactory.buildList(5),
loading: false,
};

describe('PlacementGroupsLanding', () => {
it('renders an error state when encountering an API error', () => {
const { getByText } = renderWithTheme(
<PlacementGroupsLinodesTable
{...defaultProps}
error={[{ reason: 'Not found' }]}
/>
);

expect(getByText(/not found/i)).toBeInTheDocument();
});

it('renders a loading skeleton based on the loading prop', () => {
const { getByTestId } = renderWithTheme(
<PlacementGroupsLinodesTable {...defaultProps} loading />
);

expect(getByTestId('table-row-loading')).toBeInTheDocument();
});

it('should have the correct number of columns', () => {
const { getAllByRole } = renderWithTheme(
<PlacementGroupsLinodesTable {...defaultProps} />
);

expect(getAllByRole('columnheader')).toHaveLength(3);
});

it('should have the correct number of rows', () => {
const { getAllByTestId } = renderWithTheme(
<PlacementGroupsLinodesTable {...defaultProps} />
);

expect(getAllByTestId(/placement-group-linode-/i)).toHaveLength(5);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from 'react';

import OrderBy from 'src/components/OrderBy';
import Paginate from 'src/components/Paginate';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { TableContentWrapper } from 'src/components/TableContentWrapper/TableContentWrapper';
import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TableSortCell } from 'src/components/TableSortCell';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';

import { PLACEMENT_GROUP_LINODES_ERROR_MESSAGE } from '../../constants';
import { PlacementGroupsLinodesTableRow } from './PlacementGroupsLinodesTableRow';

import type { APIError, Linode } from '@linode/api-v4';

export interface Props {
error?: APIError[];
linodes: Linode[];
loading: boolean;
}

export const PlacementGroupsLinodesTable = React.memo((props: Props) => {
const { error, linodes, loading } = props;

const orderLinodeKey = 'label';
const orderStatusKey = 'status';

const _error = error
? getAPIErrorOrDefault(error, PLACEMENT_GROUP_LINODES_ERROR_MESSAGE)
: undefined;

return (
<OrderBy data={linodes} order="asc" orderBy={orderLinodeKey}>
{({ data: orderedData, handleOrderChange, order, orderBy }) => (
<Paginate data={orderedData}>
{({
count,
data: paginatedAndOrderedLinodes,
handlePageChange,
handlePageSizeChange,
page,
pageSize,
}) => (
<>
<Table aria-label="List of Linodes in this Placement Group">
<TableHead>
<TableRow>
<TableSortCell
active={orderBy === orderLinodeKey}
data-qa-placement-group-linode-header
direction={order}
handleClick={handleOrderChange}
label={orderLinodeKey}
sx={{ width: '30%' }}
>
Linode
</TableSortCell>
<TableSortCell
active={orderBy === orderStatusKey}
data-qa-placement-group-linode-status-header
direction={order}
handleClick={handleOrderChange}
label={orderStatusKey}
>
Linode Status
</TableSortCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
<TableContentWrapper
loadingProps={{
columns: 3,
}}
error={_error}
length={paginatedAndOrderedLinodes.length}
loading={loading}
>
{paginatedAndOrderedLinodes.map((linode) => (
<PlacementGroupsLinodesTableRow
key={`placement-group-linode-${linode.id}`}
linode={linode}
/>
))}
</TableContentWrapper>
</TableBody>
</Table>
<PaginationFooter
count={count}
eventCategory="Placement Group Linodes Table"
handlePageChange={handlePageChange}
handleSizeChange={handlePageSizeChange}
page={page}
pageSize={pageSize}
/>
</>
)}
</Paginate>
)}
</OrderBy>
);
});
Loading

0 comments on commit 3bb8475

Please sign in to comment.