-
Notifications
You must be signed in to change notification settings - Fork 400
upcoming: [UIE-10430] - Reserved IP: Implement Landing Screen. #13549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@linode/manager": Upcoming Features | ||
| --- | ||
|
|
||
| Implemented Reserved IPs Landing Page ([#13549](https://github.com/linode/manager/pull/13549)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { userEvent } from '@testing-library/user-event'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { reservedIPsFactory } from 'src/factories/networking'; | ||
| import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
|
||
| import { ReservedIpsActionMenu } from './ReservedIpsActionMenu'; | ||
|
|
||
| import type { ReservedIpsActionHandlers } from './ReservedIpsActionMenu'; | ||
|
|
||
| describe('ReservedIpsActionMenu', () => { | ||
| const mockHandlers: ReservedIpsActionHandlers = { | ||
| onEdit: vi.fn(), | ||
| onUnreserve: vi.fn(), | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('renders the action menu with the correct aria-label', () => { | ||
| const ip = reservedIPsFactory.build({ address: '203.0.113.5' }); | ||
|
|
||
| const { getByLabelText } = renderWithTheme( | ||
| <ReservedIpsActionMenu handlers={mockHandlers} ip={ip} /> | ||
| ); | ||
|
|
||
| expect( | ||
| getByLabelText('Action menu for Reserved IP 203.0.113.5') | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| it('calls onEdit when Edit is clicked', async () => { | ||
| const ip = reservedIPsFactory.build(); | ||
|
|
||
| const { getByLabelText, getByText } = renderWithTheme( | ||
| <ReservedIpsActionMenu handlers={mockHandlers} ip={ip} /> | ||
| ); | ||
|
|
||
| await userEvent.click( | ||
| getByLabelText(`Action menu for Reserved IP ${ip.address}`) | ||
| ); | ||
| await userEvent.click(getByText('Edit')); | ||
|
|
||
| expect(mockHandlers.onEdit).toHaveBeenCalledWith(ip); | ||
| }); | ||
|
|
||
| it('calls onUnreserve when Unreserve is clicked', async () => { | ||
| const ip = reservedIPsFactory.build(); | ||
|
|
||
| const { getByLabelText, getByText } = renderWithTheme( | ||
| <ReservedIpsActionMenu handlers={mockHandlers} ip={ip} /> | ||
| ); | ||
|
|
||
| await userEvent.click( | ||
| getByLabelText(`Action menu for Reserved IP ${ip.address}`) | ||
| ); | ||
| await userEvent.click(getByText('Unreserve')); | ||
|
|
||
| expect(mockHandlers.onUnreserve).toHaveBeenCalledWith(ip); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import * as React from 'react'; | ||
|
|
||
| import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; | ||
|
|
||
| import type { IPAddress } from '@linode/api-v4'; | ||
| import type { Action } from 'src/components/ActionMenu/ActionMenu'; | ||
|
|
||
| export interface ReservedIpsActionHandlers { | ||
| onEdit: (ip: IPAddress) => void; | ||
| onUnreserve: (ip: IPAddress) => void; | ||
| } | ||
|
|
||
| interface Props { | ||
| handlers: ReservedIpsActionHandlers; | ||
| ip: IPAddress; | ||
| } | ||
|
|
||
| export const ReservedIpsActionMenu = ({ handlers, ip }: Props) => { | ||
| const actions: Action[] = [ | ||
| { | ||
| onClick: () => handlers.onEdit(ip), | ||
| title: 'Edit', | ||
| }, | ||
| { | ||
| onClick: () => handlers.onUnreserve(ip), | ||
| title: 'Unreserve', | ||
| }, | ||
| ]; | ||
|
|
||
| return ( | ||
| <ActionMenu | ||
| actionsList={actions} | ||
| ariaLabel={`Action menu for Reserved IP ${ip.address}`} | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import * as React from 'react'; | ||
|
|
||
| import { routeTree } from 'src/routes'; | ||
| import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
|
||
| import { ReservedIpsLanding } from './ReservedIpsLanding'; | ||
|
|
||
| const mockQueryReturn = vi.hoisted(() => | ||
| vi.fn().mockReturnValue({ | ||
| data: undefined, | ||
|
Check warning on line 10 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.test.tsx
|
||
| error: null, | ||
| isLoading: true, | ||
| }) | ||
| ); | ||
|
|
||
| vi.mock('@linode/queries', async () => { | ||
| const actual = await vi.importActual('@linode/queries'); | ||
| return { | ||
| ...actual, | ||
| useReservedIPsQuery: mockQueryReturn, | ||
| }; | ||
| }); | ||
|
|
||
| describe('ReservedIpsLanding', () => { | ||
| it('renders a loading state while data is fetching', () => { | ||
| const { getByTestId } = renderWithTheme(<ReservedIpsLanding />, { | ||
| initialRoute: '/reserved-ips', | ||
|
Check warning on line 27 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.test.tsx
|
||
| routeTree, | ||
| }); | ||
|
|
||
| expect(getByTestId('circle-progress')).toBeInTheDocument(); | ||
|
Check warning on line 31 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.test.tsx
|
||
| }); | ||
|
|
||
| it('renders an error state when the query fails', () => { | ||
| mockQueryReturn.mockReturnValue({ | ||
| data: undefined, | ||
|
Check warning on line 36 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.test.tsx
|
||
| error: [{ reason: 'Something went wrong.' }], | ||
| isLoading: false, | ||
| }); | ||
|
|
||
| const { getByText } = renderWithTheme(<ReservedIpsLanding />, { | ||
| initialRoute: '/reserved-ips', | ||
| routeTree, | ||
| }); | ||
|
|
||
| expect(getByText('Something went wrong.')).toBeVisible(); | ||
| }); | ||
|
|
||
| it('renders the empty state when there are no reserved IPs', () => { | ||
| mockQueryReturn.mockReturnValue({ | ||
| data: { data: [], results: 0 }, | ||
| error: null, | ||
| isLoading: false, | ||
| }); | ||
|
|
||
| const { getByText } = renderWithTheme(<ReservedIpsLanding />, { | ||
| initialRoute: '/reserved-ips', | ||
| routeTree, | ||
| }); | ||
|
|
||
| expect(getByText('Reserve an IP Address')).toBeVisible(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This button is shown even when there are IPs to show in the table. So let's look for text within empty state. |
||
| }); | ||
|
|
||
| it('renders the table when reserved IPs are returned', () => { | ||
| mockQueryReturn.mockReturnValue({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could use |
||
| data: { | ||
| data: [ | ||
| { | ||
| address: '203.0.113.1', | ||
| assigned_entity: null, | ||
| gateway: '203.0.113.0', | ||
| interface_id: null, | ||
| linode_id: null, | ||
| prefix: 24, | ||
| public: true, | ||
| rdns: null, | ||
| region: 'us-east', | ||
| reserved: true, | ||
| subnet_mask: '255.255.255.0', | ||
| tags: ['web'], | ||
| type: 'ipv4', | ||
| }, | ||
| ], | ||
| results: 1, | ||
| }, | ||
| error: null, | ||
| isLoading: false, | ||
| }); | ||
|
|
||
| const { getByText } = renderWithTheme(<ReservedIpsLanding />, { | ||
| initialRoute: '/reserved-ips', | ||
| routeTree, | ||
| }); | ||
|
|
||
| expect(getByText('Reserved IP Addresses')).toBeVisible(); | ||
| expect(getByText('203.0.113.1')).toBeVisible(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,113 @@ | ||
| import { Notice } from '@linode/ui'; | ||
| import { useReservedIPsQuery } from '@linode/queries'; | ||
| import { CircleProgress, ErrorState } from '@linode/ui'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { LandingHeader } from 'src/components/LandingHeader'; | ||
| import { useOrderV2 } from 'src/hooks/useOrderV2'; | ||
| import { usePaginationV2 } from 'src/hooks/usePaginationV2'; | ||
| import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; | ||
|
|
||
| import { ReservedIpsLandingEmptyState } from './ReservedIpsLandingEmptyState'; | ||
| import { ReservedIpsLandingTable } from './ReservedIpsLandingTable'; | ||
|
|
||
| import type { ReservedIpsActionHandlers } from './ReservedIpsActionMenu'; | ||
|
|
||
| const preferenceKey = 'reserved-ips'; | ||
|
|
||
| export const ReservedIpsLanding = () => { | ||
| // TODO: Replace with actual data check once API queries are implemented | ||
| const showEmptyState = true; | ||
| // TODO: These will be used by the Edit drawer and Unreserve dialog component | ||
|
Check warning on line 18 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.tsx
|
||
| // const [_selectedIP, setSelectedIP] = React.useState<IPAddress>(); | ||
| // const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); | ||
|
|
||
| // const [_isEditDrawerOpen, setIsEditDrawerOpen] = React.useState(false); | ||
|
Check warning on line 22 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.tsx
|
||
|
|
||
| // const [_isUnreserveDialogOpen, setIsUnreserveDialogOpen] = React.useState(false); | ||
|
Check warning on line 24 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.tsx
|
||
|
|
||
| const handlers: ReservedIpsActionHandlers = { | ||
| onEdit: (ip) => { | ||
|
Check warning on line 27 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.tsx
|
||
| // setSelectedIP(ip); | ||
|
Check warning on line 28 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.tsx
|
||
| // setIsEditDrawerOpen(true); | ||
| }, | ||
| onUnreserve: (ip) => { | ||
|
Check warning on line 31 in packages/manager/src/features/ReservedIps/ReservedIpsLanding/ReservedIpsLanding.tsx
|
||
| // setSelectedIP(ip); | ||
| // setIsUnreserveDialogOpen(true); | ||
| }, | ||
| }; | ||
|
|
||
| if (showEmptyState) { | ||
| const pagination = usePaginationV2({ | ||
| currentRoute: '/reserved-ips', | ||
| preferenceKey, | ||
| }); | ||
|
|
||
| const { handleOrderChange, order, orderBy } = useOrderV2({ | ||
| initialRoute: { | ||
| defaultOrder: { | ||
| order: 'asc', | ||
| orderBy: 'address', | ||
| }, | ||
| from: '/reserved-ips', | ||
| }, | ||
| preferenceKey: `${preferenceKey}-order`, | ||
| }); | ||
|
|
||
| const filter = { | ||
| ['+order']: order, | ||
| ['+order_by']: orderBy, | ||
| }; | ||
|
|
||
| const { | ||
| data: reservedIps, | ||
| error, | ||
| isLoading, | ||
| } = useReservedIPsQuery( | ||
| { | ||
| page: pagination.page, | ||
| page_size: pagination.pageSize, | ||
| }, | ||
| filter | ||
| ); | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <ErrorState | ||
| errorText={ | ||
| getAPIErrorOrDefault( | ||
| error, | ||
| 'Error loading your Reserved IP addresses.' | ||
| )[0].reason | ||
| } | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| if (isLoading) { | ||
| return <CircleProgress />; | ||
| } | ||
|
|
||
| if (!reservedIps?.data.length) { | ||
| return <ReservedIpsLandingEmptyState />; | ||
| } | ||
tanushree-akamai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <> | ||
| <LandingHeader | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also need to show docs link before the create button. Placeholder URL would be "https://techdocs.akamai.com/cloud-computing/update/docs/reserved-ips" |
||
| breadcrumbProps={{ | ||
| pathname: 'Reserved IPs', | ||
| removeCrumbX: 1, | ||
| createButtonText="Reserve an IP Address" | ||
| onButtonClick={() => { | ||
| /*To be updated | ||
| setIsDrawerOpen(true) */ | ||
| }} | ||
tanushree-akamai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| spacingBottom={16} | ||
| title="Reserved IPs" | ||
| title="Reserved IP Addresses" | ||
| /> | ||
| <ReservedIpsLandingTable | ||
| data={reservedIps?.data} | ||
| handleOrderChange={handleOrderChange} | ||
| handlers={handlers} | ||
| order={order} | ||
| orderBy={orderBy} | ||
| pagination={pagination} | ||
| results={reservedIps?.results} | ||
| /> | ||
| <Notice variant="info">Reserved IPs is coming soon...</Notice> | ||
| </> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null