- import('./PlacementGroupsLanding/PlacementGroupsLanding').then((module) => ({
- default: module.PlacementGroupsLanding,
- }))
-);
-
-const PlacementGroupsDetail = React.lazy(() =>
- import('./PlacementGroupsDetail/PlacementGroupsDetail').then((module) => ({
- default: module.PlacementGroupsDetail,
- }))
-);
-
-export const PlacementGroups = () => {
- const { path } = useRouteMatch();
-
- return (
- }>
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/packages/manager/src/features/PlacementGroups/types.ts b/packages/manager/src/features/PlacementGroups/types.ts
index 15e78d12e3b..dc264c25074 100644
--- a/packages/manager/src/features/PlacementGroups/types.ts
+++ b/packages/manager/src/features/PlacementGroups/types.ts
@@ -1,4 +1,4 @@
-import { PlacementGroup, Region } from '@linode/api-v4';
+import type { PlacementGroup, Region } from '@linode/api-v4';
export interface PlacementGroupsDrawerPropsBase {
onClose: () => void;
@@ -15,6 +15,7 @@ export interface PlacementGroupsCreateDrawerProps {
export interface PlacementGroupsEditDrawerProps {
disableEditButton: boolean;
+ isFetching: boolean;
onClose: PlacementGroupsDrawerPropsBase['onClose'];
onPlacementGroupEdit?: (placementGroup: PlacementGroup) => void;
open: PlacementGroupsDrawerPropsBase['open'];
diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx
index 9aa1261c7a6..10f2cecc5cf 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,
+ placementGroupsRouteTree,
volumesRouteTree,
]);
export type MigrationRouteTree = typeof migrationRouteTree;
diff --git a/packages/manager/src/routes/placementGroups/PlacementGroupsRoute.tsx b/packages/manager/src/routes/placementGroups/PlacementGroupsRoute.tsx
index cd6ab27ee14..588ded3f83a 100644
--- a/packages/manager/src/routes/placementGroups/PlacementGroupsRoute.tsx
+++ b/packages/manager/src/routes/placementGroups/PlacementGroupsRoute.tsx
@@ -2,15 +2,19 @@ import { Outlet } from '@tanstack/react-router';
import React from 'react';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
+import { NotFound } from 'src/components/NotFound';
import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner';
import { SuspenseLoader } from 'src/components/SuspenseLoader';
+import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils';
export const PlacementGroupsRoute = () => {
+ const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();
+
return (
}>
-
+ {isPlacementGroupsEnabled ? : }
);
};
diff --git a/packages/manager/src/routes/placementGroups/index.ts b/packages/manager/src/routes/placementGroups/index.ts
index 9088af62b15..d15820870c7 100644
--- a/packages/manager/src/routes/placementGroups/index.ts
+++ b/packages/manager/src/routes/placementGroups/index.ts
@@ -1,8 +1,27 @@
-import { createRoute } from '@tanstack/react-router';
+import { createRoute, redirect } from '@tanstack/react-router';
import { rootRoute } from '../root';
import { PlacementGroupsRoute } from './PlacementGroupsRoute';
+import type { TableSearchParams } from '../types';
+
+export interface PlacementGroupsSearchParams extends TableSearchParams {
+ query?: string;
+}
+
+const placementGroupAction = {
+ delete: 'delete',
+ edit: 'edit',
+} as const;
+
+const placementGroupLinodeAction = {
+ assign: 'assign',
+ unassign: 'unassign',
+} as const;
+
+export type PlacementGroupAction = typeof placementGroupAction[keyof typeof placementGroupAction];
+export type PlacementGroupLinodesAction = typeof placementGroupLinodeAction[keyof typeof placementGroupLinodeAction];
+
export const placementGroupsRoute = createRoute({
component: PlacementGroupsRoute,
getParentRoute: () => rootRoute,
@@ -12,43 +31,53 @@ export const placementGroupsRoute = createRoute({
const placementGroupsIndexRoute = createRoute({
getParentRoute: () => placementGroupsRoute,
path: '/',
+ validateSearch: (search: PlacementGroupsSearchParams) => search,
}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding'
- ).then((m) => m.placementGroupsLandingLazyRoute)
+ import('./placementGroupsLazyRoutes').then(
+ (m) => m.placementGroupsLandingLazyRoute
+ )
);
const placementGroupsCreateRoute = createRoute({
getParentRoute: () => placementGroupsRoute,
path: 'create',
}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding'
- ).then((m) => m.placementGroupsLandingLazyRoute)
+ import('./placementGroupsLazyRoutes').then(
+ (m) => m.placementGroupsLandingLazyRoute
+ )
);
-const placementGroupsEditRoute = createRoute({
- getParentRoute: () => placementGroupsRoute,
- parseParams: (params) => ({
- id: Number(params.id),
- }),
- path: 'edit/$id',
-}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding'
- ).then((m) => m.placementGroupsLandingLazyRoute)
-);
+type PlacementGroupActionRouteParams = {
+ action: PlacementGroupAction;
+ id: P;
+};
-const placementGroupsDeleteRoute = createRoute({
+const placementGroupActionRoute = createRoute({
+ beforeLoad: async ({ params }) => {
+ if (!(params.action in placementGroupAction)) {
+ throw redirect({
+ search: () => ({}),
+ to: '/placement-groups',
+ });
+ }
+ },
getParentRoute: () => placementGroupsRoute,
- parseParams: (params) => ({
- id: Number(params.id),
- }),
- path: 'delete/$id',
+ params: {
+ parse: ({ action, id }: PlacementGroupActionRouteParams) => ({
+ action,
+ id: Number(id),
+ }),
+ stringify: ({ action, id }: PlacementGroupActionRouteParams) => ({
+ action,
+ id: String(id),
+ }),
+ },
+ path: '$action/$id',
+ validateSearch: (search: PlacementGroupsSearchParams) => search,
}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding'
- ).then((m) => m.placementGroupsLandingLazyRoute)
+ import('./placementGroupsLazyRoutes').then(
+ (m) => m.placementGroupsLandingLazyRoute
+ )
);
const placementGroupsDetailRoute = createRoute({
@@ -58,49 +87,61 @@ const placementGroupsDetailRoute = createRoute({
}),
path: '$id',
}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail'
- ).then((m) => m.placementGroupsDetailLazyRoute)
+ import('./placementGroupsLazyRoutes').then(
+ (m) => m.placementGroupsDetailLazyRoute
+ )
);
-const placementGroupsDetailLinodesRoute = createRoute({
- getParentRoute: () => placementGroupsDetailRoute,
- path: 'linodes',
-}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail'
- ).then((m) => m.placementGroupsDetailLazyRoute)
-);
+type PlacementGroupLinodesActionRouteParams = {
+ action: PlacementGroupLinodesAction;
+};
-const placementGroupsAssignRoute = createRoute({
- getParentRoute: () => placementGroupsDetailLinodesRoute,
- path: 'assign',
+const placementGroupLinodesActionBaseRoute = createRoute({
+ beforeLoad: async ({ params }) => {
+ if (!(params.action in placementGroupLinodeAction)) {
+ throw redirect({
+ search: () => ({}),
+ to: `/placement-groups/${params.id}`,
+ });
+ }
+ },
+ getParentRoute: () => placementGroupsDetailRoute,
+ params: {
+ parse: ({ action }: PlacementGroupLinodesActionRouteParams) => ({
+ action,
+ }),
+ stringify: ({
+ action,
+ }: PlacementGroupLinodesActionRouteParams) => ({
+ action,
+ }),
+ },
+ path: 'linodes/$action',
+ validateSearch: (search: PlacementGroupsSearchParams) => search,
}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail'
- ).then((m) => m.placementGroupsUnassignLazyRoute)
+ import('./placementGroupsLazyRoutes').then(
+ (m) => m.placementGroupsDetailLazyRoute
+ )
);
const placementGroupsUnassignRoute = createRoute({
- getParentRoute: () => placementGroupsDetailLinodesRoute,
+ getParentRoute: () => placementGroupLinodesActionBaseRoute,
parseParams: (params) => ({
linodeId: Number(params.linodeId),
}),
- path: 'unassign/$linodeId',
+ path: '$linodeId',
}).lazy(() =>
- import(
- 'src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail'
- ).then((m) => m.placementGroupsUnassignLazyRoute)
+ import('./placementGroupsLazyRoutes').then(
+ (m) => m.placementGroupsDetailLazyRoute
+ )
);
export const placementGroupsRouteTree = placementGroupsRoute.addChildren([
- placementGroupsIndexRoute,
+ placementGroupsIndexRoute.addChildren([placementGroupActionRoute]),
placementGroupsCreateRoute,
- placementGroupsEditRoute,
- placementGroupsDeleteRoute,
placementGroupsDetailRoute.addChildren([
- placementGroupsDetailLinodesRoute,
- placementGroupsAssignRoute,
- placementGroupsUnassignRoute,
+ placementGroupLinodesActionBaseRoute.addChildren([
+ placementGroupsUnassignRoute,
+ ]),
]),
]);
diff --git a/packages/manager/src/routes/placementGroups/placementGroupsLazyRoutes.ts b/packages/manager/src/routes/placementGroups/placementGroupsLazyRoutes.ts
new file mode 100644
index 00000000000..a49a13a3d87
--- /dev/null
+++ b/packages/manager/src/routes/placementGroups/placementGroupsLazyRoutes.ts
@@ -0,0 +1,16 @@
+import { createLazyRoute } from '@tanstack/react-router';
+
+import { PlacementGroupsDetail } from 'src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail';
+import { PlacementGroupsLanding } from 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding';
+
+export const placementGroupsDetailLazyRoute = createLazyRoute(
+ '/placement-groups/$id'
+)({
+ component: PlacementGroupsDetail,
+});
+
+export const placementGroupsLandingLazyRoute = createLazyRoute(
+ '/placement-groups'
+)({
+ component: PlacementGroupsLanding,
+});
diff --git a/packages/manager/src/routes/routes.test.tsx b/packages/manager/src/routes/routes.test.tsx
deleted file mode 100644
index b760499ec50..00000000000
--- a/packages/manager/src/routes/routes.test.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { RouterProvider } from '@tanstack/react-router';
-import { screen, waitFor } from '@testing-library/react';
-import React from 'react';
-
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { migrationRouter } from './index';
-import { getAllRoutePaths } from './utils/allPaths';
-
-import type { useQuery } from '@tanstack/react-query';
-// TODO: Tanstack Router - replace AnyRouter once migration is complete.
-import type { AnyRouter } from '@tanstack/react-router';
-
-vi.mock('@tanstack/react-query', async () => {
- const actual = await vi.importActual('@tanstack/react-query');
- return {
- ...actual,
- useQuery: vi
- .fn()
- .mockImplementation((...args: Parameters) => {
- const actualResult = (actual.useQuery as typeof useQuery)(...args);
- return {
- ...actualResult,
- isLoading: false,
- };
- }),
- };
-});
-
-const allMigrationPaths = getAllRoutePaths(migrationRouter);
-
-describe('Migration Router', () => {
- const renderWithRouter = (initialEntry: string) => {
- migrationRouter.invalidate();
- migrationRouter.navigate({ replace: true, to: initialEntry });
-
- return renderWithTheme(
- ,
- {
- flags: {
- selfServeBetas: true,
- },
- }
- );
- };
-
- /**
- * This test is meant to incrementally test all routes being added to the migration router.
- * It will hopefully catch any issues with routes not being added or set up correctly:
- * - Route is not found in the router
- * - Route is found in the router but the component is not rendered
- * - Route is found in the router and the component is rendered but missing a heading (which should be a requirement for all routes)
- */
- test.each(allMigrationPaths)('route: %s', async (path) => {
- renderWithRouter(path);
-
- await waitFor(
- async () => {
- const migrationRouter = screen.getByTestId('migration-router');
- const h1 = screen.getByRole('heading', { level: 1 });
- expect(migrationRouter).toBeInTheDocument();
- expect(h1).toBeInTheDocument();
- expect(h1).not.toHaveTextContent('Not Found');
- },
- {
- timeout: 5000,
- }
- );
- });
-
- it('should render the NotFound component for broken routes', async () => {
- renderWithRouter('/broken-route');
-
- await waitFor(
- async () => {
- const migrationRouter = screen.getByTestId('migration-router');
- const h1 = screen.getByRole('heading', { level: 1 });
- expect(migrationRouter).toBeInTheDocument();
- expect(h1).toBeInTheDocument();
- expect(h1).toHaveTextContent('Not Found');
- },
- {
- timeout: 5000,
- }
- );
- });
-});
diff --git a/packages/manager/src/utilities/testHelpers.tsx b/packages/manager/src/utilities/testHelpers.tsx
index 0ae3424c2bf..43918a2acbc 100644
--- a/packages/manager/src/utilities/testHelpers.tsx
+++ b/packages/manager/src/utilities/testHelpers.tsx
@@ -18,6 +18,7 @@ import { Provider } from 'react-redux';
import { MemoryRouter, Route } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import { BrowserRouter } from 'react-router-dom';
import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper';
import { queryClientFactory } from 'src/queries/base';
@@ -206,7 +207,9 @@ export const wrapWithThemeAndRouter = (
options={{ bootstrap: options.flags }}
>
-
+
+
+