diff --git a/packages/api-v4/.changeset/pr-10639-removed-1727977150621.md b/packages/api-v4/.changeset/pr-10639-removed-1727977150621.md new file mode 100644 index 00000000000..eff1567b660 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10639-removed-1727977150621.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Removed +--- + +`edge` type reference in `LinodeTypeClass` and `RegionSite` ([#10639](https://github.com/linode/manager/pull/10639)) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index aa74b71ac70..1bca1ac4c64 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -337,8 +337,7 @@ export type LinodeTypeClass = | 'gpu' | 'metal' | 'prodedicated' - | 'premium' - | 'edge'; + | 'premium'; export interface IPAllocationRequest { type: 'ipv4'; diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index 46fcdf2278f..4d6b16a6205 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -28,7 +28,7 @@ export interface DNSResolvers { export type RegionStatus = 'ok' | 'outage'; -export type RegionSite = 'core' | 'distributed' | 'edge'; +export type RegionSite = 'core' | 'distributed'; export interface Region { id: string; diff --git a/packages/manager/.changeset/pr-10639-upcoming-features-1721164284962.md b/packages/manager/.changeset/pr-10639-upcoming-features-1721164284962.md new file mode 100644 index 00000000000..2cbe85f2962 --- /dev/null +++ b/packages/manager/.changeset/pr-10639-upcoming-features-1721164284962.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Region filtering to Linodes landing table ([#10639](https://github.com/linode/manager/pull/10639)) diff --git a/packages/manager/cypress/e2e/core/images/create-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-image.spec.ts index 8ea89934bab..2b9c9d017d4 100644 --- a/packages/manager/cypress/e2e/core/images/create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-image.spec.ts @@ -22,9 +22,9 @@ const mockRegions: Region[] = [ }), regionFactory.build({ capabilities: ['Linodes', 'Disk Encryption'], - id: 'us-den-edge-1', - label: 'Edge - Denver, CO', - site_type: 'edge', + id: 'us-den-1', + label: 'Distributed - Denver, CO', + site_type: 'distributed', }), ]; @@ -34,7 +34,7 @@ const mockLinodes: Linode[] = [ region: mockRegions[0].id, }), linodeFactory.build({ - label: 'edge-region-linode', + label: 'distributed-region-linode', region: mockRegions[1].id, }), ]; @@ -124,7 +124,7 @@ describe('create image (e2e)', () => { }); }); - it('displays notice informing user that Images are not encrypted, provided the LDE feature is enabled and the selected linode is not in an Edge region', () => { + it('displays notice informing user that Images are not encrypted, provided the LDE feature is enabled and the selected linode is not in a distributed region', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ linodeDiskEncryption: true, @@ -198,7 +198,7 @@ describe('create image (e2e)', () => { cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('not.exist'); }); - it('does not display a notice informing user that Images are not encrypted if the selected linode is in an Edge region', () => { + it('does not display a notice informing user that Images are not encrypted if the selected linode is in a distributed region', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ linodeDiskEncryption: true, diff --git a/packages/manager/src/__data__/distributedRegionsData.ts b/packages/manager/src/__data__/distributedRegionsData.ts index 051230aae49..349695ef995 100644 --- a/packages/manager/src/__data__/distributedRegionsData.ts +++ b/packages/manager/src/__data__/distributedRegionsData.ts @@ -9,8 +9,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'us', - id: 'us-den-edge-1', - label: 'Edge - Denver, CO', + id: 'us-den-1', + label: 'Distributed - Denver, CO', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -30,8 +30,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'de', - id: 'de-ham-edge-1', - label: 'Edge - Hamburg, DE', + id: 'de-ham-1', + label: 'Distributed - Hamburg, DE', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -51,8 +51,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'fr', - id: 'fr-mrs-edge-1', - label: 'Edge - Marseille, FR', + id: 'fr-mrs-1', + label: 'Distributed - Marseille, FR', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -72,8 +72,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'za', - id: 'za-jnb-edge-1', - label: 'Edge - Johannesburg, ZA\t', + id: 'za-jnb-1', + label: 'Distributed - Johannesburg, ZA\t', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -93,8 +93,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'my', - id: 'my-kul-edge-1', - label: 'Edge - Kuala Lumpur, MY', + id: 'my-kul-1', + label: 'Distributed - Kuala Lumpur, MY', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -114,8 +114,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'co', - id: 'co-bog-edge-1', - label: 'Edge - Bogotá, CO', + id: 'co-bog-1', + label: 'Distributed - Bogotá, CO', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -135,8 +135,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'mx', - id: 'mx-qro-edge-1', - label: 'Edge - Querétaro, MX', + id: 'mx-qro-1', + label: 'Distributed - Querétaro, MX', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -156,8 +156,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'us', - id: 'us-hou-edge-1', - label: 'Edge - Houston, TX', + id: 'us-hou-1', + label: 'Distributed - Houston, TX', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, @@ -177,8 +177,8 @@ export const distributedRegions: Region[] = [ 'Placement Group', ], country: 'cl', - id: 'cl-scl-edge-1', - label: 'Edge - Santiago, CL', + id: 'cl-scl-1', + label: 'Distributed - Santiago, CL', placement_group_limits: { maximum_linodes_per_pg: 5, maximum_pgs_per_customer: null, diff --git a/packages/manager/src/__data__/regionsData.ts b/packages/manager/src/__data__/regionsData.ts index 0a3ab6eaf2e..eb104775dc0 100644 --- a/packages/manager/src/__data__/regionsData.ts +++ b/packages/manager/src/__data__/regionsData.ts @@ -681,7 +681,7 @@ export const regions: Region[] = [ status: 'ok', }, { - capabilities: ['Linodes'], + capabilities: ['Linodes', 'Distributed Plans'], country: 'us', id: 'us-den-10', label: 'Gecko Distributed Region Test', diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx index f7f82490e3f..9b497b0cece 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx @@ -38,11 +38,10 @@ export const getRegionOptions = ({ if (distributedContinentCode && distributedContinentCode !== 'ALL') { const group = getRegionCountryGroup(region); return ( - region.site_type === 'edge' || - (region.site_type === 'distributed' && - CONTINENT_CODE_TO_CONTINENT[ - distributedContinentCode as keyof typeof CONTINENT_CODE_TO_CONTINENT - ] === group) + region.site_type === 'distributed' && + CONTINENT_CODE_TO_CONTINENT[ + distributedContinentCode as keyof typeof CONTINENT_CODE_TO_CONTINENT + ] === group ); } return regionFilter.includes(region.site_type); @@ -149,7 +148,7 @@ export const getIsDistributedRegion = ( const region = regionsData.find( (region) => region.id === selectedRegion || region.label === selectedRegion ); - return region?.site_type === 'distributed' || region?.site_type === 'edge'; + return region?.site_type === 'distributed'; }; export const getNewRegionLabel = (region: Region) => { diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx index 1605532560b..8128e69e72c 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx @@ -120,7 +120,7 @@ export const KubernetesPlansPanel = (props: Props) => { ); }, - title: planTabInfoContent[plan === 'edge' ? 'dedicated' : plan]?.title, + title: planTabInfoContent[plan]?.title, }; } ); diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.tsx index a8652f9eeec..23cbd33253a 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.tsx @@ -24,8 +24,7 @@ export const Addons = () => { ); const isDistributedRegionSelected = - selectedRegion?.site_type === 'distributed' || - selectedRegion?.site_type === 'edge'; + selectedRegion?.site_type === 'distributed'; return ( diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.tsx index 421f0c8f92f..ee213c28063 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.tsx @@ -52,8 +52,7 @@ export const Backups = () => { const isAccountBackupsEnabled = accountSettings?.backups_enabled ?? false; const isDistributedRegionSelected = - selectedRegion?.site_type === 'distributed' || - selectedRegion?.site_type === 'edge'; + selectedRegion?.site_type === 'distributed'; const checked = getBackupsEnabledValue({ accountBackupsEnabled: isAccountBackupsEnabled, diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.tsx index 9c640e5fb2f..d13be07239d 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.tsx @@ -29,8 +29,7 @@ export const PrivateIP = () => { ); const isDistributedRegionSelected = - selectedRegion?.site_type === 'distributed' || - selectedRegion?.site_type === 'edge'; + selectedRegion?.site_type === 'distributed'; return ( { const showDistributedRegionIconHelperText = isGeckoBetaEnabled && !hideDistributedRegions; - regions?.some( - (region) => - region.site_type === 'distributed' || region.site_type === 'edge' - ); + regions?.some((region) => region.site_type === 'distributed'); const disabledRegions = getDisabledRegions({ regions: regions ?? [], diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx index a58b321c9d3..b7e7d09c21f 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx @@ -1,4 +1,3 @@ -import { Config } from '@linode/api-v4/lib/linodes'; import Grid from '@mui/material/Unstable_Grid2'; import { compose } from 'ramda'; import * as React from 'react'; @@ -6,39 +5,46 @@ import * as React from 'react'; import GridView from 'src/assets/icons/grid-view.svg'; import GroupByTag from 'src/assets/icons/group-by-tag.svg'; import { Box } from 'src/components/Box'; -import { OrderByProps } from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; import { MIN_PAGE_SIZE, PaginationFooter, getMinimumPageSizeForNumberOfItems, } from 'src/components/PaginationFooter/PaginationFooter'; +import { Paper } from 'src/components/Paper'; +import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; import { TableBody } from 'src/components/TableBody'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { Tooltip } from 'src/components/Tooltip'; import { Typography } from 'src/components/Typography'; -import { Action } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; -import { DialogType } from 'src/features/Linodes/types'; import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; import { groupByTags, sortGroups } from 'src/utilities/groupByTags'; -import { LinodeWithMaintenance } from 'src/utilities/linodes'; -import { RenderLinodesProps } from './DisplayLinodes'; import { StyledControlHeader, StyledTagHeader, StyledTagHeaderRow, StyledToggleButton, } from './DisplayLinodes.styles'; +import { RegionTypeFilter } from './RegionTypeFilter'; import TableWrapper from './TableWrapper'; +import type { RenderLinodesProps } from './DisplayLinodes'; +import type { Config } from '@linode/api-v4/lib/linodes'; +import type { OrderByProps } from 'src/components/OrderBy'; +import type { Action } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; +import type { DialogType } from 'src/features/Linodes/types'; +import type { LinodeWithMaintenance } from 'src/utilities/linodes'; +import type { RegionFilter } from 'src/utilities/storage'; + interface DisplayGroupedLinodesProps extends OrderByProps { component: React.ComponentType; data: LinodeWithMaintenance[]; display: 'grid' | 'list'; + handleRegionFilter: (regionFilter: RegionFilter) => void; isVLAN?: boolean; linodeViewPreference: 'grid' | 'list'; linodesAreGrouped: boolean; @@ -60,6 +66,7 @@ export const DisplayGroupedLinodes = (props: DisplayGroupedLinodesProps) => { data, display, handleOrderChange, + handleRegionFilter, isVLAN, linodeViewPreference, linodesAreGrouped, @@ -93,44 +100,53 @@ export const DisplayGroupedLinodes = (props: DisplayGroupedLinodesProps) => { return acc; }, 0); + const { isGeckoLAEnabled } = useIsGeckoEnabled(); + if (display === 'grid') { return ( <> - + {isGeckoLAEnabled && ( + + + + )} +
Currently in {linodeViewPreference} view
- - - - - + + + + + + -
- {linodesAreGrouped - ? 'group by tag is currently enabled' - : 'group by tag is currently disabled'} -
- - - - - +
+ {linodesAreGrouped + ? 'group by tag is currently enabled' + : 'group by tag is currently disabled'} +
+ + + + + +
{orderedGroupedLinodes.length === 0 ? ( @@ -207,79 +223,88 @@ export const DisplayGroupedLinodes = (props: DisplayGroupedLinodesProps) => { if (display === 'list') { return ( - - {orderedGroupedLinodes.length === 0 ? ( - - - - ) : null} - {orderedGroupedLinodes.map(([tag, linodes]) => { - return ( - - - {({ - count, - data: paginatedData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => { - const finalProps = { - ...rest, + <> + {isGeckoLAEnabled && ( + + + + )} + + {orderedGroupedLinodes.length === 0 ? ( + + + + ) : null} + {orderedGroupedLinodes.map(([tag, linodes]) => { + return ( + + + {({ count, data: paginatedData, - handleOrderChange, handlePageChange, handlePageSizeChange, - isVLAN, - order, - orderBy, page, pageSize, - }; - return ( - - - - {tag} - - - - {count > MIN_PAGE_SIZE && ( - - - + }) => { + const finalProps = { + ...rest, + count, + data: paginatedData, + handleOrderChange, + handlePageChange, + handlePageSizeChange, + isVLAN, + order, + orderBy, + page, + pageSize, + }; + return ( + + + + + {tag} + - - )} - - ); - }} - - - ); - })} - + + + {count > MIN_PAGE_SIZE && ( + + + + + + )} + + ); + }} + + + ); + })} + + ); } diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.styles.ts b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.styles.ts index b0495eb8197..56e75d7fe40 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.styles.ts +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.styles.ts @@ -27,14 +27,13 @@ export const StyledTagHeader = styled(Typography, { export const StyledControlHeader = styled('div', { label: 'StyledControlHeader', - shouldForwardProp: omittedProps(['isGroupedByTag']), -})<{ isGroupedByTag: boolean }>(({ isGroupedByTag, theme }) => ({ +})(({ theme }) => ({ alignItems: 'center', backgroundColor: theme.bg.tableHeader, display: 'flex', height: 46, justifyContent: 'flex-end', - marginBottom: isGroupedByTag ? theme.spacing(4) : 0, + marginBottom: theme.spacing(4), })); export const StyledToggleButton = styled(IconButton, { diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx index b3df714781b..6513f6e6a40 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx @@ -1,3 +1,4 @@ +import { Box } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { useLocation } from 'react-router-dom'; @@ -5,8 +6,10 @@ import { useLocation } from 'react-router-dom'; import GridView from 'src/assets/icons/grid-view.svg'; import GroupByTag from 'src/assets/icons/group-by-tag.svg'; import Paginate from 'src/components/Paginate'; -import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { getMinimumPageSizeForNumberOfItems } from 'src/components/PaginationFooter/PaginationFooter'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Paper } from 'src/components/Paper'; +import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; import { TableBody } from 'src/components/TableBody'; import { Tooltip } from 'src/components/Tooltip'; import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; @@ -16,6 +19,7 @@ import { StyledControlHeader, StyledToggleButton, } from './DisplayLinodes.styles'; +import { RegionTypeFilter } from './RegionTypeFilter'; import TableWrapper from './TableWrapper'; import type { Config } from '@linode/api-v4/lib/linodes'; @@ -25,6 +29,7 @@ import type { Action } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; import type { DialogType } from 'src/features/Linodes/types'; import type { LinodeWithMaintenance } from 'src/utilities/linodes'; import type { BaseQueryParams } from 'src/utilities/queryParams'; +import type { RegionFilter } from 'src/utilities/storage'; interface QueryParams extends BaseQueryParams { page: string; @@ -42,6 +47,7 @@ interface DisplayLinodesProps extends OrderByProps { component: React.ComponentType; data: LinodeWithMaintenance[]; display: 'grid' | 'list'; + handleRegionFilter: (regionFilter: RegionFilter) => void; linodeViewPreference: 'grid' | 'list'; linodesAreGrouped: boolean; openDialog: (type: DialogType, linodeID: number, linodeLabel: string) => void; @@ -63,6 +69,7 @@ export const DisplayLinodes = React.memo((props: DisplayLinodesProps) => { data, display, handleOrderChange, + handleRegionFilter, linodeViewPreference, linodesAreGrouped, order, @@ -75,8 +82,8 @@ export const DisplayLinodes = React.memo((props: DisplayLinodesProps) => { const displayViewDescriptionId = React.useId(); const groupByDescriptionId = React.useId(); - const { infinitePageSize, setInfinitePageSize } = useInfinitePageSize(); + const numberOfLinodesWithMaintenance = React.useMemo(() => { return data.reduce((acc, thisLinode) => { if (thisLinode.maintenance) { @@ -85,7 +92,9 @@ export const DisplayLinodes = React.memo((props: DisplayLinodesProps) => { return acc; }, 0); }, [JSON.stringify(data)]); + const count = data.length; + const pageSize = numberOfLinodesWithMaintenance > infinitePageSize ? getMinimumPageSizeForNumberOfItems(numberOfLinodesWithMaintenance) @@ -96,6 +105,8 @@ export const DisplayLinodes = React.memo((props: DisplayLinodesProps) => { const params = getQueryParamsFromQueryString(search); const queryPage = Math.min(Number(params.page), maxPageNumber) || 1; + const { isGeckoLAEnabled } = useIsGeckoEnabled(); + return ( { return ( {display === 'list' && ( - - - - - + <> + {isGeckoLAEnabled && ( + + + + )} + + + + + + )} {display === 'grid' && ( <> - + {isGeckoLAEnabled && ( + + + + )} +
Currently in {linodeViewPreference} view
- - - - - - -
- {linodesAreGrouped - ? 'group by tag is currently enabled' - : 'group by tag is currently disabled'} -
- - + + + + + +
- - - + {linodesAreGrouped + ? 'group by tag is currently enabled' + : 'group by tag is currently disabled'} +
+ + + + + +
diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx index 3be0ed060c4..72d21d9f21e 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx @@ -48,6 +48,7 @@ import type { WithFeatureFlagProps } from 'src/containers/flags.container'; import type { WithProfileProps } from 'src/containers/profile.container'; import type { DialogType } from 'src/features/Linodes/types'; import type { LinodeWithMaintenance } from 'src/utilities/linodes'; +import type { RegionFilter } from 'src/utilities/storage'; interface State { deleteDialogOpen: boolean; @@ -82,11 +83,14 @@ type RouteProps = RouteComponentProps; export interface LinodesLandingProps { LandingHeader?: React.ReactElement; + handleRegionFilter: (regionFilter: RegionFilter) => void; linodesData: LinodeWithMaintenance[]; linodesInTransition: Set; linodesRequestError?: APIError[]; linodesRequestLoading: boolean; someLinodesHaveScheduledMaintenance: boolean; + /** Keep track of total number of linodes for filtering and empty state landing page logic */ + totalNumLinodes: number; } type CombinedProps = LinodesLandingProps & @@ -187,11 +191,13 @@ class ListLinodes extends React.Component { render() { const { grants, + handleRegionFilter, linodesData, linodesInTransition, linodesRequestError, linodesRequestLoading, profile, + totalNumLinodes, } = this.props; const isLinodesGrantReadOnly = @@ -241,7 +247,7 @@ class ListLinodes extends React.Component { return ; } - if (this.props.linodesData.length === 0) { + if (totalNumLinodes === 0 && linodesData.length === 0) { return ( <> @@ -402,6 +408,7 @@ class ListLinodes extends React.Component { : ListView } display={linodeViewPreference} + handleRegionFilter={handleRegionFilter} linodeViewPreference={linodeViewPreference} linodesAreGrouped={true} toggleGroupLinodes={toggleGroupLinodes} @@ -416,6 +423,7 @@ class ListLinodes extends React.Component { : ListView } display={linodeViewPreference} + handleRegionFilter={handleRegionFilter} linodeViewPreference={linodeViewPreference} linodesAreGrouped={false} toggleGroupLinodes={toggleGroupLinodes} diff --git a/packages/manager/src/features/Linodes/LinodesLanding/RegionTypeFilter.tsx b/packages/manager/src/features/Linodes/LinodesLanding/RegionTypeFilter.tsx new file mode 100644 index 00000000000..1453568ec1f --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesLanding/RegionTypeFilter.tsx @@ -0,0 +1,78 @@ +import { Typography } from '@mui/material'; +import * as React from 'react'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { Box } from 'src/components/Box'; +import { FormLabel } from 'src/components/FormLabel'; +import { storage } from 'src/utilities/storage'; + +import type { RegionFilter } from 'src/utilities/storage'; + +interface RegionFilterOption { + label: string; + value: RegionFilter; +} + +const regionFilterOptions: RegionFilterOption[] = [ + { + label: 'All', + value: 'all', + }, + { + label: 'Core', + value: 'core', + }, + { + label: 'Distributed', + value: 'distributed', + }, +]; + +const regionFilterMap = { + all: 'All', + core: 'Core', + distributed: 'Distributed', +}; + +const ariaIdentifier = 'region-type-filter'; + +interface Props { + handleRegionFilter: (regionFilter: RegionFilter) => void; +} + +export const RegionTypeFilter = ({ handleRegionFilter }: Props) => { + const regionFilter = storage.regionFilter.get(); + + return ( + + + + Region Type: + + + filter.value === regionFilter) ?? + regionFilterOptions[0] + } + onChange={(_, selectedOption) => { + if (selectedOption?.value) { + handleRegionFilter(selectedOption.value); + } + }} + sx={{ + display: 'inline-block', + width: 140, + }} + textFieldProps={{ + hideLabel: true, + }} + disableClearable + id={ariaIdentifier} + label="Region Type" + options={regionFilterOptions} + placeholder={regionFilterMap[regionFilter]} + /> + + ); +}; diff --git a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx index da0a2bae841..1964707fa22 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx @@ -146,8 +146,7 @@ export const ConfigureForm = React.memo((props: Props) => { ); const linodeIsInDistributedRegion = - currentActualRegion?.site_type === 'distributed' || - currentActualRegion?.site_type === 'edge'; + currentActualRegion?.site_type === 'distributed'; const { isGeckoBetaEnabled } = useIsGeckoEnabled(); diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 2d88d5001bb..2081e1f9322 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -2,14 +2,18 @@ import { createLazyRoute } from '@tanstack/react-router'; import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; +import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { useAllAccountMaintenanceQuery } from 'src/queries/account/maintenance'; import { useInProgressEvents } from 'src/queries/events/events'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { addMaintenanceToLinodes } from 'src/utilities/linodes'; +import { storage } from 'src/utilities/storage'; import { linodesInTransition } from './transitions'; +import type { RegionFilter } from 'src/utilities/storage'; + const LinodesLanding = React.lazy( () => import('./LinodesLanding/LinodesLanding') ); @@ -48,7 +52,18 @@ export const LinodesLandingWrapper = React.memo(() => { { status: { '+or': ['pending, started'] } } ); - const { data: linodes, error, isLoading } = useAllLinodesQuery(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(); + + const [regionFilter, setRegionFilter] = React.useState< + RegionFilter | undefined + >(storage.regionFilter.get()); + + // We need to grab all linodes so a filtered result of 0 does not display the empty state landing page + const { data: allLinodes } = useAllLinodesQuery(); + const { data: filteredLinodes, error, isLoading } = useAllLinodesQuery( + {}, + isGeckoLAEnabled ? generateLinodesXFilter(regionFilter) : {} + ); const someLinodesHaveScheduledMaintenance = accountMaintenanceData?.some( (thisAccountMaintenance) => thisAccountMaintenance.entity.type === 'linode' @@ -56,24 +71,40 @@ export const LinodesLandingWrapper = React.memo(() => { const { data: events } = useInProgressEvents(); - const linodesData = addMaintenanceToLinodes( + const filteredLinodesData = addMaintenanceToLinodes( accountMaintenanceData ?? [], - linodes ?? [] + filteredLinodes ?? [] ); + const handleRegionFilter = (regionFilter: RegionFilter) => { + setRegionFilter(regionFilter); + storage.regionFilter.set(regionFilter); + }; + return ( ); }); +const generateLinodesXFilter = (regionFilter: RegionFilter | undefined) => { + if (regionFilter === 'core' || regionFilter === 'distributed') { + return { + site_type: regionFilter, + }; + } + return {}; +}; + export const linodesLandingLazyRoute = createLazyRoute('/linodes')({ component: LinodesLandingWrapper, }); diff --git a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx index 2a9ba6bb9ec..833cd24a11e 100644 --- a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx @@ -112,9 +112,7 @@ export const PlansPanel = (props: PlansPanelProps) => { const getDedicatedDistributedRegionPlanType = () => { return types.filter( (type) => - type.id.includes('dedicated-edge') || - type.id.includes('nanode-edge') || - type.class === 'edge' + type.id.includes('dedicated-edge') || type.id.includes('nanode-edge') ); }; @@ -194,7 +192,7 @@ export const PlansPanel = (props: PlansPanelProps) => { ); }, - title: planTabInfoContent[plan === 'edge' ? 'dedicated' : plan]?.title, + title: planTabInfoContent[plan]?.title, }; } ); diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 2439c495cb6..108a65122b7 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -603,6 +603,7 @@ export const handlers = [ image: 'distributed-region-test-image', label: 'Gecko Distributed Region Test', region: 'us-den-10', + site_type: 'distributed', }); const onlineLinodes = linodeFactory.buildList(40, { backups: { enabled: false }, diff --git a/packages/manager/src/queries/linodes/linodes.ts b/packages/manager/src/queries/linodes/linodes.ts index 9d33a64984a..e10750fce3e 100644 --- a/packages/manager/src/queries/linodes/linodes.ts +++ b/packages/manager/src/queries/linodes/linodes.ts @@ -176,6 +176,7 @@ export const useAllLinodesQuery = ( ...linodeQueries.linodes._ctx.all(params, filter), ...queryPresets.longLived, enabled, + placeholderData: keepPreviousData, }); }; diff --git a/packages/manager/src/queries/linodes/stats.ts b/packages/manager/src/queries/linodes/stats.ts index 8849d10888a..df827fd266a 100644 --- a/packages/manager/src/queries/linodes/stats.ts +++ b/packages/manager/src/queries/linodes/stats.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { linodeQueries } from './linodes'; @@ -15,7 +15,7 @@ export const STATS_NOT_READY_MESSAGE = 'Stats for this Linode are not available yet'; const queryOptions = { - keepPreviousData: true, + placeholderData: keepPreviousData, refetchInterval: 300_000, // 5 minutes refetchOnMount: false, refetchOnWindowFocus: false, diff --git a/packages/manager/src/utilities/storage.ts b/packages/manager/src/utilities/storage.ts index 063863e42cf..7489257b1d8 100644 --- a/packages/manager/src/utilities/storage.ts +++ b/packages/manager/src/utilities/storage.ts @@ -1,5 +1,6 @@ import { shouldLoadDevTools } from 'src/dev-tools/load'; +import type { RegionSite } from '@linode/api-v4'; import type { StackScriptPayload } from '@linode/api-v4/lib/stackscripts/types'; import type { SupportTicketFormFields } from 'src/features/Support/SupportTickets/SupportTicketDialog'; @@ -54,8 +55,10 @@ const SUPPORT = 'support'; const TICKET = 'ticket'; const STACKSCRIPT = 'stackscript'; const DEV_TOOLS_ENV = 'devTools/env'; +const REGION_FILTER = 'regionFilter'; export type PageSize = number; +export type RegionFilter = 'all' | RegionSite; interface AuthGetAndSet { get: () => any; @@ -113,6 +116,10 @@ export interface Storage { get: () => PageSize; set: (perPage: PageSize) => void; }; + regionFilter: { + get: () => RegionFilter; + set: (v: RegionFilter) => void; + }; stackScriptInProgress: { get: () => StackScriptData; set: (s: StackScriptData) => void; @@ -181,6 +188,10 @@ export const storage: Storage = { }, set: (v) => setStorage(PAGE_SIZE, `${v}`), }, + regionFilter: { + get: () => getStorage(REGION_FILTER), + set: (v) => setStorage(REGION_FILTER, v), + }, stackScriptInProgress: { get: () => getStorage(STACKSCRIPT, {