diff --git a/packages/manager/.changeset/pr-11704-tech-stories-1740340564875.md b/packages/manager/.changeset/pr-11704-tech-stories-1740340564875.md new file mode 100644 index 00000000000..d8e44456431 --- /dev/null +++ b/packages/manager/.changeset/pr-11704-tech-stories-1740340564875.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Migrate Firewalls feature to Tanstack routing ([#11704](https://github.com/linode/manager/pull/11704)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 454bac78a4e..1febffd7198 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -95,6 +95,7 @@ module.exports = { // for each new features added to the migration router, add its directory here 'src/features/Betas/**/*', 'src/features/Domains/**/*', + 'src/features/Firewalls/**/*', 'src/features/Images/**/*', 'src/features/Longview/**/*', 'src/features/PlacementGroups/**/*', @@ -141,6 +142,12 @@ module.exports = { 'Please use useOrderV2 hook for components being migrated to TanStack Router.', name: 'src/components/OrderBy', }, + { + importNames: ['Prompt'], + message: + 'Please use the TanStack useBlocker hook for components/features being migrated to TanStack Router.', + name: 'src/components/Prompt/Prompt', + }, ], }, ], diff --git a/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx b/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx index 5cedacb9062..2b036c5b1ae 100644 --- a/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx +++ b/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx @@ -393,220 +393,226 @@ const testDiscardOutboundRuleDragViaKeyboard = () => { .should('have.attr', 'aria-disabled', 'true'); }; -componentTests('Firewall Rules Table', (mount) => { - /** - * Keyboard keys used to perform interactions with rows in the Firewall Rules table: - * - Press `Space/Enter` key once to activate keyboard sensor on the selected row. - * - Use `Up/Down` arrow keys to move the row up or down. - * - Press `Space/Enter` key again to drop the focused row. - * - Press `Esc` key to discard drag and drop operation. - * - * Confirms: - * - All keyboard interactions on Firewall Rules table rows work as expected for - * both normal (no vertical scrollbar) and smaller window sizes (with vertical scrollbar). - * - `CustomKeyboardSensor` works as expected. - * - All Mouse interactions on Firewall Rules table rows work as expected. - */ - describe('Keyboard and Mouse Drag and Drop Interactions', () => { - describe('Normal window (no vertical scrollbar)', () => { - beforeEach(() => { - cy.viewport(1536, 960); - }); - - describe('Inbound Rules:', () => { +componentTests( + 'Firewall Rules Table', + (mount) => { + /** + * Keyboard keys used to perform interactions with rows in the Firewall Rules table: + * - Press `Space/Enter` key once to activate keyboard sensor on the selected row. + * - Use `Up/Down` arrow keys to move the row up or down. + * - Press `Space/Enter` key again to drop the focused row. + * - Press `Esc` key to discard drag and drop operation. + * + * Confirms: + * - All keyboard interactions on Firewall Rules table rows work as expected for + * both normal (no vertical scrollbar) and smaller window sizes (with vertical scrollbar). + * - `CustomKeyboardSensor` works as expected. + * - All Mouse interactions on Firewall Rules table rows work as expected. + */ + describe('Keyboard and Mouse Drag and Drop Interactions', () => { + describe('Normal window (no vertical scrollbar)', () => { beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: true, - includeOutbound: false, - }); + cy.viewport(1536, 960); }); - it('should move Inbound rule rows using keyboard interaction', () => { - testMoveInboundRuleRowsViaKeyboard(); - }); + describe('Inbound Rules:', () => { + beforeEach(() => { + mount( + + ); + verifyFirewallWithRules({ + includeInbound: true, + includeOutbound: false, + }); + }); - it('should cancel the Inbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardInboundRuleDragViaKeyboard(); - }); + it('should move Inbound rule rows using keyboard interaction', () => { + testMoveInboundRuleRowsViaKeyboard(); + }); - it('should move Inbound rules rows using mouse interaction', () => { - // Drag the 1st row rule to 2nd row position. - dragRowToPositionViaMouse(inboundAriaLabel, 1, 2); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule2, - inboundRule1, - inboundRule3, - ]); - - // Drag the 3rd row rule to 2nd row position. - dragRowToPositionViaMouse(inboundAriaLabel, 3, 2); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule2, - inboundRule3, - inboundRule1, - ]); - - // Drag the 3rd row rule to 1st position. - dragRowToPositionViaMouse(inboundAriaLabel, 3, 1); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule1, - inboundRule2, - inboundRule3, - ]); - }); - }); + it('should cancel the Inbound rules drag operation with the keyboard `Esc` key', () => { + testDiscardInboundRuleDragViaKeyboard(); + }); - describe('Outbound Rules:', () => { - beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: false, - includeOutbound: true, + it('should move Inbound rules rows using mouse interaction', () => { + // Drag the 1st row rule to 2nd row position. + dragRowToPositionViaMouse(inboundAriaLabel, 1, 2); + + // Verify the order and labels in the 1st, 2nd, and 3rd rows. + verifyTableRowOrder(inboundAriaLabel, [ + inboundRule2, + inboundRule1, + inboundRule3, + ]); + + // Drag the 3rd row rule to 2nd row position. + dragRowToPositionViaMouse(inboundAriaLabel, 3, 2); + + // Verify the order and labels in the 1st, 2nd, and 3rd rows. + verifyTableRowOrder(inboundAriaLabel, [ + inboundRule2, + inboundRule3, + inboundRule1, + ]); + + // Drag the 3rd row rule to 1st position. + dragRowToPositionViaMouse(inboundAriaLabel, 3, 1); + + // Verify the order and labels in the 1st, 2nd, and 3rd rows. + verifyTableRowOrder(inboundAriaLabel, [ + inboundRule1, + inboundRule2, + inboundRule3, + ]); }); }); - it('should move Outbound rule rows using keyboard interaction', () => { - testMoveOutboundRulesViaKeyboard(); - }); + describe('Outbound Rules:', () => { + beforeEach(() => { + mount( + + ); + verifyFirewallWithRules({ + includeInbound: false, + includeOutbound: true, + }); + }); - it('should cancel the Outbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardOutboundRuleDragViaKeyboard(); - }); + it('should move Outbound rule rows using keyboard interaction', () => { + testMoveOutboundRulesViaKeyboard(); + }); - it('should move Outbound rules rows using mouse interaction', () => { - // Drag the 1st row rule to 2nd row position. - dragRowToPositionViaMouse(outboundAriaLabel, 1, 2); - - // Verify the labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule2, - outboundRule1, - outboundRule3, - ]); - - // Drag the 3rd row rule to 2nd row position. - dragRowToPositionViaMouse(outboundAriaLabel, 3, 2); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule2, - outboundRule3, - outboundRule1, - ]); - - // Drag the 3rd row rule to 1st position. - dragRowToPositionViaMouse(outboundAriaLabel, 3, 1); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule1, - outboundRule2, - outboundRule3, - ]); - }); - }); - }); + it('should cancel the Outbound rules drag operation with the keyboard `Esc` key', () => { + testDiscardOutboundRuleDragViaKeyboard(); + }); - describe('Window with vertical scrollbar', () => { - beforeEach(() => { - // Browser window with vertical scroll bar enabled (smaller screens). - cy.viewport(800, 400); - cy.window().should('have.property', 'innerWidth', 800); - cy.window().should('have.property', 'innerHeight', 400); + it('should move Outbound rules rows using mouse interaction', () => { + // Drag the 1st row rule to 2nd row position. + dragRowToPositionViaMouse(outboundAriaLabel, 1, 2); + + // Verify the labels in the 1st, 2nd, and 3rd rows. + verifyTableRowOrder(outboundAriaLabel, [ + outboundRule2, + outboundRule1, + outboundRule3, + ]); + + // Drag the 3rd row rule to 2nd row position. + dragRowToPositionViaMouse(outboundAriaLabel, 3, 2); + + // Verify the order and labels in the 1st, 2nd, and 3rd rows. + verifyTableRowOrder(outboundAriaLabel, [ + outboundRule2, + outboundRule3, + outboundRule1, + ]); + + // Drag the 3rd row rule to 1st position. + dragRowToPositionViaMouse(outboundAriaLabel, 3, 1); + + // Verify the order and labels in the 1st, 2nd, and 3rd rows. + verifyTableRowOrder(outboundAriaLabel, [ + outboundRule1, + outboundRule2, + outboundRule3, + ]); + }); + }); }); - describe('Inbound Rules:', () => { + describe('Window with vertical scrollbar', () => { beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: true, - includeOutbound: false, - isSmallViewport: true, - }); + // Browser window with vertical scroll bar enabled (smaller screens). + cy.viewport(800, 400); + cy.window().should('have.property', 'innerWidth', 800); + cy.window().should('have.property', 'innerHeight', 400); }); - it('should move Inbound rule rows using keyboard interaction', () => { - testMoveInboundRuleRowsViaKeyboard(); - }); + describe('Inbound Rules:', () => { + beforeEach(() => { + mount( + + ); + verifyFirewallWithRules({ + includeInbound: true, + includeOutbound: false, + isSmallViewport: true, + }); + }); - it('should cancel the Inbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardInboundRuleDragViaKeyboard(); - }); - }); + it('should move Inbound rule rows using keyboard interaction', () => { + testMoveInboundRuleRowsViaKeyboard(); + }); - describe('Outbound Rules:', () => { - beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: false, - includeOutbound: true, - isSmallViewport: true, + it('should cancel the Inbound rules drag operation with the keyboard `Esc` key', () => { + testDiscardInboundRuleDragViaKeyboard(); }); }); - it('should move Outbound rule rows using keyboard interaction', () => { - testMoveOutboundRulesViaKeyboard(); - }); + describe('Outbound Rules:', () => { + beforeEach(() => { + mount( + + ); + verifyFirewallWithRules({ + includeInbound: false, + includeOutbound: true, + isSmallViewport: true, + }); + }); + + it('should move Outbound rule rows using keyboard interaction', () => { + testMoveOutboundRulesViaKeyboard(); + }); - it('should cancel the Outbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardOutboundRuleDragViaKeyboard(); + it('should cancel the Outbound rules drag operation with the keyboard `Esc` key', () => { + testDiscardOutboundRuleDragViaKeyboard(); + }); }); }); }); - }); -}); + }, + { + useTanstackRouter: true, + } +); diff --git a/packages/manager/cypress/support/component/setup.tsx b/packages/manager/cypress/support/component/setup.tsx index 6f4f2e0b5a4..18ff5ac6909 100644 --- a/packages/manager/cypress/support/component/setup.tsx +++ b/packages/manager/cypress/support/component/setup.tsx @@ -15,6 +15,13 @@ import { queryClientFactory } from '@src/queries/base'; import { QueryClientProvider } from '@tanstack/react-query'; +import { + RouterProvider, + createMemoryHistory, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/react-router'; import '@testing-library/cypress/add-commands'; import 'cypress-axe'; import { mount } from 'cypress/react'; @@ -28,6 +35,8 @@ import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper'; import { storeFactory } from 'src/store'; import type { ThemeName } from '@linode/ui'; +import type { AnyRouter } from '@tanstack/react-router'; +import type { Flags } from 'src/featureFlags'; /** * Mounts a component with a Cloud Manager theme applied. @@ -38,10 +47,23 @@ import type { ThemeName } from '@linode/ui'; export const mountWithTheme = ( jsx: React.ReactNode, theme: ThemeName = 'light', - flags: any = {} + flags: Partial = {}, + useTanstackRouter: boolean = false ) => { const queryClient = queryClientFactory(); const store = storeFactory(); + const rootRoute = createRootRoute({}); + const indexRoute = createRoute({ + component: () => jsx, + getParentRoute: () => rootRoute, + path: '/', + }); + const router: AnyRouter = createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree: rootRoute.addChildren([indexRoute]), + }); return mount( @@ -54,7 +76,13 @@ export const mountWithTheme = ( options={{ bootstrap: flags }} > - {jsx} + {useTanstackRouter ? ( + + + + ) : ( + {jsx} + )} diff --git a/packages/manager/cypress/support/util/components.ts b/packages/manager/cypress/support/util/components.ts index 8940550bc40..7eca479968c 100644 --- a/packages/manager/cypress/support/util/components.ts +++ b/packages/manager/cypress/support/util/components.ts @@ -4,6 +4,7 @@ import type { ThemeName } from '@linode/ui'; import type { MountReturn } from 'cypress/react'; +import type { Flags } from 'src/featureFlags'; /** * Array of themes for which to test components. @@ -46,10 +47,13 @@ export type MountCommand = ( */ export const componentTests = ( componentName: string, - callback: (mountCommand: MountCommand) => void + callback: (mountCommand: MountCommand) => void, + options: { + useTanstackRouter?: boolean; + } = {} ) => { - const mountCommand = (jsx: React.ReactNode, flags?: any) => - cy.mountWithTheme(jsx, defaultTheme, flags); + const mountCommand = (jsx: React.ReactNode, flags?: Flags) => + cy.mountWithTheme(jsx, defaultTheme, flags, options.useTanstackRouter); describe(`${componentName} component tests`, () => { callback(mountCommand); }); diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index f79a5f56024..ebe5c399f1d 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -162,7 +162,6 @@ const EventsLanding = React.lazy(() => const AccountActivationLanding = React.lazy( () => import('src/components/AccountActivation/AccountActivationLanding') ); -const Firewalls = React.lazy(() => import('src/features/Firewalls')); const Databases = React.lazy(() => import('src/features/Databases')); const VPC = React.lazy(() => import('src/features/VPCs')); @@ -384,7 +383,6 @@ export const MainContent = () => { - {isDatabasesEnabled && ( )} diff --git a/packages/manager/src/features/Events/factories/firewall.tsx b/packages/manager/src/features/Events/factories/firewall.tsx index c9db016c1e5..140dd563b4d 100644 --- a/packages/manager/src/features/Events/factories/firewall.tsx +++ b/packages/manager/src/features/Events/factories/firewall.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { formattedTypes } from 'src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding'; +import { formattedTypes } from 'src/features/Firewalls/FirewallDetail/Devices/constants'; import { EventLink } from '../EventLink'; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.test.tsx index 2e14b866959..b45d1cc0291 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.test.tsx @@ -12,7 +12,24 @@ const props = { onClose, open: true, }; + +const queryMocks = vi.hoisted(() => ({ + useParams: vi.fn().mockReturnValue({}), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useParams: queryMocks.useParams, + }; +}); + describe('AddLinodeDrawer', () => { + beforeEach(() => { + queryMocks.useParams.mockReturnValue({ id: '1' }); + }); + it('should contain helper text', () => { const { getByText } = renderWithTheme(); expect(getByText(helperText)).toBeInTheDocument(); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index ba5f2a15afb..99607a9e36b 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -1,8 +1,8 @@ import { Notice } from '@linode/ui'; import { useTheme } from '@mui/material'; +import { useParams } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; @@ -29,7 +29,7 @@ interface Props { export const AddLinodeDrawer = (props: Props) => { const { helperText, onClose, open } = props; - const { id } = useParams<{ id: string }>(); + const { id } = useParams({ strict: false }); const { enqueueSnackbar } = useSnackbar(); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.test.tsx index 36d229be748..c537eaa1497 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.test.tsx @@ -13,7 +13,23 @@ const props = { open: true, }; +const queryMocks = vi.hoisted(() => ({ + useParams: vi.fn().mockReturnValue({}), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useParams: queryMocks.useParams, + }; +}); + describe('AddNodeBalancerDrawer', () => { + beforeEach(() => { + queryMocks.useParams.mockReturnValue({ id: '1' }); + }); + it('should contain helper text', () => { const { getByText } = renderWithTheme(); expect(getByText(helperText)).toBeInTheDocument(); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index 435fd687795..bb1b0df38b6 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -1,8 +1,8 @@ import { Notice } from '@linode/ui'; import { useTheme } from '@mui/material'; +import { useParams } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; @@ -30,7 +30,7 @@ interface Props { export const AddNodebalancerDrawer = (props: Props) => { const { helperText, onClose, open } = props; const { enqueueSnackbar } = useSnackbar(); - const { id } = useParams<{ id: string }>(); + const { id } = useParams({ strict: false }); const { data: grants } = useGrants(); const { data: profile } = useProfile(); const isRestrictedUser = Boolean(profile?.restricted); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceActionMenu.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceActionMenu.tsx index ff489aae8f8..bd80e4a2859 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceActionMenu.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceActionMenu.tsx @@ -3,29 +3,26 @@ import * as React from 'react'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; export interface ActionHandlers { - triggerRemoveDevice: (deviceID: number, label: string) => void; + handleRemoveDevice: (device: FirewallDevice) => void; } -import type { FirewallDeviceEntityType } from '@linode/api-v4'; +import type { FirewallDevice } from '@linode/api-v4'; export interface FirewallDeviceActionMenuProps extends ActionHandlers { - deviceEntityID: number; - deviceID: number; - deviceLabel: string; - deviceType: FirewallDeviceEntityType; + device: FirewallDevice; disabled: boolean; } export const FirewallDeviceActionMenu = React.memo( (props: FirewallDeviceActionMenuProps) => { - const { deviceID, deviceLabel, disabled, triggerRemoveDevice } = props; + const { device, disabled, handleRemoveDevice } = props; return ( triggerRemoveDevice(deviceID, deviceLabel)} + onClick={() => handleRemoveDevice(device)} /> ); } diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.test.tsx index 4ee53e57dd6..5ce6a4024d0 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.test.tsx @@ -1,17 +1,47 @@ -import { fireEvent } from '@testing-library/react'; -import { createMemoryHistory } from 'history'; +import { fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { Router } from 'react-router-dom'; import { firewallDeviceFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { + renderWithTheme, + renderWithThemeAndRouter, +} from 'src/utilities/testHelpers'; import { FirewallDeviceLanding } from './FirewallDeviceLanding'; import type { FirewallDeviceLandingProps } from './FirewallDeviceLanding'; import type { FirewallDeviceEntityType } from '@linode/api-v4'; +const queryMocks = vi.hoisted(() => ({ + useLocation: vi.fn().mockReturnValue({}), + useNavigate: vi.fn(() => vi.fn()), + useOrderV2: vi.fn().mockReturnValue({ + handleOrderChange: vi.fn(), + }), + useParams: vi.fn().mockReturnValue({}), + useSearch: vi.fn().mockReturnValue({}), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useLocation: queryMocks.useLocation, + useNavigate: queryMocks.useNavigate, + useParams: queryMocks.useParams, + useSearch: queryMocks.useSearch, + }; +}); + +vi.mock('src/hooks/useOrderV2', async () => { + const actual = await vi.importActual('src/hooks/useOrderV2'); + return { + ...actual, + useOrderV2: queryMocks.useOrderV2, + }; +}); + const baseProps = ( type: FirewallDeviceEntityType ): FirewallDeviceLandingProps => ({ @@ -32,6 +62,14 @@ services.forEach((service: FirewallDeviceEntityType) => { const serviceName = service === 'linode' ? 'Linode' : 'NodeBalancer'; describe(`Firewall ${serviceName} landing page`, () => { + beforeEach(() => { + queryMocks.useLocation.mockReturnValue({ + pathname: '/firewalls/1/linodes', + }); + queryMocks.useParams.mockReturnValue({ + id: '1', + }); + }); const props = [baseProps(service), disabledProps(service)]; props.forEach((prop) => { @@ -60,6 +98,7 @@ services.forEach((service: FirewallDeviceEntityType) => { expect(addButton).toHaveAttribute('aria-disabled', 'true'); }); + it('should contain permission notice when disabled', () => { const { getByRole } = renderWithTheme( @@ -78,17 +117,26 @@ services.forEach((service: FirewallDeviceEntityType) => { expect(addButton).toHaveAttribute('aria-disabled', 'false'); }); - it(`should navigate to Add ${serviceName} To Firewall drawer when enabled`, () => { - const history = createMemoryHistory(); - const { getByTestId } = renderWithTheme( - - - + + it(`should navigate to Add ${serviceName} To Firewall drawer when enabled`, async () => { + const mockNavigate = vi.fn(); + queryMocks.useNavigate.mockReturnValue(mockNavigate); + + const { getByTestId } = await renderWithThemeAndRouter( + , + { + initialRoute: `/firewalls/1/${service}`, + } ); const addButton = getByTestId('add-device-button'); fireEvent.click(addButton); - const baseUrl = '/'; - expect(history.location.pathname).toBe(baseUrl + '/add'); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith({ + params: { id: '1' }, + to: `/firewalls/$id/${service}s/add`, + }); + }); }); } }); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx index 139515fc7ce..60f9ffa7c71 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx @@ -1,15 +1,15 @@ import { Button, Notice, Typography } from '@linode/ui'; -import { useTheme } from '@mui/material/styles'; -import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Grid2'; +import { styled } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; +import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; -import { useAllFirewallDevicesQuery } from 'src/queries/firewalls'; import { AddLinodeDrawer } from './AddLinodeDrawer'; import { AddNodebalancerDrawer } from './AddNodebalancerDrawer'; +import { formattedTypes } from './constants'; import { FirewallDeviceTable } from './FirewallDeviceTable'; import { RemoveDeviceDialog } from './RemoveDeviceDialog'; @@ -22,83 +22,60 @@ export interface FirewallDeviceLandingProps { type: FirewallDeviceEntityType; } -export const formattedTypes: Record = { - interface: 'Interface', // @TODO Linode Interface: double check this when working on UI tickets - linode: 'Linode', - nodebalancer: 'NodeBalancer', -}; - export const FirewallDeviceLanding = React.memo( (props: FirewallDeviceLandingProps) => { const { disabled, firewallId, firewallLabel, type } = props; - - const { data: allDevices, error, isLoading } = useAllFirewallDevicesQuery( - firewallId - ); - const theme = useTheme(); - - const history = useHistory(); - const routeMatch = useRouteMatch(); + const navigate = useNavigate(); const location = useLocation(); - const helperText = 'Assign one or more services to this firewall. You can add services later if you want to customize your rules first.'; - React.useEffect(() => { - if (location.pathname.endsWith('add')) { - setDeviceDrawerOpen(true); - } - }, [location.pathname]); - - const devices = - allDevices?.filter((device) => device.entity.type === type) || []; - - const [filteredDevices, setFilteredDevices] = React.useState< - FirewallDevice[] - >([]); - - React.useEffect(() => { - setFilteredDevices(devices); - }, [allDevices]); - - const [ - isRemoveDeviceDialogOpen, - setIsRemoveDeviceDialogOpen, - ] = React.useState(false); - - const [selectedDeviceId, setSelectedDeviceId] = React.useState(-1); - - const selectedDevice = filteredDevices?.find( - (device) => device.id === selectedDeviceId - ); - - const [addDeviceDrawerOpen, setDeviceDrawerOpen] = React.useState( - false - ); - const handleClose = () => { - setDeviceDrawerOpen(false); - history.push(routeMatch.url); + navigate({ + params: { id: String(firewallId) }, + to: + type === 'linode' + ? '/firewalls/$id/linodes' + : '/firewalls/$id/nodebalancers', + }); }; const handleOpen = () => { - setDeviceDrawerOpen(true); - history.push(routeMatch.url + '/add'); + navigate({ + params: { id: String(firewallId) }, + to: + type === 'linode' + ? '/firewalls/$id/linodes/add' + : '/firewalls/$id/nodebalancers/add', + }); }; const [searchText, setSearchText] = React.useState(''); const filter = (value: string) => { setSearchText(value); - const filtered = devices?.filter((device) => { - return device.entity.label.toLowerCase().includes(value.toLowerCase()); - }); - setFilteredDevices(filtered ?? []); }; + const [device, setDevice] = React.useState( + undefined + ); const formattedType = formattedTypes[type]; + // If the user initiates a history -/+ to a /remove route and the device is not found, + // push navigation to the appropriate /linodes or /nodebalancers route. + React.useEffect(() => { + if (!device && location.pathname.endsWith('remove')) { + navigate({ + params: { id: String(firewallId) }, + to: + type === 'linode' + ? '/firewalls/$id/linodes' + : '/firewalls/$id/nodebalancers', + }); + } + }, [device, location.pathname, firewallId, type, navigate]); + return ( <> {disabled ? ( @@ -120,12 +97,12 @@ export const FirewallDeviceLanding = React.memo( A {formattedType} can only be assigned to a single Firewall. { - setSelectedDeviceId(id); - setIsRemoveDeviceDialogOpen(true); + handleRemoveDevice={(device) => { + setDevice(device); + navigate({ + params: { id: String(firewallId) }, + to: + type === 'linode' + ? '/firewalls/$id/linodes/remove' + : '/firewalls/$id/nodebalancers/remove', + }); }} deviceType={type} - devices={filteredDevices ?? []} disabled={disabled} - error={error ?? undefined} - loading={isLoading} + firewallId={firewallId} + type={type} /> {type === 'linode' ? ( ) : ( )} + navigate({ + params: { id: String(firewallId) }, + to: + type === 'linode' + ? '/firewalls/$id/linodes' + : '/firewalls/$id/nodebalancers', + }) + } + device={device} firewallId={firewallId} firewallLabel={firewallLabel} - onClose={() => setIsRemoveDeviceDialogOpen(false)} onService={undefined} - open={isRemoveDeviceDialogOpen} + open={location.pathname.endsWith('remove')} /> ); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx index 7b1cfcb8fc6..c44b59a22e6 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx @@ -4,27 +4,24 @@ import { Link } from 'react-router-dom'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { - FirewallDeviceActionMenu, - FirewallDeviceActionMenuProps, -} from './FirewallDeviceActionMenu'; +import { FirewallDeviceActionMenu } from './FirewallDeviceActionMenu'; + +import type { FirewallDeviceActionMenuProps } from './FirewallDeviceActionMenu'; export const FirewallDeviceRow = React.memo( (props: FirewallDeviceActionMenuProps) => { - const { deviceEntityID, deviceID, deviceLabel, deviceType } = props; + const { device } = props; + const { id, label, type } = device.entity; return ( - + - - {deviceLabel} + + {label} - + ); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.test.tsx index f56aa9160f1..b57b40568d6 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.test.tsx @@ -12,11 +12,33 @@ const devices = ['linode', 'nodebalancer']; const props = (type: FirewallDeviceEntityType): FirewallDeviceTableProps => ({ deviceType: type, - devices: firewallDeviceFactory.buildList(2), disabled: false, - error: undefined, - loading: false, - triggerRemoveDevice: vi.fn(), + firewallId: 1, + handleRemoveDevice: vi.fn(), + type, +}); + +const queryMocks = vi.hoisted(() => ({ + useAllFirewallDevicesQuery: vi.fn().mockReturnValue({}), + useNavigate: vi.fn(() => vi.fn()), + useSearch: vi.fn().mockReturnValue({}), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + useSearch: queryMocks.useSearch, + }; +}); + +vi.mock('src/queries/firewalls', async () => { + const actual = await vi.importActual('src/queries/firewalls'); + return { + ...actual, + useAllFirewallDevicesQuery: queryMocks.useAllFirewallDevicesQuery, + }; }); devices.forEach((device: FirewallDeviceEntityType) => { @@ -28,13 +50,24 @@ devices.forEach((device: FirewallDeviceEntityType) => { const table = getByRole('table'); expect(table).toBeInTheDocument(); }); - }); - it('should contain two rows', () => { - const { getAllByRole } = renderWithTheme( - - ); - const rows = getAllByRole('row'); - expect(rows.length - 1).toBe(2); + it('should contain two rows', () => { + queryMocks.useAllFirewallDevicesQuery.mockReturnValue({ + data: firewallDeviceFactory.buildList(2, { + entity: { + id: 1, + label: `test-${device}`, + type: device, + }, + }), + error: null, + isLoading: false, + }); + const { getAllByRole } = renderWithTheme( + + ); + const rows = getAllByRole('row'); + expect(rows.length - 1).toBe(2); + }); }); }); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx index 0e001a071c6..69745f8baf8 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx @@ -1,7 +1,5 @@ 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'; @@ -9,35 +7,40 @@ import { TableContentWrapper } from 'src/components/TableContentWrapper/TableCon import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; +import { useOrderV2 } from 'src/hooks/useOrderV2'; +import { usePaginationV2 } from 'src/hooks/usePaginationV2'; +import { useAllFirewallDevicesQuery } from 'src/queries/firewalls'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { formattedTypes } from './FirewallDeviceLanding'; +import { formattedTypes } from './constants'; import { FirewallDeviceRow } from './FirewallDeviceRow'; -import type { FirewallDeviceEntityType } from '@linode/api-v4'; -import type { FirewallDevice } from '@linode/api-v4/lib/firewalls/types'; -import type { APIError } from '@linode/api-v4/lib/types'; +import type { FirewallDevice, FirewallDeviceEntityType } from '@linode/api-v4'; export interface FirewallDeviceTableProps { deviceType: FirewallDeviceEntityType; - devices: FirewallDevice[]; disabled: boolean; - error?: APIError[]; - loading: boolean; - triggerRemoveDevice: (deviceID: number) => void; + firewallId: number; + handleRemoveDevice: (device: FirewallDevice) => void; + type: FirewallDeviceEntityType; } export const FirewallDeviceTable = React.memo( (props: FirewallDeviceTableProps) => { const { deviceType, - devices, disabled, - error, - loading, - triggerRemoveDevice, + firewallId, + handleRemoveDevice, + type, } = props; + const { data: allDevices, error, isLoading } = useAllFirewallDevicesQuery( + firewallId + ); + const devices = + allDevices?.filter((device) => device.entity.type === type) || []; + const _error = error ? getAPIErrorOrDefault( error, @@ -47,67 +50,77 @@ export const FirewallDeviceTable = React.memo( const ariaLabel = `List of ${formattedTypes[deviceType]}s attached to this firewall`; + const { + handleOrderChange, + order, + orderBy, + sortedData: sortedDevices, + } = useOrderV2({ + data: devices, + initialRoute: { + defaultOrder: { + order: 'asc', + orderBy: `entity:label`, + }, + from: + deviceType === 'linode' + ? '/firewalls/$id/linodes' + : '/firewalls/$id/nodebalancers', + }, + preferenceKey: `${deviceType}s-order`, + }); + + const pagination = usePaginationV2({ + currentRoute: + deviceType === 'linode' + ? '/firewalls/$id/linodes' + : '/firewalls/$id/nodebalancers', + preferenceKey: `${deviceType}s-pagination`, + }); + return ( - - {({ data: orderedData, handleOrderChange, order, orderBy }) => ( - - {({ - count, - data: paginatedAndOrderedData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => ( - <> - - - - - {formattedTypes[deviceType]} - - - - - - {paginatedAndOrderedData.map((thisDevice) => ( - - ))} - - -
- + + + + + {formattedTypes[deviceType]} + + + + + + {sortedDevices?.map((thisDevice) => ( + - - )} - - )} - + ))} + + +
+ + ); } ); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts new file mode 100644 index 00000000000..9234255a29c --- /dev/null +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts @@ -0,0 +1,7 @@ +import type { FirewallDeviceEntityType } from '@linode/api-v4'; + +export const formattedTypes: Record = { + interface: 'Interface', // @TODO Linode Interface: double check this when working on UI tickets + linode: 'Linode', + nodebalancer: 'NodeBalancer', +}; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.test.tsx index f9277c7c6d9..3888e665cb8 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.test.tsx @@ -3,17 +3,16 @@ import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { - FirewallRuleActionMenu, - FirewallRuleActionMenuProps, -} from './FirewallRuleActionMenu'; +import { FirewallRuleActionMenu } from './FirewallRuleActionMenu'; + +import type { FirewallRuleActionMenuProps } from './FirewallRuleActionMenu'; const props: FirewallRuleActionMenuProps = { disabled: false, + handleCloneFirewallRule: vi.fn(), + handleDeleteFirewallRule: vi.fn(), + handleOpenRuleDrawerForEditing: vi.fn(), idx: 1, - triggerCloneFirewallRule: vi.fn(), - triggerDeleteFirewallRule: vi.fn(), - triggerOpenRuleDrawerForEditing: vi.fn(), }; describe('Firewall rule action menu', () => { diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.tsx index 8ea1e58f141..3f3313022a3 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleActionMenu.tsx @@ -1,20 +1,22 @@ -import { Theme, useTheme } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; -import { +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; + +import type { Theme } from '@mui/material/styles'; +import type { Action, - ActionMenu, ActionMenuProps, } from 'src/components/ActionMenu/ActionMenu'; -import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; export interface FirewallRuleActionMenuProps extends Partial { disabled: boolean; + handleCloneFirewallRule: (idx: number) => void; + handleDeleteFirewallRule: (idx: number) => void; + handleOpenRuleDrawerForEditing: (idx: number) => void; idx: number; - triggerCloneFirewallRule: (idx: number) => void; - triggerDeleteFirewallRule: (idx: number) => void; - triggerOpenRuleDrawerForEditing: (idx: number) => void; } export const FirewallRuleActionMenu = React.memo( @@ -24,10 +26,10 @@ export const FirewallRuleActionMenu = React.memo( const { disabled, + handleCloneFirewallRule, + handleDeleteFirewallRule, + handleOpenRuleDrawerForEditing, idx, - triggerCloneFirewallRule, - triggerDeleteFirewallRule, - triggerOpenRuleDrawerForEditing, ...actionMenuProps } = props; @@ -35,21 +37,21 @@ export const FirewallRuleActionMenu = React.memo( { disabled, onClick: () => { - triggerOpenRuleDrawerForEditing(idx); + handleOpenRuleDrawerForEditing(idx); }, title: 'Edit', }, { disabled, onClick: () => { - triggerCloneFirewallRule(idx); + handleCloneFirewallRule(idx); }, title: 'Clone', }, { disabled, onClick: () => { - triggerDeleteFirewallRule(idx); + handleDeleteFirewallRule(idx); }, title: 'Delete', }, diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx index e18ba301326..f7e966e6447 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx @@ -78,11 +78,11 @@ interface RuleRow { // ============================================================================= interface RowActionHandlers { - triggerCloneFirewallRule: (idx: number) => void; - triggerDeleteFirewallRule: (idx: number) => void; - triggerOpenRuleDrawerForEditing: (idx: number) => void; - triggerReorder: (startIdx: number, endIdx: number) => void; - triggerUndo: (idx: number) => void; + handleCloneFirewallRule: (idx: number) => void; + handleDeleteFirewallRule: (idx: number) => void; + handleOpenRuleDrawerForEditing: (idx: number) => void; + handleReorder: (startIdx: number, endIdx: number) => void; + handleUndo: (idx: number) => void; } interface FirewallRuleTableProps extends RowActionHandlers { @@ -101,15 +101,15 @@ export const FirewallRuleTable = (props: FirewallRuleTableProps) => { const { category, disabled, + handleCloneFirewallRule, + handleDeleteFirewallRule, + handleOpenRuleDrawerForEditing, handlePolicyChange, + handleReorder, + handleUndo, openRuleDrawer, policy, rulesWithStatus, - triggerCloneFirewallRule, - triggerDeleteFirewallRule, - triggerOpenRuleDrawerForEditing, - triggerReorder, - triggerUndo, } = props; const theme = useTheme(); @@ -141,7 +141,7 @@ export const FirewallRuleTable = (props: FirewallRuleTableProps) => { if (active && over && active.id !== over.id) { const sourceIndex = getRowDataIndex(Number(active.id)); const destinationIndex = getRowDataIndex(Number(over.id)); - triggerReorder(sourceIndex, destinationIndex); + handleReorder(sourceIndex, destinationIndex); } // Remove focus from the initial position when the drag ends. @@ -238,16 +238,16 @@ export const FirewallRuleTable = (props: FirewallRuleTableProps) => { aria-label={ thisRuleRow.label ?? `firewall rule ${thisRuleRow.id}` } - triggerOpenRuleDrawerForEditing={ - triggerOpenRuleDrawerForEditing + handleOpenRuleDrawerForEditing={ + handleOpenRuleDrawerForEditing } aria-roledescription={screenReaderMessage} aria-selected={false} disabled={disabled} + handleCloneFirewallRule={handleCloneFirewallRule} + handleDeleteFirewallRule={handleDeleteFirewallRule} + handleUndo={handleUndo} key={thisRuleRow.id} - triggerCloneFirewallRule={triggerCloneFirewallRule} - triggerDeleteFirewallRule={triggerDeleteFirewallRule} - triggerUndo={triggerUndo} {...thisRuleRow} id={thisRuleRow.id} /> @@ -278,10 +278,10 @@ interface RowActionHandlersWithDisabled export interface FirewallRuleTableRowProps extends RuleRow { disabled: RowActionHandlersWithDisabled['disabled']; - triggerCloneFirewallRule: RowActionHandlersWithDisabled['triggerCloneFirewallRule']; - triggerDeleteFirewallRule: RowActionHandlersWithDisabled['triggerDeleteFirewallRule']; - triggerOpenRuleDrawerForEditing: RowActionHandlersWithDisabled['triggerOpenRuleDrawerForEditing']; - triggerUndo: RowActionHandlersWithDisabled['triggerUndo']; + handleCloneFirewallRule: RowActionHandlersWithDisabled['handleCloneFirewallRule']; + handleDeleteFirewallRule: RowActionHandlersWithDisabled['handleDeleteFirewallRule']; + handleOpenRuleDrawerForEditing: RowActionHandlersWithDisabled['handleOpenRuleDrawerForEditing']; + handleUndo: RowActionHandlersWithDisabled['handleUndo']; } const FirewallRuleTableRow = React.memo((props: FirewallRuleTableRowProps) => { @@ -290,6 +290,10 @@ const FirewallRuleTableRow = React.memo((props: FirewallRuleTableRowProps) => { addresses, disabled, errors, + handleCloneFirewallRule, + handleDeleteFirewallRule, + handleOpenRuleDrawerForEditing, + handleUndo, id, index, label, @@ -297,18 +301,14 @@ const FirewallRuleTableRow = React.memo((props: FirewallRuleTableRowProps) => { ports, protocol, status, - triggerCloneFirewallRule, - triggerDeleteFirewallRule, - triggerOpenRuleDrawerForEditing, - triggerUndo, } = props; const actionMenuProps = { disabled: status === 'PENDING_DELETION' || disabled, + handleCloneFirewallRule, + handleDeleteFirewallRule, + handleOpenRuleDrawerForEditing, idx: index, - triggerCloneFirewallRule, - triggerDeleteFirewallRule, - triggerOpenRuleDrawerForEditing, }; const theme = useTheme(); @@ -363,7 +363,7 @@ const FirewallRuleTableRow = React.memo((props: FirewallRuleTableRowProps) => { {label || ( triggerOpenRuleDrawerForEditing(index)} + onClick={() => handleOpenRuleDrawerForEditing(index)} > Add a label @@ -395,7 +395,7 @@ const FirewallRuleTableRow = React.memo((props: FirewallRuleTableRowProps) => { triggerUndo(index)} + onClick={() => handleUndo(index)} status={status} > diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx index c773be919e3..f71cbf8f854 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx @@ -1,11 +1,13 @@ import { Notice, Typography } from '@linode/ui'; import { styled } from '@mui/material/styles'; import { useQueryClient } from '@tanstack/react-query'; +import { useBlocker, useLocation, useNavigate } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +// eslint-disable-next-line no-restricted-imports import { Prompt } from 'src/components/Prompt/Prompt'; import { useAllFirewallDevicesQuery, @@ -44,7 +46,6 @@ interface Props { interface Drawer { category: Category; - isOpen: boolean; mode: FirewallRuleDrawerMode; ruleIdx?: number; } @@ -58,7 +59,8 @@ export const FirewallRulesLanding = React.memo((props: Props) => { ); const { data: devices } = useAllFirewallDevicesQuery(firewallID); const queryClient = useQueryClient(); - + const navigate = useNavigate(); + const location = useLocation(); const { enqueueSnackbar } = useSnackbar(); /** @@ -87,7 +89,6 @@ export const FirewallRulesLanding = React.memo((props: Props) => { */ const [ruleDrawer, setRuleDrawer] = React.useState({ category: 'inbound', - isOpen: false, mode: 'create', }); const [submitting, setSubmitting] = React.useState(false); @@ -104,15 +105,32 @@ export const FirewallRulesLanding = React.memo((props: Props) => { category: Category, mode: FirewallRuleDrawerMode, idx?: number - ) => + ) => { setRuleDrawer({ category, - isOpen: true, mode, ruleIdx: idx, }); + navigate({ + params: { id: String(firewallID), ruleId: String(idx) }, + to: + category === 'inbound' && mode === 'create' + ? '/firewalls/$id/rules/add/inbound' + : category === 'inbound' && mode === 'edit' + ? `/firewalls/$id/rules/edit/inbound/$ruleId` + : category === 'outbound' && mode === 'create' + ? '/firewalls/$id/rules/add/outbound' + : `/firewalls/$id/rules/edit/outbound/$ruleId`, + }); + }; - const closeRuleDrawer = () => setRuleDrawer({ ...ruleDrawer, isOpen: false }); + const closeRuleDrawer = () => { + setRuleDrawer({ ...ruleDrawer }); + navigate({ + params: { id: String(firewallID) }, + to: '/firewalls/$id/rules', + }); + }; /** * Rule Editor state hand handlers @@ -267,6 +285,41 @@ export const FirewallRulesLanding = React.memo((props: Props) => { [inboundState, outboundState, policy, rules] ); + const { proceed, reset, status } = useBlocker({ + enableBeforeUnload: hasUnsavedChanges, + shouldBlockFn: ({ next }) => { + // Only block if there are unsaved changes + if (!hasUnsavedChanges) { + return false; + } + + // Don't block navigation to these specific routes, since they are part of the current form + const isNavigatingToAllowedRoute = + next.routeId === '/firewalls/$id/rules' || + next.routeId === '/firewalls/$id/rules/add/inbound' || + next.routeId === '/firewalls/$id/rules/add/outbound' || + next.routeId === '/firewalls/$id/rules/edit/inbound/$ruleId' || + next.routeId === '/firewalls/$id/rules/edit/outbound/$ruleId'; + + return !isNavigatingToAllowedRoute; + }, + withResolver: true, + }); + + // Create a combined handler for proceeding with navigation + const handleProceedNavigation = React.useCallback(() => { + if (status === 'blocked' && proceed) { + proceed(); + } + }, [status, proceed]); + + // Create a combined handler for canceling navigation + const handleCancelNavigation = React.useCallback(() => { + if (status === 'blocked' && reset) { + reset(); + } + }, [status, reset]); + const inboundRules = React.useMemo(() => editorStateToRules(inboundState), [ inboundState, ]); @@ -285,34 +338,47 @@ export const FirewallRulesLanding = React.memo((props: Props) => { return ( <> + {/* + This Prompt eventually can be removed once react-router is fully deprecated + It is here only to preserve the behavior of non-Tanstack routes + */} - {({ handleCancel, handleConfirm, isModalOpen }) => { - return ( - ( - - )} - onClose={handleCancel} - open={isModalOpen} - title="Discard Firewall changes?" - > - - The changes you made to this Firewall haven’t been - applied. If you navigate away from this page, your changes will - be discarded. - - - ); - }} + {({ handleCancel, handleConfirm, isModalOpen }) => ( + ( + { + handleCancelNavigation(); + handleCancel(); + }, + }} + secondaryButtonProps={{ + buttonType: 'secondary', + color: 'error', + label: 'Leave and discard changes', + onClick: () => { + handleProceedNavigation(); + handleConfirm(); + }, + }} + /> + )} + onClose={() => { + handleCancelNavigation(); + handleCancel(); + }} + open={status === 'blocked' || isModalOpen} + title="Discard Firewall changes?" + > + + The changes you made to this Firewall haven’t been applied. + If you navigate away from this page, your changes will be + discarded. + + + )} {disabled ? ( @@ -324,56 +390,59 @@ export const FirewallRulesLanding = React.memo((props: Props) => { variant="error" /> ) : null} - {generalErrors?.length === 1 && ( )} - + handleCloneFirewallRule={(idx: number) => handleCloneRule('inbound', idx) } - triggerOpenRuleDrawerForEditing={(idx: number) => + handleOpenRuleDrawerForEditing={(idx: number) => openRuleDrawer('inbound', 'edit', idx) } - triggerReorder={(startIdx: number, endIdx: number) => + handleReorder={(startIdx: number, endIdx: number) => handleReorder('inbound', startIdx, endIdx) } category="inbound" disabled={disabled} + handleDeleteFirewallRule={(idx) => handleDeleteRule('inbound', idx)} handlePolicyChange={handlePolicyChange} + handleUndo={(idx) => handleUndo('inbound', idx)} openRuleDrawer={openRuleDrawer} policy={policy.inbound} rulesWithStatus={inboundRules} - triggerDeleteFirewallRule={(idx) => handleDeleteRule('inbound', idx)} - triggerUndo={(idx) => handleUndo('inbound', idx)} /> + handleCloneFirewallRule={(idx: number) => handleCloneRule('outbound', idx) } - triggerOpenRuleDrawerForEditing={(idx: number) => + handleOpenRuleDrawerForEditing={(idx: number) => openRuleDrawer('outbound', 'edit', idx) } - triggerReorder={(startIdx: number, endIdx: number) => + handleReorder={(startIdx: number, endIdx: number) => handleReorder('outbound', startIdx, endIdx) } category="outbound" disabled={disabled} + handleDeleteFirewallRule={(idx) => handleDeleteRule('outbound', idx)} handlePolicyChange={handlePolicyChange} + handleUndo={(idx) => handleUndo('outbound', idx)} openRuleDrawer={openRuleDrawer} policy={policy.outbound} rulesWithStatus={outboundRules} - triggerDeleteFirewallRule={(idx) => handleDeleteRule('outbound', idx)} - triggerUndo={(idx) => handleUndo('outbound', idx)} /> { onClick: () => setDiscardChangesModalOpen(true), }} /> - { setDiscardChangesModalOpen(false); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 5a186745f16..f07b9e2d668 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -1,7 +1,6 @@ import { CircleProgress, ErrorState } from '@linode/ui'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; import { AkamaiBanner } from 'src/components/AkamaiBanner/AkamaiBanner'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; @@ -10,13 +9,14 @@ import { LandingHeader } from 'src/components/LandingHeader'; import { LinkButton } from 'src/components/LinkButton'; import { NotFound } from 'src/components/NotFound'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; -import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; import { useFlags } from 'src/hooks/useFlags'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; -import { useAllFirewallDevicesQuery } from 'src/queries/firewalls'; +import { useTabs } from 'src/hooks/useTabs'; import { useFirewallQuery, useMutateFirewall } from 'src/queries/firewalls'; +import { useAllFirewallDevicesQuery } from 'src/queries/firewalls'; import { useGrants, useProfile } from 'src/queries/profile/profile'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; @@ -35,8 +35,9 @@ const FirewallDeviceLanding = React.lazy(() => ); export const FirewallDetail = () => { - const { id, tab } = useParams<{ id: string; tab?: string }>(); - const history = useHistory(); + const { id } = useParams({ + strict: false, + }); const { data: profile } = useProfile(); const { data: grants } = useGrants(); const { secureVMNoticesEnabled } = useSecureVMNoticesEnabled(); @@ -68,22 +69,20 @@ export const FirewallDetail = () => { { linodeCount: 0, nodebalancerCount: 0 } ) || { linodeCount: 0, nodebalancerCount: 0 }; - const tabs = [ + const { handleTabChange, tabIndex, tabs } = useTabs([ { - routeName: `/firewalls/${id}/rules`, title: 'Rules', + to: `/firewalls/$id/rules`, }, { - routeName: `/firewalls/${id}/linodes`, title: `Linodes (${linodeCount})`, + to: `/firewalls/$id/linodes`, }, { - routeName: `/firewalls/${id}/nodebalancers`, title: `NodeBalancers (${nodebalancerCount})`, + to: `/firewalls/$id/nodebalancers`, }, - ]; - - const tabIndex = tab ? tabs.findIndex((t) => t.routeName.endsWith(tab)) : -1; + ]); const { data: firewall, error, isLoading } = useFirewallQuery(firewallId); @@ -150,12 +149,8 @@ export const FirewallDetail = () => { {...secureVMFirewallBanner.firewallDetails} /> )} - history.push(tabs[i].routeName)} - > - - + + { ); }; - -export const firewallDetailLazyRoute = createLazyRoute('/firewalls/$id')({ - component: FirewallDetail, -}); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx index 96f5a9b7c10..060b4ef368d 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx @@ -10,6 +10,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; +// eslint-disable-next-line no-restricted-imports import { useLocation } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx index bbf06d3ecc4..28ef64108ac 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx @@ -1,7 +1,6 @@ import { Button, CircleProgress, ErrorState } from '@linode/ui'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/GenerateFirewallDialog'; @@ -34,8 +33,8 @@ import type { Mode } from './FirewallDialog'; const preferenceKey = 'firewalls'; const FirewallLanding = () => { + const navigate = useNavigate(); const location = useLocation(); - const history = useHistory(); const pagination = usePagination(1, preferenceKey); const { handleOrderChange, order, orderBy } = useOrder( { @@ -97,11 +96,11 @@ const FirewallLanding = () => { }; const onOpenCreateDrawer = () => { - history.replace('/firewalls/create'); + navigate({ to: '/firewalls/create' }); }; const onCloseCreateDrawer = () => { - history.replace('/firewalls'); + navigate({ to: '/firewalls' }); }; const handlers: FirewallHandlers = { @@ -230,8 +229,4 @@ const FirewallLanding = () => { ); }; -export const firewallLandingLazyRoute = createLazyRoute('/firewalls')({ - component: FirewallLanding, -}); - export default React.memo(FirewallLanding); diff --git a/packages/manager/src/features/Firewalls/index.tsx b/packages/manager/src/features/Firewalls/index.tsx deleted file mode 100644 index 760f34df185..00000000000 --- a/packages/manager/src/features/Firewalls/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { Route, Switch, useRouteMatch } from 'react-router-dom'; - -import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; -import { SuspenseLoader } from 'src/components/SuspenseLoader'; - -const FirewallLanding = React.lazy( - () => import('./FirewallLanding/FirewallLanding') -); - -const FirewallDetail = React.lazy(() => - import('./FirewallDetail').then((module) => ({ - default: module.FirewallDetail, - })) -); - -const Firewall = () => { - const { path } = useRouteMatch(); - - return ( - }> - - - - - - - - - - ); -}; - -export default Firewall; diff --git a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx index 166c47d6b01..b142f154096 100644 --- a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx @@ -19,6 +19,7 @@ import { useDispatch } from 'react-redux'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Link } from 'src/components/Link'; +// eslint-disable-next-line no-restricted-imports import { Prompt } from 'src/components/Prompt/Prompt'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx index 6ce1b41ba37..d65a7ddb014 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx @@ -7,7 +7,7 @@ import { number, object } from 'yup'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Link } from 'src/components/Link'; -import { formattedTypes } from 'src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding'; +import { formattedTypes } from 'src/features/Firewalls/FirewallDetail/Devices/constants'; import { useAddFirewallDeviceMutation, useAllFirewallsQuery, diff --git a/packages/manager/src/hooks/useTabs.ts b/packages/manager/src/hooks/useTabs.ts index 05a861f4341..52d5e5f0b3b 100644 --- a/packages/manager/src/hooks/useTabs.ts +++ b/packages/manager/src/hooks/useTabs.ts @@ -44,7 +44,14 @@ export function useTabs(tabs: T[]) { // Calculate current index based on route const tabIndex = React.useMemo(() => { - const index = visibleTabs.findIndex((tab) => matchRoute({ to: tab.to })); + const index = visibleTabs.findIndex((tab) => { + const tabPath = String(tab.to); + // Check if current route starts with the tab's base path + return matchRoute({ + fuzzy: true, // Allows to match parent routes + to: tabPath, + }); + }); return index === -1 ? 0 : index; }, [visibleTabs, matchRoute]); diff --git a/packages/manager/src/routes/firewalls/firewallLazyRoutes.ts b/packages/manager/src/routes/firewalls/firewallLazyRoutes.ts new file mode 100644 index 00000000000..0a7330cc765 --- /dev/null +++ b/packages/manager/src/routes/firewalls/firewallLazyRoutes.ts @@ -0,0 +1,12 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import { FirewallDetail } from 'src/features/Firewalls/FirewallDetail'; +import FirewallLanding from 'src/features/Firewalls/FirewallLanding/FirewallLanding'; + +export const firewallLandingLazyRoute = createLazyRoute('/firewalls')({ + component: FirewallLanding, +}); + +export const firewallDetailLazyRoute = createLazyRoute('/firewalls/$id')({ + component: FirewallDetail, +}); diff --git a/packages/manager/src/routes/firewalls/index.ts b/packages/manager/src/routes/firewalls/index.ts index 61a3ef27088..66436a0eb23 100644 --- a/packages/manager/src/routes/firewalls/index.ts +++ b/packages/manager/src/routes/firewalls/index.ts @@ -1,8 +1,15 @@ -import { createRoute } from '@tanstack/react-router'; +import { createRoute, redirect } from '@tanstack/react-router'; import { rootRoute } from '../root'; import { FirewallsRoute } from './FirewallsRoute'; +import type { TableSearchParams } from '../types'; +import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; + +export interface FirewallsSearchParams extends TableSearchParams { + type?: LinodeCreateType; +} + const firewallsRoute = createRoute({ component: FirewallsRoute, getParentRoute: () => rootRoute, @@ -13,65 +20,141 @@ const firewallsIndexRoute = createRoute({ getParentRoute: () => firewallsRoute, path: '/', }).lazy(() => - import('src/features/Firewalls/FirewallLanding/FirewallLanding').then( - (m) => m.firewallLandingLazyRoute - ) + import('./firewallLazyRoutes').then((m) => m.firewallLandingLazyRoute) ); const firewallCreateRoute = createRoute({ getParentRoute: () => firewallsRoute, path: 'create', + validateSearch: (search: FirewallsSearchParams) => search, }).lazy(() => - import('src/features/Firewalls/FirewallLanding/FirewallLanding').then( - (m) => m.firewallLandingLazyRoute - ) + import('./firewallLazyRoutes').then((m) => m.firewallLandingLazyRoute) ); const firewallDetailRoute = createRoute({ + beforeLoad: async ({ params }) => { + throw redirect({ + params: { id: String(params.id) }, + to: '/firewalls/$id/rules', + }); + }, getParentRoute: () => firewallsRoute, - parseParams: (params) => ({ - id: Number(params.id), - }), + params: { + parse: ({ id }: { id: string }) => ({ + id: Number(id), + }), + stringify: ({ id }: { id: number }) => ({ + id: String(id), + }), + }, path: '$id', + validateSearch: (search: { tab?: string }) => search, }).lazy(() => - import('src/features/Firewalls/FirewallDetail/index').then( - (m) => m.firewallDetailLazyRoute - ) + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) ); const firewallDetailRulesRoute = createRoute({ getParentRoute: () => firewallsRoute, path: '$id/rules', }).lazy(() => - import('src/features/Firewalls/FirewallDetail/index').then( - (m) => m.firewallDetailLazyRoute - ) + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailRulesAddRuleRoute = createRoute({ + getParentRoute: () => firewallDetailRulesRoute, + path: 'add', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailRulesEditInboundRuleRoute = createRoute({ + getParentRoute: () => firewallDetailRulesRoute, + path: 'edit/inbound/$ruleId', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailRulesEditOutboundRuleRoute = createRoute({ + getParentRoute: () => firewallDetailRulesRoute, + path: 'edit/outbound/$ruleId', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailRulesAddInboundRuleRoute = createRoute({ + getParentRoute: () => firewallDetailRulesAddRuleRoute, + path: 'inbound', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailRulesAddOutboundRuleRoute = createRoute({ + getParentRoute: () => firewallDetailRulesAddRuleRoute, + path: 'outbound', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) ); const firewallDetailLinodesRoute = createRoute({ getParentRoute: () => firewallsRoute, path: '$id/linodes', }).lazy(() => - import('src/features/Firewalls/FirewallDetail/index').then( - (m) => m.firewallDetailLazyRoute - ) + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailLinodesAddLinodeRoute = createRoute({ + getParentRoute: () => firewallDetailLinodesRoute, + path: 'add', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailLinodesRemoveLinodeRoute = createRoute({ + getParentRoute: () => firewallDetailLinodesRoute, + path: 'remove', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) ); const firewallDetailNodebalancersRoute = createRoute({ getParentRoute: () => firewallsRoute, path: '$id/nodebalancers', }).lazy(() => - import('src/features/Firewalls/FirewallDetail/index').then( - (m) => m.firewallDetailLazyRoute - ) + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailNodebalancersAddNodebalancerRoute = createRoute({ + getParentRoute: () => firewallDetailNodebalancersRoute, + path: 'add', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) +); + +const firewallDetailNodebalancersRemoveNodebalancerRoute = createRoute({ + getParentRoute: () => firewallDetailNodebalancersRoute, + path: 'remove', +}).lazy(() => + import('./firewallLazyRoutes').then((m) => m.firewallDetailLazyRoute) ); export const firewallsRouteTree = firewallsRoute.addChildren([ firewallsIndexRoute, firewallDetailRoute.addChildren([ - firewallDetailLinodesRoute, - firewallDetailRulesRoute, - firewallDetailNodebalancersRoute, + firewallDetailLinodesRoute.addChildren([ + firewallDetailLinodesAddLinodeRoute, + firewallDetailLinodesRemoveLinodeRoute, + ]), + firewallDetailRulesRoute.addChildren([ + firewallDetailRulesAddRuleRoute, + firewallDetailRulesEditInboundRuleRoute, + firewallDetailRulesEditOutboundRuleRoute, + firewallDetailRulesAddInboundRuleRoute, + firewallDetailRulesAddOutboundRuleRoute, + ]), + firewallDetailNodebalancersRoute.addChildren([ + firewallDetailNodebalancersAddNodebalancerRoute, + firewallDetailNodebalancersRemoveNodebalancerRoute, + ]), ]), firewallCreateRoute, ]); diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index c5d952cb5cb..1830aee95d1 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -87,6 +87,7 @@ declare module '@tanstack/react-router' { export const migrationRouteTree = migrationRootRoute.addChildren([ betaRouteTree, domainsRouteTree, + firewallsRouteTree, imagesRouteTree, longviewRouteTree, placementGroupsRouteTree,