From 73368184e2f0513caa7b3df762a90e90ce500f87 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:47:41 +0100 Subject: [PATCH] =?UTF-8?q?Revert=20"Release=20v.1.158.0-=20staging=20?= =?UTF-8?q?=E2=86=92=20master=20(#13311)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 189337e4b89b17434dadb0668597783cfef2c6cf. --- packages/api-v4/CHANGELOG.md | 17 - packages/api-v4/package.json | 2 +- packages/api-v4/src/account/types.ts | 3 +- packages/api-v4/src/cloudpulse/alerts.ts | 23 -- packages/api-v4/src/cloudpulse/types.ts | 15 - packages/api-v4/src/delivery/types.ts | 1 - packages/api-v4/src/iam/delegation.ts | 2 +- packages/api-v4/src/iam/types.ts | 6 - packages/api-v4/src/linodes/types.ts | 2 - packages/api-v4/src/locks/types.ts | 13 +- .../api-v4/src/marketplace/marketplace.ts | 2 +- packages/api-v4/src/marketplace/types.ts | 35 +- ...r-13299-upcoming-features-1768999121997.md | 5 - packages/manager/CHANGELOG.md | 66 ---- .../core/account/account-maintenance.spec.ts | 2 + .../e2e/core/account/network-settings.spec.ts | 16 +- .../e2e/core/account/quotas-nav.spec.ts | 82 +++++ .../restricted-user-details-pages.spec.ts | 2 + .../e2e/core/account/service-transfer.spec.ts | 9 + .../e2e/core/account/user-permissions.spec.ts | 2 + .../core/account/users-landing-page.spec.ts | 2 + .../e2e/core/billing/billing-invoices.spec.ts | 7 + .../credit-card-expired-banner.spec.ts | 5 + .../e2e/core/billing/google-pay.spec.ts | 8 + .../billing/restricted-user-billing.spec.ts | 14 +- .../billing/smoke-billing-activity.spec.ts | 2 + .../cloudpulse/alerts-listing-page.spec.ts | 2 +- .../e2e/core/cloudpulse/groupby-tags.spec.ts | 2 +- .../cloudpulse/timerange-verification.spec.ts | 283 ++++++++++++---- ...estinations-non-empty-landing-page.spec.ts | 4 +- .../core/delivery/edit-destination.spec.ts | 16 +- .../e2e/core/delivery/edit-stream.spec.ts | 28 +- .../streams-non-empty-landing-page.spec.ts | 6 +- .../e2e/core/general/smoke-deep-link.spec.ts | 5 + .../e2e/core/kubernetes/lke-update.spec.ts | 7 - .../e2e/core/linodes/alerts-create.spec.ts | 18 - .../create-linode-with-firewall.spec.ts | 18 +- .../linodes/create-linode-with-vlan.spec.ts | 12 +- .../linodes/create-linode-with-vpc.spec.ts | 12 +- .../e2e/core/linodes/create-linode.spec.ts | 10 - .../maintenance-policy-region-support.spec.ts | 6 + .../smoke-linode-landing-table.spec.ts | 10 + .../core/oneClickApps/one-click-apps.spec.ts | 5 - .../stackscripts/create-stackscripts.spec.ts | 23 +- .../smoke-community-stackscripts.spec.ts | 7 - .../support/ui/pages/logs-destination-form.ts | 3 +- .../support/ui/pages/logs-stream-form.ts | 15 +- .../manager/cypress/support/util/regions.ts | 8 +- packages/manager/package.json | 7 +- packages/manager/public/assets/chroma.svg | 22 -- packages/manager/public/assets/mistral.svg | 19 -- packages/manager/public/assets/openwebui.svg | 6 - packages/manager/public/assets/vllm.svg | 1 - .../manager/public/assets/white/chroma.svg | 17 - .../manager/public/assets/white/mistral.svg | 21 -- .../manager/public/assets/white/openwebui.svg | 13 - packages/manager/public/assets/white/vllm.svg | 38 --- packages/manager/src/GoTo.tsx | 38 ++- .../MaskableText/MaskableTextArea.tsx | 14 +- .../PaymentMethodRow.test.tsx | 61 ++-- .../PaymentMethodRow/PaymentMethodRow.tsx | 4 +- .../components/PrimaryNav/PrimaryNav.test.tsx | 50 ++- .../src/components/PrimaryNav/PrimaryNav.tsx | 58 ++-- .../QuotaUsageBar/QuotaUsageBar.test.tsx | 16 - .../QuotaUsageBar/QuotaUsageBar.tsx | 18 +- .../SelectionCard/SelectionCard.tsx | 2 +- .../TypeToConfirm/TypeToConfirm.test.tsx | 2 +- .../TypeToConfirm/TypeToConfirm.tsx | 15 +- .../TypeToConfirmDialog.tsx | 1 - .../manager/src/dev-tools/FeatureFlagTool.tsx | 9 +- packages/manager/src/dev-tools/utils.ts | 4 +- packages/manager/src/factories/account.ts | 1 - .../src/factories/cloudpulse/channels.ts | 14 +- packages/manager/src/factories/index.ts | 1 - packages/manager/src/factories/locks.ts | 13 - .../src/factories/userAccountPermissions.ts | 1 - packages/manager/src/featureFlags.ts | 5 +- .../src/features/Account/AccountLanding.tsx | 12 +- .../src/features/Account/AccountLogins.tsx | 10 +- .../Account/Maintenance/MaintenanceTable.tsx | 8 +- .../Account/NetworkInterfaceType.test.tsx | 8 +- .../features/Account/NetworkInterfaceType.tsx | 157 +++++---- .../src/features/Account/Quotas/Quotas.tsx | 6 +- .../features/Account/Quotas/QuotasTable.tsx | 6 +- .../Account/SwitchAccountButton.test.tsx | 42 ++- .../features/Account/SwitchAccountButton.tsx | 17 + .../Account/SwitchAccountDrawer.test.tsx | 37 +-- .../features/Account/SwitchAccountDrawer.tsx | 146 +------- .../SwitchAccounts/ChildAccountList.test.tsx | 52 +-- .../SwitchAccounts/ChildAccountList.tsx | 157 +++++---- .../AccountSettingsLanding.tsx | 12 + .../features/Billing/BillingDetail.test.tsx | 34 +- .../Billing/BillingLanding/BillingLanding.tsx | 35 +- .../BillingActivityPanel.tsx | 22 +- .../BillingSummary/BillingSummary.test.tsx | 26 +- .../BillingSummary/BillingSummary.tsx | 10 +- .../ContactInfoPanel/ContactInformation.tsx | 10 +- .../PaymentInformation.test.tsx | 14 +- .../PaymentInfoPanel/PaymentInformation.tsx | 16 +- .../Billing/InvoiceDetail/InvoiceDetail.tsx | 10 +- .../Billing/PdfGenerator/PdfGenerator.test.ts | 7 +- .../Billing/PdfGenerator/PdfGenerator.ts | 25 +- .../Alerts/AlertRegions/AlertRegions.tsx | 5 +- .../Alerts/AlertsListing/AlertListTable.tsx | 20 +- .../Alerts/AlertsListing/constants.ts | 2 +- .../AlertsResources/AlertsResources.tsx | 11 +- .../AlertInformationActionTable.test.tsx | 58 ---- .../AlertInformationActionTable.tsx | 31 +- .../CreateChannel/schemas.ts | 8 +- .../NotificationChannelAlerts.test.tsx | 190 ----------- .../NotificationChannelAlerts.tsx | 147 -------- ...NotificationChannelAlertsTableRow.test.tsx | 90 ----- .../NotificationChannelAlertsTableRow.tsx | 42 --- .../NotificationChannelDetail.test.tsx | 196 ----------- .../NotificationChannelDetail.tsx | 112 +------ ...NotificationChannelDetailOverview.test.tsx | 113 ------- .../NotificationChannelDetailOverview.tsx | 70 ---- ...tificationChannelDetailRecipients.test.tsx | 50 --- .../NotificationChannelDetailRecipients.tsx | 61 ---- .../NotificationChannelActionMenu.tsx | 14 +- .../NotificationChannelListTable.test.tsx | 180 ---------- .../NotificationChannelListTable.tsx | 81 +---- .../NotificationChannelTableRow.test.tsx | 2 - .../NotificationChannelTableRow.tsx | 1 - .../NotificationChannels/Utils/utils.ts | 66 ++-- .../CloudPulse/Alerts/Utils/utils.test.ts | 32 +- .../features/CloudPulse/Alerts/Utils/utils.ts | 38 +-- .../features/CloudPulse/Alerts/constants.ts | 9 - .../Dashboard/CloudPulseDashboardLanding.tsx | 14 +- .../CloudPulse/Overview/GlobalFilters.tsx | 12 +- .../src/features/CloudPulse/Utils/utils.ts | 9 +- .../shared/CloudPulseDashboardSelect.tsx | 1 - .../ConnectionDetailsHostRows.tsx | 16 +- .../DatabaseDetail/ConnectionDetailsRow.tsx | 9 +- .../DatabaseSummary/DatabaseCaCert.tsx | 107 ------ .../DatabaseSummary/DatabaseSummary.tsx | 47 +-- .../DatabaseSummaryConnectionDetails.style.ts | 39 +++ .../DatabaseSummaryConnectionDetails.tsx | 92 ++++- .../DatabaseDetail/ServiceURI.test.tsx | 46 +-- .../Databases/DatabaseDetail/ServiceURI.tsx | 163 ++++----- .../Destinations/DeleteDestinationDialog.tsx | 3 +- .../DestinationForm/DestinationEdit.test.tsx | 2 +- .../DestinationForm/DestinationForm.tsx | 1 + ...tinationAkamaiObjectStorageDetailsForm.tsx | 4 +- .../FormSubmitBar/FormSubmitBar.test.tsx | 4 +- .../Shared/FormSubmitBar/FormSubmitBar.tsx | 7 +- .../src/features/Delivery/Shared/types.ts | 5 - .../Delivery/Streams/DeleteStreamDialog.tsx | 2 +- .../Delivery/Streams/StreamActionMenu.tsx | 1 - .../Delivery/StreamFormDelivery.tsx | 4 +- .../Streams/StreamForm/StreamCreate.test.tsx | 4 +- .../Streams/StreamForm/StreamEdit.test.tsx | 48 +-- .../Streams/StreamForm/StreamForm.tsx | 21 +- .../StreamForm/StreamFormGeneralInfo.test.tsx | 4 +- .../StreamForm/StreamFormGeneralInfo.tsx | 3 +- .../Delivery/Streams/StreamTableRow.tsx | 8 +- .../Delivery/Streams/StreamsLanding.tsx | 2 +- .../EntityTransfersCreate.tsx | 8 +- .../LinodeTransferTable.tsx | 6 +- .../EntityTransfersLanding.tsx | 14 +- .../TransferControls.tsx | 7 +- .../src/features/Events/factories/index.ts | 1 - .../src/features/Events/factories/lock.tsx | 34 -- .../CreditCardExpiredBanner.tsx | 6 +- .../GlobalNotifications/EmailBounce.tsx | 7 +- .../TaxCollectionBanner.tsx | 2 +- .../Delegations/AccountDelegations.test.tsx | 2 +- .../Delegations/AccountDelegationsTable.tsx | 6 +- .../IAM/Delegations/UpdateDelegationForm.tsx | 161 --------- .../UpdateDelegationsDrawer.test.tsx | 40 +-- .../Delegations/UpdateDelegationsDrawer.tsx | 181 +++++++++- .../manager/src/features/IAM/IAMLanding.tsx | 4 +- .../Roles/Defaults/DefaultEntityAccess.tsx | 2 +- .../IAM/Roles/Defaults/DefaultRolesPanel.tsx | 2 +- .../IAM/Roles/Defaults/DefaultsLanding.tsx | 23 -- .../Defaults/defaultsLandingLazyRoute.ts | 2 +- .../src/features/IAM/Roles/Roles.test.tsx | 13 +- .../manager/src/features/IAM/Roles/Roles.tsx | 6 +- .../RolesTable/AssignSelectedRolesDrawer.tsx | 16 +- .../AssignedEntitiesTable.tsx | 32 +- .../AssignedRolesActionMenu.tsx | 18 +- .../AssignedRolesTable/AssignedRolesTable.tsx | 23 +- .../UnassignRoleConfirmationDialog.tsx | 20 +- ...emoveAssignmentConfirmationDialog.test.tsx | 2 +- .../RemoveAssignmentConfirmationDialog.tsx | 18 +- .../src/features/IAM/Shared/constants.ts | 5 +- .../src/features/IAM/Shared/utilities.test.ts | 82 +---- .../src/features/IAM/Shared/utilities.ts | 12 +- .../UserDelegations/UserDelegations.test.tsx | 14 - .../Users/UserDelegations/UserDelegations.tsx | 6 +- .../IAM/Users/UserDetails/UserProfile.tsx | 27 +- .../features/IAM/Users/UserDetailsLanding.tsx | 9 +- .../Users/UserEntities/UserEntities.test.tsx | 7 +- .../IAM/Users/UserEntities/UserEntities.tsx | 17 +- .../IAM/Users/UserRoles/UserRoles.test.tsx | 2 +- .../IAM/Users/UserRoles/UserRoles.tsx | 15 +- .../IAM/Users/UsersTable/UserRow.test.tsx | 79 ++--- .../features/IAM/Users/UsersTable/UserRow.tsx | 4 +- .../IAM/Users/UsersTable/Users.test.tsx | 32 +- .../features/IAM/Users/UsersTable/Users.tsx | 4 +- .../Users/UsersTable/UsersActionMenu.test.tsx | 4 +- .../IAM/Users/UsersTable/UsersActionMenu.tsx | 16 +- .../UsersTable/UsersLandingTableHead.test.tsx | 62 ++-- .../useGetAllUserEntitiesByPermission.ts | 25 +- .../IAM/hooks/useIsIAMEnabled.test.ts | 7 + .../src/features/IAM/hooks/useIsIAMEnabled.ts | 7 +- .../features/IAM/hooks/usePermissions.test.ts | 89 ++++- .../src/features/IAM/hooks/usePermissions.ts | 52 ++- .../CreateCluster/CreateCluster.tsx | 23 -- .../KubeCheckoutBar/KubeCheckoutBar.test.tsx | 16 - .../KubeCheckoutBar/KubeCheckoutBar.tsx | 5 +- .../KubernetesPlansPanel.tsx | 2 +- .../LinodeCreate/Addons/Addons.test.tsx | 30 -- .../Linodes/LinodeCreate/Addons/Addons.tsx | 17 +- .../Linodes/LinodeCreate/Addons/Backups.tsx | 2 +- .../Linodes/LinodeCreate/Addons/PrivateIP.tsx | 11 +- .../Networking/InterfaceGeneration.test.tsx | 165 ++------- .../Networking/InterfaceGeneration.tsx | 181 +++++----- .../Networking/InterfaceType.test.tsx | 95 ------ .../LinodeCreate/Networking/InterfaceType.tsx | 7 +- .../Networking/LinodeInterface.test.tsx | 15 +- .../Networking/LinodeInterface.tsx | 2 - .../LinodeCreate/Networking/Networking.tsx | 2 + .../Linodes/LinodeCreate/Networking/VLAN.tsx | 101 +++--- .../Linodes/LinodeCreate/Networking/VPC.tsx | 81 +---- .../LinodeCreate/Networking/VPCIPv6Ranges.tsx | 8 + .../LinodeCreate/Networking/VPCRanges.tsx | 16 +- .../features/Linodes/LinodeCreate/Plan.tsx | 2 +- .../features/Linodes/LinodeCreate/VPC/VPC.tsx | 1 - .../Linodes/LinodeCreate/utilities.test.tsx | 16 +- .../Linodes/LinodeCreate/utilities.ts | 9 +- .../Linodes/LinodeEntityDetailFooter.test.tsx | 2 +- .../AddInterfaceForm.test.tsx | 104 +----- .../AddInterfaceDrawer/AddInterfaceForm.tsx | 74 +---- .../AddInterfaceDrawer/InterfaceType.tsx | 27 +- .../VPC/AddVPCIPv6Address.tsx | 1 - .../VPCInterface/EditVPCIPv6Address.tsx | 1 - .../LinodeInterfaces/VPCIPv6Address.tsx | 35 +- .../LoginHistory/LoginHistoryLanding.tsx | 12 + .../Maintenance/MaintenanceLanding.tsx | 12 + .../Marketplace/MarketplaceLanding.tsx | 8 + .../MarketplaceLanding/CategorySection.tsx | 194 ----------- .../CategorySectionView.test.tsx | 131 -------- .../CategorySectionView.tsx | 95 ------ .../MarketplaceLanding/MarketplaceLanding.tsx | 86 ----- .../ProductSelectionCard.test.tsx | 136 -------- .../ProductSelectionCard.tsx | 183 ---------- .../ProductSelectionCardSkeleton.tsx | 76 ----- .../Marketplace/MarketplaceLanding/styles.ts | 40 --- .../marketplaceLazyRoute.tsx | 0 .../NetworkLoadBalancerDetailBody.tsx | 4 +- .../NetworkLoadBalancersDetail.test.tsx | 4 +- .../NetworkLoadBalancersDetail.tsx | 68 +--- .../NetworkLoadBalancersListenerDetail.tsx | 80 ++--- .../NodesTable/NodesTable.tsx | 38 +-- .../NodesTable/NodesTableRow.tsx | 2 +- .../NetworkLoadBalancerTableRow.tsx | 26 +- .../NetworkLoadBalancersLanding.tsx | 15 +- .../NetworkLoadBalancersLandingEmptyState.tsx | 2 +- .../ObjectStorage/BucketDetail/index.tsx | 6 +- .../hooks/useGetObjUsagePerEndpoint.ts | 7 +- .../src/features/OneClickApps/oneClickApps.ts | 47 +-- .../src/features/OneClickApps/types.ts | 5 - .../manager/src/features/Profile/Profile.tsx | 13 +- .../features/Profile/Settings/Settings.tsx | 14 +- .../Profile/Settings/settingsLazyRoute.ts | 8 + .../src/features/Quotas/QuotasLanding.tsx | 14 + .../ServiceTransfersLanding.tsx | 12 + .../TopMenu/UserMenu/UserMenuPopover.tsx | 65 +++- .../src/features/Users/CreateUserDrawer.tsx | 18 +- .../manager/src/features/Users/UserDetail.tsx | 14 +- .../Users/UserPermissionsEntitySection.tsx | 7 +- .../Users/UserProfile/DeleteUserPanel.tsx | 5 +- .../Users/UserProfile/UserProfile.tsx | 7 +- .../Users/UserProfile/UsernamePanel.tsx | 6 +- .../src/features/Users/UsersActionMenu.tsx | 14 +- .../src/features/Users/UsersLanding.tsx | 7 +- .../UsersAndGrants/UsersAndGrants.tsx | 14 + .../VPCTopSectionContent.test.tsx | 15 +- .../FormComponents/VPCTopSectionContent.tsx | 103 +++--- .../Volumes/VolumeDetails/VolumeDetails.tsx | 7 +- .../PlansPanel/DedicatedPlanFilters.tsx | 2 +- .../PlansPanel/utils/planFilters.test.ts | 29 -- .../PlansPanel/utils/planFilters.ts | 26 +- .../manager/src/hooks/useEventHandlers.ts | 4 +- packages/manager/src/hooks/useVPCDualStack.ts | 7 + packages/manager/src/mocks/mockState.ts | 1 - .../src/mocks/presets/baseline/crud.ts | 2 - .../mocks/presets/crud/handlers/delegation.ts | 4 +- .../presets/crud/handlers/linodes/configs.ts | 22 -- .../crud/handlers/linodes/interfaces.ts | 12 - .../presets/crud/handlers/linodes/linodes.ts | 92 +---- .../src/mocks/presets/crud/handlers/locks.ts | 314 ------------------ .../manager/src/mocks/presets/crud/linodes.ts | 4 - .../manager/src/mocks/presets/crud/locks.ts | 10 - .../src/mocks/presets/crud/seeds/index.ts | 2 - .../src/mocks/presets/crud/seeds/locks.ts | 52 --- .../src/mocks/presets/crud/seeds/utils.ts | 3 - packages/manager/src/mocks/serverHandlers.ts | 151 ++------- packages/manager/src/mocks/types.ts | 4 - .../manager/src/queries/cloudpulse/alerts.ts | 32 -- .../manager/src/queries/cloudpulse/queries.ts | 5 - .../src/queries/cloudpulse/requests.ts | 7 - packages/manager/src/routes/IAM/IAMRoute.tsx | 11 +- packages/manager/src/routes/IAM/index.ts | 35 +- packages/manager/src/routes/account/index.ts | 122 ++++--- .../src/routes/accountSettings/index.ts | 8 + packages/manager/src/routes/billing/index.ts | 19 +- .../manager/src/routes/loginHistory/index.ts | 8 + .../manager/src/routes/maintenance/index.ts | 8 + .../manager/src/routes/marketplace/index.ts | 6 +- packages/manager/src/routes/profile/index.ts | 22 +- packages/manager/src/routes/quotas/index.ts | 8 + .../src/routes/serviceTransfers/index.ts | 16 + .../usersAndGrants/UsersAndGrantsRoute.tsx | 5 +- packages/queries/CHANGELOG.md | 7 - packages/queries/package.json | 2 +- packages/queries/src/account/users.ts | 4 +- packages/queries/src/iam/delegation.ts | 4 - packages/queries/src/index.ts | 1 - packages/queries/src/marketplace/index.ts | 3 - packages/queries/src/marketplace/keys.ts | 109 ------ .../queries/src/marketplace/marketplace.ts | 235 ------------- packages/queries/src/marketplace/requests.ts | 60 ---- packages/ui/package.json | 4 +- .../utilities/src/__data__/regionsData.ts | 2 +- packages/utilities/src/factories/linodes.ts | 1 - .../utilities/src/factories/marketplace.ts | 27 +- packages/validation/CHANGELOG.md | 7 - packages/validation/package.json | 2 +- packages/validation/src/cloudpulse.schema.ts | 35 +- packages/validation/src/delivery.schema.ts | 10 +- pnpm-lock.yaml | 72 ++-- 333 files changed, 2804 insertions(+), 7238 deletions(-) delete mode 100644 packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md delete mode 100644 packages/manager/public/assets/chroma.svg delete mode 100644 packages/manager/public/assets/mistral.svg delete mode 100644 packages/manager/public/assets/openwebui.svg delete mode 100644 packages/manager/public/assets/vllm.svg delete mode 100644 packages/manager/public/assets/white/chroma.svg delete mode 100644 packages/manager/public/assets/white/mistral.svg delete mode 100644 packages/manager/public/assets/white/openwebui.svg delete mode 100644 packages/manager/public/assets/white/vllm.svg delete mode 100644 packages/manager/src/factories/locks.ts delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelAlerts.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelAlerts.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelAlertsTableRow.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelAlertsTableRow.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelDetail.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelDetailOverview.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelDetailOverview.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelDetailRecipients.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationChannelDetail/NotificationChannelDetailRecipients.tsx delete mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseCaCert.tsx delete mode 100644 packages/manager/src/features/Events/factories/lock.tsx delete mode 100644 packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.test.tsx create mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySection.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySectionView.test.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySectionView.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/MarketplaceLanding.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/ProductSelectionCard.test.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/ProductSelectionCard.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/ProductSelectionCardSkeleton.tsx delete mode 100644 packages/manager/src/features/Marketplace/MarketplaceLanding/styles.ts rename packages/manager/src/features/Marketplace/{MarketplaceLanding => }/marketplaceLazyRoute.tsx (100%) delete mode 100644 packages/manager/src/mocks/presets/crud/handlers/locks.ts delete mode 100644 packages/manager/src/mocks/presets/crud/locks.ts delete mode 100644 packages/manager/src/mocks/presets/crud/seeds/locks.ts delete mode 100644 packages/queries/src/marketplace/index.ts delete mode 100644 packages/queries/src/marketplace/keys.ts delete mode 100644 packages/queries/src/marketplace/marketplace.ts delete mode 100644 packages/queries/src/marketplace/requests.ts diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index f7407c4d4c8..1bc8a5eb477 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,20 +1,3 @@ -## [2026-01-26] - v0.156.0 - - -### Fixed: - -- IAM Delegation: fix payload for updateChildAccountDelegates ([#13260](https://github.com/linode/manager/pull/13260)) - -### Tech Stories: - -- Clean up unused VPC IPv6 Large Prefixes tag ([#13245](https://github.com/linode/manager/pull/13245)) - -### Upcoming Features: - -- CloudPulse-Alerts: Add `DeleteChannelPayload` type and request for deletion of a notification channel ([#13256](https://github.com/linode/manager/pull/13256)) -- Added locks property to Linode interface,added lock create and delete event keys, refactored Lock types ([#13286](https://github.com/linode/manager/pull/13286)) -- New type `NotificationChannelAlerts`, request `getAlertsByNotificationChannelId` to fetch alerts associated to a notification channel ([#13294](https://github.com/linode/manager/pull/13294)) - ## [2026-01-12] - v0.155.0 diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 47665198bc8..adf478a78a7 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.156.0", + "version": "0.155.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 22956b38a27..9c952bd2dd7 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -93,6 +93,7 @@ export const accountCapabilities = [ 'Vlans', 'VPCs', 'VPC Dual Stack', + 'VPC IPv6 Large Prefixes', ] as const; export type AccountCapability = (typeof accountCapabilities)[number]; @@ -434,8 +435,6 @@ export const EventActionKeys = [ 'lke_pool_delete', 'lke_pool_recycle', 'lke_token_rotate', - 'lock_create', - 'lock_delete', 'longviewclient_create', 'longviewclient_delete', 'longviewclient_update', diff --git a/packages/api-v4/src/cloudpulse/alerts.ts b/packages/api-v4/src/cloudpulse/alerts.ts index 5fecd5b21e4..33e59842ada 100644 --- a/packages/api-v4/src/cloudpulse/alerts.ts +++ b/packages/api-v4/src/cloudpulse/alerts.ts @@ -23,7 +23,6 @@ import type { EditAlertDefinitionPayload, EditNotificationChannelPayload, NotificationChannel, - NotificationChannelAlerts, } from './types'; export const createAlertDefinition = ( @@ -173,25 +172,3 @@ export const updateNotificationChannel = ( setMethod('PUT'), setData(data, editNotificationChannelPayloadSchema), ); - -export const deleteNotificationChannel = (channelId: number) => - Request( - setURL( - `${API_ROOT}/monitor/alert-channels/${encodeURIComponent(channelId)}`, - ), - setMethod('DELETE'), - ); - -export const getAlertsByNotificationChannelId = ( - channelId: number, - params?: Params, - filters?: Filter, -) => - Request>( - setURL( - `${API_ROOT}/monitor/alert-channels/${encodeURIComponent(channelId)}/alerts`, - ), - setMethod('GET'), - setParams(params), - setXFilter(filters), - ); diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 58661ef0b5e..43f725b6c7d 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -489,18 +489,3 @@ export interface EditNotificationChannelPayloadWithId */ channelId: number; } - -export interface DeleteChannelPayload { - /** - * The ID of the channel to delete. - */ - channelId: number; -} - -export interface NotificationChannelAlerts { - id: number; - label: string; - service_type: CloudPulseServiceType; - type: 'alerts-definitions'; - url: string; -} diff --git a/packages/api-v4/src/delivery/types.ts b/packages/api-v4/src/delivery/types.ts index 84f2d82460a..cb8691f8211 100644 --- a/packages/api-v4/src/delivery/types.ts +++ b/packages/api-v4/src/delivery/types.ts @@ -1,7 +1,6 @@ export const streamStatus = { Active: 'active', Inactive: 'inactive', - Provisioning: 'provisioning', } as const; export type StreamStatus = (typeof streamStatus)[keyof typeof streamStatus]; diff --git a/packages/api-v4/src/iam/delegation.ts b/packages/api-v4/src/iam/delegation.ts index 22e5da8f911..333546ad6e9 100644 --- a/packages/api-v4/src/iam/delegation.ts +++ b/packages/api-v4/src/iam/delegation.ts @@ -64,7 +64,7 @@ export const updateChildAccountDelegates = ({ `${BETA_API_ROOT}/iam/delegation/child-accounts/${encodeURIComponent(euuid)}/users`, ), setMethod('PUT'), - setData({ users }), + setData(users), ); export const getMyDelegatedChildAccounts = ({ diff --git a/packages/api-v4/src/iam/types.ts b/packages/api-v4/src/iam/types.ts index 4196c0c25cb..3ffeaa29952 100644 --- a/packages/api-v4/src/iam/types.ts +++ b/packages/api-v4/src/iam/types.ts @@ -97,12 +97,9 @@ export type AccountAdmin = | 'list_default_firewalls' | 'list_delegate_users' | 'list_enrolled_beta_programs' - | 'list_entities' - | 'list_role_permissions' | 'list_service_transfers' | 'list_user_delegate_accounts' | 'list_user_grants' - | 'list_user_permissions' | 'revoke_profile_app' | 'revoke_profile_device' | 'send_profile_phone_number_verification_code' @@ -257,11 +254,8 @@ export type AccountViewer = | 'list_available_services' | 'list_default_firewalls' | 'list_enrolled_beta_programs' - | 'list_entities' - | 'list_role_permissions' | 'list_service_transfers' | 'list_user_grants' - | 'list_user_permissions' | 'view_account' | 'view_account_login' | 'view_account_settings' diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 04e915b48ad..0d298c662a9 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -1,6 +1,5 @@ import type { MaintenancePolicySlug } from '../account/types'; import type { CloudPulseAlertsPayload } from '../cloudpulse/types'; -import type { LockType } from '../locks/types'; import type { IPAddress, IPRange } from '../networking/types'; import type { LinodePlacementGroupPayload } from '../placement-groups/types'; import type { Region, RegionSite } from '../regions'; @@ -45,7 +44,6 @@ export interface Linode { ipv6: null | string; label: string; lke_cluster_id: null | number; - locks: LockType[]; maintenance_policy?: MaintenancePolicySlug; placement_group: LinodePlacementGroupPayload | null; region: string; diff --git a/packages/api-v4/src/locks/types.ts b/packages/api-v4/src/locks/types.ts index e5d8e7d5c2c..5528af7b249 100644 --- a/packages/api-v4/src/locks/types.ts +++ b/packages/api-v4/src/locks/types.ts @@ -1,4 +1,3 @@ -import type { Entity } from '../account/types'; import type { EntityType } from '../entities'; /** @@ -6,6 +5,16 @@ import type { EntityType } from '../entities'; */ export type LockType = 'cannot_delete' | 'cannot_delete_with_subresources'; +/** + * Entity information attached to a lock + */ +export interface LockEntity { + id: number | string; + label?: string; + type: EntityType; + url?: string; +} + /** * Request payload for creating a lock * POST /v4beta/locks @@ -25,7 +34,7 @@ export interface CreateLockPayload { */ export interface ResourceLock { /** Information about the locked entity */ - entity: Entity; + entity: LockEntity; /** Unique identifier for the lock */ id: number; /** Type of lock applied */ diff --git a/packages/api-v4/src/marketplace/marketplace.ts b/packages/api-v4/src/marketplace/marketplace.ts index 845326f7807..e253226a592 100644 --- a/packages/api-v4/src/marketplace/marketplace.ts +++ b/packages/api-v4/src/marketplace/marketplace.ts @@ -29,7 +29,7 @@ export const getMarketplaceProducts = (params?: Params, filters?: Filter) => export const getMarketplaceProduct = (productId: number) => Request( setURL( - `${BETA_API_ROOT}/marketplace/products/${encodeURIComponent(productId)}/details`, + `${BETA_API_ROOT}/marketplace/products/${encodeURIComponent(productId)}`, ), setMethod('GET'), ); diff --git a/packages/api-v4/src/marketplace/types.ts b/packages/api-v4/src/marketplace/types.ts index 179b7591c0e..430f4dc395d 100644 --- a/packages/api-v4/src/marketplace/types.ts +++ b/packages/api-v4/src/marketplace/types.ts @@ -1,59 +1,42 @@ export interface MarketplaceProductDetail { - documentation?: string; - overview?: { + documentation: string; + overview: { description: string; }; - pricing?: string; - support?: string; + pricing: string; + support: string; } export interface MarketplaceProduct { category_ids: number[]; - created_at: string; - created_by: string; details?: MarketplaceProductDetail; id: number; info_banner?: string; - logo_url: string; name: string; partner_id: number; product_tags?: string[]; short_description: string; - tile_tag?: string; + title_tag?: string; type_id: number; - updated_at?: string; - updated_by?: string; } export interface MarketplaceCategory { - created_at: string; - created_by: string; + category: string; id: number; - name: string; - products_count: number; - updated_at?: string; - updated_by?: string; + product_count: number; } export interface MarketplaceType { - created_at: string; - created_by: string; id: number; name: string; - products_count: number; - updated_at?: string; - updated_by?: string; + product_count: number; } export interface MarketplacePartner { - created_at: string; - created_by: string; id: number; - logo_url_dark_mode: string; logo_url_light_mode: string; + logo_url_night_mode?: string; name: string; - updated_at?: string; - updated_by?: string; url: string; } diff --git a/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md b/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md deleted file mode 100644 index 5ba4c7ca4d2..00000000000 --- a/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Fix error handling in ChildAccountList component ([#13299](https://github.com/linode/manager/pull/13299)) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index d9126407e8b..b4bfcd99a93 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,72 +4,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [2026-01-26] - v1.158.0 - - -### Added: - -- Add Mistral 7B instruct and ChromaDB to the Marketplace ([#13270](https://github.com/linode/manager/pull/13270)) -- Logs Stream - Provisioning status ([#13284](https://github.com/linode/manager/pull/13284)) - -### Changed: - -- Default selection of network interface type to linode interface in Linode create flow ([#13221](https://github.com/linode/manager/pull/13221)) -- Update Generational Plans default sort to show newest (G8) -> oldest (G6) ([#13234](https://github.com/linode/manager/pull/13234)) -- Billing: Disable 'Make payment'button for Akamai users ([#13243](https://github.com/linode/manager/pull/13243)) -- NLB post-demo feedback-fix empty state title casing,rename LKE-E to Cluster and adjust column visibility for smaller screens to prioritize IPv6 ([#13250](https://github.com/linode/manager/pull/13250)) -- NLB post-demo feedback-Resolve special character filtering issue in Nodes table IPv6 column and add NLB to GoTo quick navigation ([#13251](https://github.com/linode/manager/pull/13251)) -- Changes related to private IP field in create linode flow ([#13253](https://github.com/linode/manager/pull/13253)) -- Optimize NLB table column widths and add back button on detail pages ([#13258](https://github.com/linode/manager/pull/13258)) -- Improvements in Add Network interface drawer ([#13264](https://github.com/linode/manager/pull/13264)) -- Handle Incompatibility of linode interfaces in create LKE flow ([#13272](https://github.com/linode/manager/pull/13272)) -- Apply UX and user feedback for linode interfaces feature in Account settings page ([#13280](https://github.com/linode/manager/pull/13280)) -- Apply UX and user feedback for linode interfaces feature in Create linode page ([#13281](https://github.com/linode/manager/pull/13281)) -- Logs texts updates after tech writing review ([#13291](https://github.com/linode/manager/pull/13291)) -- Update design-language-system to v5.3.2 ([#13293](https://github.com/linode/manager/pull/13293)) - -### Fixed: - -- Hide dual stack option if no IPv6 prefixes available in create VPC flow ([#13245](https://github.com/linode/manager/pull/13245)) -- IAM Delegation: UX copy update, wrong breadcrumb fix ([#13259](https://github.com/linode/manager/pull/13259)) -- IAM Delegation: fix payload for updateChildAccountDelegates ([#13260](https://github.com/linode/manager/pull/13260)) -- IAM hydration error on User Detail pages ([#13265](https://github.com/linode/manager/pull/13265)) -- IAM: removing entity/role can cause an empty page ([#13268](https://github.com/linode/manager/pull/13268)) -- IAM Delegation: UI issues in Default Entity Access table, Default Roles labels/messages, and missing Make a Payment tooltip ([#13275](https://github.com/linode/manager/pull/13275)) -- IAM Delegation: fix payload for changing role flow ([#13279](https://github.com/linode/manager/pull/13279)) -- IAM Delegation: User selector not working in Assign Role/Roles drawer ([#13282](https://github.com/linode/manager/pull/13282)) -- IAM: changing entity/role can cause an empty page ([#13285](https://github.com/linode/manager/pull/13285)) -- Wrong time range sent in metrics payload on preference reload in `CloudPulse metrics` ([#13287](https://github.com/linode/manager/pull/13287)) -- IAM routing cleanup ([#13288](https://github.com/linode/manager/pull/13288)) -- Copy in Plans Panel generational plans tooltip ([#13289](https://github.com/linode/manager/pull/13289)) -- ACLP-Alerting List sorting from service_type to service_type label ([#13295](https://github.com/linode/manager/pull/13295)) -- End character validation for ACLP-Alerting Notification Channel form for name field ([#13297](https://github.com/linode/manager/pull/13297)) -- IAM DElegation: remove restriction to update user delegation with empty array, update the delegations after reopening a drawer ([#13300](https://github.com/linode/manager/pull/13300)) - -### Tech Stories: - -- IAM: Cleanup `iamRbacPrimaryNavChanges` feature flag ([#13232](https://github.com/linode/manager/pull/13232)) -- Bump jspdf to 4.0.0 ([#13248](https://github.com/linode/manager/pull/13248)) -- IAM - Clean up beta flag + BETA/LA logic ([#13266](https://github.com/linode/manager/pull/13266)) - -### Tests: - -- Fix time range specification in `timerange-verification.spec.ts` ([#13240](https://github.com/linode/manager/pull/13240)) -- Fix issue in 'chooseRegion' util when specifying an override region ([#13277](https://github.com/linode/manager/pull/13277)) - -### Upcoming Features: - -- CloudPulse-Alerts: Filter linode resources based on associated aclp alerts ([#13163](https://github.com/linode/manager/pull/13163)) -- Add reusable Product Selection Card component for Marketplace ([#13247](https://github.com/linode/manager/pull/13247)) -- CloudPulse-Alerts: Add support for delete action for user alert channels ([#13256](https://github.com/linode/manager/pull/13256)) -- Add Breadcrumb to Marketplace product landing page ([#13257](https://github.com/linode/manager/pull/13257)) -- Add service URIs to Database summary tab ([#13261](https://github.com/linode/manager/pull/13261)) -- Implement the main product grid with category grouping ([#13267](https://github.com/linode/manager/pull/13267)) -- IAM Parent/Child - Various fixes to Parent Account Flow ([#13278](https://github.com/linode/manager/pull/13278)) -- Add MSW crud for Resource Locking feature(RESPROT2) ([#13286](https://github.com/linode/manager/pull/13286)) -- Associated Alerts Table to ACLP-Alerting Notification Channel Details ([#13294](https://github.com/linode/manager/pull/13294)) -- CloudPulse-Alerts: Exclude account/region alerts in api payload while updating alerts for a linode and fix state reset issue on save ([#13301](https://github.com/linode/manager/pull/13301)) - ## [2026-01-12] - v1.157.0 diff --git a/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts b/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts index 9890547fdbc..14e4ada60c6 100644 --- a/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts @@ -27,6 +27,8 @@ describe('Maintenance', () => { // TODO When the Host & VM Maintenance feature rolls out, we want to enable the feature flag and update the test. mockAppendFeatureFlags({ + // TODO M3-10491 - Remove "iamRbacPrimaryNavChanges" feature flag mock once feature flag is deleted. + iamRbacPrimaryNavChanges: true, vmHostMaintenance: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/account/network-settings.spec.ts b/packages/manager/cypress/e2e/core/account/network-settings.spec.ts index 50a9cea238f..ef19a5788e8 100644 --- a/packages/manager/cypress/e2e/core/account/network-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/account/network-settings.spec.ts @@ -28,9 +28,9 @@ import type { LinodeInterfaceAccountSetting } from '@linode/api-v4'; const interfaceTypeMap = { legacy_config_default_but_linode_allowed: - 'Configuration Profile Interfaces (default) but allow Linode Interfaces', + 'Configuration Profile Interfaces but allow Linode Interfaces', linode_default_but_legacy_config_allowed: - 'Linode Interfaces (default) but allow Configuration Profile Interfaces', + 'Linode Interfaces but allow Configuration Profile Interfaces', legacy_config_only: 'Configuration Profile Interfaces Only', linode_only: 'Linode Interfaces Only', }; @@ -54,7 +54,7 @@ describe('Account network settings', () => { describe('Network interface types', () => { /* * - Confirms that customers can update their account-level Linode interface type. - * - Confirms that "Allowed interfaces for new Linodes" drop-down displays user's set value on page load. + * - Confirms that "Interfaces for new Linodes" drop-down displays user's set value on page load. * - Confirms that save button is initially disabled, but becomes enabled upon changing the selection. * - Confirms that outgoing API request contains expected payload data for chosen interface type. * - Confirms that toast appears upon successful settings update operation. @@ -81,7 +81,7 @@ describe('Account network settings', () => { // Confirm that selected interface type matches API response, and that // "Save" button is disabled by default. - cy.findByLabelText('Allowed interfaces for new Linodes').should( + cy.findByLabelText('Interfaces for new Linodes').should( 'have.value', interfaceTypeMap[defaultInterface] ); @@ -89,7 +89,7 @@ describe('Account network settings', () => { // Confirm that changing selection causes "Save" button to become enabled, // then changing back causes it to become disabled again. - cy.findByLabelText('Allowed interfaces for new Linodes').click(); + cy.findByLabelText('Interfaces for new Linodes').click(); ui.autocompletePopper.find().within(() => { cy.findByText(interfaceTypeMap[otherInterfaces[0]]) .should('be.visible') @@ -97,7 +97,7 @@ describe('Account network settings', () => { }); ui.button.findByTitle('Save').should('be.enabled'); - cy.findByLabelText('Allowed interfaces for new Linodes').click(); + cy.findByLabelText('Interfaces for new Linodes').click(); ui.autocompletePopper.find().within(() => { cy.findByText(interfaceTypeMap[defaultInterface]) .should('be.visible') @@ -108,7 +108,7 @@ describe('Account network settings', () => { // Confirm that we can update our setting using every other choice, // and that the outgoing API request payload contains the expected value. otherInterfaces.forEach((otherInterface, index) => { - cy.findByLabelText('Allowed interfaces for new Linodes').click(); + cy.findByLabelText('Interfaces for new Linodes').click(); ui.autocompletePopper.find().within(() => { cy.findByText(interfaceTypeMap[otherInterface]) .should('be.visible') @@ -161,7 +161,7 @@ describe('Account network settings', () => { .should('be.visible') .within(() => { cy.findByText('Network Interface Type').should('be.visible'); - cy.findByLabelText('Allowed interfaces for new Linodes') + cy.findByLabelText('Interfaces for new Linodes') .should('be.visible') .click(); ui.autocompletePopper.find().within(() => { diff --git a/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts b/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts index bfaf543c4a2..6d972eb36b9 100644 --- a/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts +++ b/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts @@ -1,12 +1,94 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; +describe('Quotas accessible when limitsEvolution feature flag enabled', () => { + // TODO M3-10491 - Remove `describe` block and move tests to parent scope once `iamRbacPrimaryNavChanges` feature flag is removed. + describe('When IAM RBAC account navigation feature flag is enabled', () => { + beforeEach(() => { + // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. + mockAppendFeatureFlags({ + limitsEvolution: { + enabled: true, + }, + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` mock once feature flag is removed. + iamRbacPrimaryNavChanges: true, + }).as('getFeatureFlags'); + }); + + it('can navigate directly to Quotas page', () => { + cy.visitWithLogin('/quotas'); + cy.wait('@getFeatureFlags'); + cy.url().should('endWith', '/quotas'); + cy.contains( + 'View your Object Storage quotas by applying the endpoint filter below' + ).should('be.visible'); + }); + + it('can navigate to the Quotas page via the User Menu', () => { + cy.visitWithLogin('/'); + cy.wait('@getFeatureFlags'); + // Open user menu + ui.userMenuButton.find().click(); + ui.userMenu.find().within(() => { + cy.get('[data-testid="menu-item-Quotas"]').should('be.visible').click(); + cy.url().should('endWith', '/quotas'); + }); + }); + }); + + // TODO M3-10491 - Remove `describe` block and tests once "iamRbacPrimaryNavChanges" feature flag is removed. + describe('When IAM RBAC account navigation feature flag is disabled', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + limitsEvolution: { + enabled: true, + }, + iamRbacPrimaryNavChanges: false, + }).as('getFeatureFlags'); + }); + + it('can navigate directly to Quotas page', () => { + cy.visitWithLogin('/account/quotas'); + cy.wait('@getFeatureFlags'); + cy.url().should('endWith', '/account/quotas'); + cy.contains( + 'View your Object Storage quotas by applying the endpoint filter below' + ).should('be.visible'); + }); + + it('can navigate to the Quotas page via the User Menu', () => { + cy.visitWithLogin('/'); + cy.wait('@getFeatureFlags'); + // Open user menu + ui.userMenuButton.find().click(); + ui.userMenu.find().within(() => { + cy.get('[data-testid="menu-item-Quotas"]').should('be.visible').click(); + cy.url().should('endWith', '/quotas'); + }); + }); + + it('Quotas tab is visible from all other tabs in Account tablist', () => { + cy.visitWithLogin('/account/billing'); + cy.wait('@getFeatureFlags'); + ui.tabList.find().within(() => { + cy.get('a').each(($link) => { + cy.wrap($link).click(); + cy.get('[data-testid="Quotas"]').should('be.visible'); + }); + }); + cy.get('[data-testid="Quotas"]').should('be.visible').click(); + cy.url().should('endWith', '/quotas'); + }); + }); +}); + describe('Quotas inaccessible when limitsEvolution feature flag disabled', () => { beforeEach(() => { mockAppendFeatureFlags({ limitsEvolution: { enabled: false, }, + iamRbacPrimaryNavChanges: true, }).as('getFeatureFlags'); }); diff --git a/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts b/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts index a03280a344b..61c93e1f830 100644 --- a/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts +++ b/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts @@ -106,6 +106,8 @@ describe('restricted user details pages', () => { mockAppendFeatureFlags({ apl: false, dbaasV2: { beta: false, enabled: false }, + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + iamRbacPrimaryNavChanges: true, }); }); diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 16d14933dab..6d0d8e4649e 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -14,6 +14,7 @@ import { mockInitiateEntityTransferError, mockReceiveEntityTransfer, } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; @@ -126,6 +127,14 @@ describe('Account service transfers', () => { cleanUp(['service-transfers', 'linodes', 'lke-clusters']); }); + beforeEach(() => { + // Mock the iamRbacPrimaryNavChanges feature flag to be disabled. + mockAppendFeatureFlags({ + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + iamRbacPrimaryNavChanges: true, + }).as('getFeatureFlags'); + }); + /* * - Confirms user can navigate to service transfer page via user menu. */ diff --git a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts index 3a392ef5f89..8efd2e884d0 100644 --- a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts @@ -170,7 +170,9 @@ const assertBillingAccessSelected = ( describe('User permission management', () => { beforeEach(() => { + // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts index 91afe8661f6..b343ac638d5 100644 --- a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts @@ -81,7 +81,9 @@ const initTestUsers = (profile: Profile, enableChildAccountAccess: boolean) => { describe('Users landing page', () => { beforeEach(() => { + // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts index 774e8461400..90de20fa000 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts @@ -9,6 +9,7 @@ import { mockGetInvoice, mockGetInvoiceItems, } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { buildArray } from 'support/util/arrays'; import { formatUsd } from 'support/util/currency'; @@ -30,6 +31,12 @@ const getRegionLabel = (regionId: string) => { }; describe('Account invoices', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + iamRbacPrimaryNavChanges: true, + }); + }); /* * - Confirms that invoice items are listed on invoice details page using mock API data. * - Confirms that each invoice item is displayed with correct accompanying info. diff --git a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts b/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts index 758d517e18d..4ebaf70e8be 100644 --- a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts @@ -1,4 +1,5 @@ import { mockGetAccount } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { ui } from 'support/ui'; @@ -12,6 +13,10 @@ describe('Credit Card Expired Banner', () => { mockGetUserPreferences({ dismissed_notifications: {}, }); + mockAppendFeatureFlags({ + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + iamRbacPrimaryNavChanges: true, + }).as('getFeatureFlags'); }); it('appears when the expiration date is in the past', () => { diff --git a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts index 4b164903407..b13bfd3f8c9 100644 --- a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts @@ -1,4 +1,5 @@ import { mockGetPaymentMethods } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import type { CreditCardData, PaymentMethod } from '@linode/api-v4'; @@ -56,6 +57,13 @@ const braintreeURL = 'https://+(payments.braintree-api.com|payments.sandbox.braintree-api.com)/*'; describe('Google Pay', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + iamRbacPrimaryNavChanges: true, + }); + }); + it('adds google pay method', () => { cy.intercept(braintreeURL).as('braintree'); mockGetPaymentMethods(mockPaymentMethods).as('getPaymentMethods'); diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index 4a95323e0c7..febde9e4661 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -6,6 +6,7 @@ import { grantsFactory, profileFactory } from '@linode/utilities'; import { paymentMethodFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { mockGetPaymentMethods, mockGetUser } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetProfile, mockGetProfileGrants, @@ -223,6 +224,13 @@ const assertMakeAPaymentEnabled = () => { describe('restricted user billing flows', () => { beforeEach(() => { mockGetPaymentMethods(mockPaymentMethods); + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, + iam: { + enabled: false, + }, + }); }); /* @@ -259,7 +267,8 @@ describe('restricted user billing flows', () => { assertEditBillingInfoDisabled(restrictedUserTooltip); assertAddPaymentMethodDisabled(restrictedUserTooltip); assertMakeAPaymentDisabled( - `You don't have permissions to make a payment. Please contact your ${ADMINISTRATOR} to request the necessary permissions.` + restrictedUserTooltip + + ` Please contact your ${ADMINISTRATOR} to request the necessary permissions.` ); }); @@ -288,7 +297,8 @@ describe('restricted user billing flows', () => { assertEditBillingInfoDisabled(restrictedUserTooltip); assertAddPaymentMethodDisabled(restrictedUserTooltip); assertMakeAPaymentDisabled( - `You don't have permissions to make a payment. Please contact your ${PARENT_USER} to request the necessary permissions.` + restrictedUserTooltip + + ` Please contact your ${PARENT_USER} to request the necessary permissions.` ); }); diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index 7d5925f57a8..d130776eb56 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -118,6 +118,8 @@ authenticate(); describe('Billing Activity Feed', () => { beforeEach(() => { mockAppendFeatureFlags({ + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index f2b1701a8de..adb9afa816c 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -235,7 +235,7 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { { ascending: [2, 4, 1, 3], column: 'status', descending: [1, 3, 2, 4] }, { ascending: [1, 2, 3, 4], - column: 'service_type_label', + column: 'service_type', descending: [3, 4, 1, 2], }, { diff --git a/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts index 8f6165c5ce7..e6eb5094c01 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts @@ -99,7 +99,7 @@ describe('Integration Tests for Grouping Alerts by Tags on the CloudPulse Alerts ui.button.findByAttribute('aria-label', 'Toggle group by tag').click(); // Validate table headers are visible - ['label', 'status', 'service_type_label', 'created_by', 'updated'].forEach( + ['label', 'status', 'service_type', 'created_by', 'updated'].forEach( (header) => { ui.heading.findByText(header).should('be.visible'); } diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index e6c47d2cd93..416da6e8546 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -99,6 +99,49 @@ const databaseMock: Database = databaseFactory.build({ const mockProfile = profileFactory.build({ timezone: 'UTC', }); +/** + * Generates a date in Indian Standard Time (IST) based on a specified number of days offset, + * hour, and minute. The function also provides individual date components such as day, hour, + * minute, month, and AM/PM. + * + * @param {number} daysOffset - The number of days to adjust from the current date. Positive + * values give a future date, negative values give a past date. + * @param {number} hour - The hour to set for the resulting date (0-23). + * @param {number} [minute=0] - The minute to set for the resulting date (0-59). Defaults to 0. + * + * @returns {Object} - Returns an object containing: + * - `actualDate`: The formatted date and time in GMT (YYYY-MM-DD HH:mm). + * - `day`: The day of the month as a number. + * - `hour`: The hour in the 24-hour format as a number. + * - `minute`: The minute of the hour as a number. + * - `month`: The month of the year as a number. + */ +const getDateRangeInGMT = ( + hour: number, + minute: number = 0, + isStart: boolean = false +) => { + const now = DateTime.now().setZone('GMT'); // Set the timezone to GMT + const targetDate = isStart + ? now.startOf('month').set({ hour, minute }).setZone('GMT') + : now.set({ hour, minute }).setZone('GMT'); + const actualDate = targetDate.setZone('GMT').toFormat('yyyy-LL-dd HH:mm'); + + const previousMonthDate = targetDate.minus({ months: 1 }); + + return { + actualDate, + day: targetDate.day, + hour: targetDate.hour, + minute: targetDate.minute, + month: targetDate.toFormat('LLLL'), + year: targetDate.year, + daysInMonth: targetDate.daysInMonth, + previousMonth: previousMonthDate.toFormat('LLLL'), + previousYear: previousMonthDate.year, + }; +}; + /** * This function calculates the start of the current month and the current date and time, * adjusted by subtracting 5 hours and 30 minutes, and returns them in the ISO 8601 format (UTC). @@ -133,7 +176,33 @@ const getLastMonthRange = (): DateTimeWithPreset => { }; }; -describe('Integration tests for verifying Cloudpulse custom and preset configurations', () => { +const convertToGmt = (dateStr: string): string => { + return DateTime.fromISO(dateStr.replace(' ', 'T')).toFormat( + 'yyyy-MM-dd HH:mm' + ); +}; +const formatToUtcDateTime = (dateStr: string): string => { + return DateTime.fromISO(dateStr) + .toUTC() // 🌍 keep it in UTC + .toFormat('yyyy-MM-dd HH:mm'); +}; + +/* + * TODO Fix or migrate the tests in `timerange-verification.spec.ts`. + * + * The tests in this spec frequently fail during specific dates and time periods + * throughout the day and year. Because there are so many tests in this spec, the + * timeouts and subsequent failures can delay test runs by several (45+) minutes + * which frequently interferes with unrelated test runs. + * + * Other considerations: + * + * - Would unit tests or component tests be a better fit for this? + * + * - Are these tests adding any value? They fail frequently and the failures do + * not get reviewed. They do not seem to be protecting us from regressions. + */ +describe.skip('Integration tests for verifying Cloudpulse custom and preset configurations', () => { /* * - Mocks user preferences for dashboard details (dashboard, engine, resources, and region). * - Simulates loading test data without real API calls. @@ -177,10 +246,6 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura mockGetDatabases([databaseMock]).as('fetchDatabases'); cy.visitWithLogin('/metrics'); - - cy.get('[aria-label="Content is loading"]', { timeout: 1000 }).should( - 'not.exist' - ); cy.wait([ '@fetchServices', '@fetchDashboard', @@ -188,58 +253,55 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura '@fetchDatabases', ]); }); - it('should implement and validate custom date/time picker for a specific date and time range', () => { - // --- Mock the start date to ensure consistent test results --- - - const MOCK_START_DATE = new Date('2025-08-01'); - - cy.clock(MOCK_START_DATE.getTime(), ['Date']); - - // --- Define date/time values for the test --- - - const startDayOfMonth = 1; - const endDayOfMonth = 3; - const startHour = 1; - const startMinute = 15; - const endHour = 2; - const endMinute = 45; - - const ACTUAL_START_DATE_TIME = '2025-08-01 01:15'; - const ACTUAL_END_DATE_TIME = '2025-08-03 02:45'; - // --- Reset the date/time picker to a known state --- - ui.button.findByTitle('Last hour').click(); - - ui.button.findByTitle('Reset').should('be.visible').click(); + it('should implement and validate custom date/time picker for a specific date and time range', () => { + // --- Generate start and end date/time in GMT --- + const { + actualDate: startActualDate, + day: startDay, + hour: startHour, + minute: startMinute, + month: startMonth, + year: startYear, + previousMonth, + previousYear, + daysInMonth, + } = getDateRangeInGMT(12, 15, true); + + const { + actualDate: endActualDate, + day: endDay, + hour: endHour, + minute: endMinute, + } = getDateRangeInGMT(12, 30); + + cy.wait(1000); + // --- Select start date --- + ui.button.findByTitle('Last hour').as('startDateInput'); - cy.get('[data-qa-preset="Reset"]').should( - 'have.attr', - 'aria-selected', - 'true' - ); + cy.get('@startDateInput').scrollIntoView(); - // --- Open the date picker dialog and select start/end days --- + cy.get('@startDateInput').click(); cy.get('[role="dialog"]').within(() => { - // --- Select start and end day --- - cy.findAllByText(startDayOfMonth).first().click(); - cy.findAllByText(endDayOfMonth).first().click(); + cy.findAllByText(startDay).first().click(); + cy.findAllByText(endDay).first().click(); }); - // --- Select start time (hours and minutes) in the time picker --- ui.button .findByAttribute('aria-label^', 'Choose time') .first() - .should('be.visible', { timeout: 10000 }) + .should('be.visible', { timeout: 10000 }) // waits up to 10 seconds .as('timePickerButton'); - cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. + cy.get(`[aria-label="${startHour} hours"]`).click(); + cy.wait(1000); ui.button .findByAttribute('aria-label^', 'Choose time') .first() @@ -248,9 +310,9 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton', { timeout: 15000 }).wait(300).first().click(); + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); - cy.get(`[aria-label="${startMinute} minutes"]`).first().click(); + cy.get(`[aria-label="${startMinute} minutes"]`).click(); ui.button .findByAttribute('aria-label^', 'Choose time') @@ -265,48 +327,44 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.findByLabelText('Select meridiem') .as('startMeridiemSelect') .scrollIntoView(); - cy.get('@startMeridiemSelect').find('[aria-label="AM"]').click(); + cy.get('@startMeridiemSelect').find('[aria-label="PM"]').click(); - // --- Select end time (hours and minutes) in the time picker --- + // --- Select end time --- ui.button .findByAttribute('aria-label^', 'Choose time') .last() .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); - cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - - cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); + cy.get('@timePickerButton', { timeout: 15000 }).click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. - cy.get(`[aria-label="${endHour} hours"]`).last().click(); + cy.findByLabelText('Select hours').scrollIntoView({ + duration: 500, + easing: 'linear', + }); + cy.get(`[aria-label="${endHour} hours"]`).click(); - ui.button - .findByAttribute('aria-label^', 'Choose time') + cy.get('[aria-label^="Choose time"]') .last() - .should('be.visible', { timeout: 10000 }) + .should('be.visible') .as('timePickerButton'); - cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - - cy.get('@timePickerButton', { timeout: 15000 }).wait(300).last().click(); + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); - cy.get(`[aria-label="${endMinute} minutes"]`).last().click(); + cy.get(`[aria-label="${endMinute} minutes"]`).click(); - ui.button - .findByAttribute('aria-label^', 'Choose time') + cy.get('[aria-label^="Choose time"]') .last() .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); - cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); cy.findByLabelText('Select meridiem') .as('endMeridiemSelect') .scrollIntoView(); - cy.get('@endMeridiemSelect').find('[aria-label="AM"]').click(); + cy.get('@endMeridiemSelect').find('[aria-label="PM"]').click(); // --- Set timezone --- cy.findByPlaceholderText('Choose a Timezone').as('timezoneInput').clear(); @@ -318,32 +376,119 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura .and('be.enabled') .click(); - // --- Validate that the UI shows the expected start/end date/time --- + // --- Re-validate after apply --- cy.get('[aria-labelledby="start-date"]').should( 'have.value', - `${ACTUAL_START_DATE_TIME} AM` + `${startActualDate} PM` ); - cy.get('[aria-labelledby="end-date"]').should( 'have.value', - `${ACTUAL_END_DATE_TIME} AM` + `${endActualDate} PM` ); + ui.button.findByTitle('Cancel').and('be.enabled').click(); + + // --- Select Node Type --- ui.autocomplete.findByLabel('Node Type').type('Primary{enter}'); - // --- Validate API requests triggered for the selected range --- - cy.wait(['@getMetrics', '@getMetrics', '@getMetrics', '@getMetrics']); + // --- Validate API requests --- + cy.wait(Array(4).fill('@getMetrics')); cy.get('@getMetrics.all') .should('have.length', 4) .each((xhr: unknown) => { const { request: { body }, } = xhr as Interception; - expect(body.absolute_time_duration.start).to.equal( - '2025-08-01T01:15:00Z' + expect(formatToUtcDateTime(body.absolute_time_duration.start)).to.equal( + convertToGmt(startActualDate) + ); + expect(formatToUtcDateTime(body.absolute_time_duration.end)).to.equal( + convertToGmt(endActualDate) + ); + }); + + // --- Test Time Range Presets --- + mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( + 'getPresets' + ); + + // Open the date range picker to apply the "Last 30 Days" preset + + cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + cy.get('@startDateInput').click(); + + ui.button.findByTitle('Last 30 days').should('be.visible').click(); + + cy.get('[data-qa-preset="Last 30 days"]').should( + 'have.attr', + 'aria-selected', + 'true' + ); + + cy.contains(`${previousMonth} ${previousYear}`) + .closest('div') + .next() + .find('[aria-selected="true"]') + .then(($els) => { + const selectedDays = Array.from($els).map((el) => + Number(el.textContent?.trim()) + ); + + expect(daysInMonth, 'daysInMonth should be defined').to.be.a('number'); + + const totalDays = daysInMonth as number; + const expectedCount = totalDays - endDay; + + expect( + selectedDays.length, + 'number of selected days from the previous month for the last-30-days range' + ).to.eq(expectedCount); + expect( + totalDays - selectedDays.length, + 'start day of Last 30 days' + ).to.eq(endDay); + }); + + cy.contains(`${startMonth} ${startYear}`) + .closest('div') + .next() + .find('[aria-selected="true"]') + .then(($els) => { + const selectedDays = Array.from($els).map((el) => + Number(el.textContent?.trim()) + ); + + expect( + selectedDays.length, + 'number of selected days in the current month for the last-30-days range' + ).to.eq(endDay); + expect(Math.max(...selectedDays), 'end day of Last 30 days').to.eq( + endDay + ); + }); + cy.get('[data-qa-buttons="apply"]') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.button + .findByTitle('Last 30 days') + .should('be.visible') + .should('be.enabled'); + + cy.get('@getPresets.all') + .should('have.length', 4) + .each((xhr: unknown) => { + const { + request: { body }, + } = xhr as Interception; + expect(body).to.have.nested.property( + 'relative_time_duration.unit', + 'days' ); - expect(body.absolute_time_duration.end).to.equal( - '2025-08-03T02:45:00Z' + expect(body).to.have.nested.property( + 'relative_time_duration.value', + 30 ); }); }); diff --git a/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts index c1848efea09..ab17f15ce77 100644 --- a/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts @@ -61,7 +61,9 @@ function deleteDestinationViaActionMenu( ui.actionMenuItem.findByTitle('Delete').click(); // Find confirmation modal - cy.findByText(`Are you sure you want to delete "${destination.label}"?`); + cy.findByText( + `Are you sure you want to delete "${destination.label}" destination?` + ); ui.button.findByTitle('Delete').click(); cy.wait('@deleteDestination'); diff --git a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts index 9ae689d23b5..5c9f4ba619e 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts @@ -19,8 +19,6 @@ import { getDestinationTypeOption } from 'src/features/Delivery/deliveryUtils'; import type { AkamaiObjectStorageDetailsExtended } from '@linode/api-v4'; describe('Edit Destination', () => { - const saveChangesButtonText = 'Save Changes'; - beforeEach(() => { mockAppendFeatureFlags({ aclpLogs: { @@ -50,9 +48,7 @@ describe('Edit Destination', () => { ); // Save button should be disabled before test connection - cy.findByRole('button', { name: saveChangesButtonText }).should( - 'be.disabled' - ); + cy.findByRole('button', { name: 'Save' }).should('be.disabled'); // Test connection of the destination form mockTestConnection(400); ui.button @@ -66,9 +62,7 @@ describe('Edit Destination', () => { ); // Save button should be disabled after test connection failed - cy.findByRole('button', { name: saveChangesButtonText }).should( - 'be.disabled' - ); + cy.findByRole('button', { name: 'Save' }).should('be.disabled'); }); it('edit destination with correct data', () => { @@ -81,9 +75,7 @@ describe('Edit Destination', () => { ); // Save button should be disabled before test connection - cy.findByRole('button', { name: saveChangesButtonText }).should( - 'be.disabled' - ); + cy.findByRole('button', { name: 'Save' }).should('be.disabled'); // Test connection of the destination form mockTestConnection(); ui.button @@ -100,7 +92,7 @@ describe('Edit Destination', () => { mockUpdateDestination(mockDestinationPayloadWithId, updatedDestination); mockGetDestinations([updatedDestination]); // Submit the destination edit form - cy.findByRole('button', { name: saveChangesButtonText }) + cy.findByRole('button', { name: 'Save' }) .should('be.enabled') .should('have.attr', 'type', 'button') .click(); diff --git a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts index 95942ae99b4..ee47317290e 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts @@ -22,8 +22,6 @@ import { randomLabel } from 'support/util/random'; import { kubernetesClusterFactory } from 'src/factories'; describe('Edit Stream', () => { - const saveChangesButtonText = 'Save Changes'; - beforeEach(() => { mockAppendFeatureFlags({ aclpLogs: { @@ -50,14 +48,14 @@ describe('Edit Stream', () => { const updatedLabel = randomLabel(); // Change the Name - cy.findByLabelText('Stream Name') + cy.findByLabelText('Name') .should('be.visible') .should('be.enabled') .should('have.value', mockAuditLogsStream.label); logsStreamForm.setLabel(updatedLabel); - cy.findByLabelText('Stream Name') + cy.findByLabelText('Name') .should('be.visible') .should('be.enabled') .should('have.value', updatedLabel); @@ -69,7 +67,7 @@ describe('Edit Stream', () => { .should('have.attr', 'value', 'Audit Logs'); // Save button should be enabled initially - ui.button.findByTitle(saveChangesButtonText).should('be.enabled'); + ui.button.findByTitle('Save').should('be.enabled'); // Test Connection should be disabled for existing destination ui.button.findByTitle('Test Connection').should('be.disabled'); @@ -84,7 +82,7 @@ describe('Edit Stream', () => { ui.button.findByTitle('Test Connection').should('be.enabled'); // Save button should be disabled after changing destination - ui.button.findByTitle(saveChangesButtonText).should('be.disabled'); + ui.button.findByTitle('Save').should('be.disabled'); // Test connection with failure mockTestConnection(400); @@ -95,7 +93,7 @@ describe('Edit Stream', () => { ); // Save button should remain disabled after failed test - ui.button.findByTitle(saveChangesButtonText).should('be.disabled'); + ui.button.findByTitle('Save').should('be.disabled'); // Test connection with success mockTestConnection(200); @@ -106,11 +104,11 @@ describe('Edit Stream', () => { ); // Save button should now be enabled - ui.button.findByTitle(saveChangesButtonText).should('be.enabled'); + ui.button.findByTitle('Save').should('be.enabled'); // Submit the stream edit form - failure in creating destination mockCreateDestination({}, 400); - ui.button.findByTitle(saveChangesButtonText).should('be.enabled').click(); + ui.button.findByTitle('Save').should('be.enabled').click(); ui.toast.assertMessage(`There was an issue creating your destination`); @@ -125,7 +123,7 @@ describe('Edit Stream', () => { mockAuditLogsStream ).as('updateStream'); - ui.button.findByTitle(saveChangesButtonText).click(); + ui.button.findByTitle('Save').click(); cy.wait('@updateStream') .its('request.body') .then((body) => { @@ -178,14 +176,14 @@ describe('Edit Stream', () => { const updatedLabel = randomLabel(); // Change the Name - cy.findByLabelText('Stream Name') + cy.findByLabelText('Name') .should('be.visible') .should('be.enabled') .should('have.value', mockLKEAuditLogsStream.label); logsStreamForm.setLabel(updatedLabel); - cy.findByLabelText('Stream Name') + cy.findByLabelText('Name') .should('be.visible') .should('be.enabled') .should('have.value', updatedLabel); @@ -231,7 +229,7 @@ describe('Edit Stream', () => { logsStreamForm.findClusterCheckbox('all').check(); // Save button should be enabled - ui.button.findByTitle(saveChangesButtonText).should('be.enabled'); + ui.button.findByTitle('Save').should('be.enabled'); // Submit the stream edit form - failure mockUpdateStream( @@ -248,7 +246,7 @@ describe('Edit Stream', () => { 400 ).as('updateStreamFail'); - ui.button.findByTitle(saveChangesButtonText).click(); + ui.button.findByTitle('Save').click(); cy.wait('@updateStreamFail'); ui.toast.assertMessage('There was an issue editing your stream'); @@ -266,7 +264,7 @@ describe('Edit Stream', () => { mockLKEAuditLogsStream ).as('updateStream'); - ui.button.findByTitle(saveChangesButtonText).click(); + ui.button.findByTitle('Save').click(); cy.wait('@updateStream') .its('request.body') .then((body) => { diff --git a/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts index 1ba21b19d60..ad9df1e518b 100644 --- a/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts @@ -64,7 +64,9 @@ function deleteStreamViaActionMenu(tableAlias: string, stream: Stream) { ui.actionMenuItem.findByTitle('Delete').click(); // Find confirmation modal - cy.findByText(`Are you sure you want to delete "${stream.label}"?`); + cy.findByText( + `Are you sure you want to delete "${stream.label}" stream?` + ); ui.button.findByTitle('Delete').click(); cy.wait('@deleteStream'); @@ -113,7 +115,7 @@ function deactivateStreamViaActionMenu(tableAlias: string, stream: Stream) { // Deactivate stream ui.actionMenuItem.findByTitle('Deactivate').click(); - ui.toast.assertMessage(`${stream.label} deactivated`); + ui.toast.assertMessage(`Stream ${stream.label} deactivated`); }); } diff --git a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts index 821ea4a8103..183dc7dd047 100644 --- a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts +++ b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts @@ -1,3 +1,4 @@ +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { pages } from 'support/ui/constants'; import type { Page } from 'support/ui/constants'; @@ -8,6 +9,10 @@ beforeEach(() => { describe('smoke - deep links', () => { beforeEach(() => { cy.visitWithLogin('/null'); + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, + }).as('getFeatureFlags'); }); it('Go to each route and validate deep links', () => { diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 4623ef33e52..07bd5f6edd6 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -1405,13 +1405,6 @@ describe('LKE cluster updates', () => { .findByTitle(`Add a Node Pool: ${mockCluster.label}`) .should('be.visible') .within(() => { - // For the "Dedicated 4 GB", use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - cy.findByText('Dedicated 4 GB') .should('be.visible') .closest('tr') diff --git a/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts b/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts index 43325319275..dcb02fd5f66 100644 --- a/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts @@ -123,12 +123,6 @@ describe('Create flow when beta alerts enabled by region and feature flag', func cy.get('[data-qa-tp="Linode Plan"]') .should('be.visible') .within(() => { - // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - cy.get('[data-qa-plan-row="Dedicated 8 GB"]').click(); }); cy.get('[type="password"]').should('be.visible').scrollIntoView(); @@ -289,12 +283,6 @@ describe('Create flow when beta alerts enabled by region and feature flag', func cy.get('[data-qa-tp="Linode Plan"]') .should('be.visible') .within(() => { - // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - cy.get('[data-qa-plan-row="Dedicated 8 GB"]').click(); }); cy.get('[type="password"]').should('be.visible').scrollIntoView(); @@ -444,12 +432,6 @@ describe('Create flow when beta alerts enabled by region and feature flag', func cy.get('[data-qa-tp="Linode Plan"]') .should('be.visible') .within(() => { - // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - cy.get('[data-qa-plan-row="Dedicated 8 GB"]').click(); }); cy.get('[type="password"]').should('be.visible').scrollIntoView(); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts index b87f8dea920..bfae1112c27 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts @@ -421,9 +421,6 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. cy.findByLabelText('Firewall').should('be.visible'); cy.get('[data-qa-autocomplete="Firewall"]').within(() => { @@ -494,6 +491,9 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. cy.findByLabelText('Public Interface Firewall').should('be.visible'); cy.get('[data-qa-autocomplete="Public Interface Firewall"]').within(() => { @@ -566,9 +566,6 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - cy.findByText('Create Firewall').should('be.visible').click(); ui.drawer @@ -664,6 +661,9 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + cy.findByText('Create Firewall').should('be.visible').click(); ui.drawer @@ -766,9 +766,6 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - // Creating the linode without a firewall should display a warning. ui.button .findByTitle('Create Linode') @@ -872,6 +869,9 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + // Creating the linode without a firewall should display a warning. ui.button .findByTitle('Create Linode') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index 1ed9356b23b..1b4a1c62f3e 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -319,9 +319,6 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - // select existing VLAN. linodeCreatePage.selectInterface('vlan'); // Confirm that mocked VLAN is shown in the Autocomplete, and then select it. @@ -403,6 +400,9 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + // Select VLAN card linodeCreatePage.selectInterface('vlan'); @@ -485,9 +485,6 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - // Select VLAN card linodeCreatePage.selectInterface('vlan'); @@ -570,6 +567,9 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + // Select VLAN card linodeCreatePage.selectInterface('vlan'); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts index 0f9fa5ab253..ea1688c42d8 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -476,9 +476,6 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - // Select VPC linodeCreatePage.selectInterface('vpc'); @@ -615,6 +612,9 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + // Select VPC option linodeCreatePage.selectInterface('vpc'); @@ -750,9 +750,6 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to legacy Config Interfaces - linodeCreatePage.selectLegacyConfigInterfacesType(); - // Select VPC card linodeCreatePage.selectInterface('vpc'); @@ -936,6 +933,9 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to Linode Interfaces + linodeCreatePage.selectLinodeInterfacesType(); + // Select VPC card linodeCreatePage.selectInterface('vpc'); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index eb6eec87f92..ef7213c5fae 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -93,16 +93,6 @@ describe('Create Linode', () => { linodeCreatePage.setLabel(linodeLabel); linodeCreatePage.selectImage('Debian 12'); linodeCreatePage.selectRegionById(linodeRegion.id); - - // For the "Dedicated 4 GB" plan under the "Dedicated CPU" plan type, use filter to select G6 Dedicated instead of relying on pagination - if (planConfig.planType === 'Dedicated CPU') { - ui.autocomplete.findByLabel('Dedicated Plans').click(); - - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - } - linodeCreatePage.selectPlan( planConfig.planType, planConfig.planLabel diff --git a/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts b/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts index 3481b60fa99..8126eac56bf 100644 --- a/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts @@ -123,6 +123,12 @@ describe('maintenance policy region support - Linode Details > Settings', () => cleanUp(['linodes', 'lke-clusters']); }); + beforeEach(() => { + mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: false, + }).as('getFeatureFlags'); + }); + it('disables maintenance policy selector when region does not support it', () => { // Mock a linode in a region that doesn't support maintenance policies const mockRegion = regionFactory.build({ diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index efaa1174406..3cacea66f7f 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -69,7 +69,9 @@ const preferenceOverrides = { authenticate(); describe('linode landing checks', () => { beforeEach(() => { + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, @@ -476,6 +478,10 @@ describe('linode landing checks', () => { describe('linode landing checks for empty state', () => { beforeEach(() => { + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, + }); // Mock setup to display the Linode landing page in an empty state mockGetLinodes([]).as('getLinodes'); }); @@ -578,6 +584,10 @@ describe('linode landing checks for empty state', () => { describe('linode landing checks for non-empty state with restricted user', () => { beforeEach(() => { + // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. + mockAppendFeatureFlags({ + iamRbacPrimaryNavChanges: true, + }); // Mock setup to display the Linode landing page in an non-empty state const mockLinodes: Linode[] = new Array(1) .fill(null) diff --git a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts index 8c1e15ab9bd..bd5f13f4f28 100644 --- a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts @@ -221,11 +221,6 @@ describe('OneClick Apps (OCA)', () => { cy.focused().type(`${region.id}{enter}`); // Choose a Linode plan - // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); cy.get('[data-qa-plan-row="Dedicated 8 GB"]') .closest('tr') .within(() => { diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index 2d793df73f5..01a7dbe891e 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -80,15 +80,9 @@ const fillOutStackscriptForm = ( cy.findByLabelText('Description').should('be.visible').click(); cy.focused().type(description); } - ui.autocomplete - .findByLabel('Target Images') - .should('be.visible') - .type(targetImage); - // need selector in case item label is same as category label - ui.autocompletePopper - .findByTitle(targetImage, { selector: 'li div p' }) - .should('be.visible') - .click(); + + ui.autocomplete.findByLabel('Target Images').should('be.visible').click(); + ui.autocompletePopper.findByTitle(targetImage).should('be.visible').click(); ui.autocomplete.findByLabel('Target Images').click(); // Close autocomplete popper // Insert a script. @@ -120,13 +114,6 @@ const fillOutLinodeForm = (label: string, regionName: string) => { cy.focused().type(label); cy.findByText('Dedicated CPU').should('be.visible').click(); - - // Use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - cy.get('[id="g6-dedicated-2"]').click(); cy.findByLabelText('Root Password').should('be.visible').type(password); }; @@ -200,9 +187,7 @@ describe('Create stackscripts', () => { const stackscriptLabel = randomLabel(); const stackscriptDesc = randomPhrase(); // use random image. can specify image w/ getImageByLabel, then set images option in chooseImage - const randomImage = chooseImage({ - capabilities: ['cloud-init', 'distributed-sites'], - }); + const randomImage = chooseImage(); const stackscriptImage = randomImage.label; const linodeLabel = randomLabel(); const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts index 7e137192517..26b17237855 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts @@ -379,13 +379,6 @@ describe('Community Stackscripts integration tests', () => { // An error message shows up when no region is selected cy.contains('Plan is required.').should('be.visible'); - - // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination - ui.autocomplete.findByLabel('Dedicated Plans').click(); - ui.autocompletePopper.find().within(() => { - cy.findByText('G6 Dedicated').should('be.visible').click(); - }); - cy.get('[data-qa-plan-row="Dedicated 8 GB"]') .closest('tr') .within(() => { diff --git a/packages/manager/cypress/support/ui/pages/logs-destination-form.ts b/packages/manager/cypress/support/ui/pages/logs-destination-form.ts index 01621e540b8..1a15feffa7a 100644 --- a/packages/manager/cypress/support/ui/pages/logs-destination-form.ts +++ b/packages/manager/cypress/support/ui/pages/logs-destination-form.ts @@ -16,6 +16,7 @@ export const logsDestinationForm = { cy.findByLabelText('Destination Name') .should('be.visible') .should('be.enabled') + .should('have.attr', 'placeholder', 'Destination Name') .clear(); cy.focused().type(label); }, @@ -29,7 +30,7 @@ export const logsDestinationForm = { cy.findByLabelText('Host') .should('be.visible') .should('be.enabled') - .should('have.attr', 'placeholder', 'Host for the destination') + .should('have.attr', 'placeholder', 'Host') .clear(); cy.focused().type(host); }, diff --git a/packages/manager/cypress/support/ui/pages/logs-stream-form.ts b/packages/manager/cypress/support/ui/pages/logs-stream-form.ts index 0e97a983285..7f2ac7f42dc 100644 --- a/packages/manager/cypress/support/ui/pages/logs-stream-form.ts +++ b/packages/manager/cypress/support/ui/pages/logs-stream-form.ts @@ -21,9 +21,10 @@ export const logsStreamForm = { * @param label - stream label to set */ setLabel: (label: string) => { - cy.findByLabelText('Stream Name') + cy.findByLabelText('Name') .should('be.visible') .should('be.enabled') + .should('have.attr', 'placeholder', 'Stream name') .clear(); cy.focused().type(label); }, @@ -56,11 +57,7 @@ export const logsStreamForm = { cy.findByLabelText('Destination Name') .should('be.visible') .should('be.enabled') - .should( - 'have.attr', - 'placeholder', - 'Select existing or enter new destination' - ) + .should('have.attr', 'placeholder', 'Create or Select Destination Name') .clear(); // Select the Destination Name ui.autocompletePopper @@ -96,11 +93,7 @@ export const logsStreamForm = { cy.findByLabelText('Destination Name') .should('be.visible') .should('be.enabled') - .should( - 'have.attr', - 'placeholder', - 'Select existing or enter new destination' - ) + .should('have.attr', 'placeholder', 'Create or Select Destination Name') .clear(); cy.focused().type(label); cy.findByText(new RegExp(`"${label}"`)).click(); diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index 8fb42e0da7f..6c399851cbe 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -326,12 +326,8 @@ const resolveSearchRegions = ( ]; // If the user has specified an override region for this run, it takes precedent - // over any other specified criteria unless mock regions are passed in `options`. - if ( - overrideRegion && - detectOverrideRegion && - (!options?.regions || options.regions.length === 0) - ) { + // over any other specified criteria. + if (overrideRegion && detectOverrideRegion) { // TODO Consider skipping instead of failing when test isn't applicable to override region. if (!regionHasCapabilities(overrideRegion, requiredCapabilities)) { throw new Error( diff --git a/packages/manager/package.json b/packages/manager/package.json index 20510a085fa..3c1bd6e0a91 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.158.0", + "version": "1.157.0", "private": true, "type": "module", "bugs": { @@ -24,7 +24,7 @@ "@fontsource/nunito-sans": "^5.1.1", "@hookform/resolvers": "3.9.1", "@linode/api-v4": "workspace:*", - "@linode/design-language-system": "^5.3.2", + "@linode/design-language-system": "^5.0.0", "@linode/queries": "workspace:*", "@linode/search": "workspace:*", "@linode/shared": "workspace:*", @@ -59,7 +59,7 @@ "he": "^1.2.0", "immer": "^9.0.6", "ipaddr.js": "^1.9.1", - "jspdf": "^4.0.0", + "jspdf": "^3.0.2", "jspdf-autotable": "^5.0.2", "launchdarkly-react-client-sdk": "3.0.10", "libphonenumber-js": "^1.10.6", @@ -140,6 +140,7 @@ "@types/eslint-plugin-jsx-a11y": "^6.10.0", "@types/he": "^1.1.0", "@types/history": "4", + "@types/jspdf": "^1.3.3", "@types/luxon": "3.4.2", "@types/markdown-it": "^14.1.2", "@types/md5": "^2.1.32", diff --git a/packages/manager/public/assets/chroma.svg b/packages/manager/public/assets/chroma.svg deleted file mode 100644 index f6f7ea3da05..00000000000 --- a/packages/manager/public/assets/chroma.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/packages/manager/public/assets/mistral.svg b/packages/manager/public/assets/mistral.svg deleted file mode 100644 index ebbdfd9d0a5..00000000000 --- a/packages/manager/public/assets/mistral.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/manager/public/assets/openwebui.svg b/packages/manager/public/assets/openwebui.svg deleted file mode 100644 index c28acbd8c8a..00000000000 --- a/packages/manager/public/assets/openwebui.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/manager/public/assets/vllm.svg b/packages/manager/public/assets/vllm.svg deleted file mode 100644 index 6b9e5d1df89..00000000000 --- a/packages/manager/public/assets/vllm.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/manager/public/assets/white/chroma.svg b/packages/manager/public/assets/white/chroma.svg deleted file mode 100644 index e9276cb4b0c..00000000000 --- a/packages/manager/public/assets/white/chroma.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/manager/public/assets/white/mistral.svg b/packages/manager/public/assets/white/mistral.svg deleted file mode 100644 index 998f945024e..00000000000 --- a/packages/manager/public/assets/white/mistral.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/manager/public/assets/white/openwebui.svg b/packages/manager/public/assets/white/openwebui.svg deleted file mode 100644 index eff91d30859..00000000000 --- a/packages/manager/public/assets/white/openwebui.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/packages/manager/public/assets/white/vllm.svg b/packages/manager/public/assets/white/vllm.svg deleted file mode 100644 index b85e94fd71d..00000000000 --- a/packages/manager/public/assets/white/vllm.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/manager/src/GoTo.tsx b/packages/manager/src/GoTo.tsx index 791cb6d675c..93a0bddf0b7 100644 --- a/packages/manager/src/GoTo.tsx +++ b/packages/manager/src/GoTo.tsx @@ -4,9 +4,10 @@ import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; import { useIsDatabasesEnabled } from './features/Databases/utilities'; +import { usePermissions } from './features/IAM/hooks/usePermissions'; import { useIsMarketplaceV2Enabled } from './features/Marketplace/utils'; -import { useIsNetworkLoadBalancerEnabled } from './features/NetworkLoadBalancers/utils'; import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils'; +import { useFlags } from './hooks/useFlags'; import { useGlobalKeyboardListener } from './hooks/useGlobalKeyboardListener'; import type { SelectOption } from '@linode/ui'; @@ -16,12 +17,15 @@ export const GoTo = React.memo(() => { const { data: accountSettings } = useAccountSettings(); + const { iamRbacPrimaryNavChanges } = useFlags(); + const isManagedAccount = accountSettings?.managed ?? false; + const { data: permissions } = usePermissions('account', ['is_account_admin']); + const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled(); const { isDatabasesEnabled } = useIsDatabasesEnabled(); const { isMarketplaceV2FeatureEnabled } = useIsMarketplaceV2Enabled(); - const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); const { goToOpen, setGoToOpen } = useGlobalKeyboardListener(); @@ -54,11 +58,6 @@ export const GoTo = React.memo(() => { display: 'VPC', href: '/vpcs', }, - { - display: 'Network Load Balancer', - hide: !isNetworkLoadBalancerEnabled, - href: '/netloadbalancers', - }, { display: 'NodeBalancers', href: '/nodebalancers', @@ -109,12 +108,22 @@ export const GoTo = React.memo(() => { : 'Quick Deploy Apps', href: '/linodes/create/marketplace', }, - { display: 'Billing', href: '/billing' }, - { display: 'Identity & Access', href: '/iam' }, - { display: 'Login History', href: '/login-history' }, - { display: 'Service Transfers', href: '/service-transfers' }, - { display: 'Maintenance', href: '/maintenance' }, - { display: 'Settings', href: '/settings' }, + ...(iamRbacPrimaryNavChanges + ? [ + { display: 'Billing', href: '/billing' }, + { display: 'Identity & Access', href: '/iam' }, + { display: 'Login History', href: '/login-history' }, + { display: 'Service Transfers', href: '/service-transfers' }, + { display: 'Maintenance', href: '/maintenance' }, + { display: 'Settings', href: '/settings' }, + ] + : [ + { + display: 'Account', + hide: !permissions.is_account_admin, + href: '/account/billing', + }, + ]), { display: 'Help & Support', href: '/support', @@ -125,11 +134,12 @@ export const GoTo = React.memo(() => { }, ], [ + permissions.is_account_admin, isDatabasesEnabled, isManagedAccount, isMarketplaceV2FeatureEnabled, - isNetworkLoadBalancerEnabled, isPlacementGroupsEnabled, + iamRbacPrimaryNavChanges, ] ); diff --git a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx index 921e7181ef6..5075bc68dc9 100644 --- a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx +++ b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx @@ -1,6 +1,8 @@ import { Typography } from '@linode/ui'; import React from 'react'; +import { useFlags } from 'src/hooks/useFlags'; + import { Link } from '../Link'; /** @@ -8,11 +10,21 @@ import { Link } from '../Link'; * Example: Billing Contact info, rather than masking many individual fields */ export const MaskableTextAreaCopy = () => { + const { iamRbacPrimaryNavChanges } = useFlags(); return ( This data is sensitive and hidden for privacy. To unmask all sensitive data by default, go to{' '} - profile settings. + + profile settings + + . ); }; diff --git a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx index f4b47bbceff..62810bba634 100644 --- a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx +++ b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx @@ -259,13 +259,6 @@ describe('Payment Method Row', () => { }); it('Opens "Make a Payment" drawer with the payment method preselected if "Make a Payment" action is clicked', async () => { - queryMocks.userPermissions.mockReturnValue({ - data: { - make_billing_payment: true, - set_default_payment_method: false, - delete_payment_method: false, - }, - }); const paymentMethods = [ paymentMethodFactory.build({ type: 'paypal', @@ -281,22 +274,23 @@ describe('Payment Method Row', () => { * The component is responsible for rendering the "Make a Payment" drawer, * and is required for this test. We may want to consider decoupling these components in the future. */ - const { getByLabelText, getByText, router } = renderWithTheme( - - - - , - { - initialRoute: '/billing', - } - ); + const { getByLabelText, getByTestId, getByText, getAllByTestId } = + renderWithTheme( + + + + , + { + initialRoute: '/account/billing', + } + ); const actionMenu = getByLabelText('Action menu for card ending in 1881'); await userEvent.click(actionMenu); @@ -306,12 +300,21 @@ describe('Payment Method Row', () => { await userEvent.click(makePaymentButton); await waitFor(() => { - expect(router.state.location.pathname).toBe('/billing'); - }); - await waitFor(() => { - expect(router.state.location.searchStr).toBe( - '?action=make-payment&paymentMethodId=12' - ); + expect(getByTestId('drawer')).toBeVisible(); }); + + expect(getByTestId('drawer-title')).toHaveTextContent('Make a Payment'); + + const expectedSelectionCard = getAllByTestId('selection-card')[1]; + + expect(expectedSelectionCard).toBeVisible(); + expect(expectedSelectionCard).toHaveTextContent('1881'); + expect(expectedSelectionCard).toHaveAttribute( + 'data-qa-selection-card-checked', + 'true' + ); + + // In the future, if we have access to the router's state, + // we can assert the search params. }); }); diff --git a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx index 40134cfb8d4..f01ad45292e 100644 --- a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx +++ b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx @@ -8,6 +8,7 @@ import * as React from 'react'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import CreditCard from 'src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/CreditCard'; import { usePermissions } from 'src/features/IAM/hooks/usePermissions'; +import { useFlags } from 'src/hooks/useFlags'; import { ThirdPartyPayment } from './ThirdPartyPayment'; @@ -39,6 +40,7 @@ export const PaymentMethodRow = (props: Props) => { const { is_default, type } = paymentMethod; const { enqueueSnackbar } = useSnackbar(); const navigate = useNavigate(); + const flags = useFlags(); const { mutateAsync: makePaymentMethodDefault } = useMakeDefaultPaymentMethodMutation(props.paymentMethod.id); @@ -63,7 +65,7 @@ export const PaymentMethodRow = (props: Props) => { disabled: isChildUser || !permissions.make_billing_payment, onClick: () => { navigate({ - to: '/billing', + to: flags?.iamRbacPrimaryNavChanges ? '/billing' : '/account/billing', search: (prev) => ({ ...prev, action: 'make-payment', diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx index 05870b0fede..7c078ccc248 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx @@ -22,6 +22,7 @@ const queryString = 'menu-item-Managed'; const queryMocks = vi.hoisted(() => ({ useIsIAMEnabled: vi.fn(() => ({ + isIAMBeta: false, isIAMEnabled: false, })), usePreferences: vi.fn().mockReturnValue({}), @@ -493,9 +494,11 @@ describe('PrimaryNav', () => { expect(queryByTestId('menu-item-Logs')).toBeNull(); }); - it('should show Administration links', async () => { + it('should show Administration links if iamRbacPrimaryNavChanges flag is enabled', async () => { const flags: Partial = { + iamRbacPrimaryNavChanges: true, iam: { + beta: true, enabled: true, }, limitsEvolution: { @@ -506,6 +509,7 @@ describe('PrimaryNav', () => { }; queryMocks.useIsIAMEnabled.mockReturnValue({ + isIAMBeta: true, isIAMEnabled: true, }); @@ -540,12 +544,15 @@ describe('PrimaryNav', () => { it('should hide Identity & Access link for non beta users', async () => { const flags: Partial = { + iamRbacPrimaryNavChanges: true, iam: { + beta: true, enabled: false, }, }; queryMocks.useIsIAMEnabled.mockReturnValue({ + isIAMBeta: true, isIAMEnabled: false, }); @@ -563,6 +570,47 @@ describe('PrimaryNav', () => { }); }); + it('should show Account link and hide Administration if iamRbacPrimaryNavChanges flag is disabled', async () => { + const flags: Partial = { + iamRbacPrimaryNavChanges: false, + iam: { + beta: true, + enabled: true, + }, + }; + + queryMocks.useIsIAMEnabled.mockReturnValue({ + isIAMBeta: true, + isIAMEnabled: true, + }); + + renderWithTheme(, { + flags, + }); + + const adminLink = screen.queryByRole('button', { name: 'Administration' }); + expect(adminLink).toBeNull(); + + await waitFor(() => { + expect(screen.queryByRole('link', { name: 'Billing' })).toBeNull(); + expect(screen.queryByRole('link', { name: 'Quotas' })).toBeNull(); + expect(screen.queryByRole('link', { name: 'Login History' })).toBeNull(); + expect( + screen.queryByRole('link', { name: 'Service Transfers' }) + ).toBeNull(); + expect(screen.queryByRole('link', { name: 'Maintenance' })).toBeNull(); + expect( + screen.queryByRole('link', { name: 'Account' }) + ).toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: 'Identity & Access' }) + ).toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: 'Account Settings' }) + ).toBeNull(); + }); + }); + it('should show Network Load Balancers menu item if the user has the account capability and the flag is enabled', async () => { const account = accountFactory.build({ capabilities: ['Network LoadBalancer'], diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index ebe54255732..1a4d0499baa 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -114,12 +114,12 @@ export const PrimaryNav = (props: PrimaryNavProps) => { flags.aclpAlerting?.recentActivity || flags.aclpAlerting?.notificationChannels); - const { limitsEvolution } = flags; + const { iamRbacPrimaryNavChanges, limitsEvolution } = flags; const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled(); const { isDatabasesEnabled, isDatabasesV2Beta } = useIsDatabasesEnabled(); - const { isIAMEnabled } = useIsIAMEnabled(); + const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); const showLimitedAvailabilityBadges = flags.iamLimitedAvailabilityBadges; const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); @@ -282,6 +282,36 @@ export const PrimaryNav = (props: PrimaryNavProps) => { name: 'Monitor', }, { + icon: , + links: [ + { + display: 'Betas', + hide: !flags.selfServeBetas, + to: '/betas', + }, + { + display: 'Identity & Access', + hide: !isIAMEnabled || iamRbacPrimaryNavChanges, + to: '/iam', + isBeta: isIAMBeta, + isNew: !isIAMBeta && showLimitedAvailabilityBadges, + }, + { + display: 'Account', + hide: iamRbacPrimaryNavChanges, + to: '/account', + }, + { + display: 'Help & Support', + to: '/support', + }, + ], + name: 'More', + }, + ]; + + if (iamRbacPrimaryNavChanges) { + groups.splice(groups.length - 1, 0, { icon: , links: [ { @@ -297,7 +327,8 @@ export const PrimaryNav = (props: PrimaryNavProps) => { display: 'Identity & Access', hide: !isIAMEnabled, to: '/iam', - isNew: isIAMEnabled && showLimitedAvailabilityBadges, + isBeta: isIAMBeta, + isNew: !isIAMBeta && showLimitedAvailabilityBadges, }, { display: 'Quotas', @@ -322,23 +353,8 @@ export const PrimaryNav = (props: PrimaryNavProps) => { }, ], name: 'Administration', - }, - { - icon: , - links: [ - { - display: 'Betas', - hide: !flags.selfServeBetas, - to: '/betas', - }, - { - display: 'Help & Support', - to: '/support', - }, - ], - name: 'More', - }, - ]; + }); + } return groups; }, @@ -351,7 +367,9 @@ export const PrimaryNav = (props: PrimaryNavProps) => { isACLPEnabled, isACLPLogsBeta, isACLPLogsEnabled, + isIAMBeta, isIAMEnabled, + iamRbacPrimaryNavChanges, isMarketplaceV2FeatureEnabled, isNetworkLoadBalancerEnabled, limitsEvolution, diff --git a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx index 665c47ba819..8b8da628179 100644 --- a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx +++ b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx @@ -13,20 +13,4 @@ describe('QuotaUsageBanner', () => { const quotaUsageText = getByText('1 of 10 Bytes used'); expect(quotaUsageText).toBeVisible(); }); - - it.each([1000000000, 100000000, 10000000, 1000000])( - 'should display content usage in proper format', - (usage) => { - const { getByText } = renderWithTheme( - - ); - - const quotaUsageText = getByText('<0.01 of 100 TB used'); - expect(quotaUsageText).toBeVisible(); - } - ); }); diff --git a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx index d041a256567..d7a19a57304 100644 --- a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx +++ b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx @@ -23,18 +23,6 @@ export const QuotaUsageBar = ({ limit, usage, resourceMetric }: Props) => { initialLimit: limit, }); - function getUsageText() { - let convertedUsageString = convertedUsage.toLocaleString(); - const convertedLimitString = convertedLimit.toLocaleString(); - - // Special case to display storage usage - if (convertedUsage === 0 && convertedResourceMetric === 'TB') { - convertedUsageString = '<0.01'; - } - - return `${convertedUsageString} of ${convertedLimitString} ${convertedResourceMetric} used`; - } - return ( <> { sx={{ mb: 1, mt: 2, padding: '3px' }} value={usage} /> - {getUsageText()} + + {`${convertedUsage?.toLocaleString() ?? 'unknown'} of ${ + convertedLimit?.toLocaleString() ?? 'unknown' + } ${convertedResourceMetric} used`} + ); }; diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index 4e4ab92047e..cc9985b5f56 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -39,7 +39,7 @@ export interface SelectionCardProps { * The heading of the card. * @example Linode 1GB */ - heading: JSX.Element | string; + heading: string; /** * An optional decoration to display next to the heading. * @example (Current) diff --git a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx index c21a5742fb9..2c18dc04d40 100644 --- a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx +++ b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx @@ -72,7 +72,7 @@ describe('TypeToConfirm Component', () => { expect( queryByTestId('instructions-to-enable-or-disable') ).toBeInTheDocument(); - expect(getByRole('link')).toHaveAttribute('href', '/profile/preferences'); + expect(getByRole('link')).toHaveAttribute('href', '/profile/settings'); }); it('Should not display instructions when toggled to hidden', () => { diff --git a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx index 389401f1698..29bb8314cee 100644 --- a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx +++ b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx @@ -5,6 +5,7 @@ import type { JSX } from 'react'; import { FormGroup } from 'src/components/FormGroup'; import { Link } from 'src/components/Link'; +import { useFlags } from 'src/hooks/useFlags'; import type { TextFieldProps, TypographyProps } from '@linode/ui'; import type { Theme } from '@mui/material'; @@ -52,6 +53,8 @@ export const TypeToConfirm = (props: TypeToConfirmProps) => { (preferences) => preferences?.type_to_confirm ?? true ); + const { iamRbacPrimaryNavChanges } = useFlags(); + /* There is an edge case where preferences?.type_to_confirm is undefined when the user has not yet set a preference as seen in /profile/settings?preferenceEditor=true. @@ -115,7 +118,17 @@ export const TypeToConfirm = (props: TypeToConfirmProps) => { sx={{ marginTop: 1 }} > To {disableOrEnable} type-to-confirm, go to the Type-to-Confirm - section of Preferences. + section of{' '} + + {iamRbacPrimaryNavChanges ? 'Preferences' : 'My Settings'} + + . ) : null} diff --git a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx index 40b33ec8478..fc3f9e1337f 100644 --- a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx +++ b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx @@ -37,7 +37,6 @@ interface EntityInfo { | 'Managed Credential' | 'Managed Service Monitor' | 'NodeBalancer' - | 'Notification Channel' | 'Placement Group' | 'Subnet' | 'Volume' diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 6a6d8e27948..a3b79b4945d 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -68,6 +68,7 @@ const options: { flag: keyof Flags; label: string }[] = [ label: 'IAM Limited Availability Badges', }, { flag: 'iamDelegation', label: 'IAM Delegation (Parent/Child)' }, + { flag: 'iamRbacPrimaryNavChanges', label: 'IAM Primary Nav Changes' }, { flag: 'linodeCloneFirewall', label: 'Linode Clone Firewall', @@ -77,14 +78,6 @@ const options: { flag: keyof Flags; label: string }[] = [ label: 'VM Host Maintenance Policy', }, { flag: 'volumeSummaryPage', label: 'Volume Summary Page' }, - { - flag: 'blockStorageContextualMetrics', - label: 'Block Storage Contextual Metrics', - }, - { - flag: 'objectStorageContextualMetrics', - label: 'Object Storage Contextual Metrics', - }, { flag: 'objSummaryPage', label: 'OBJ Summary Page' }, { flag: 'vpcIpv6', label: 'VPC IPv6' }, ]; diff --git a/packages/manager/src/dev-tools/utils.ts b/packages/manager/src/dev-tools/utils.ts index dbed6769e15..88839cdf6b8 100644 --- a/packages/manager/src/dev-tools/utils.ts +++ b/packages/manager/src/dev-tools/utils.ts @@ -92,9 +92,9 @@ export const saveSeedsCountMap = (countMap: { [key: string]: number }) => { /** * Retrieves the presets map from local storage. */ -export const getExtraPresetsMap = (): Partial<{ +export const getExtraPresetsMap = (): { [K in MockPresetExtraId]: number; -}> => { +} => { const encodedPresetsMap = localStorage.getItem(LOCAL_STORAGE_PRESETS_MAP_KEY); return encodedPresetsMap ? JSON.parse(encodedPresetsMap) : {}; diff --git a/packages/manager/src/factories/account.ts b/packages/manager/src/factories/account.ts index de029b12345..f463fe6f83c 100644 --- a/packages/manager/src/factories/account.ts +++ b/packages/manager/src/factories/account.ts @@ -56,7 +56,6 @@ export const accountFactory = Factory.Sync.makeFactory({ 'Placement Group', 'Vlans', 'Kubernetes Enterprise', - 'VPC Dual Stack', ], city: 'Philadelphia', company: Factory.each((i) => `company-${i}`), diff --git a/packages/manager/src/factories/cloudpulse/channels.ts b/packages/manager/src/factories/cloudpulse/channels.ts index 970569d5df3..ee361063bb0 100644 --- a/packages/manager/src/factories/cloudpulse/channels.ts +++ b/packages/manager/src/factories/cloudpulse/channels.ts @@ -1,9 +1,6 @@ import { Factory } from '@linode/utilities'; -import type { - NotificationChannel, - NotificationChannelAlerts, -} from '@linode/api-v4'; +import type { NotificationChannel } from '@linode/api-v4'; export const notificationChannelFactory = Factory.Sync.makeFactory({ @@ -29,12 +26,3 @@ export const notificationChannelFactory = updated: new Date().toISOString(), updated_by: 'user1', }); - -export const notificationChannelAlertsFactory = - Factory.Sync.makeFactory({ - type: 'alerts-definitions', - id: Factory.each((i) => i), - service_type: 'linode', - label: Factory.each((id) => `Alert-${id}`), - url: Factory.each((i) => `monitor/alert-definitions/${i}`), - }); diff --git a/packages/manager/src/factories/index.ts b/packages/manager/src/factories/index.ts index 39b2e8ae99f..03dd55cb4cf 100644 --- a/packages/manager/src/factories/index.ts +++ b/packages/manager/src/factories/index.ts @@ -24,7 +24,6 @@ export * from './images'; export * from './kernels'; export * from './kubernetesCluster'; export * from './linodeConfigs'; -export * from './locks'; export * from './longviewClient'; export * from './longviewDisks'; export * from './longviewProcess'; diff --git a/packages/manager/src/factories/locks.ts b/packages/manager/src/factories/locks.ts deleted file mode 100644 index 59685657a6f..00000000000 --- a/packages/manager/src/factories/locks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Factory } from '@linode/utilities'; - -import { entityFactory } from './events'; - -import type { ResourceLock } from '@linode/api-v4'; - -export const lockFactory = Factory.Sync.makeFactory({ - id: Factory.each((i) => i), - lock_type: 'cannot_delete', - entity: entityFactory.build({ - type: 'linode', - }), -}); diff --git a/packages/manager/src/factories/userAccountPermissions.ts b/packages/manager/src/factories/userAccountPermissions.ts index db1385b263a..1aa5ed98ca6 100644 --- a/packages/manager/src/factories/userAccountPermissions.ts +++ b/packages/manager/src/factories/userAccountPermissions.ts @@ -3,5 +3,4 @@ import type { PermissionType } from '@linode/api-v4'; export const userAccountPermissionsFactory: PermissionType[] = [ 'create_linode', 'create_firewall', - 'create_vpc', ]; diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 2fe9b36763d..c0bfac6e2fa 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -203,7 +203,6 @@ export interface Flags { apl: boolean; aplGeneralAvailability: boolean; aplLkeE: boolean; - blockStorageContextualMetrics: boolean; blockStorageEncryption: boolean; blockStorageVolumeLimit: boolean; cloudManagerDesignUpdatesBanner: DesignUpdatesBannerFlag; @@ -224,9 +223,10 @@ export interface Flags { gecko2: GeckoFeatureFlag; generationalPlansv2: GenerationalPlansFlag; gpuv2: GpuV2; - iam: BaseFeatureFlag; + iam: BetaFeatureFlag; iamDelegation: BaseFeatureFlag; iamLimitedAvailabilityBadges: boolean; + iamRbacPrimaryNavChanges: boolean; ipv6Sharing: boolean; kubernetesBlackwellPlans: boolean; limitsEvolution: LimitsEvolution; @@ -242,7 +242,6 @@ export interface Flags { networkLoadBalancer: boolean; nodebalancerIpv6: boolean; nodebalancerVpc: boolean; - objectStorageContextualMetrics: boolean; objectStorageGen2: BaseFeatureFlag; objMultiCluster: boolean; objSummaryPage: boolean; diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index 9e87f756a9b..0f626cc9850 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -1,4 +1,5 @@ import { useAccount, useProfile } from '@linode/queries'; +import { NotFound } from '@linode/ui'; import { Outlet, useLocation, @@ -38,7 +39,7 @@ export const AccountLanding = () => { }); const { data: account } = useAccount(); const { data: profile } = useProfile(); - const { limitsEvolution } = useFlags(); + const { iamRbacPrimaryNavChanges, limitsEvolution } = useFlags(); const { data: permissions } = usePermissions('account', [ 'make_billing_payment', @@ -100,13 +101,16 @@ export const AccountLanding = () => { React.useEffect(() => { if (match.routeId === '/account/quotas' && !showQuotasTab) { navigate({ - to: '/quotas', + to: iamRbacPrimaryNavChanges ? '/quotas' : '/account/billing', }); } - }, [match.routeId, showQuotasTab, navigate]); + }, [match.routeId, showQuotasTab, navigate, iamRbacPrimaryNavChanges]); // This is the default route for the account route, so we need to redirect to the billing tab but keep /account as legacy if (location.pathname === '/account') { + if (iamRbacPrimaryNavChanges) { + return ; + } navigate({ to: '/account/billing', }); @@ -149,7 +153,7 @@ export const AccountLanding = () => { if (!isAkamaiAccount) { landingHeaderProps.onButtonClick = () => navigate({ - to: '/billing', + to: iamRbacPrimaryNavChanges ? '/billing' : '/account/billing', search: { action: 'make-payment' }, }); } diff --git a/packages/manager/src/features/Account/AccountLogins.tsx b/packages/manager/src/features/Account/AccountLogins.tsx index 79cdae2ec1c..232a99f4f13 100644 --- a/packages/manager/src/features/Account/AccountLogins.tsx +++ b/packages/manager/src/features/Account/AccountLogins.tsx @@ -15,6 +15,7 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { TableSortCell } from 'src/components/TableSortCell'; +import { useFlags } from 'src/hooks/useFlags'; import { useOrderV2 } from 'src/hooks/useOrderV2'; import { usePaginationV2 } from 'src/hooks/usePaginationV2'; @@ -43,11 +44,14 @@ const useStyles = makeStyles()((theme: Theme) => ({ const AccountLogins = () => { const { classes } = useStyles(); + const flags = useFlags(); const { data: permissions } = usePermissions('account', [ 'list_account_logins', ]); const pagination = usePaginationV2({ - currentRoute: '/login-history', + currentRoute: flags?.iamRbacPrimaryNavChanges + ? '/login-history' + : '/account/login-history', preferenceKey: 'account-logins-pagination', }); @@ -57,7 +61,9 @@ const AccountLogins = () => { order: 'desc', orderBy: 'datetime', }, - from: '/login-history', + from: flags?.iamRbacPrimaryNavChanges + ? '/login-history' + : '/account/login-history', }, preferenceKey: `${preferenceKey}-order`, }); diff --git a/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx b/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx index ec2c6cb9bb7..8a283a565d7 100644 --- a/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx +++ b/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx @@ -73,7 +73,9 @@ export const MaintenanceTable = ({ type }: Props) => { const flags = useFlags(); const pagination = usePaginationV2({ - currentRoute: '/maintenance', + currentRoute: flags?.iamRbacPrimaryNavChanges + ? `/maintenance` + : `/account/maintenance`, preferenceKey: `${preferenceKey}-${type}`, queryParamsPrefix: type, }); @@ -84,7 +86,9 @@ export const MaintenanceTable = ({ type }: Props) => { order: 'desc', orderBy: 'status', }, - from: '/maintenance', + from: flags?.iamRbacPrimaryNavChanges + ? `/maintenance` + : `/account/maintenance`, }, preferenceKey: `${preferenceKey}-order-${type}`, prefix: type, diff --git a/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx b/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx index f43ba89293a..41ba4809a79 100644 --- a/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx +++ b/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx @@ -21,7 +21,7 @@ describe('NetworkInterfaces', () => { const { getByText } = renderWithTheme(); expect(getByText('Network Interface Type')).toBeVisible(); - expect(getByText('Allowed interfaces for new Linodes')).toBeVisible(); + expect(getByText('Interfaces for new Linodes')).toBeVisible(); expect(getByText('Save')).toBeVisible(); }); @@ -49,9 +49,9 @@ describe('NetworkInterfaces', () => { ); - expect( - getByLabelText('Allowed interfaces for new Linodes') - ).toHaveAttribute('disabled'); + expect(getByLabelText('Interfaces for new Linodes')).toHaveAttribute( + 'disabled' + ); expect(getByText('Save')).toHaveAttribute('aria-disabled', 'true'); }); }); diff --git a/packages/manager/src/features/Account/NetworkInterfaceType.tsx b/packages/manager/src/features/Account/NetworkInterfaceType.tsx index 37837ebd431..9e42490ddd0 100644 --- a/packages/manager/src/features/Account/NetworkInterfaceType.tsx +++ b/packages/manager/src/features/Account/NetworkInterfaceType.tsx @@ -1,20 +1,11 @@ import { useAccountSettings, useMutateAccountSettings } from '@linode/queries'; -import { - Box, - Button, - Paper, - Select, - Stack, - Typography, - useTheme, -} from '@linode/ui'; +import { Box, Button, Paper, Select, Stack, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; import React from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { Link } from 'src/components/Link'; - import { usePermissions } from '../IAM/hooks/usePermissions'; +import { LinodeInterfaceFeatureStatusChip } from '../Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaceFeatureChip'; import type { AccountSettings, @@ -27,44 +18,29 @@ type InterfaceSettingValues = Pick< 'interfaces_for_new_linodes' >; -interface AccountSettingInterfaceOptionType - extends SelectOption { - tooltipText: string; -} - -const accountSettingInterfaceOptions: AccountSettingInterfaceOptionType[] = [ - { - label: - 'Linode Interfaces (default) but allow Configuration Profile Interfaces', - value: 'linode_default_but_legacy_config_allowed', - tooltipText: - 'Linode Interfaces are used by default unless you select Configuration Profile Interfaces. Linodes with Configuration Profile Interfaces can be upgraded to Linode Interfaces.', - }, - { - label: 'Linode Interfaces Only', - value: 'linode_only', - tooltipText: - 'Existing Linodes with Configuration Profile Interfaces will continue to work. You can upgrade these Linodes to use Linode Interfaces.', - }, - { - label: - 'Configuration Profile Interfaces (default) but allow Linode Interfaces', - value: 'legacy_config_default_but_linode_allowed', - tooltipText: - 'Configuration Profile Interfaces are used by default unless you select Linode Interfaces. You can upgrade to Linode Interfaces at any time.', - }, - { - label: 'Configuration Profile Interfaces Only', - value: 'legacy_config_only', - tooltipText: - 'Existing Linodes with Linode Interfaces will continue to work. Upgrades to Linode Interfaces are not available.', - }, -]; +const accountSettingInterfaceOptions: SelectOption[] = + [ + { + label: 'Linode Interfaces but allow Configuration Profile Interfaces', + value: 'linode_default_but_legacy_config_allowed', + }, + { + label: 'Linode Interfaces Only', + value: 'linode_only', + }, + { + label: 'Configuration Profile Interfaces but allow Linode Interfaces', + value: 'legacy_config_default_but_linode_allowed', + }, + { + label: 'Configuration Profile Interfaces Only', + value: 'legacy_config_only', + }, + ]; export const NetworkInterfaceType = () => { const { enqueueSnackbar } = useSnackbar(); const { data: accountSettings } = useAccountSettings(); - const theme = useTheme(); const { mutateAsync: updateAccountSettings } = useMutateAccountSettings(); const { data: permissions } = usePermissions('account', [ @@ -99,23 +75,14 @@ export const NetworkInterfaceType = () => { return ( - - Network Interface Type - + Network Interface Type
- - When creating new Linodes or upgrading existing ones, select between - Configuration Profile Interfaces and Linode Interfaces.{' '} - - Learn more - - . - - - Linode Interfaces are recommended. However, use Configuration - Profile Interfaces with LKE or when a Linode needs a private IP - address. + + Choose whether to use Configuration Profile Interfaces or Linode + Interfaces + + when creating new Linodes or upgrading existing ones. {