From ac15267b6e0f93de7a43f842ca75987dda151933 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:08:39 -0500 Subject: [PATCH] refactor: [M3-8783] - Migrate /volumes to Tanstack Router (#11154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit: wire new router * Migrate test to use new helper * volumes landing routing * doin the filtering * wire actions * cleanup * cleanup * moar cleanup * more work on params * usePaginationV2 * cleanup * oops fix types * fix e2e * useOrderV2 * and... dont forget the util 🤦 * revert unwarranted change * useOrderV2 test * console error * A bit more cleanup * usePaginationV2 test * usePaginationV2 test * route level error handling * xFilter util * xFilter util improvements * post rebase fix * testing xQuery builder * moar testing and cleanup * Added changeset: Migrate `/volumes` to Tanstack router * Save progress * fix one test * fix remaining test * feedback @jaalah-akamai * more work on preloading * save progress * Walking back and using a more progressive approach * cleanup * entity not found logic * post rebase fix * post rebase fix * update loading state * fix the smoke tests * Feedback @bnussman-akamai * JsDocs for Tanstack link components * Improve new useOrder hook * feedback @coliu-akamai @hkhalil-akamai --- .../pr-11154-tech-stories-1730404184309.md | 5 + packages/manager/.eslintrc.cjs | 6 +- packages/manager/.storybook/preview.tsx | 13 +- .../core/volumes/create-volume.smoke.spec.ts | 7 +- .../e2e/core/volumes/upgrade-volume.spec.ts | 9 +- packages/manager/src/MainContent.tsx | 6 +- packages/manager/src/Router.tsx | 2 + .../src/components/TanstackLink.stories.tsx | 38 +++ .../manager/src/components/TanstackLinks.tsx | 83 ++++++ .../TypeToConfirmDialog.tsx | 2 + .../Volumes/AttachVolumeDrawer.test.tsx | 8 +- .../features/Volumes/AttachVolumeDrawer.tsx | 4 +- .../Volumes/CloneVolumeDrawer.test.tsx | 8 +- .../features/Volumes/CloneVolumeDrawer.tsx | 10 +- .../features/Volumes/DeleteVolumeDialog.tsx | 4 +- .../features/Volumes/DetachVolumeDialog.tsx | 4 +- .../Volumes/EditVolumeDrawer.test.tsx | 8 +- .../src/features/Volumes/EditVolumeDrawer.tsx | 12 +- .../features/Volumes/ResizeVolumeDrawer.tsx | 10 +- .../features/Volumes/UpgradeVolumeDialog.tsx | 4 +- .../features/Volumes/VolumeCreate.test.tsx | 12 +- .../src/features/Volumes/VolumeCreate.tsx | 20 +- .../features/Volumes/VolumeDetailsDrawer.tsx | 10 +- .../LinodeVolumeAddDrawer.test.tsx | 4 +- .../Volumes/VolumeDrawer/SizeField.tsx | 2 +- .../features/Volumes/VolumeTableRow.test.tsx | 23 +- .../src/features/Volumes/VolumeTableRow.tsx | 1 + .../manager/src/features/Volumes/Volumes.tsx | 29 -- .../Volumes/VolumesActionMenu.test.tsx | 22 +- .../src/features/Volumes/VolumesLanding.tsx | 224 +++++++++------ .../Volumes/VolumesLandingEmptyState.test.tsx | 6 +- .../Volumes/VolumesLandingEmptyState.tsx | 6 +- .../Volumes/VolumesLandingEmptyStateData.ts | 3 +- .../manager/src/features/Volumes/constants.ts | 1 + .../manager/src/features/Volumes/index.tsx | 2 - packages/manager/src/hooks/useDialogData.ts | 100 +++++++ packages/manager/src/hooks/useOrder.ts | 2 +- .../manager/src/hooks/useOrderV2.test.tsx | 155 +++++++++++ packages/manager/src/hooks/useOrderV2.ts | 120 ++++++++ .../manager/src/hooks/usePaginationV2.test.ts | 258 ++++++++++++++++++ packages/manager/src/hooks/usePaginationV2.ts | 125 +++++++++ .../manager/src/mocks/utilities/response.ts | 2 +- packages/manager/src/routes/index.tsx | 13 +- packages/manager/src/routes/types.ts | 9 + .../{VolumesRoute.tsx => VolumesRoot.tsx} | 2 +- .../manager/src/routes/volumes/constants.ts | 3 + packages/manager/src/routes/volumes/index.ts | 77 +++++- .../src/routes/volumes/volumesLazyRoutes.ts | 12 + .../manager/src/types/ManagerPreferences.ts | 4 + 49 files changed, 1284 insertions(+), 206 deletions(-) create mode 100644 packages/manager/.changeset/pr-11154-tech-stories-1730404184309.md create mode 100644 packages/manager/src/components/TanstackLink.stories.tsx create mode 100644 packages/manager/src/components/TanstackLinks.tsx delete mode 100644 packages/manager/src/features/Volumes/Volumes.tsx create mode 100644 packages/manager/src/features/Volumes/constants.ts delete mode 100644 packages/manager/src/features/Volumes/index.tsx create mode 100644 packages/manager/src/hooks/useDialogData.ts create mode 100644 packages/manager/src/hooks/useOrderV2.test.tsx create mode 100644 packages/manager/src/hooks/useOrderV2.ts create mode 100644 packages/manager/src/hooks/usePaginationV2.test.ts create mode 100644 packages/manager/src/hooks/usePaginationV2.ts rename packages/manager/src/routes/volumes/{VolumesRoute.tsx => VolumesRoot.tsx} (92%) create mode 100644 packages/manager/src/routes/volumes/constants.ts create mode 100644 packages/manager/src/routes/volumes/volumesLazyRoutes.ts diff --git a/packages/manager/.changeset/pr-11154-tech-stories-1730404184309.md b/packages/manager/.changeset/pr-11154-tech-stories-1730404184309.md new file mode 100644 index 00000000000..f1920ce7895 --- /dev/null +++ b/packages/manager/.changeset/pr-11154-tech-stories-1730404184309.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Migrate `/volumes` to Tanstack router ([#11154](https://github.com/linode/manager/pull/11154)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 99870119ff7..3e3b3de5a66 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -89,10 +89,14 @@ module.exports = { { files: [ // for each new features added to the migration router, add its directory here - 'src/features/Betas/*', + 'src/features/Betas/**/*', + 'src/features/Volumes/**/*', ], rules: { 'no-restricted-imports': [ + // This needs to remain an error however trying to link to a feature that is not yet migrated will break the router + // For those cases react-router-dom history.push is still needed + // using `eslint-disable-next-line no-restricted-imports` can help bypass those imports 'error', { paths: [ diff --git a/packages/manager/.storybook/preview.tsx b/packages/manager/.storybook/preview.tsx index 55197641f90..6de6f42f5ce 100644 --- a/packages/manager/.storybook/preview.tsx +++ b/packages/manager/.storybook/preview.tsx @@ -9,7 +9,10 @@ import { Controls, Stories, } from '@storybook/blocks'; -import { wrapWithTheme } from '../src/utilities/testHelpers'; +import { + wrapWithTheme, + wrapWithThemeAndRouter, +} from '../src/utilities/testHelpers'; import { useDarkMode } from 'storybook-dark-mode'; import { DocsContainer as BaseContainer } from '@storybook/addon-docs'; import { themes } from '@storybook/theming'; @@ -42,9 +45,13 @@ export const DocsContainer = ({ children, context }) => { const preview: Preview = { decorators: [ - (Story) => { + (Story, context) => { const isDark = useDarkMode(); - return wrapWithTheme(, { theme: isDark ? 'dark' : 'light' }); + return context.parameters.tanStackRouter + ? wrapWithThemeAndRouter(, { + theme: isDark ? 'dark' : 'light', + }) + : wrapWithTheme(, { theme: isDark ? 'dark' : 'light' }); }, ], loaders: [ diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts index 3c645ace145..0ca5c8ce4e2 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts @@ -11,6 +11,7 @@ import { } from 'support/intercepts/linodes'; import { mockCreateVolume, + mockGetVolume, mockGetVolumes, mockDetachVolume, mockGetVolumeTypesError, @@ -85,6 +86,7 @@ describe('volumes', () => { mockGetVolumes([]).as('getVolumes'); mockCreateVolume(mockVolume).as('createVolume'); + mockGetVolume(mockVolume).as('getVolume'); mockGetVolumeTypes(mockVolumeTypes).as('getVolumeTypes'); cy.visitWithLogin('/volumes', { @@ -114,7 +116,7 @@ describe('volumes', () => { mockGetVolumes([mockVolume]).as('getVolumes'); ui.button.findByTitle('Create Volume').should('be.visible').click(); - cy.wait(['@createVolume', '@getVolumes']); + cy.wait(['@createVolume', '@getVolume', '@getVolumes']); validateBasicVolume(mockVolume.label); ui.actionMenu @@ -193,6 +195,7 @@ describe('volumes', () => { mockDetachVolume(mockAttachedVolume.id).as('detachVolume'); mockGetVolumes([mockAttachedVolume]).as('getAttachedVolumes'); + mockGetVolume(mockAttachedVolume).as('getVolume'); cy.visitWithLogin('/volumes', { preferenceOverrides, localStorageOverrides, @@ -209,6 +212,8 @@ describe('volumes', () => { ui.actionMenuItem.findByTitle('Detach').click(); + cy.wait('@getVolume'); + ui.dialog .findByTitle(`Detach Volume ${mockAttachedVolume.label}?`) .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts index f6d807e4c15..27f5e379b84 100644 --- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -10,7 +10,11 @@ import { mockGetLinodeDisks, mockGetLinodeVolumes, } from 'support/intercepts/linodes'; -import { mockMigrateVolumes, mockGetVolumes } from 'support/intercepts/volumes'; +import { + mockMigrateVolumes, + mockGetVolumes, + mockGetVolume, +} from 'support/intercepts/volumes'; import { ui } from 'support/ui'; describe('volume upgrade/migration', () => { @@ -23,6 +27,7 @@ describe('volume upgrade/migration', () => { }); mockGetVolumes([volume]).as('getVolumes'); + mockGetVolume(volume).as('getVolume'); mockMigrateVolumes().as('migrateVolumes'); mockGetNotifications([migrationScheduledNotification]).as( 'getNotifications' @@ -53,7 +58,7 @@ describe('volume upgrade/migration', () => { .click(); }); - cy.wait(['@migrateVolumes', '@getNotifications']); + cy.wait(['@migrateVolumes', '@getVolume', '@getNotifications']); cy.findByText('UPGRADE PENDING').should('be.visible'); diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index e0bfc2f2af3..82a2b3eb6d5 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -1,5 +1,6 @@ import { Box } from '@linode/ui'; import Grid from '@mui/material/Unstable_Grid2'; +import { useQueryClient } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; @@ -130,7 +131,6 @@ const LinodesRoutes = React.lazy(() => default: module.LinodesRoutes, })) ); -const Volumes = React.lazy(() => import('src/features/Volumes')); const Domains = React.lazy(() => import('src/features/Domains').then((module) => ({ default: module.DomainsRoutes, @@ -207,6 +207,7 @@ export const MainContent = () => { const { classes, cx } = useStyles(); const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); + const queryClient = useQueryClient(); const globalErrors = useGlobalErrors(); @@ -336,8 +337,6 @@ export const MainContent = () => { path="/placement-groups" /> )} - - { */} diff --git a/packages/manager/src/Router.tsx b/packages/manager/src/Router.tsx index 7d89f4fa297..a8e12fd54f3 100644 --- a/packages/manager/src/Router.tsx +++ b/packages/manager/src/Router.tsx @@ -1,3 +1,4 @@ +import { QueryClient } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import * as React from 'react'; @@ -24,6 +25,7 @@ export const Router = () => { isACLPEnabled, isDatabasesEnabled, isPlacementGroupsEnabled, + queryClient: new QueryClient(), }, }); diff --git a/packages/manager/src/components/TanstackLink.stories.tsx b/packages/manager/src/components/TanstackLink.stories.tsx new file mode 100644 index 00000000000..badc132c03f --- /dev/null +++ b/packages/manager/src/components/TanstackLink.stories.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { TanstackLink } from './TanstackLinks'; + +import type { TanstackLinkComponentProps } from './TanstackLinks'; +import type { Meta, StoryObj } from '@storybook/react'; + +export const AsButtonPrimary: StoryObj = { + render: () => ( + + Home + + ), +}; + +export const AsButtonSecondary: StoryObj = { + render: () => ( + + Home + + ), +}; + +export const AsLink: StoryObj = { + render: () => ( + + Home + + ), +}; + +const meta: Meta = { + parameters: { + tanStackRouter: true, + }, + title: 'Foundations/Link/Tanstack Link', +}; +export default meta; diff --git a/packages/manager/src/components/TanstackLinks.tsx b/packages/manager/src/components/TanstackLinks.tsx new file mode 100644 index 00000000000..bea4ddd332d --- /dev/null +++ b/packages/manager/src/components/TanstackLinks.tsx @@ -0,0 +1,83 @@ +import { Button } from '@linode/ui'; +import { omitProps } from '@linode/ui'; +import { LinkComponent } from '@tanstack/react-router'; +import { createLink } from '@tanstack/react-router'; +import * as React from 'react'; + +import { MenuItem } from 'src/components/MenuItem'; + +import type { ButtonProps, ButtonType } from '@linode/ui'; +import type { LinkProps as TanStackLinkProps } from '@tanstack/react-router'; + +export interface TanstackLinkComponentProps + extends Omit { + linkType: 'link' | ButtonType; + tooltipAnalyticsEvent?: (() => void) | undefined; + tooltipText?: string; +} + +export interface TanStackLinkRoutingProps { + linkType: TanstackLinkComponentProps['linkType']; + params?: TanStackLinkProps['params']; + preload?: TanStackLinkProps['preload']; + search?: TanStackLinkProps['search']; + to: TanStackLinkProps['to']; +} + +const LinkComponent = React.forwardRef< + HTMLButtonElement, + TanstackLinkComponentProps +>((props, ref) => { + const _props = omitProps(props, ['linkType']); + + return