From 18984b0d229ecbb7f83d84b213036333379a8b4d Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Mon, 6 Jan 2025 20:44:04 -0500 Subject: [PATCH] steps 6-7 ish --- .../PlacementGroupsCreateDrawer.tsx | 6 +- .../PlacementGroupsDetail.tsx | 4 +- .../PlacementGroupsLinodes.tsx | 30 +++--- .../PlacementGroupsLinodesTableRow.tsx | 6 +- .../PlacementGroupsEditDrawer.tsx | 8 +- .../PlacementGroupsLanding.tsx | 92 ++++++++++++++----- .../PlacementGroupsUnassignModal.tsx | 17 ++-- .../src/features/PlacementGroups/constants.ts | 6 ++ .../src/routes/placementGroups/index.ts | 61 ++++++++---- 9 files changed, 155 insertions(+), 75 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index ccd9a0058ca..5a440a3eebb 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -8,10 +8,10 @@ import { Typography, } from '@linode/ui'; import { createPlacementGroupSchema } from '@linode/validation'; +import { useLocation } from '@tanstack/react-router'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useLocation } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { DescriptionList } from 'src/components/DescriptionList/DescriptionList'; @@ -73,7 +73,9 @@ export const PlacementGroupsCreateDrawer = ( const location = useLocation(); const isFromLinodeCreate = location.pathname.includes('/linodes/create'); - const queryParams = getQueryParamsFromQueryString(location.search); + const queryParams = getQueryParamsFromQueryString( + location.pathname.split('?')[1] // todo connie - fix this to be more robust, but just getting rid of type errors for now + ); const handleRegionSelect = (region: Region['id']) => { setFieldValue('region', region); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx index 06535fa9eda..e59399b4e12 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx @@ -1,7 +1,7 @@ import { PLACEMENT_GROUP_TYPES } from '@linode/api-v4'; import { CircleProgress, Notice } from '@linode/ui'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams } from '@tanstack/react-router'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; @@ -22,7 +22,7 @@ import { PlacementGroupsLinodes } from './PlacementGroupsLinodes/PlacementGroups import { PlacementGroupsSummary } from './PlacementGroupsSummary/PlacementGroupsSummary'; export const PlacementGroupsDetail = () => { - const { id } = useParams<{ id: string }>(); + const { id } = useParams({ from: '/placement-groups/$id' }); const placementGroupId = +id; const { diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx index c89332c6e19..f2ef1a19687 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx @@ -1,7 +1,7 @@ import { Button, Stack } from '@linode/ui'; import Grid from '@mui/material/Unstable_Grid2/Grid2'; +import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; @@ -33,7 +33,8 @@ export const PlacementGroupsLinodes = (props: Props) => { placementGroup, region, } = props; - const history = useHistory(); + const navigate = useNavigate(); + const location = useLocation(); const [searchText, setSearchText] = React.useState(''); const [selectedLinode, setSelectedLinode] = React.useState< Linode | undefined @@ -63,24 +64,27 @@ export const PlacementGroupsLinodes = (props: Props) => { }); const handleAssignLinodesDrawer = () => { - history.replace(`/placement-groups/${placementGroup.id}/linodes/assign`); + navigate({ + params: { id: placementGroup.id }, + to: '/placement-groups/$id/linodes/assign', + }); }; const handleUnassignLinodeModal = (linode: Linode) => { setSelectedLinode(linode); - history.replace( - `/placement-groups/${placementGroup.id}/linodes/unassign/${linode.id}` - ); + navigate({ + params: { id: placementGroup.id, linodeId: linode.id }, + to: '/placement-groups/$id/linodes/unassign/$linodeId', + }); }; const handleCloseDrawer = () => { setSelectedLinode(undefined); - history.replace(`/placement-groups/${placementGroup.id}/linodes`); + navigate({ + params: { id: placementGroup.id }, + to: '/placement-groups/$id', + }); }; - const isAssignLinodesDrawerOpen = history.location.pathname.includes( - '/assign' - ); - const isUnassignLinodesDrawerOpen = history.location.pathname.includes( - '/unassign' - ); + const isAssignLinodesDrawerOpen = location.pathname.includes('/assign'); + const isUnassignLinodesDrawerOpen = location.pathname.includes('/unassign'); return ( diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx index 5b53be099bc..6aafac59542 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx @@ -1,5 +1,5 @@ +import { useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; import { Link } from 'src/components/Link'; @@ -36,7 +36,9 @@ type MigrationType = 'inbound' | 'outbound' | null; export const PlacementGroupsLinodesTableRow = React.memo((props: Props) => { const { handleUnassignLinodeModal, linode } = props; const { label, status } = linode; - const { id: placementGroupId } = useParams<{ id: string }>(); + const { id: placementGroupId } = useParams({ + from: '/placement-groups/$id', // todo connie - check about $id/linode + }); const notificationContext = React.useContext(notificationCenterContext); const isLinodeMigrating = Boolean(linode.placement_group?.migrating_to); const { data: placementGroup } = usePlacementGroupQuery( diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx index 728ae6759e9..9e142f92247 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx @@ -7,7 +7,7 @@ import { updatePlacementGroupSchema } from '@linode/validation'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams } from '@tanstack/react-router'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { DescriptionList } from 'src/components/DescriptionList/DescriptionList'; @@ -36,13 +36,15 @@ export const PlacementGroupsEditDrawer = ( region, selectedPlacementGroup: placementGroupFromProps, } = props; - const { id } = useParams<{ id: string }>(); + // todo connie - try to consolidate params/remove params from drawers? + // figure out error with NaN + const params = useParams({ strict: false }); const { data: placementGroupFromParam, isFetching, status, } = usePlacementGroupQuery( - Number(id), + Number(params.id), open && placementGroupFromProps === undefined ); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 5628d7158db..ff26a01fdf9 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -1,8 +1,12 @@ import { CircleProgress } from '@linode/ui'; import { useMediaQuery, useTheme } from '@mui/material'; +import { + useLocation, + useNavigate, + useParams, + useSearch, +} from '@tanstack/react-router'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { useHistory } from 'react-router-dom'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; @@ -17,15 +21,21 @@ import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { useOrder } from 'src/hooks/useOrder'; -import { usePagination } from 'src/hooks/usePagination'; +import { useOrderV2 } from 'src/hooks/useOrderV2'; +import { usePaginationV2 } from 'src/hooks/usePaginationV2'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { usePlacementGroupsQuery } from 'src/queries/placementGroups'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { PLACEMENT_GROUPS_DOCS_LINK } from '../constants'; +import { + PG_LANDING_TABLE_DEFAULT_ORDER, + PG_LANDING_TABLE_DEFAULT_ORDER_BY, + PG_LANDING_TABLE_PREFERENCE_KEY, + PLACEMENT_GROUPS_DOCS_LINK, + PLACEMENT_GROUPS_LANDING_ROUTE, +} from '../constants'; import { PlacementGroupsCreateDrawer } from '../PlacementGroupsCreateDrawer'; import { PlacementGroupsDeleteModal } from '../PlacementGroupsDeleteModal'; import { PlacementGroupsEditDrawer } from '../PlacementGroupsEditDrawer'; @@ -34,23 +44,36 @@ import { PlacementGroupsLandingEmptyState } from './PlacementGroupsLandingEmptyS import { PlacementGroupsRow } from './PlacementGroupsRow'; import type { Filter, PlacementGroup } from '@linode/api-v4'; - -const preferenceKey = 'placement-groups'; +import type { PlacementGroupsSearchParams } from 'src/routes/placementGroups'; export const PlacementGroupsLanding = React.memo(() => { - const history = useHistory(); - const pagination = usePagination(1, preferenceKey); - const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const location = useLocation(); + const pagination = usePaginationV2({ + currentRoute: PLACEMENT_GROUPS_LANDING_ROUTE, + preferenceKey: PG_LANDING_TABLE_PREFERENCE_KEY, + searchParams: (prev) => ({ + ...prev, + query: search.query, + }), + }); + const params = useParams({ strict: false }); + const search: PlacementGroupsSearchParams = useSearch({ + from: PLACEMENT_GROUPS_LANDING_ROUTE, + }); + const { query } = search; const theme = useTheme(); - const [query, setQuery] = React.useState(''); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); - const { handleOrderChange, order, orderBy } = useOrder( - { - order: 'asc', - orderBy: 'label', + const { handleOrderChange, order, orderBy } = useOrderV2({ + initialRoute: { + defaultOrder: { + order: PG_LANDING_TABLE_DEFAULT_ORDER, + orderBy: PG_LANDING_TABLE_DEFAULT_ORDER_BY, + }, + from: PLACEMENT_GROUPS_LANDING_ROUTE, }, - `${preferenceKey}-order` - ); + preferenceKey: `${PG_LANDING_TABLE_PREFERENCE_KEY}-order`, + }); const filter: Filter = { ['+order']: order, @@ -72,7 +95,7 @@ export const PlacementGroupsLanding = React.memo(() => { ); const selectedPlacementGroup = placementGroups?.data.find( - (pg) => pg.id === Number(id) + (pg) => pg.id === Number(params.id) ); const allLinodeIDsAssigned = placementGroups?.data.reduce( @@ -103,25 +126,44 @@ export const PlacementGroupsLanding = React.memo(() => { }); const handleCreatePlacementGroup = () => { - history.push('/placement-groups/create'); + navigate({ search: (prev) => prev, to: '/placement-groups/create' }); }; const handleEditPlacementGroup = (placementGroup: PlacementGroup) => { - history.push(`/placement-groups/edit/${placementGroup.id}`); + navigate({ + params: { action: 'edit', id: placementGroup.id }, + search: (prev) => prev, + to: '/placement-groups/$action/$id', + }); }; const handleDeletePlacementGroup = (placementGroup: PlacementGroup) => { - history.push(`/placement-groups/delete/${placementGroup.id}`); + navigate({ + params: { action: 'delete', id: placementGroup.id }, + search: (prev) => prev, + to: '/placement-groups/$action/$id', + }); }; const onClosePlacementGroupDrawer = () => { - history.push('/placement-groups'); + navigate({ search: (prev) => prev, to: PLACEMENT_GROUPS_LANDING_ROUTE }); }; const isPlacementGroupCreateDrawerOpen = location.pathname.endsWith('create'); const isPlacementGroupDeleteModalOpen = location.pathname.includes('delete'); const isPlacementGroupEditDrawerOpen = location.pathname.includes('edit'); + const onSearch = (searchString: string) => { + navigate({ + search: (prev) => ({ + ...prev, + page: undefined, + query: searchString || undefined, + }), + to: PLACEMENT_GROUPS_LANDING_ROUTE, + }); + }; + if (placementGroupsLoading) { return ; } @@ -163,7 +205,7 @@ export const PlacementGroupsLanding = React.memo(() => { resourceType: 'Placement Groups', }), }} - breadcrumbProps={{ pathname: '/placement-groups' }} + breadcrumbProps={{ pathname: PLACEMENT_GROUPS_LANDING_ROUTE }} disabledCreateButton={isLinodeReadOnly} docsLink={PLACEMENT_GROUPS_DOCS_LINK} entity="Placement Group" @@ -176,10 +218,10 @@ export const PlacementGroupsLanding = React.memo(() => { hideLabel isSearching={isFetching} label="Search" - onSearch={setQuery} + onSearch={onSearch} placeholder="Search Placement Groups" sx={{ mb: 4 }} - value={query} + value={query ?? ''} /> diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx index e7c03e7bcd4..a0b59cce925 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx @@ -1,7 +1,7 @@ import { CircleProgress, Notice, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams } from '@tanstack/react-router'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; @@ -25,10 +25,9 @@ export const PlacementGroupsUnassignModal = (props: Props) => { const { onClose, open, selectedLinode } = props; const { enqueueSnackbar } = useSnackbar(); - const { id: placementGroupId, linodeId } = useParams<{ - id: string; - linodeId: string; - }>(); + const { id: placementGroupId, linodeId } = useParams({ + strict: false, + }); const [linode, setLinode] = React.useState( selectedLinode @@ -38,10 +37,12 @@ export const PlacementGroupsUnassignModal = (props: Props) => { error, isPending, mutateAsync: unassignLinodes, - } = useUnassignLinodesFromPlacementGroup(+placementGroupId); + } = useUnassignLinodesFromPlacementGroup( + placementGroupId ? +placementGroupId : -1 + ); const { data: linodeFromQuery, isFetching } = useLinodeQuery( - +linodeId, + linodeId ? +linodeId : -1, open && selectedLinode === undefined ); @@ -69,7 +70,7 @@ export const PlacementGroupsUnassignModal = (props: Props) => { const isLinodeReadOnly = useIsResourceRestricted({ grantLevel: 'read_write', grantType: 'linode', - id: +linodeId, + id: linodeId ? +linodeId : -1, }); const actions = ( diff --git a/packages/manager/src/features/PlacementGroups/constants.ts b/packages/manager/src/features/PlacementGroups/constants.ts index 506556f8120..2c73079f10f 100644 --- a/packages/manager/src/features/PlacementGroups/constants.ts +++ b/packages/manager/src/features/PlacementGroups/constants.ts @@ -45,3 +45,9 @@ export const PLACEMENT_GROUP_MIGRATION_INBOUND_MESSAGE = export const PLACEMENT_GROUP_MIGRATION_OUTBOUND_MESSAGE = 'This Linode is being migrated. It will be removed from this placement group after the migration completes.'; + +export const PLACEMENT_GROUPS_LANDING_ROUTE = '/placement-groups'; +// default order constants +export const PG_LANDING_TABLE_DEFAULT_ORDER = 'asc'; +export const PG_LANDING_TABLE_DEFAULT_ORDER_BY = 'label'; +export const PG_LANDING_TABLE_PREFERENCE_KEY = 'placement-groups'; diff --git a/packages/manager/src/routes/placementGroups/index.ts b/packages/manager/src/routes/placementGroups/index.ts index 95f62bc597f..2a2af0e1c5e 100644 --- a/packages/manager/src/routes/placementGroups/index.ts +++ b/packages/manager/src/routes/placementGroups/index.ts @@ -1,8 +1,21 @@ -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; + +export type PlacementGroupAction = typeof placementGroupAction[keyof typeof placementGroupAction]; + export const placementGroupsRoute = createRoute({ component: PlacementGroupsRoute, getParentRoute: () => rootRoute, @@ -12,6 +25,7 @@ export const placementGroupsRoute = createRoute({ const placementGroupsIndexRoute = createRoute({ getParentRoute: () => placementGroupsRoute, path: '/', + validateSearch: (search: PlacementGroupsSearchParams) => search, }).lazy(() => import('./placementGroupsLazyRoutes').then( (m) => m.placementGroupsLandingLazyRoute @@ -27,24 +41,33 @@ const placementGroupsCreateRoute = createRoute({ ) ); -const placementGroupsEditRoute = createRoute({ - getParentRoute: () => placementGroupsRoute, - parseParams: (params) => ({ - id: Number(params.id), - }), - path: 'edit/$id', -}).lazy(() => - import('./placementGroupsLazyRoutes').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('./placementGroupsLazyRoutes').then( (m) => m.placementGroupsLandingLazyRoute @@ -94,10 +117,8 @@ const placementGroupsUnassignRoute = createRoute({ ); export const placementGroupsRouteTree = placementGroupsRoute.addChildren([ - placementGroupsIndexRoute, + placementGroupsIndexRoute.addChildren([placementGroupActionRoute]), placementGroupsCreateRoute, - placementGroupsEditRoute, - placementGroupsDeleteRoute, placementGroupsDetailRoute.addChildren([ placementGroupsDetailLinodesRoute, placementGroupsAssignRoute,