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,