From c6e9da1cb8af4575701c79d69c9d0afaa2151ca6 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:24:08 -0500 Subject: [PATCH] =?UTF-8?q?Revert=20"Release=20v1.134.0=20-=20staging=20?= =?UTF-8?q?=E2=86=92=20master"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development-guide/13-coding-standards.md | 12 - docs/tooling/analytics.md | 53 +- package.json | 16 +- packages/api-v4/CHANGELOG.md | 32 - packages/api-v4/package.json | 2 +- packages/api-v4/src/account/account.ts | 12 + packages/api-v4/src/account/types.ts | 59 +- packages/api-v4/src/cloudpulse/alerts.ts | 32 +- packages/api-v4/src/cloudpulse/services.ts | 6 +- packages/api-v4/src/cloudpulse/types.ts | 47 +- packages/api-v4/src/databases/types.ts | 12 +- packages/api-v4/src/iam/types.ts | 40 +- packages/api-v4/src/kubernetes/kubernetes.ts | 12 - packages/api-v4/src/kubernetes/types.ts | 2 - packages/api-v4/src/linodes/types.ts | 7 +- packages/api-v4/src/nodebalancers/types.ts | 102 +-- packages/api-v4/src/object-storage/objects.ts | 2 +- packages/manager/.eslintrc.cjs | 7 +- packages/manager/.storybook/preview.tsx | 13 +- packages/manager/CHANGELOG.md | 121 --- packages/manager/Dockerfile | 8 +- .../components/autocomplete.spec.tsx | 688 ------------------ .../component/components/select.spec.tsx | 348 --------- .../firewalls/firewall-rule-table.spec.tsx | 604 --------------- .../{components => poc}/beta-chip.spec.tsx | 5 +- .../region-select.spec.tsx | 79 +- .../core/account/account-cancellation.spec.ts | 83 +-- .../e2e/core/account/account-logout.spec.ts | 6 +- .../credit-card-expired-banner.spec.ts | 36 - .../billing/smoke-billing-activity.spec.ts | 21 +- .../cloudpulse-dashboard-errors.spec.ts | 42 +- .../dbaas-widgets-verification.spec.ts | 117 +-- .../linode-widget-verification.spec.ts | 89 +-- .../core/domains/smoke-clone-domain.spec.ts | 6 +- .../smoke-create-domain-records.spec.ts | 111 +-- .../core/domains/smoke-delete-domain.spec.ts | 4 +- .../general/account-login-redirect.spec.ts | 4 +- .../e2e/core/images/create-image.spec.ts | 157 +++- .../images-non-empty-landing-page.spec.ts | 136 ---- .../core/images/machine-image-upload.spec.ts | 3 +- .../e2e/core/images/search-images.spec.ts | 2 +- .../core/images/smoke-create-image.spec.ts | 2 +- .../e2e/core/kubernetes/lke-create.spec.ts | 671 ++++------------- .../core/kubernetes/lke-landing-page.spec.ts | 186 ----- .../e2e/core/kubernetes/lke-update.spec.ts | 311 +------- .../e2e/core/linodes/backup-linode.spec.ts | 9 +- .../create-linode-view-code-snippet.spec.ts | 2 +- .../create-linode-with-add-ons.spec.ts | 4 +- .../create-linode-with-firewall.spec.ts | 6 +- .../create-linode-with-ssh-key.spec.ts | 4 +- .../create-linode-with-user-data.spec.ts | 4 +- .../linodes/create-linode-with-vlan.spec.ts | 4 +- .../linodes/create-linode-with-vpc.spec.ts | 83 +-- .../e2e/core/linodes/create-linode.spec.ts | 37 +- .../e2e/core/linodes/linode-network.spec.ts | 75 +- .../e2e/core/linodes/linode-storage.spec.ts | 2 +- .../e2e/core/linodes/rebuild-linode.spec.ts | 15 +- .../e2e/core/linodes/rescue-linode.spec.ts | 5 +- .../e2e/core/linodes/resize-linode.spec.ts | 232 ++---- .../e2e/core/linodes/search-linodes.spec.ts | 42 +- .../core/linodes/switch-linode-state.spec.ts | 33 +- .../core/linodes/update-linode-labels.spec.ts | 9 +- .../bucket-details-gen2.spec.ts | 94 +++ .../stackscripts/create-stackscripts.spec.ts | 6 +- .../e2e/core/volumes/clone-volume.spec.ts | 18 +- .../core/volumes/create-volume.smoke.spec.ts | 7 +- .../e2e/core/volumes/delete-volume.spec.ts | 19 +- .../e2e/core/volumes/update-volume.spec.ts | 103 +-- .../e2e/core/volumes/upgrade-volume.spec.ts | 9 +- .../cypress/e2e/core/vpc/vpc-create.spec.ts | 17 +- .../e2e/core/vpc/vpc-details-page.spec.ts | 125 +--- packages/manager/cypress/support/api/lke.ts | 28 +- .../manager/cypress/support/api/volumes.ts | 28 +- .../cypress/support/constants/account.ts | 12 +- .../support/constants/dc-specific-pricing.ts | 7 +- .../cypress/support/constants/linodes.ts | 4 +- .../manager/cypress/support/constants/lke.ts | 36 +- .../cypress/support/constants/login.ts | 8 - .../cypress/support/intercepts/betas.ts | 21 +- .../cypress/support/intercepts/cloudpulse.ts | 6 +- .../manager/cypress/support/intercepts/lke.ts | 76 +- .../cypress/support/setup/defer-command.ts | 5 +- .../manager/cypress/support/util/linodes.ts | 10 +- packages/manager/cypress/support/util/lke.ts | 18 - .../manager/cypress/support/util/polling.ts | 24 +- packages/manager/cypress/tsconfig.json | 3 +- packages/manager/index.html | 2 +- packages/manager/package.json | 14 +- packages/manager/src/MainContent.tsx | 22 +- packages/manager/src/Root.tsx | 8 +- packages/manager/src/Router.tsx | 2 - packages/manager/src/__data__/linodes.ts | 4 - .../src/assets/icons/emptynotification.svg | 8 - .../src/assets/icons/entityIcons/alerts.svg | 10 - .../src/assets/icons/uploadPending.svg | 7 + .../components/ActionsPanel/ActionsPanel.tsx | 25 +- .../manager/src/components/Avatar/Avatar.tsx | 9 +- .../src/components/BarPercent/BarPercent.tsx | 6 +- .../Breadcrumb/FinalCrumb.styles.tsx | 4 +- .../src/components/Breadcrumb/FinalCrumb.tsx | 13 +- .../CheckoutSummary/CheckoutSummary.tsx | 2 +- .../components/CodeBlock/CodeBlock.styles.ts | 14 +- .../components/ColorPalette/ColorPalette.tsx | 8 +- .../DatePicker/DatePicker.stories.tsx | 124 ---- .../components/DatePicker/DatePicker.test.tsx | 80 -- .../src/components/DatePicker/DatePicker.tsx | 91 --- .../DatePicker/DateTimePicker.stories.tsx | 145 ---- .../DatePicker/DateTimePicker.test.tsx | 143 ---- .../components/DatePicker/DateTimePicker.tsx | 296 -------- .../DateTimeRangePicker.stories.tsx | 193 ----- .../DatePicker/DateTimeRangePicker.test.tsx | 354 --------- .../DatePicker/DateTimeRangePicker.tsx | 313 -------- .../components/DatePicker/TimeZoneSelect.tsx | 64 -- .../DeletionDialog/DeletionDialog.stories.tsx | 4 + .../DeletionDialog/DeletionDialog.test.tsx | 40 +- .../DeletionDialog/DeletionDialog.tsx | 23 +- .../src/components/Encryption/constants.tsx | 3 + .../EnhancedSelect/Select.styles.ts | 4 +- .../components/IconTextLink/IconTextLink.tsx | 10 +- .../components/ImageSelect/ImageSelect.tsx | 164 ++--- .../src/components/LineGraph/LineGraph.tsx | 6 +- packages/manager/src/components/Link.tsx | 2 +- .../src/components/MainContentBanner.tsx | 8 +- .../MaskableText/MaskableText.test.tsx | 18 +- .../components/MaskableText/MaskableText.tsx | 52 +- .../MaskableText/MaskableTextArea.tsx | 18 - .../MultipleIPInput.stories.tsx | 75 -- .../MultipleIPInput/MultipleIPInput.tsx | 68 -- .../manager/src/components/OrderBy.test.tsx | 13 +- packages/manager/src/components/OrderBy.tsx | 12 +- .../PreferenceToggle/PreferenceToggle.tsx | 44 +- .../PrimaryNav/PrimaryNav.styles.ts | 6 +- .../src/components/PrimaryNav/PrimaryNav.tsx | 13 +- .../PromotionalOfferCard.tsx | 12 +- .../RegionSelect/RegionSelect.styles.ts | 4 +- .../SelectFirewallPanel.tsx | 5 +- .../SelectRegionPanel/RegionHelperText.tsx | 10 +- .../SelectionCard/CardBase.styles.ts | 4 +- .../SelectionCard/SelectionCard.tsx | 4 +- .../src/components/ShowMore/ShowMore.tsx | 2 +- .../src/components/TanstackLink.stories.tsx | 38 - .../manager/src/components/TanstackLinks.tsx | 83 --- .../TypeToConfirm/TypeToConfirm.test.tsx | 48 +- .../TypeToConfirm/TypeToConfirm.tsx | 69 +- .../TypeToConfirmDialog.test.tsx | 34 - .../TypeToConfirmDialog.tsx | 178 ++--- .../components/Uploaders/FileUpload.styles.ts | 8 +- .../src/components/Uploaders/FileUpload.tsx | 4 +- .../ImageUploader/ImageUploader.styles.ts | 2 +- .../manager/src/components/VPCSelect.test.tsx | 37 + packages/manager/src/components/VPCSelect.tsx | 36 + .../VerticalLinearStepper.styles.ts | 4 +- .../manager/src/dev-tools/FeatureFlagTool.tsx | 136 +--- .../src/dev-tools/ServiceWorkerTool.tsx | 189 ++--- .../src/dev-tools/components/Draggable.tsx | 6 +- .../components/ExtraPresetAccount.tsx | 370 ---------- .../components/ExtraPresetOptionCheckbox.tsx | 21 +- .../components/ExtraPresetOptionSelect.tsx | 3 +- .../components/ExtraPresetOptions.tsx | 32 +- .../components/ExtraPresetProfile.tsx | 310 -------- .../src/dev-tools/components/JsonTextArea.tsx | 91 --- .../src/dev-tools/components/SeedOptions.tsx | 17 +- packages/manager/src/dev-tools/constants.ts | 4 - packages/manager/src/dev-tools/dev-tools.css | 71 +- .../dev-tools/{DevTools.tsx => dev-tools.tsx} | 0 packages/manager/src/dev-tools/load.ts | 3 +- packages/manager/src/dev-tools/utils.ts | 43 -- packages/manager/src/factories/account.ts | 14 +- .../src/factories/accountPermissions.ts | 18 +- packages/manager/src/factories/betas.ts | 17 +- .../src/factories/cloudpulse/alerts.ts | 8 +- packages/manager/src/factories/dashboards.ts | 4 +- packages/manager/src/factories/databases.ts | 11 +- packages/manager/src/factories/index.ts | 1 - .../src/factories/kubernetesCluster.ts | 16 - packages/manager/src/factories/linodes.ts | 4 +- packages/manager/src/factories/profile.ts | 4 +- packages/manager/src/factories/subnets.ts | 5 +- packages/manager/src/factories/types.ts | 11 - packages/manager/src/featureFlags.ts | 4 +- .../features/Account/CloseAccountDialog.tsx | 61 +- .../Account/ObjectStorageSettings.test.tsx | 24 - .../manager/src/features/Account/constants.ts | 5 - .../src/features/Backups/BackupsCTA.tsx | 6 +- .../src/features/Betas/BetaDetails.test.tsx | 27 +- .../manager/src/features/Betas/BetaSignup.tsx | 2 +- .../PaymentDrawer/GooglePayButton.tsx | 4 +- .../PaymentDrawer/PaymentDrawer.tsx | 4 +- .../ContactInfoPanel/ContactInformation.tsx | 22 +- .../Billing/InvoiceDetail/InvoiceDetail.tsx | 269 ++++--- .../features/CancelLanding/CancelLanding.tsx | 21 +- .../Alerts/AlertsDetail/AlertDetail.test.tsx | 96 --- .../Alerts/AlertsDetail/AlertDetail.tsx | 116 --- .../AlertsDetail/AlertDetailOverview.test.tsx | 69 -- .../AlertsDetail/AlertDetailOverview.tsx | 73 -- .../Alerts/AlertsDetail/AlertDetailRow.tsx | 75 -- .../AlertsLanding/AlertsDefinitionLanding.tsx | 21 +- .../Alerts/AlertsListing/AlertActionMenu.tsx | 35 - .../AlertsListing/AlertListing.test.tsx | 79 -- .../Alerts/AlertsListing/AlertListing.tsx | 88 --- .../AlertsListing/AlertTableRow.test.tsx | 43 -- .../Alerts/AlertsListing/AlertTableRow.tsx | 54 -- .../Alerts/AlertsListing/constants.ts | 22 - .../CreateAlertDefinition.test.tsx | 52 +- .../CreateAlert/CreateAlertDefinition.tsx | 68 +- .../Criteria/ClearIconButton.test.tsx | 15 - .../CreateAlert/Criteria/ClearIconButton.tsx | 24 - .../Criteria/DimensionFilter.test.tsx | 142 ---- .../CreateAlert/Criteria/DimensionFilter.tsx | 70 -- .../Criteria/DimensionFilterField.test.tsx | 228 ------ .../Criteria/DimensionFilterField.tsx | 186 ----- .../CreateAlert/Criteria/Metric.test.tsx | 237 ------ .../Alerts/CreateAlert/Criteria/Metric.tsx | 294 -------- .../Criteria/MetricCriteria.test.tsx | 246 ------- .../CreateAlert/Criteria/MetricCriteria.tsx | 110 --- .../Criteria/TriggerConditions.test.tsx | 204 ------ .../Criteria/TriggerConditions.tsx | 179 ----- .../AlertSeveritySelect.test.tsx | 6 +- .../AlertSeveritySelect.tsx | 2 +- .../GeneralInformation/EngineOption.test.tsx | 6 +- .../ResourceMultiSelect.tsx | 4 +- .../CloudPulse/Alerts/CreateAlert/types.ts | 35 +- .../Alerts/CreateAlert/utilities.ts | 81 +-- .../Alerts/Utils/AlertsActionMenu.ts | 25 - .../CloudPulse/Alerts/Utils/utils.test.ts | 15 - .../features/CloudPulse/Alerts/Utils/utils.ts | 31 - .../features/CloudPulse/Alerts/constants.ts | 109 +-- .../Dashboard/CloudPulseDashboard.tsx | 5 - .../Dashboard/CloudPulseDashboardLanding.tsx | 70 +- .../Dashboard/CloudPulseDashboardRenderer.tsx | 7 +- .../CloudPulseDashboardWithFilters.test.tsx | 39 +- .../CloudPulseDashboardWithFilters.tsx | 120 +-- .../Overview/GlobalFilters.test.tsx | 2 - .../CloudPulse/Overview/GlobalFilters.tsx | 15 +- .../Utils/CloudPulseWidgetUtils.test.ts | 237 ------ .../CloudPulse/Utils/CloudPulseWidgetUtils.ts | 7 +- .../CloudPulse/Utils/FilterBuilder.test.ts | 24 +- .../CloudPulse/Utils/FilterBuilder.ts | 76 +- .../features/CloudPulse/Utils/FilterConfig.ts | 21 +- .../Utils/ReusableDashboardFilterUtils.ts | 4 +- .../features/CloudPulse/Utils/constants.ts | 2 - .../src/features/CloudPulse/Utils/models.ts | 16 +- .../Widget/CloudPulseWidget.test.tsx | 196 ----- .../CloudPulse/Widget/CloudPulseWidget.tsx | 4 +- .../Widget/CloudPulseWidgetRenderer.tsx | 8 +- .../components/CloudPulseLineGraph.test.tsx | 74 -- .../shared/CloudPulseAppliedFilter.test.tsx | 39 - .../shared/CloudPulseAppliedFilter.tsx | 63 -- .../CloudPulseAppliedFilterRenderer.tsx | 38 - .../CloudPulseComponentRenderer.test.tsx | 37 - .../shared/CloudPulseComponentRenderer.tsx | 5 - .../shared/CloudPulseCustomSelect.tsx | 2 - .../shared/CloudPulseCustomSelectUtils.ts | 18 +- .../CloudPulseDashboardFilterBuilder.test.tsx | 4 +- .../CloudPulseDashboardFilterBuilder.tsx | 63 +- .../shared/CloudPulseDashboardSelect.tsx | 36 +- .../shared/CloudPulseRegionSelect.test.tsx | 112 +-- .../shared/CloudPulseRegionSelect.tsx | 58 +- .../shared/CloudPulseResourcesSelect.tsx | 3 - .../shared/CloudPulseTagsFilter.test.tsx | 181 ----- .../shared/CloudPulseTagsFilter.tsx | 120 --- .../shared/CloudPulseTimeRangeSelect.tsx | 12 +- .../DatabaseCreate/DatabaseCreate.tsx | 8 +- .../DatabaseSummarySection.test.tsx | 56 +- .../DatabaseCreate/DatabaseSummarySection.tsx | 21 +- .../Databases/DatabaseCreate/utilities.tsx | 19 - .../DatabaseDetail/AddAccessControlDrawer.tsx | 6 +- .../DatabaseBackups/DatabaseBackups.test.tsx | 103 ++- .../DatabaseResize/DatabaseResize.test.tsx | 205 ++++-- .../DatabaseResize/DatabaseResize.tsx | 31 +- .../DatabaseResize/DatabaseResize.utils.tsx | 39 - .../DatabaseResizeCurrentConfiguration.tsx | 4 +- .../DatabaseSettingsDeleteClusterDialog.tsx | 1 - .../DatabaseSettingsMaintenance.test.tsx | 54 +- .../DatabaseSettingsMaintenance.tsx | 19 +- .../Databases/DatabaseDetail/index.tsx | 6 +- .../Databases/DatabaseEngineVersion.tsx | 2 - .../src/features/Databases/constants.ts | 6 - .../src/features/Databases/utilities.test.ts | 34 +- .../src/features/Databases/utilities.ts | 4 +- .../features/Domains/CloneDomainDrawer.tsx | 19 +- .../Domains/CreateDomain/CreateDomain.tsx | 22 +- .../src/features/Domains/DeleteDomain.tsx | 1 + .../features/Domains/DisableDomainDialog.tsx | 4 +- .../Domains/DomainActionMenu.test.tsx | 4 +- .../src/features/Domains/DomainActionMenu.tsx | 2 +- .../Domains/DomainDetail/DomainDetail.tsx | 26 +- .../features/Domains/DomainDetail/index.tsx | 11 +- .../Domains/DomainZoneImportDrawer.tsx | 9 +- .../features/Domains/DomainsLanding.test.tsx | 22 +- .../src/features/Domains/DomainsLanding.tsx | 185 ++--- .../DownloadDNSZoneFileButton.test.tsx | 8 +- .../src/features/Domains/EditDomainDrawer.tsx | 10 +- .../manager/src/features/Domains/constants.ts | 3 - .../manager/src/features/Domains/index.tsx | 46 ++ .../manager/src/features/Events/EventRow.tsx | 22 +- .../src/features/Events/EventsLanding.tsx | 6 +- .../features/Events/FormattedEventMessage.tsx | 16 +- .../src/features/Events/factories/domain.tsx | 14 +- .../src/features/Events/utils.test.tsx | 16 +- .../manager/src/features/Events/utils.tsx | 24 +- .../DocumentationResults.tsx | 4 +- .../manager/src/features/IAM/IAMLanding.tsx | 26 +- .../NoAssignedRoles/NoAssignedRoles.test.tsx | 25 - .../NoAssignedRoles/NoAssignedRoles.tsx | 21 - .../src/features/IAM/Shared/constants.ts | 4 - .../src/features/IAM/Shared/utilities.ts | 2 +- .../UserDetails/DeleteUserPanel.test.tsx | 87 --- .../IAM/Users/UserDetails/DeleteUserPanel.tsx | 57 -- .../UserDetails/UserDeleteConfirmation.tsx | 73 -- .../UserDetails/UserDetailsPanel.test.tsx | 118 --- .../Users/UserDetails/UserDetailsPanel.tsx | 140 ---- .../Users/UserDetails/UserEmailPanel.test.tsx | 71 -- .../IAM/Users/UserDetails/UserEmailPanel.tsx | 86 --- .../IAM/Users/UserDetails/UserProfile.tsx | 45 -- .../Users/UserDetails/UsernamePanel.test.tsx | 41 -- .../IAM/Users/UserDetails/UsernamePanel.tsx | 83 --- .../features/IAM/Users/UserDetailsLanding.tsx | 43 +- .../IAM/Users/UserEntities/UserEntities.tsx | 37 - .../IAM/Users/UserRoles/UserRoles.tsx | 50 -- .../manager/src/features/IAM/Users/Users.tsx | 34 + .../IAM/Users/UsersTable/UserRow.test.tsx | 117 --- .../features/IAM/Users/UsersTable/UserRow.tsx | 99 --- .../features/IAM/Users/UsersTable/Users.tsx | 101 --- .../Users/UsersTable/UsersActionMenu.test.tsx | 142 ---- .../IAM/Users/UsersTable/UsersActionMenu.tsx | 64 -- .../UsersTable/UsersLandingTableBody.test.tsx | 97 --- .../UsersTable/UsersLandingTableBody.tsx | 41 -- .../UsersTable/UsersLandingTableHead.tsx | 52 -- packages/manager/src/features/IAM/index.tsx | 36 +- .../ImagesCreate/CreateImageTab.test.tsx | 32 + .../Images/ImagesCreate/CreateImageTab.tsx | 26 +- .../Images/ImagesCreate/ImageUpload.utils.ts | 4 +- .../ClusterList/ClusterChips.test.tsx | 119 --- .../Kubernetes/ClusterList/ClusterChips.tsx | 45 -- .../ClusterList/KubernetesClusterRow.tsx | 27 +- .../Kubernetes/ClusterList/constants.ts | 5 +- .../CreateCluster/ApplicationPlatform.tsx | 13 +- .../CreateCluster/ClusterTypePanel.tsx | 6 +- .../CreateCluster/CreateCluster.tsx | 63 +- .../CreateCluster/HAControlPlane.tsx | 2 +- .../CreateCluster/NodePoolPanel.tsx | 9 +- .../KubeCheckoutBar/KubeCheckoutBar.test.tsx | 23 - .../KubeCheckoutBar/KubeCheckoutBar.tsx | 77 +- .../KubeCheckoutSummary.styles.ts | 48 -- .../KubeCheckoutBar/NodePoolSummary.test.tsx | 12 +- ...oolSummaryItem.tsx => NodePoolSummary.tsx} | 77 +- .../DeleteKubernetesClusterDialog.test.tsx | 29 +- .../DeleteKubernetesClusterDialog.tsx | 1 - .../KubeClusterSpecs.tsx | 21 +- .../KubeConfigDisplay.test.tsx | 65 -- .../KubeConfigDisplay.tsx | 12 +- .../KubeConfigDrawer.tsx | 2 +- .../KubeSummaryPanel.tsx | 52 +- .../KubernetesClusterDetail.tsx | 5 +- .../NodePoolsDisplay/NodePool.tsx | 6 - .../NodePoolsDisplay/NodePoolsDisplay.tsx | 4 +- .../NodePoolsDisplay/NodeRow.tsx | 6 +- .../NodePoolsDisplay/NodeTable.styles.ts | 37 +- .../NodePoolsDisplay/NodeTable.test.tsx | 6 +- .../NodePoolsDisplay/NodeTable.tsx | 96 +-- .../UpgradeKubernetesVersionBanner.tsx | 15 +- .../KubernetesLanding/KubernetesLanding.tsx | 8 +- .../Kubernetes/UpgradeVersionModal.tsx | 21 +- .../src/features/Kubernetes/kubeUtils.test.ts | 257 ++----- .../src/features/Kubernetes/kubeUtils.ts | 61 +- .../Linodes/HighPerformanceVolumeIcon.tsx | 33 - .../ApiAwarenessModal.test.tsx | 14 +- .../ApiAwarenessModal/CurlTabPanel.tsx | 22 +- .../ApiAwarenessModal/LinodeCLIPanel.tsx | 15 +- .../features/Linodes/LinodeCreate/VPC/VPC.tsx | 30 +- .../Linodes/LinodeCreate/resolvers.ts | 6 +- .../Linodes/LinodeCreate/utilities.test.tsx | 9 - .../Linodes/LinodeCreate/utilities.ts | 1 - .../features/Linodes/LinodeEntityDetail.tsx | 1 - .../Linodes/LinodeEntityDetailBody.tsx | 38 +- .../LinodeConfigs/LinodeConfigDialog.tsx | 12 +- .../LinodeNetworking/LinodeIPAddressRow.tsx | 6 +- .../LinodeNetworkingActionMenu.test.tsx | 67 -- .../LinodeNetworkingActionMenu.tsx | 2 +- .../TransferContent.styles.ts | 10 +- .../TransferHistory.tsx | 2 +- .../LinodeRebuild/RebuildFromImage.tsx | 8 +- .../LinodeRebuild/RebuildFromStackScript.tsx | 8 +- .../LinodeResize/LinodeResize.tsx | 12 +- .../LinodeSettingsDeletePanel.tsx | 1 - .../LinodesDetail/LinodeSettings/VPCPanel.tsx | 13 +- .../LinodeStorage/LinodeVolumes.tsx | 30 +- .../UpgradeVolumesDialog.tsx | 2 +- .../LinodesLanding/DeleteLinodeDialog.tsx | 3 +- .../LinodesLanding/DisplayLinodes.styles.ts | 2 +- .../Linodes/LinodesLanding/IPAddress.tsx | 6 +- .../LinodeRow/LinodeRow.test.tsx | 1 - .../Linodes/LinodesLanding/LinodesLanding.tsx | 9 +- .../Linodes/LinodesLanding/ListView.tsx | 1 - .../ActiveConnections/ConnectionRow.tsx | 6 +- .../ListeningServices/LongviewServiceRow.tsx | 8 +- .../DetailTabs/Processes/ProcessesTable.tsx | 10 +- .../shared/InstallationInstructions.styles.ts | 8 +- .../shared/InstallationInstructions.tsx | 41 +- .../features/Managed/Contacts/Contacts.tsx | 12 +- .../Managed/Contacts/ContactsActionMenu.tsx | 36 +- .../Managed/Contacts/ContactsDrawer.tsx | 7 +- .../features/Managed/Contacts/ContactsRow.tsx | 26 +- .../Managed/Contacts/ContactsTableContent.tsx | 19 +- .../Credentials/CredentialActionMenu.tsx | 7 +- .../src/features/Managed/MonitorDrawer.tsx | 35 +- .../Managed/Monitors/MonitorTable.styles.tsx | 14 +- .../Managed/Monitors/MonitorTable.tsx | 10 +- .../Managed/Monitors/MonitorTableContent.tsx | 15 +- .../Managed/SSHAccess/LinodePubKey.tsx | 55 +- .../Managed/SSHAccess/SSHAccessRow.tsx | 18 +- .../SSHAccess/SSHAccessTableContent.tsx | 21 +- .../NodeBalancerConfigNode.test.tsx | 11 +- .../NodeBalancers/NodeBalancerConfigNode.tsx | 34 +- .../NodeBalancerConfigPanel.test.tsx | 87 +-- .../NodeBalancers/NodeBalancerConfigPanel.tsx | 103 ++- .../NodeBalancers/NodeBalancerCreate.tsx | 116 ++- .../NodeBalancerDeleteDialog.test.tsx | 24 - .../NodeBalancerDeleteDialog.tsx | 1 - .../NodeBalancerConfigurations.tsx | 9 +- .../NodeBalancerSummary/TablesPanel.tsx | 4 +- .../src/features/NodeBalancers/constants.ts | 17 - .../src/features/NodeBalancers/types.ts | 73 +- .../src/features/NodeBalancers/utils.ts | 83 +-- .../Events/NotificationCenterEvent.tsx | 4 +- .../BucketDetail/AccessSelect.test.tsx | 5 + .../BucketDetail/BucketDetail.tsx | 3 +- .../BucketDetail/ObjectDetailsDrawer.test.tsx | 2 + .../ObjectStorage/BucketDetail/index.tsx | 18 + .../BucketLanding/BucketTable.tsx | 4 +- .../BucketLanding/ClusterSelect.test.tsx | 2 + .../BucketLanding/CreateBucketDrawer.test.tsx | 2 + .../BucketLanding/OMC_BucketLanding.tsx | 1 - .../src/features/ObjectStorage/utilities.ts | 17 +- .../PlacementGroupsDeleteModal.test.tsx | 31 +- .../PlacementGroupsDeleteModal.tsx | 1 - .../APITokens/CreateAPITokenDrawer.tsx | 10 +- .../features/Profile/APITokens/utils.test.ts | 12 - .../src/features/Profile/APITokens/utils.ts | 20 +- .../PhoneVerification.styles.ts | 9 +- .../TPAProviders.styles.ts | 5 +- .../AuthenticationSettings/TPAProviders.tsx | 2 +- .../AvatarColorPickerDialog.tsx | 8 +- .../DisplaySettings/DisplaySettings.tsx | 23 +- .../Profile/Referrals/Referrals.styles.ts | 7 +- .../Settings/MaskSensitiveData.test.tsx | 67 -- .../Profile/Settings/MaskSensitiveData.tsx | 40 - .../Profile/Settings/Notifications.test.tsx | 54 -- .../Profile/Settings/Notifications.tsx | 35 - .../features/Profile/Settings/Settings.tsx | 153 +++- .../features/Profile/Settings/Theme.test.tsx | 70 -- .../src/features/Profile/Settings/Theme.tsx | 47 -- .../Profile/Settings/TypeToConfirm.test.tsx | 65 -- .../Profile/Settings/TypeToConfirm.tsx | 42 -- .../StackScriptForm/StackScriptForm.tsx | 34 +- .../StackScripts/StackScriptsDetail.tsx | 2 - .../StackScripts/StackScriptsLanding.tsx | 8 +- .../SupportTicketProductSelectionFields.tsx | 25 +- .../TopMenu/CreateMenu/CreateMenu.styles.ts | 2 +- .../TopMenu/SearchBar/SearchBar.styles.ts | 2 +- .../src/features/Users/UserPermissions.tsx | 35 +- .../FormComponents/SubnetContent.test.tsx | 26 +- .../FormComponents/SubnetContent.tsx | 44 +- .../VPCTopSectionContent.test.tsx | 17 +- .../FormComponents/VPCTopSectionContent.tsx | 84 +-- .../VPCCreate/MultipleSubnetInput.test.tsx | 141 ++-- .../VPCs/VPCCreate/MultipleSubnetInput.tsx | 80 +- .../VPCs/VPCCreate/SubnetNode.test.tsx | 135 ++-- .../features/VPCs/VPCCreate/SubnetNode.tsx | 126 ++-- .../VPCs/VPCCreate/VPCCreate.test.tsx | 48 +- .../src/features/VPCs/VPCCreate/VPCCreate.tsx | 36 +- .../VPCCreateDrawer/VPCCreateDrawer.test.tsx | 43 +- .../VPCs/VPCCreateDrawer/VPCCreateDrawer.tsx | 98 +-- .../VPCDetail/SubnetDeleteDialog.test.tsx | 24 - .../VPCs/VPCDetail/SubnetDeleteDialog.tsx | 1 - .../VPCs/VPCDetail/SubnetEditDrawer.tsx | 80 +- .../VPCs/VPCLanding/VPCDeleteDialog.tsx | 1 - .../VPCs/VPCLanding/VPCEditDrawer.tsx | 122 ++-- .../{Drawers => }/AttachVolumeDrawer.test.tsx | 8 +- .../{Drawers => }/AttachVolumeDrawer.tsx | 4 +- .../{Drawers => }/CloneVolumeDrawer.test.tsx | 8 +- .../{Drawers => }/CloneVolumeDrawer.tsx | 11 +- .../{Dialogs => }/DeleteVolumeDialog.tsx | 5 +- .../{Dialogs => }/DetachVolumeDialog.tsx | 5 +- .../Volumes/Drawers/ManageTagsDrawer.spec.tsx | 48 -- .../Volumes/Drawers/ManageTagsDrawer.tsx | 121 --- .../{Drawers => }/EditVolumeDrawer.test.tsx | 8 +- .../{Drawers => }/EditVolumeDrawer.tsx | 39 +- .../{Drawers => }/ResizeVolumeDrawer.tsx | 10 +- .../{Dialogs => }/UpgradeVolumeDialog.tsx | 4 +- .../features/Volumes/VolumeCreate.test.tsx | 12 +- .../src/features/Volumes/VolumeCreate.tsx | 24 +- .../{Drawers => }/VolumeDetailsDrawer.tsx | 10 +- .../VolumeDrawer/ConfigSelect.tsx | 0 .../LinodeVolumeAddDrawer.test.tsx | 4 +- .../VolumeDrawer/LinodeVolumeAddDrawer.tsx | 0 .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 0 .../VolumeDrawer/LinodeVolumeCreateForm.tsx | 0 .../VolumeDrawer/ModeSelection.tsx | 0 .../{Drawers => }/VolumeDrawer/PricePanel.tsx | 0 .../{Drawers => }/VolumeDrawer/SizeField.tsx | 2 +- .../VolumeDrawer/VolumeSelect.tsx | 0 .../features/Volumes/VolumeTableRow.test.tsx | 44 +- .../src/features/Volumes/VolumeTableRow.tsx | 22 +- .../manager/src/features/Volumes/Volumes.tsx | 29 + .../Volumes/VolumesActionMenu.test.tsx | 23 +- .../features/Volumes/VolumesActionMenu.tsx | 44 +- .../src/features/Volumes/VolumesLanding.tsx | 256 ++++--- .../Volumes/VolumesLandingEmptyState.test.tsx | 6 +- .../Volumes/VolumesLandingEmptyState.tsx | 6 +- .../Volumes/VolumesLandingEmptyStateData.ts | 3 +- .../manager/src/features/Volumes/constants.ts | 1 - .../manager/src/features/Volumes/index.tsx | 2 + .../components/PlansPanel/PlanSelection.tsx | 6 +- .../PlansPanel/PlansPanel.styles.ts | 2 +- .../components/PlansPanel/PlansPanel.tsx | 3 - .../components/PlansPanel/constants.ts | 2 - .../features/components/PlansPanel/types.ts | 1 - .../components/PlansPanel/utils.test.ts | 25 - .../features/components/PlansPanel/utils.ts | 17 +- packages/manager/src/hooks/useCreateVPC.ts | 199 +++-- packages/manager/src/hooks/useDialogData.ts | 89 --- .../src/hooks/useDismissibleNotifications.ts | 6 +- .../src/hooks/useGlobalKeyboardListener.ts | 7 +- packages/manager/src/hooks/useOrder.ts | 10 +- .../manager/src/hooks/useOrderV2.test.tsx | 155 ---- packages/manager/src/hooks/useOrderV2.ts | 122 ---- packages/manager/src/hooks/usePagination.ts | 8 +- .../manager/src/hooks/usePaginationV2.test.ts | 258 ------- packages/manager/src/hooks/usePaginationV2.ts | 127 ---- packages/manager/src/hooks/usePendo.ts | 3 +- .../src/hooks/useSecureVMNoticesEnabled.ts | 10 +- .../manager/src/hooks/useUnassignLinode.ts | 4 +- packages/manager/src/index.tsx | 8 +- packages/manager/src/mocks/mockState.ts | 2 - .../src/mocks/presets/baseline/crud.ts | 2 - .../src/mocks/presets/baseline/noMocks.ts | 4 +- .../manager/src/mocks/presets/crud/domains.ts | 24 - .../mocks/presets/crud/handlers/domains.ts | 252 ------- .../src/mocks/presets/crud/seeds/domains.ts | 32 - .../src/mocks/presets/crud/seeds/index.ts | 2 - .../src/mocks/presets/crud/seeds/linodes.ts | 4 +- .../src/mocks/presets/crud/seeds/utils.ts | 3 - .../presets/extra/account/childAccount.ts | 49 ++ .../presets/extra/account/customAccount.ts | 33 - .../presets/extra/account/customProfile.ts | 33 - .../extra/account/lkeEnterpriseEnabled.ts | 33 + .../presets/extra/account/parentAccount.ts | 52 ++ packages/manager/src/mocks/presets/index.ts | 10 +- packages/manager/src/mocks/serverHandlers.ts | 145 +--- packages/manager/src/mocks/types.ts | 30 +- .../manager/src/mocks/utilities/events.ts | 2 +- .../manager/src/mocks/utilities/response.ts | 2 +- .../manager/src/queries/account/account.ts | 16 + .../manager/src/queries/account/queries.ts | 8 + .../manager/src/queries/cloudpulse/alerts.ts | 32 +- .../manager/src/queries/cloudpulse/queries.ts | 18 +- .../src/queries/cloudpulse/requests.ts | 16 - .../src/queries/cloudpulse/services.ts | 5 +- packages/manager/src/queries/domains.ts | 7 +- packages/manager/src/queries/kubernetes.ts | 28 +- .../manager/src/queries/linodes/linodes.ts | 2 +- .../src/queries/object-storage/queries.ts | 37 +- .../src/queries/profile/preferences.ts | 35 +- .../manager/src/queries/regions/regions.ts | 9 +- packages/manager/src/queries/vpcs/requests.ts | 6 +- packages/manager/src/queries/vpcs/vpcs.ts | 29 +- .../src/routes/domains/domainsLazyRoutes.ts | 17 - packages/manager/src/routes/domains/index.ts | 79 +- packages/manager/src/routes/index.tsx | 14 +- packages/manager/src/routes/routes.test.tsx | 17 - packages/manager/src/routes/types.ts | 9 - .../{VolumesRoot.tsx => VolumesRoute.tsx} | 2 +- .../manager/src/routes/volumes/constants.ts | 3 - packages/manager/src/routes/volumes/index.ts | 78 +- .../src/routes/volumes/volumesLazyRoutes.ts | 12 - .../src/store/selectors/getSearchEntities.ts | 2 +- .../manager/src/types/ManagerPreferences.ts | 10 +- .../src/utilities/CustomKeyboardSensor.ts | 11 +- .../utilities/codesnippets/generate-cli.ts | 18 +- .../manager/src/utilities/eventUtils.test.ts | 39 + packages/manager/src/utilities/eventUtils.ts | 32 + .../src/utilities/formikErrorUtils.test.ts | 95 +++ .../manager/src/utilities/formikErrorUtils.ts | 54 ++ .../src/utilities/pricing/constants.ts | 2 - .../src/utilities/pricing/kubernetes.ts | 11 +- packages/manager/src/utilities/subnets.ts | 17 + packages/manager/src/utilities/theme.ts | 12 +- packages/ui/CHANGELOG.md | 16 - packages/ui/package.json | 2 +- .../ui/src/components/Accordion/Accordion.tsx | 4 +- .../components/Autocomplete/Autocomplete.tsx | 71 +- .../src/components/Button/Button.stories.tsx | 8 - packages/ui/src/components/Button/Button.tsx | 32 +- .../src/components/Button/StyledTagButton.ts | 2 +- .../EditableText/EditableText.stories.tsx | 20 - .../components/EditableText/EditableText.tsx | 53 +- packages/ui/src/components/Notice/Notice.tsx | 31 +- .../src/components/Select/Select.stories.tsx | 137 ---- .../ui/src/components/Select/Select.test.tsx | 122 ---- packages/ui/src/components/Select/Select.tsx | 304 -------- .../components/TextField/TextField.test.tsx | 32 - .../ui/src/components/TextField/TextField.tsx | 19 +- packages/ui/src/components/index.ts | 1 - packages/ui/src/foundations/themes/dark.ts | 28 +- packages/ui/src/foundations/themes/index.ts | 6 - packages/ui/src/foundations/themes/light.ts | 28 +- packages/validation/CHANGELOG.md | 12 - packages/validation/package.json | 2 +- packages/validation/src/buckets.schema.ts | 13 +- packages/validation/src/cloudpulse.schema.ts | 33 +- packages/validation/src/images.schema.ts | 14 +- packages/validation/src/kubernetes.schema.ts | 14 +- .../validation/src/nodebalancers.schema.ts | 95 +-- packages/validation/src/volumes.schema.ts | 3 +- packages/validation/src/vpcs.schema.ts | 22 +- scripts/changelog/generate-changelogs.mjs | 1 + scripts/junit-summary/index.ts | 2 +- scripts/package-versions/index.js | 12 +- scripts/package.json | 25 - scripts/tod-payload/index.ts | 2 +- yarn.lock | 222 +----- 623 files changed, 5168 insertions(+), 22360 deletions(-) delete mode 100644 packages/manager/cypress/component/components/autocomplete.spec.tsx delete mode 100644 packages/manager/cypress/component/components/select.spec.tsx delete mode 100644 packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx rename packages/manager/cypress/component/{components => poc}/beta-chip.spec.tsx (99%) rename packages/manager/cypress/component/{components => poc}/region-select.spec.tsx (99%) delete mode 100644 packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts delete mode 100644 packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts delete mode 100644 packages/manager/cypress/support/constants/login.ts delete mode 100644 packages/manager/cypress/support/util/lke.ts delete mode 100644 packages/manager/src/assets/icons/emptynotification.svg delete mode 100644 packages/manager/src/assets/icons/entityIcons/alerts.svg create mode 100644 packages/manager/src/assets/icons/uploadPending.svg delete mode 100644 packages/manager/src/components/DatePicker/DatePicker.stories.tsx delete mode 100644 packages/manager/src/components/DatePicker/DatePicker.test.tsx delete mode 100644 packages/manager/src/components/DatePicker/DatePicker.tsx delete mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx delete mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.test.tsx delete mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.tsx delete mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx delete mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx delete mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx delete mode 100644 packages/manager/src/components/DatePicker/TimeZoneSelect.tsx delete mode 100644 packages/manager/src/components/MaskableText/MaskableTextArea.tsx delete mode 100644 packages/manager/src/components/MultipleIPInput/MultipleIPInput.stories.tsx delete mode 100644 packages/manager/src/components/TanstackLink.stories.tsx delete mode 100644 packages/manager/src/components/TanstackLinks.tsx create mode 100644 packages/manager/src/components/VPCSelect.test.tsx create mode 100644 packages/manager/src/components/VPCSelect.tsx delete mode 100644 packages/manager/src/dev-tools/components/ExtraPresetAccount.tsx delete mode 100644 packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx delete mode 100644 packages/manager/src/dev-tools/components/JsonTextArea.tsx rename packages/manager/src/dev-tools/{DevTools.tsx => dev-tools.tsx} (100%) delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailOverview.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailOverview.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailRow.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertActionMenu.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListing.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListing.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertTableRow.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertTableRow.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/AlertsListing/constants.ts delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/ClearIconButton.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/ClearIconButton.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/TriggerConditions.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/TriggerConditions.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/Utils/AlertsActionMenu.ts delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/Utils/utils.test.ts delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/Utils/utils.ts delete mode 100644 packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.test.ts delete mode 100644 packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Widget/components/CloudPulseLineGraph.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseAppliedFilter.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseAppliedFilter.tsx delete mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseAppliedFilterRenderer.tsx delete mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseTagsFilter.test.tsx delete mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseTagsFilter.tsx delete mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.utils.tsx delete mode 100644 packages/manager/src/features/Domains/constants.ts create mode 100644 packages/manager/src/features/Domains/index.tsx delete mode 100644 packages/manager/src/features/IAM/Shared/NoAssignedRoles/NoAssignedRoles.test.tsx delete mode 100644 packages/manager/src/features/IAM/Shared/NoAssignedRoles/NoAssignedRoles.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/DeleteUserPanel.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/DeleteUserPanel.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UserDeleteConfirmation.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UserEmailPanel.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UserEmailPanel.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UserProfile.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UsernamePanel.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserDetails/UsernamePanel.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserEntities/UserEntities.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UserRoles/UserRoles.tsx create mode 100644 packages/manager/src/features/IAM/Users/Users.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UserRow.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/Users.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UsersActionMenu.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UsersActionMenu.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableBody.test.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableBody.tsx delete mode 100644 packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableHead.tsx delete mode 100644 packages/manager/src/features/Kubernetes/ClusterList/ClusterChips.test.tsx delete mode 100644 packages/manager/src/features/Kubernetes/ClusterList/ClusterChips.tsx delete mode 100644 packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutSummary.styles.ts rename packages/manager/src/features/Kubernetes/KubeCheckoutBar/{NodePoolSummaryItem.tsx => NodePoolSummary.tsx} (52%) delete mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeConfigDisplay.test.tsx delete mode 100644 packages/manager/src/features/Linodes/HighPerformanceVolumeIcon.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.test.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/constants.ts delete mode 100644 packages/manager/src/features/Profile/Settings/MaskSensitiveData.test.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/MaskSensitiveData.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/Notifications.test.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/Notifications.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/Theme.test.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/Theme.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/TypeToConfirm.test.tsx delete mode 100644 packages/manager/src/features/Profile/Settings/TypeToConfirm.tsx rename packages/manager/src/features/Volumes/{Drawers => }/AttachVolumeDrawer.test.tsx (90%) rename packages/manager/src/features/Volumes/{Drawers => }/AttachVolumeDrawer.tsx (98%) rename packages/manager/src/features/Volumes/{Drawers => }/CloneVolumeDrawer.test.tsx (90%) rename packages/manager/src/features/Volumes/{Drawers => }/CloneVolumeDrawer.tsx (95%) rename packages/manager/src/features/Volumes/{Dialogs => }/DeleteVolumeDialog.tsx (91%) rename packages/manager/src/features/Volumes/{Dialogs => }/DetachVolumeDialog.tsx (94%) delete mode 100644 packages/manager/src/features/Volumes/Drawers/ManageTagsDrawer.spec.tsx delete mode 100644 packages/manager/src/features/Volumes/Drawers/ManageTagsDrawer.tsx rename packages/manager/src/features/Volumes/{Drawers => }/EditVolumeDrawer.test.tsx (90%) rename packages/manager/src/features/Volumes/{Drawers => }/EditVolumeDrawer.tsx (80%) rename packages/manager/src/features/Volumes/{Drawers => }/ResizeVolumeDrawer.tsx (94%) rename packages/manager/src/features/Volumes/{Dialogs => }/UpgradeVolumeDialog.tsx (95%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDetailsDrawer.tsx (91%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/ConfigSelect.tsx (100%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx (89%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/LinodeVolumeAddDrawer.tsx (100%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/LinodeVolumeAttachForm.tsx (100%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/LinodeVolumeCreateForm.tsx (100%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/ModeSelection.tsx (100%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/PricePanel.tsx (100%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/SizeField.tsx (98%) rename packages/manager/src/features/Volumes/{Drawers => }/VolumeDrawer/VolumeSelect.tsx (100%) create mode 100644 packages/manager/src/features/Volumes/Volumes.tsx delete mode 100644 packages/manager/src/features/Volumes/constants.ts create mode 100644 packages/manager/src/features/Volumes/index.tsx delete mode 100644 packages/manager/src/hooks/useDialogData.ts delete mode 100644 packages/manager/src/hooks/useOrderV2.test.tsx delete mode 100644 packages/manager/src/hooks/useOrderV2.ts delete mode 100644 packages/manager/src/hooks/usePaginationV2.test.ts delete mode 100644 packages/manager/src/hooks/usePaginationV2.ts delete mode 100644 packages/manager/src/mocks/presets/crud/domains.ts delete mode 100644 packages/manager/src/mocks/presets/crud/handlers/domains.ts delete mode 100644 packages/manager/src/mocks/presets/crud/seeds/domains.ts create mode 100644 packages/manager/src/mocks/presets/extra/account/childAccount.ts delete mode 100644 packages/manager/src/mocks/presets/extra/account/customAccount.ts delete mode 100644 packages/manager/src/mocks/presets/extra/account/customProfile.ts create mode 100644 packages/manager/src/mocks/presets/extra/account/lkeEnterpriseEnabled.ts create mode 100644 packages/manager/src/mocks/presets/extra/account/parentAccount.ts delete mode 100644 packages/manager/src/queries/cloudpulse/requests.ts delete mode 100644 packages/manager/src/routes/domains/domainsLazyRoutes.ts rename packages/manager/src/routes/volumes/{VolumesRoot.tsx => VolumesRoute.tsx} (92%) delete mode 100644 packages/manager/src/routes/volumes/constants.ts delete mode 100644 packages/manager/src/routes/volumes/volumesLazyRoutes.ts create mode 100644 packages/manager/src/utilities/eventUtils.test.ts create mode 100644 packages/manager/src/utilities/eventUtils.ts delete mode 100644 packages/ui/src/components/Select/Select.stories.tsx delete mode 100644 packages/ui/src/components/Select/Select.test.tsx delete mode 100644 packages/ui/src/components/Select/Select.tsx delete mode 100644 scripts/package.json diff --git a/docs/development-guide/13-coding-standards.md b/docs/development-guide/13-coding-standards.md index d840fadc201..159281d7989 100644 --- a/docs/development-guide/13-coding-standards.md +++ b/docs/development-guide/13-coding-standards.md @@ -14,19 +14,7 @@ We use [ESLint](https://eslint.org/) to enforce coding and formatting standards. If you are using VSCode it is **highly** recommended to use the [ESlint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). The [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) is also recommended, as it can be configured to format your code on save. ## React -### useEffect() -`useEffect()` should only be used for handling true side effects - specifically API calls, subscriptions, and DOM mutations that must occur outside React's render cycle. While you may encounter instances where `useEffect()` is used differently throughout our existing codebase, we're actively working to remove those instances. Existing code that does not adhere to the hook's proper use should not be used as precedent for implementing new `useEffect()` instances. All state updates and data transformations should be handled through event handlers and direct state management. -When Not to Use Effects: -- Prop synchronization with state -- Derived state calculations -- Post-render state updates -- Props/state triggers for child components -- Chaining state updates - -Reference: https://react.dev/learn/you-might-not-need-an-effect - -### useId() [Several new hooks were introduced with the release of React 18](https://react.dev/blog/2022/03/29/react-v18#new-hooks). It should be noted that the `useId()` hook is particularly useful for generating unique IDs for accessibility attributes. For this use case, `useId()` is preferred over hardcoding the ID because components may be rendered more than once on a page, but IDs must be unique. diff --git a/docs/tooling/analytics.md b/docs/tooling/analytics.md index 9c8c994c08a..a0ab6b9e474 100644 --- a/docs/tooling/analytics.md +++ b/docs/tooling/analytics.md @@ -1,35 +1,8 @@ # Analytics -## Pendo - -Cloud Manager uses [Pendo](https://www.pendo.io/pendo-for-your-customers/) to capture analytics, guide users, and improve the user experience. Pendo is the **preferred** method for collecting analytics, including user events, since it requires no development effort and can be accomplished via the Pendo UI. - -To view Pendo dashboards, Cloud Manager developers must follow internal processes to request access. - -### Set Up and Initialization - -Pendo is configured in [`usePendo.js`](https://github.com/linode/manager/blob/develop/packages/manager/src/hooks/usePendo.ts). This custom hook allows us to initialize the Pendo analytics script when the [App](https://github.com/linode/manager/blob/develop/packages/manager/src/App.tsx#L56) is mounted. - -Important notes: - -- Pendo is only loaded if a valid `PENDO_API_KEY` is configured as an environment variable. In our development, staging, and production environments, `PENDO_API_KEY` is available at build time. See **Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo** for set up with local environments. -- We load the Pendo agent from the CDN, rather than [self-hosting](https://support.pendo.io/hc/en-us/articles/360038969692-Self-hosting-the-Pendo-agent), and we have configured a [CNAME](https://support.pendo.io/hc/en-us/articles/360043539891-CNAME-for-Pendo). -- We are hashing account and visitor IDs in a way that is consistent with Akamai's standards. -- At initialization, we do string transformation on select URL patterns to **remove sensitive data**. When new URL patterns are added to Cloud Manager, verify that existing transforms remove sensitive data; if not, update the transforms. -- Pendo is currently not using any client-side (cookies or local) storage. -- Pendo makes use of the existing `data-testid` properties, used in our automated testing, for tagging elements. They are more persistent and reliable than CSS properties, which are liable to change. - -### Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo - -1. Set the `REACT_APP_PENDO_API_KEY` environment variable in `.env`. -2. Use the browser tools Network tab, filter requests by "psp.cloud", and check that successful network requests have been made to load Pendo scripts (also visible in the browser tools Sources tab). -3. In the browser console, type `pendo.validateEnvironment()`. -4. You should see command output in the console, and it should include a hashed `accountId` and hashed `visitorId`. Each page view change or custom event that fires should be visible as a request in the Network tab. -5. If the console does not output the expected ids and instead outputs something like `Cookies are disabled in Pendo config. Is this expected?` in response to the above command, clear app storage with the browser tools. Once redirected back to Login, update the OneTrust cookie settings to enable cookies via "Manage Preferences" in the banner at the bottom of the screen. Log back into Cloud Manager and Pendo should load. - ## Adobe Analytics -Cloud Manager uses Adobe Analytics to capture page view and custom event statistics, although Pendo is the preferred method for collecting this data where possible, as of Q4 2024. To view analytics, Cloud Manager developers must follow internal processes to request access to Adobe Analytics dashboards. +Cloud Manager uses Adobe Analytics to capture page view and custom event statistics. To view analytics, Cloud Manager developers must follow internal processes to request access to Adobe Analytics dashboards. ### Writing a Custom Event @@ -90,3 +63,27 @@ See the `LinodeCreateForm` form events as an example. 3. In the browser console, type `_satellite.setDebug(true)`. 4. Refresh the page. You should see Adobe debug log output in the console. Each page view change or custom event that fires should be visible in the logs. 5. When viewing dashboards in Adobe Analytics, it may take ~1 hour for analytics data to update. Once this happens, locally fired events will be visible in the dev dashboard. + +## Pendo + +Cloud Manager uses [Pendo](https://www.pendo.io/pendo-for-your-customers/) to capture analytics, guide users, and improve the user experience. To view Pendo dashboards, Cloud Manager developers must follow internal processes to request access. + +### Set Up and Initialization + +Pendo is configured in [`usePendo.js`](https://github.com/linode/manager/blob/develop/packages/manager/src/hooks/usePendo.ts). This custom hook allows us to initialize the Pendo analytics script when the [App](https://github.com/linode/manager/blob/develop/packages/manager/src/App.tsx#L56) is mounted. + +Important notes: + +- Pendo is only loaded if a valid `PENDO_API_KEY` is configured as an environment variable. In our development, staging, and production environments, `PENDO_API_KEY` is available at build time. See **Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo** for set up with local environments. +- We load the Pendo agent from the CDN, rather than [self-hosting](https://support.pendo.io/hc/en-us/articles/360038969692-Self-hosting-the-Pendo-agent). +- We are hashing account and visitor IDs in a way that is consistent with Akamai's standards. +- At initialization, we do string transformation on select URL patterns to **remove sensitive data**. When new URL patterns are added to Cloud Manager, verify that existing transforms remove sensitive data; if not, update the transforms. +- Pendo is currently not using any client-side (cookies or local) storage. +- Pendo makes use of the existing `data-testid` properties, used in our automated testing, for tagging elements. They are more persistent and reliable than CSS properties, which are liable to change. + +### Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo + +1. Set the `REACT_APP_PENDO_API_KEY` environment variable in `.env`. +2. Use the browser tools Network tab, filter requests by "pendo", and check that successful network requests have been made to load Pendo scripts. (Also visible in browser tools Sources tab.) +3. In the browser console, type `pendo.validateEnvironment()`. +4. You should see command output in the console, and it should include a hashed `accountId` and hashed `visitorId`. Each page view change or custom event that fires should be visible as a request in the Network tab. diff --git a/package.json b/package.json index 46ba965031f..b4fbea4ca1f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test:sdk": "yarn workspace @linode/api-v4 test", "test:search": "yarn workspace @linode/search test", "test:ui": "yarn workspace @linode/ui test", - "package-versions": "yarn workspace @linode/scripts package-versions", + "package-versions": "node ./scripts/package-versions/index.js", "storybook": "yarn workspace linode-manager storybook", "cy:run": "yarn workspace linode-manager cy:run", "cy:e2e": "yarn workspace linode-manager cy:e2e", @@ -39,25 +39,23 @@ "cy:component": "yarn workspace linode-manager cy:component", "cy:component:run": "yarn workspace linode-manager cy:component:run", "cy:rec-snap": "yarn workspace linode-manager cy:rec-snap", - "changeset": "yarn workspace @linode/scripts changeset", - "generate-changelogs": "yarn workspace @linode/scripts generate-changelogs", + "changeset": "node scripts/changelog/changeset.mjs", + "generate-changelogs": "node scripts/changelog/generate-changelogs.mjs", "coverage": "yarn workspace linode-manager coverage", "coverage:summary": "yarn workspace linode-manager coverage:summary", - "junit:summary": "YARN_SILENT=1 yarn workspace @linode/scripts junit:summary", - "generate-tod": "YARN_SILENT=1 yarn workspace @linode/scripts generate-tod", + "junit:summary": "tsx scripts/junit-summary/index.ts", + "generate-tod": "tsx scripts/tod-payload/index.ts", "docs": "bunx vitepress@1.0.0-rc.44 dev docs", "prepare": "husky" }, "resolutions": { "node-fetch": "^2.6.7", "yaml": "^2.3.0", - "semver": "^7.5.2", - "cookie": "^0.7.0" + "semver": "^7.5.2" }, "workspaces": { "packages": [ - "packages/*", - "scripts" + "packages/*" ] }, "version": "0.0.0", diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 2094dabd71e..06eefea7905 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,35 +1,3 @@ -## [2025-01-14] - v0.132.0 - -### Added: - -- Types for UDP NodeBalancer support ([#11321](https://github.com/linode/manager/pull/11321)) -- Tags to `KubeNodePoolResponse` and `UpdateNodePoolData` ([#11368](https://github.com/linode/manager/pull/11368)) - -### Changed: - -- Type of `AlertDefinitionType` to `'system'|'user'` ([#11346](https://github.com/linode/manager/pull/11346)) -- Property names, and types of the CreateAlertDefinitionPayload and Alert interfaces ([#11392](https://github.com/linode/manager/pull/11392)) -- BaseDatabase total_disk_size_gb and used_disk_size_gb are always expected and used_disk_size_gb can be null ([#11426](https://github.com/linode/manager/pull/11426)) -- Renamed `AvailableMetrics` type to `MetricDefinition` ([#11433](https://github.com/linode/manager/pull/11433)) -- Changed MetricCritera, DimensionFilter and Alert Interfaces ([#11445](https://github.com/linode/manager/pull/11445)) - -### Fixed: - -- Nullable AccountBeta ended & description properties ([#11347](https://github.com/linode/manager/pull/11347)) -- Incorrect return type of `updateObjectACL` ([#11369](https://github.com/linode/manager/pull/11369)) - -### Removed: - -- getAccountInfoBeta endpoint ([#11413](https://github.com/linode/manager/pull/11413)) -- `MetricDefinitions` type ([#11433](https://github.com/linode/manager/pull/11433)) - -### Upcoming Features: - -- Fix types for IAM API ([#11397](https://github.com/linode/manager/pull/11397)) -- Add new `getAlertDefinitionByServiceTypeAndId` endpoint to fetch Cloud Pulse alert details by id and service type ([#11399](https://github.com/linode/manager/pull/11399)) -- New `Block Storage Performance B1` linode capability ([#11400](https://github.com/linode/manager/pull/11400)) -- Add `getKubernetesTypesBeta` function ([#11419](https://github.com/linode/manager/pull/11419)) - ## [2024-12-10] - v0.131.0 ### Added: diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index ccb47929f77..d26261a5949 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.132.0", + "version": "0.131.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/account.ts b/packages/api-v4/src/account/account.ts index b612da3d082..0b704f230b6 100644 --- a/packages/api-v4/src/account/account.ts +++ b/packages/api-v4/src/account/account.ts @@ -35,6 +35,18 @@ export const getAccountInfo = () => { return Request(setURL(`${API_ROOT}/account`), setMethod('GET')); }; +/** + * getAccountInfoBeta + * + * Return beta endpoint account information, + * including contact and billing info. + * + * @TODO LKE-E - M3-8838: Clean up after released to GA, if not otherwise in use + */ +export const getAccountInfoBeta = () => { + return Request(setURL(`${BETA_API_ROOT}/account`), setMethod('GET')); +}; + /** * getNetworkUtilization * diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 08b575c7606..6bd9ee37d3f 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -59,35 +59,32 @@ export interface Account { export type BillingSource = 'linode' | 'akamai'; -export const accountCapabilities = [ - 'Akamai Cloud Load Balancer', - 'Akamai Cloud Pulse', - 'Block Storage', - 'Block Storage Encryption', - 'Cloud Firewall', - 'CloudPulse', - 'Disk Encryption', - 'Kubernetes', - 'Kubernetes Enterprise', - 'Linodes', - 'LKE HA Control Planes', - 'LKE Network Access Control List (IP ACL)', - 'Machine Images', - 'Managed Databases', - 'Managed Databases Beta', - 'NETINT Quadra T1U', - 'NodeBalancers', - 'Object Storage Access Key Regions', - 'Object Storage Endpoint Types', - 'Object Storage', - 'Placement Group', - 'SMTP Enabled', - 'Support Ticket Severity', - 'Vlans', - 'VPCs', -] as const; - -export type AccountCapability = typeof accountCapabilities[number]; +export type AccountCapability = + | 'Akamai Cloud Load Balancer' + | 'Akamai Cloud Pulse' + | 'Block Storage' + | 'Block Storage Encryption' + | 'Cloud Firewall' + | 'CloudPulse' + | 'Disk Encryption' + | 'Kubernetes' + | 'Kubernetes Enterprise' + | 'Linodes' + | 'LKE HA Control Planes' + | 'LKE Network Access Control List (IP ACL)' + | 'Machine Images' + | 'Managed Databases' + | 'Managed Databases Beta' + | 'NETINT Quadra T1U' + | 'NodeBalancers' + | 'Object Storage Access Key Regions' + | 'Object Storage Endpoint Types' + | 'Object Storage' + | 'Placement Group' + | 'SMTP Enabled' + | 'Support Ticket Severity' + | 'Vlans' + | 'VPCs'; export interface AccountAvailability { region: string; // will be slug of dc (matches id field of region object returned by API) @@ -607,8 +604,8 @@ export interface AccountBeta { label: string; started: string; id: string; - ended: string | null; - description: string | null; + ended?: string; + description?: string; /** * The datetime the account enrolled into the beta * @example 2024-10-23T14:22:29 diff --git a/packages/api-v4/src/cloudpulse/alerts.ts b/packages/api-v4/src/cloudpulse/alerts.ts index 54ece904adc..3c6f909b9db 100644 --- a/packages/api-v4/src/cloudpulse/alerts.ts +++ b/packages/api-v4/src/cloudpulse/alerts.ts @@ -1,14 +1,7 @@ import { createAlertDefinitionSchema } from '@linode/validation'; -import Request, { - setURL, - setMethod, - setData, - setParams, - setXFilter, -} from '../request'; +import Request, { setURL, setMethod, setData } from '../request'; import { Alert, AlertServiceType, CreateAlertDefinitionPayload } from './types'; -import { BETA_API_ROOT as API_ROOT } from '../constants'; -import { Params, Filter, ResourcePage } from '../types'; +import { BETA_API_ROOT as API_ROOT } from 'src/constants'; export const createAlertDefinition = ( data: CreateAlertDefinitionPayload, @@ -23,24 +16,3 @@ export const createAlertDefinition = ( setMethod('POST'), setData(data, createAlertDefinitionSchema) ); - -export const getAlertDefinitions = (params?: Params, filters?: Filter) => - Request>( - setURL(`${API_ROOT}/monitor/alert-definitions`), - setMethod('GET'), - setParams(params), - setXFilter(filters) - ); - -export const getAlertDefinitionByServiceTypeAndId = ( - serviceType: string, - alertId: number -) => - Request( - setURL( - `${API_ROOT}/monitor/services/${encodeURIComponent( - serviceType - )}/alert-definitions/${encodeURIComponent(alertId)}` - ), - setMethod('GET') - ); diff --git a/packages/api-v4/src/cloudpulse/services.ts b/packages/api-v4/src/cloudpulse/services.ts index 902cb2ec4a2..5eb06faa0e5 100644 --- a/packages/api-v4/src/cloudpulse/services.ts +++ b/packages/api-v4/src/cloudpulse/services.ts @@ -3,13 +3,13 @@ import Request, { setData, setMethod, setURL } from '../request'; import { JWEToken, JWETokenPayLoad, - MetricDefinition, + MetricDefinitions, ServiceTypesList, } from './types'; -import { ResourcePage } from 'src/types'; +import { ResourcePage as Page } from 'src/types'; export const getMetricDefinitionsByServiceType = (serviceType: string) => { - return Request>( + return Request>( setURL( `${API_ROOT}/monitor/services/${encodeURIComponent( serviceType diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 2e4e6c4658a..4b64bf16c30 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -2,24 +2,9 @@ export type AlertSeverityType = 0 | 1 | 2 | 3; export type MetricAggregationType = 'avg' | 'sum' | 'min' | 'max' | 'count'; export type MetricOperatorType = 'eq' | 'gt' | 'lt' | 'gte' | 'lte'; export type AlertServiceType = 'linode' | 'dbaas'; -export type DimensionFilterOperatorType = - | 'eq' - | 'neq' - | 'startswith' - | 'endswith'; -export type AlertDefinitionType = 'system' | 'user'; +type DimensionFilterOperatorType = 'eq' | 'neq' | 'startswith' | 'endswith'; +export type AlertDefinitionType = 'default' | 'custom'; export type AlertStatusType = 'enabled' | 'disabled'; -export type CriteriaConditionType = 'ALL'; -export type MetricUnitType = - | 'number' - | 'byte' - | 'second' - | 'percent' - | 'bit_per_second' - | 'millisecond' - | 'KB' - | 'MB' - | 'GB'; export interface Dashboard { id: number; label: string; @@ -89,7 +74,11 @@ export interface AclpWidget { size: number; } -export interface MetricDefinition { +export interface MetricDefinitions { + data: AvailableMetrics[]; +} + +export interface AvailableMetrics { label: string; metric: string; metric_type: string; @@ -153,49 +142,37 @@ export interface ServiceTypesList { export interface CreateAlertDefinitionPayload { label: string; - tags?: string[]; description?: string; entity_ids?: string[]; severity: AlertSeverityType; rule_criteria: { rules: MetricCriteria[]; }; - trigger_conditions: TriggerCondition; + triggerCondition: TriggerCondition; channel_ids: number[]; } export interface MetricCriteria { metric: string; aggregation_type: MetricAggregationType; operator: MetricOperatorType; - threshold: number; - dimension_filters?: DimensionFilter[]; + value: number; + dimension_filters: DimensionFilter[]; } -export interface AlertDefinitionMetricCriteria - extends Omit { - unit: string; - label: string; - dimension_filters?: AlertDefinitionDimensionFilter[]; -} export interface DimensionFilter { dimension_label: string; operator: DimensionFilterOperatorType; value: string; } -export interface AlertDefinitionDimensionFilter extends DimensionFilter { - label: string; -} export interface TriggerCondition { polling_interval_seconds: number; evaluation_period_seconds: number; trigger_occurrences: number; - criteria_condition: CriteriaConditionType; } export interface Alert { id: number; label: string; - tags: string[]; description: string; has_more_resources: boolean; status: AlertStatusType; @@ -204,9 +181,9 @@ export interface Alert { service_type: AlertServiceType; entity_ids: string[]; rule_criteria: { - rules: AlertDefinitionMetricCriteria[]; + rules: MetricCriteria[]; }; - trigger_conditions: TriggerCondition; + triggerCondition: TriggerCondition; channels: { id: string; label: string; diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 711dbb3aec3..87b4458308a 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -165,8 +165,16 @@ interface BaseDatabase extends DatabaseInstance { port: number; /** @Deprecated used by rdbms-legacy only, rdbms-default always uses TLS */ ssl_connection: boolean; - total_disk_size_gb: number; - used_disk_size_gb: number | null; + /** + * total_disk_size_gb is feature flagged by the API. + * It may not be defined. + */ + total_disk_size_gb?: number; + /** + * used_disk_size_gb is feature flagged by the API. + * It may not be defined. + */ + used_disk_size_gb?: number; } /** @deprecated TODO (UIE-8214) remove POST GA */ diff --git a/packages/api-v4/src/iam/types.ts b/packages/api-v4/src/iam/types.ts index 3749b644e64..8aa9fd0ce17 100644 --- a/packages/api-v4/src/iam/types.ts +++ b/packages/api-v4/src/iam/types.ts @@ -1,4 +1,9 @@ -export type ResourceTypePermissions = +export interface IamUserPermissions { + account_access: AccountAccessType[]; + resource_access: ResourceAccess[]; +} + +type ResourceType = | 'linode' | 'firewall' | 'nodebalancer' @@ -11,28 +16,24 @@ export type ResourceTypePermissions = | 'account' | 'vpc'; -export type AccountAccessType = +type AccountAccessType = | 'account_linode_admin' | 'linode_creator' - | 'linode_contributor' | 'firewall_creator'; -export type RoleType = - | 'linode_contributor' - | 'firewall_admin' - | 'linode_creator' - | 'firewall_creator'; +type RoleType = 'linode_contributor' | 'firewall_admin'; -export interface IamUserPermissions { - account_access: AccountAccessType[]; - resource_access: ResourceAccess[]; -} export interface ResourceAccess { resource_id: number; - resource_type: ResourceTypePermissions; + resource_type: ResourceType; roles: RoleType[]; } +export interface IamAccountPermissions { + account_access: Access[]; + resource_access: Access[]; +} + type PermissionType = | 'create_linode' | 'update_linode' @@ -40,20 +41,13 @@ type PermissionType = | 'delete_linode' | 'view_linode'; -export interface IamAccountPermissions { - account_access: IamAccess[]; - resource_access: IamAccess[]; -} - -export interface IamAccess { - resource_type: ResourceTypePermissions; +interface Access { + resource_type: ResourceType; roles: Roles[]; } export interface Roles { name: string; description: string; - permissions: PermissionType[]; + permissions?: PermissionType[]; } - -export type IamAccessType = keyof IamAccountPermissions; diff --git a/packages/api-v4/src/kubernetes/kubernetes.ts b/packages/api-v4/src/kubernetes/kubernetes.ts index 3a3d68917f0..4281ab35c1c 100644 --- a/packages/api-v4/src/kubernetes/kubernetes.ts +++ b/packages/api-v4/src/kubernetes/kubernetes.ts @@ -275,18 +275,6 @@ export const getKubernetesTypes = (params?: Params) => setParams(params) ); -/** - * getKubernetesTypesBeta - * - * Returns a paginated list of available Kubernetes types from beta API; used for dynamic pricing. - */ -export const getKubernetesTypesBeta = (params?: Params) => - Request>( - setURL(`${BETA_API_ROOT}/lke/types`), - setMethod('GET'), - setParams(params) - ); - /** * getKubernetesClusterControlPlaneACL * diff --git a/packages/api-v4/src/kubernetes/types.ts b/packages/api-v4/src/kubernetes/types.ts index 6642fcec895..262db13dfc0 100644 --- a/packages/api-v4/src/kubernetes/types.ts +++ b/packages/api-v4/src/kubernetes/types.ts @@ -23,7 +23,6 @@ export interface KubeNodePoolResponse { count: number; id: number; nodes: PoolNodeResponse[]; - tags: string[]; type: string; autoscaler: AutoscaleSettings; disk_encryption?: EncryptionStatus; // @TODO LDE: remove optionality once LDE is fully rolled out @@ -43,7 +42,6 @@ export interface CreateNodePoolData { export interface UpdateNodePoolData { autoscaler: AutoscaleSettings; count: number; - tags: string[]; } export interface AutoscaleSettings { diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 5689c24ba34..6f3d94caad3 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -20,7 +20,7 @@ export interface Linode { id: number; alerts: LinodeAlerts; backups: LinodeBackups; - capabilities: LinodeCapabilities[]; + capabilities?: LinodeCapabilities[]; // @TODO BSE: Remove optionality once BSE is fully rolled out created: string; disk_encryption?: EncryptionStatus; // @TODO LDE: Remove optionality once LDE is fully rolled out region: string; @@ -55,10 +55,7 @@ export interface LinodeBackups { last_successful: string | null; } -export type LinodeCapabilities = - | 'Block Storage Encryption' - | 'SMTP Enabled' - | 'Block Storage Performance B1'; +export type LinodeCapabilities = 'Block Storage Encryption' | 'SMTP Enabled'; export type Window = | 'Scheduling' diff --git a/packages/api-v4/src/nodebalancers/types.ts b/packages/api-v4/src/nodebalancers/types.ts index 68f89c7ac32..f8e93154b0b 100644 --- a/packages/api-v4/src/nodebalancers/types.ts +++ b/packages/api-v4/src/nodebalancers/types.ts @@ -1,31 +1,8 @@ -type TCPAlgorithm = 'roundrobin' | 'leastconn' | 'source'; -type UDPAlgorithm = 'roundrobin' | 'leastconn' | 'ring_hash'; - -export type Algorithm = TCPAlgorithm | UDPAlgorithm; - -export type Protocol = 'http' | 'https' | 'tcp' | 'udp'; - -type TCPStickiness = 'none' | 'table' | 'http_cookie'; -type UDPStickiness = 'none' | 'session' | 'source_ip'; - -export type Stickiness = TCPStickiness | UDPStickiness; - export interface NodeBalancer { id: number; label: string; hostname: string; - /** - * Maximum number of new TCP connections that a client (identified by a specific source IP) - * is allowed to initiate every second. - */ client_conn_throttle: number; - /** - * Maximum number of new UDP sessions that a client (identified by a specific source IP) - * is allowed to initiate every second. - * - * @todo Remove optionality once UDP support is live - */ - client_udp_sess_throttle?: number; region: string; ipv4: string; ipv6: null | string; @@ -54,15 +31,11 @@ export interface BalancerTransfer { total: number; } -/** - * 'none' is reserved for nodes used in UDP configurations. They don't support different modes. - */ export type NodeBalancerConfigNodeMode = | 'accept' | 'reject' | 'backup' - | 'drain' - | 'none'; + | 'drain'; export interface NodeBalancerConfig { id: number; @@ -71,31 +44,22 @@ export interface NodeBalancerConfig { check_passive: boolean; ssl_cert: string; nodes_status: NodesStatus; - protocol: Protocol; + protocol: 'http' | 'https' | 'tcp'; ssl_commonname: string; check_interval: number; check_attempts: number; check_timeout: number; check_body: string; check_path: string; - /** - * @todo Remove optionality once UDP support is live - */ - udp_check_port?: number; - /** - * @readonly This is returned by the API but *not* editable - * @todo Remove optionality once UDP support is live - * @default 16 - */ - udp_session_timeout?: number; proxy_protocol: NodeBalancerProxyProtocol; check: 'none' | 'connection' | 'http' | 'http_body'; ssl_key: string; - stickiness: Stickiness; - algorithm: Algorithm; + stickiness: 'none' | 'table' | 'http_cookie'; + algorithm: 'roundrobin' | 'leastconn' | 'source'; ssl_fingerprint: string; cipher_suite: 'recommended' | 'legacy'; nodes: NodeBalancerConfigNode[]; + modifyStatus?: 'new'; } export type NodeBalancerProxyProtocol = 'none' | 'v1' | 'v2'; @@ -118,36 +82,9 @@ export interface NodeBalancerStats { export interface CreateNodeBalancerConfig { port?: number; - /** - * If `udp` is chosen: - * - `check_passive` must be `false` or unset - * - `proxy_protocol` must be `none` or unset - * - The various SSL related fields like `ssl_cert`, `ssl_key`, `cipher_suite_recommended` should not be set - */ - protocol?: Protocol; - /** - * @default "none" - */ - proxy_protocol?: NodeBalancerProxyProtocol; - /** - * The algorithm for this configuration. - * - * TCP and HTTP support `roundrobin`, `leastconn`, and `source` - * UDP supports `roundrobin`, `leastconn`, and `ring_hash` - * - * @default roundrobin - */ - algorithm?: Algorithm; - /** - * Session stickiness for this configuration. - * - * TCP and HTTP support `none`, `table`, and `http_cookie` - * UDP supports `none`, `session`, and `source_ip` - * - * @default `session` for UDP - * @default `none` for TCP and HTTP - */ - stickiness?: Stickiness; + protocol?: 'http' | 'https' | 'tcp'; + algorithm?: 'roundrobin' | 'leastconn' | 'source'; + stickiness?: 'none' | 'table' | 'http_cookie'; check?: 'none' | 'connection' | 'http' | 'http_body'; check_interval?: number; check_timeout?: number; @@ -155,11 +92,6 @@ export interface CreateNodeBalancerConfig { check_path?: string; check_body?: string; check_passive?: boolean; - /** - * Must be between 1 and 65535 - * @default 80 - */ - udp_check_port?: number; cipher_suite?: 'recommended' | 'legacy'; ssl_cert?: string; ssl_key?: string; @@ -170,9 +102,6 @@ export type UpdateNodeBalancerConfig = CreateNodeBalancerConfig; export interface CreateNodeBalancerConfigNode { address: string; label: string; - /** - * Should not be specified when creating a node used on a UDP configuration - */ mode?: NodeBalancerConfigNodeMode; weight?: number; } @@ -197,21 +126,8 @@ export interface NodeBalancerConfigNodeWithPort extends NodeBalancerConfigNode { export interface CreateNodeBalancerPayload { region?: string; label?: string; - /** - * The connections per second throttle for TCP and HTTP connections - * - * Must be between 0 and 20. Set to 0 to disable throttling. - * @default 0 - */ client_conn_throttle?: number; - /** - * The connections per second throttle for UDP sessions - * - * Must be between 0 and 20. Set to 0 to disable throttling. - * @default 0 - */ - client_udp_sess_throttle?: number; - configs: CreateNodeBalancerConfig[]; + configs: any; firewall_id?: number; tags?: string[]; } diff --git a/packages/api-v4/src/object-storage/objects.ts b/packages/api-v4/src/object-storage/objects.ts index 7cb8f718dcc..892b79ddb7f 100644 --- a/packages/api-v4/src/object-storage/objects.ts +++ b/packages/api-v4/src/object-storage/objects.ts @@ -64,7 +64,7 @@ export const updateObjectACL = ( name: string, acl: Omit ) => - Request<{}>( + Request( setMethod('PUT'), setURL( `${API_ROOT}/object-storage/buckets/${encodeURIComponent( diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index cc85bbb89d0..99870119ff7 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -89,15 +89,10 @@ module.exports = { { files: [ // for each new features added to the migration router, add its directory here - 'src/features/Betas/**/*', - 'src/features/Domains/**/*', - 'src/features/Volumes/**/*', + 'src/features/Betas/*', ], rules: { 'no-restricted-imports': [ - // This needs to remain an error however trying to link to a feature that is not yet migrated will break the router - // For those cases react-router-dom history.push is still needed - // using `eslint-disable-next-line no-restricted-imports` can help bypass those imports 'error', { paths: [ diff --git a/packages/manager/.storybook/preview.tsx b/packages/manager/.storybook/preview.tsx index 6de6f42f5ce..55197641f90 100644 --- a/packages/manager/.storybook/preview.tsx +++ b/packages/manager/.storybook/preview.tsx @@ -9,10 +9,7 @@ import { Controls, Stories, } from '@storybook/blocks'; -import { - wrapWithTheme, - wrapWithThemeAndRouter, -} from '../src/utilities/testHelpers'; +import { wrapWithTheme } from '../src/utilities/testHelpers'; import { useDarkMode } from 'storybook-dark-mode'; import { DocsContainer as BaseContainer } from '@storybook/addon-docs'; import { themes } from '@storybook/theming'; @@ -45,13 +42,9 @@ export const DocsContainer = ({ children, context }) => { const preview: Preview = { decorators: [ - (Story, context) => { + (Story) => { const isDark = useDarkMode(); - return context.parameters.tanStackRouter - ? wrapWithThemeAndRouter(, { - theme: isDark ? 'dark' : 'light', - }) - : wrapWithTheme(, { theme: isDark ? 'dark' : 'light' }); + return wrapWithTheme(, { theme: isDark ? 'dark' : 'light' }); }, ], loaders: [ diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 820634b5612..662734981eb 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,127 +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/). -## [2025-01-14] - v1.134.0 - -### Added: - -- New DatePicker Component ([#11151](https://github.com/linode/manager/pull/11151)) -- Date Presets Functionality to Date Picker component ([#11395](https://github.com/linode/manager/pull/11395)) -- Notice for OS Distro Nearing EOL/EOS ([#11253](https://github.com/linode/manager/pull/11253)) -- aria-describedby to TextField with helper text ([#11351](https://github.com/linode/manager/pull/11351)) -- Node Pool Tags to LKE Cluster details page ([#11368](https://github.com/linode/manager/pull/11368)) -- MultipleIPInput Story in Storybook ([#11389](https://github.com/linode/manager/pull/11389)) -- Manage Tags to Volumes table action menu and moved actions inside menu ([#11421](https://github.com/linode/manager/pull/11421)) - -### Changed: - -- Database Resize: Updated tooltip text, plan selection descriptions, and summary text for new databases ([#11406](https://github.com/linode/manager/pull/11406)) -- Database Resize: Disable plans when the usable storage equals the used storage of the database cluster ([#11481](https://github.com/linode/manager/pull/11481)) -- DBaaS Settings Maintenance field Upgrade Version pending updates tooltip should display accurate text ([#11417](https://github.com/linode/manager/pull/11417)) - -### Fixed: - -- Create support ticket for buckets created through legacy flow ([#11300](https://github.com/linode/manager/pull/11300)) -- Incorrect Cloning Commands in Linode CLI Modal ([#11303](https://github.com/linode/manager/pull/11303)) -- Events landing page lists events in wrong order ([#11339](https://github.com/linode/manager/pull/11339)) -- Disallow word-break in billing contact info ([#11379](https://github.com/linode/manager/pull/11379)) -- Object Storage object uploader spinner spinning backwards ([#11384](https://github.com/linode/manager/pull/11384)) -- Document title from URL to appropriate keyword ([#11385](https://github.com/linode/manager/pull/11385)) -- DBaaS settings maintenance does not display review state and allows version upgrade when updates are available ([#11387](https://github.com/linode/manager/pull/11387)) -- Misplaced `errorGroup` prop causing console error in NodeBalancerConfigPanel ([#11398](https://github.com/linode/manager/pull/11398)) -- Account Cancellation Survey Button Color Issues ([#11412](https://github.com/linode/manager/pull/11412)) -- DBaaS Manage Access IP fields are displaying an IPv4 validation error message when both IPv6 and IPv4 are available. ([#11414](https://github.com/linode/manager/pull/11414)) -- `RegionHelperText` causing console errors ([#11416](https://github.com/linode/manager/pull/11416)) -- Linode Edit Config warning message when initially selecting a VPC as the primary interface ([#11424](https://github.com/linode/manager/pull/11424)) -- DBaaS Resize tab Used field is displaying just GB on provisioning database cluster ([#11426](https://github.com/linode/manager/pull/11426)) -- Various bugs in Managed tables ([#11431](https://github.com/linode/manager/pull/11431)) -- ARIA label of action menu in Domains Landing table row ([#11437](https://github.com/linode/manager/pull/11437)) -- VPC interface not being set as the primary interface when creating a Linode ([#11450](https://github.com/linode/manager/pull/11450)) -- `Create Token` button becomes disabled when all permissions are selected individually (without using 'select all') and child-account is hidden ([#11453](https://github.com/linode/manager/pull/11453)) -- Discrepancy in Object Storage Bucket size in CM ([#11460](https://github.com/linode/manager/pull/11460)) -- Object Storage `endpoint_type` sorting ([#11472](https://github.com/linode/manager/pull/11472)) -- Visibility of sensitive data in Managed and Longview with Mask Sensitive Data setting enabled ([#11476](https://github.com/linode/manager/pull/11476)) -- Display Kubernetes API endpoint for LKE-E cluster ([#11485](https://github.com/linode/manager/pull/11485)) -- Accuracy of "Add Node Pools" section on LKE Create page ([#11516](https://github.com/linode/manager/pull/11516)) - -### Removed: - -- `Images are not encrypted warning` warning ([#11443](https://github.com/linode/manager/pull/11443)) -- Temporarily remove Properties tab from Gen2 buckets ([#11491](https://github.com/linode/manager/pull/11491)) - -### Tech Stories: - -- Migrate `/volumes` to Tanstack router ([#11154](https://github.com/linode/manager/pull/11154)) -- Clean up NodeBalancer related types ([#11321](https://github.com/linode/manager/pull/11321)) -- Dev Tools fixes and improvements ([#11328](https://github.com/linode/manager/pull/11328)) -- Replace one-off hardcoded color values with color tokens pt4 ([#11345](https://github.com/linode/manager/pull/11345)) -- Refactor VPC Create to use `react-hook-form` instead of `formik` ([#11357](https://github.com/linode/manager/pull/11357)) -- Refactor VPCEditDrawer and SubnetEditDrawer to use `react-hook-form` instead of `formik` ([#11393](https://github.com/linode/manager/pull/11393)) -- Add `IMAGE_REGISTRY` Docker build argument ([#11360](https://github.com/linode/manager/pull/11360)) -- Remove `reselect` dependency ([#11364](https://github.com/linode/manager/pull/11364)) -- Update `useObjectAccess` to use a query key factory ([#11369](https://github.com/linode/manager/pull/11369)) -- Replace instances of `react-select` in Managed ([#11391](https://github.com/linode/manager/pull/11391)) -- Update our docs regarding useEffect best practices ([#11410](https://github.com/linode/manager/pull/11410)) -- Refactor Domains Routing (Tanstack Router) ([#11418](https://github.com/linode/manager/pull/11418)) -- Update Pendo URL with CNAME and update Analytics developer docs ([#11427](https://github.com/linode/manager/pull/11427)) -- Add MSW crud domains ([#11428](https://github.com/linode/manager/pull/11428)) -- Replace react-select instances in /Users with new Select ([#11430](https://github.com/linode/manager/pull/11430)) -- Fixed CloudPulse metric definition types ([#11433](https://github.com/linode/manager/pull/11433)) -- Patch `cookie` version as resolution for dependabot ([#11434](https://github.com/linode/manager/pull/11434)) -- Replace Select with Autocomplete component in Object Storage ([#11456](https://github.com/linode/manager/pull/11456)) -- Update `react-vnc` to 2.0.2 ([#11467](https://github.com/linode/manager/pull/11467)) - -### Tests: - -- Cypress component test for firewall inbound and outbound rules for mouse drag and drop ([#11344](https://github.com/linode/manager/pull/11344)) -- Cypress component tests for firewall rules drag and drop keyboard interaction ([#11341](https://github.com/linode/manager/pull/11341)) -- Mock LKE creation flow + APL coverage ([#11347](https://github.com/linode/manager/pull/11347)) -- Improve Linode end-to-end test stability by increasing timeouts ([#11350](https://github.com/linode/manager/pull/11350)) -- Fix `delete-volume.spec.ts` flaky test ([#11365](https://github.com/linode/manager/pull/11365)) -- Add Cypress test for Credit Card Expired banner ([#11383](https://github.com/linode/manager/pull/11383)) -- Cypress test flake: Rebuild Linode ([#11390](https://github.com/linode/manager/pull/11390)) -- Improve assertions made in `smoke-billing-activity.spec.ts` ([#11394](https://github.com/linode/manager/pull/11394)) -- Clean up `DatabaseBackups.test.tsx` ([#11394](https://github.com/linode/manager/pull/11394)) -- Fix account login and logout tests when using non-Prod environment ([#11407](https://github.com/linode/manager/pull/11407)) -- Add Cypress component tests for Autocomplete ([#11408](https://github.com/linode/manager/pull/11408)) -- Update mock region for LKE cluster creation test ([#11411](https://github.com/linode/manager/pull/11411)) -- Cypress tests to validate errors in Linode Create Backups tab ([#11422](https://github.com/linode/manager/pull/11422)) -- Cypress test to validate aria label of Linode IP Addresses action menu ([#11435](https://github.com/linode/manager/pull/11435)) -- Cypress test to validate CAA records are editable ([#11440](https://github.com/linode/manager/pull/11440)) -- Add test for LKE cluster rename flow ([#11444](https://github.com/linode/manager/pull/11444)) -- Add unit tests to validate aria-labels of Action Menu for Linode IPs & ranges ([#11448](https://github.com/linode/manager/pull/11448)) -- Add Cypress tests confirming Lionde Config Unrecommended status displays as expected in VPC Subnet table ([#11450](https://github.com/linode/manager/pull/11450)) -- Add Cypress test for LKE node pool tagging ([#11368](https://github.com/linode/manager/pull/11368)) -- Add coverage for Kube version upgrades in landing page ([#11478](https://github.com/linode/manager/pull/11478)) -- Fix Cypress test failures stemming from Debian 10 Image deprecation ([#11486](https://github.com/linode/manager/pull/11486)) -- Added Cypress test for restricted user Image non-Empty landing page ([#11335](https://github.com/linode/manager/pull/11335)) - -### Upcoming Features: - -- Update Kubernetes Versions in Create Cluster flow to support tiers for LKE-E ([#11359](https://github.com/linode/manager/pull/11359)) -- Switch from v4beta to v4 account endpoint for LKE-E ([#11413](https://github.com/linode/manager/pull/11413)) -- Update Kubernetes version upgrade components for LKE-E ([#11415](https://github.com/linode/manager/pull/11415)) -- Display LKE-E pricing in checkout bar ([#11419](https://github.com/linode/manager/pull/11419)) -- Designate LKE-E clusters with 'Enterprise' chip ([#11442](https://github.com/linode/manager/pull/11442)) -- Update LKE cluster details kube specs for LKE-E monthly pricing ([#11475](https://github.com/linode/manager/pull/11475)) -- Add new users table component for IAM ([#11367](https://github.com/linode/manager/pull/11367)) -- Add new user details components for IAM ([#11397](https://github.com/linode/manager/pull/11397)) -- High performance volume indicator ([#11400](https://github.com/linode/manager/pull/11400)) -- Add new no assigned roles component for IAM ([#11401](https://github.com/linode/manager/pull/11401)) -- Fix invalid routes in the IAM ([#11436](https://github.com/linode/manager/pull/11436)) -- Initial support for NodeBalancer UDP protocol ([#11405](https://github.com/linode/manager/pull/11405)) -- Add support for new optional filter - 'Tags' in monitor ([#11457](https://github.com/linode/manager/pull/11457)) -- Show ACLP supported regions per service type in region select ([#11382](https://github.com/linode/manager/pull/11382)) -- Add `CloudPulseAppliedFilter` and `CloudPulseAppliedFilterRenderer` components, update filter change handler function to add another parameter `label` ([#11354](https://github.com/linode/manager/pull/11354)) -- Add column for actions to Cloud Pulse alert definitions listing view and scaffolding for Definition Details page ([#11399](https://github.com/linode/manager/pull/11399)) -- Exhaustive unit tests for CloudPulse widgets ([#11464](https://github.com/linode/manager/pull/11464)) -- Add Alert Details Overview section in Cloud Pulse Alert Details page ([#11466](https://github.com/linode/manager/pull/11466)) -- AlertListing component and AlertTableRow component with Unit Tests ([#11346](https://github.com/linode/manager/pull/11346)) -- Update layout in CloudPulseDashboardWithFilters component, add a `getFilters` util method in `FilterBuilder.ts` ([#11388](https://github.com/linode/manager/pull/11388)) -- Metric, MetricCriteria, ClearIconButton components with Unit Tests ([#11392](https://github.com/linode/manager/pull/11392)) -- DimensionFilter, DimensionFilterField, TriggerCondition component along with Unit Tests ([#11445](https://github.com/linode/manager/pull/11445)) -- Improve Close Account Dialog UI ([#11469](https://github.com/linode/manager/pull/11469)) - ## [2024-12-20] - v1.133.2 ### Fixed: diff --git a/packages/manager/Dockerfile b/packages/manager/Dockerfile index d7ba0a3eb4d..37e1785a48a 100644 --- a/packages/manager/Dockerfile +++ b/packages/manager/Dockerfile @@ -1,12 +1,8 @@ -# Registry to use when pulling images. -# Defaults to Docker Hub, but can be overriden to point to another registry if needed. -ARG IMAGE_REGISTRY=docker.io - # Node.js base image for Cloud Manager CI tasks. # # Extends from the Node.js base image that corresponds with our latest supported # version of Node, and includes other tools that we rely on like pnpm and bun. -FROM ${IMAGE_REGISTRY}/node:20.17-bullseye-slim as nodejs-cloud-manager +FROM node:20.17-bullseye-slim as nodejs-cloud-manager RUN npm install -g pnpm bun # `manager` @@ -32,7 +28,7 @@ CMD yarn start:manager:ci # # Builds an image containing Cypress and miscellaneous system utilities required # by the tests. -FROM ${IMAGE_REGISTRY}/cypress/included:13.11.0 as e2e-build +FROM cypress/included:13.11.0 as e2e-build RUN npm install -g pnpm bun USER node WORKDIR /home/node/app diff --git a/packages/manager/cypress/component/components/autocomplete.spec.tsx b/packages/manager/cypress/component/components/autocomplete.spec.tsx deleted file mode 100644 index b9b8e27d20e..00000000000 --- a/packages/manager/cypress/component/components/autocomplete.spec.tsx +++ /dev/null @@ -1,688 +0,0 @@ -import { Autocomplete } from '@linode/ui'; -import * as React from 'react'; -import { ui } from 'support/ui'; -import { checkComponentA11y } from 'support/util/accessibility'; -import { componentTests, visualTests } from 'support/util/components'; -import { createSpy } from 'support/util/components'; - -type Option = { - label: string; - value: string; -}; - -componentTests('Autocomplete', (mount) => { - const options: Option[] = Array.from({ length: 3 }, (_, index) => { - const num = index + 1; - return { - label: `my-option-${num}`, - value: `my-option-${num}`, - }; - }); - - describe('Autocomplete interactions', () => { - describe('Open menu', () => { - /** - * - Confirms dropbdown can be opened by clicking the arrow button - */ - it('can open the drop-down menu by clicking the drop-down arrow', () => { - mount(); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.autocompletePopper - .findByTitle(`${options[0].label}`) - .should('be.visible'); - ui.autocompletePopper - .findByTitle(`${options[1].label}`) - .should('be.visible'); - ui.autocompletePopper - .findByTitle(`${options[2].label}`) - .should('be.visible'); - }); - - /** - * - Confirms dropdown can be opened by typing in the textfield - */ - it('can open the drop-down menu by typing into the textfield area', () => { - mount(); - - // Focus text field by clicking "Autocomplete" label. - cy.findByText('Autocomplete').should('be.visible').click(); - - cy.focused().type(options[0].label); - - ui.autocompletePopper.find().within(() => { - cy.findByText(options[0].label).should('be.visible'); - cy.findByText(options[1].label).should('not.exist'); - cy.findByText(options[2].label).should('not.exist'); - }); - }); - - /** - * - Confirms dropdown menu when there are no options - */ - it('shows the open dropdown menu with no options text', () => { - mount(); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.contains('You have no options to choose from').should('be.visible'); - }); - }); - - describe('Closing menu', () => { - // esc, click away, up arrow - /** - * - Confirms autocomplete popper can be closed with the ESC key - */ - it('can close the autocomplete menu with ESC key', () => { - mount( - {}} - options={options} - /> - ); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.autocompletePopper - .findByTitle(options[0].label) - .should('be.visible'); - - cy.get('input').type('{esc}'); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - - /** - * Confirms autocomplete can be closed by clicking away - */ - it('can close autocomplete popper by clicking away', () => { - mount( - <> - Other Element - {}} - options={options} - /> - - ); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.autocompletePopper - .findByTitle(options[0].label) - .should('be.visible'); - - cy.get('#other-element').click(); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - - /** - * Confirms autocomplete can be closed by clicking the close button - */ - it('can close autocomplete popper by clicking the close button', () => { - mount( - {}} - options={options} - /> - ); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.autocompletePopper - .findByTitle(`${options[0].label}`) - .should('be.visible'); - - ui.button - .findByAttribute('title', 'Close') - .should('be.visible') - .should('be.enabled') - .click(); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - }); - - describe('Single-select', () => { - /** - * - Confirms user can select an initial option - */ - it('can select an initial option', () => { - mount( - {}} - options={options} - placeholder="this is a placeholder" - value={undefined} - /> - ); - - cy.get('input').should( - 'have.attr', - 'placeholder', - 'this is a placeholder' - ); - cy.get('input').should('have.attr', 'value', ''); - cy.findByText('Autocomplete').should('be.visible').click(); - cy.focused().type(options[0].label); - - ui.autocompletePopper - .findByTitle(options[0].label) - .scrollIntoView() - .should('be.visible') - .click(); - - // Confirm that selection change is reflected by input field value, and that - // the autocomplete popper has been dismissed. - cy.get('input').should('have.attr', 'value', `${options[0].label}`); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - - /** - * - Confirms user can change selection after having selected an option - */ - it('can change the selected option', () => { - mount( - {}} - options={options} - placeholder="this is a placeholder" - value={options[0]} - /> - ); - - cy.get('input').should('have.attr', 'value', `${options[0].label}`); - cy.findByText('Autocomplete').should('be.visible').click(); - cy.focused().type(options[1].label); - - ui.autocompletePopper - .findByTitle(options[1].label) - .scrollIntoView() - .should('be.visible') - .click(); - - // Confirm that selection change is reflected by input field value, and that - // the autocomplete popper has been dismissed. - cy.get('input').should('have.attr', 'value', `${options[1].label}`); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - - /** - * - Confirms selection option can be cleared - */ - it('clears the selected option', () => { - mount( - {}} - options={options} - placeholder="this is a placeholder" - value={options[0]} - /> - ); - - cy.get('input').should('have.attr', 'value', `${options[0].label}`); - - cy.findByLabelText('Clear') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.get('input').should('have.attr', 'value', ''); - - cy.get('input').should( - 'have.attr', - 'placeholder', - 'this is a placeholder' - ); - }); - - /** - * - Confirms selection cannot be cleared when clearable is disabled - */ - it('cannot clear the selected option when clearable is disabled', () => { - mount( - {}} - options={options} - placeholder="this is a placeholder" - value={options[0]} - /> - ); - - cy.get('input').should('have.attr', 'value', `${options[0].label}`); - cy.findByLabelText('Clear').should('not.exist'); - }); - - /** - * - Confirms selection cannot be cleared if nothing was chosen - */ - it('cannot clear selection when nothing is selected', () => { - mount( - {}} - options={options} - placeholder="this is a placeholder" - value={undefined} - /> - ); - - cy.get('input').should('have.attr', 'value', ''); - cy.get('input').should( - 'have.attr', - 'placeholder', - 'this is a placeholder' - ); - - cy.findByLabelText('Clear').should('not.exist'); - }); - - describe('onChange', () => { - /** - * - Confirms onChange is called when option is selected - */ - it('calls `onChange` callback when initially selecting option', () => { - const spyFn = createSpy(() => {}, 'changeSpy'); - mount( - - ); - - cy.findByText('Autocomplete').should('be.visible').click(); - - cy.focused().type(options[0].label); - - ui.autocompletePopper - .findByTitle(`${options[0].label}`) - .scrollIntoView() - .should('be.visible') - .click(); - - cy.get('@changeSpy').should('have.been.calledOnce'); - }); - - /** - * - Confirms `onChange` callback when option is cleared - */ - it('calls `onChange` callback when clearing selection', () => { - const spyFn = createSpy(() => {}, 'changeSpy'); - mount( - - ); - - cy.findByLabelText('Clear') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.get('@changeSpy').should('have.been.calledOnce'); - }); - - /** - * - Confirms `onChange` callback when option is changed - */ - it('calls `onChange` callback changing selection', () => { - const spyFn = createSpy(() => {}, 'changeSpy'); - mount( - - ); - - cy.findByText('Autocomplete').should('be.visible').click(); - - cy.focused().type(options[0].label); - - ui.autocompletePopper - .findByTitle(`${options[0].label}`) - .scrollIntoView() - .should('be.visible') - .click(); - - cy.get('@changeSpy').should('have.been.calledOnce'); - }); - }); - - /** - * - Confirms onBlur is called when focusing away from selection - */ - it('calls `onBlur` callback when focusing away from selection', () => { - const spyFn = createSpy(() => {}, 'changeSpy'); - mount( - <> - Other Element - {}} - options={options} - placeholder="this is a placeholder" - value={undefined} - /> - - ); - - cy.findByText('Autocomplete').should('be.visible').click(); - - cy.focused().type(options[0].label); - - ui.autocompletePopper - .findByTitle(options[0].label) - .scrollIntoView() - .should('be.visible') - .click(); - cy.get('#other-element').click(); - - cy.get('@changeSpy').should('have.been.calledOnce'); - }); - }); - - describe('Multiselection', () => { - /** - * - Confirms multiple selections can be chosen - * - Confirms clear button clears all options - */ - it('can select multiple options and clears all selected options', () => { - // figure out how to confirm multi selections - // input value doesn't work anymore... (this feels hacky) - const MultiSelect = () => { - const [selectedOptions, setSelectedOptions] = React.useState< - Option[] - >([]); - return ( - <> -
Number of selected options: {selectedOptions.length}
- setSelectedOptions(value)} - options={options} - value={selectedOptions} - /> - - ); - }; - - mount(); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - ui.autocompletePopper.findByTitle('Select All').should('be.visible'); - - ui.autocompletePopper - .findByTitle(options[0].label) - .should('be.visible') - .click(); - cy.findByText('Number of selected options: 1').should('be.visible'); - - ui.autocompletePopper - .findByTitle(options[1].label) - .should('be.visible') - .click(); - cy.findByText('Number of selected options: 2').should('be.visible'); - - cy.findByLabelText('Clear') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.findByText('Number of selected options: 0').should('be.visible'); - }); - - /** - * - Confirms 'Select All' and 'Deselect All' work as expected - */ - it('can select all and deselect all', () => { - const MultiSelect = () => { - const [selectedOptions, setSelectedOptions] = React.useState< - Option[] - >([]); - return ( - setSelectedOptions(value)} - options={options} - value={selectedOptions} - /> - ); - }; - - mount(); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.autocompletePopper - .findByTitle('Select All') - .should('be.visible') - .click(); - - cy.findByLabelText('Clear').should('be.visible').should('be.enabled'); - cy.contains('Select All').should('not.exist'); - - // After selecting all elements, 'Deselect All' appears as an option - ui.autocompletePopper - .findByTitle('Deselect All') - .should('be.visible') - .click(); - - cy.findByLabelText('Clear').should('not.exist'); - ui.autocompletePopper.findByTitle('Select All').should('be.visible'); - }); - - /** - * - Confirms 'Deselect All' appears only when all options are selected (even if 'Select All' wasn't clicked) - * - Confirms 'Select All' appears if not all options have been selected - */ - it('shows Deselect All if all options are selected', () => { - const MultiSelect = () => { - const [selectedOptions, setSelectedOptions] = React.useState< - Option[] - >([]); - return ( - setSelectedOptions(value)} - options={options} - value={selectedOptions} - /> - ); - }; - - mount(); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - // select all options manually, confirm Select all is still visible if not all options selected yet - ui.autocompletePopper.findByTitle('Select All').should('be.visible'); - ui.autocompletePopper - .findByTitle('my-option-1') - .should('be.visible') - .click(); - ui.autocompletePopper.findByTitle('Select All').should('be.visible'); - ui.autocompletePopper - .findByTitle('my-option-2') - .should('be.visible') - .click(); - ui.autocompletePopper.findByTitle('Select All').should('be.visible'); - ui.autocompletePopper - .findByTitle('my-option-3') - .should('be.visible') - .click(); - - // Confirm Deselect All appears, and Select All doesn't exist anymore - ui.autocompletePopper.findByTitle('Deselect All').should('be.visible'); - cy.contains('Select All').should('not.exist'); - }); - - /** - * - Confirms popper remains open in multiselect after selecting an element - */ - it('keeps the popper open even after an element is selected', () => { - mount( - {}} - options={options} - /> - ); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.autocompletePopper - .findByTitle(`${options[1].label}`) - .should('be.visible') - .click(); - - ui.autocompletePopper - .findByTitle(`${options[1].label}`) - .should('be.visible'); - cy.get('[data-qa-autocomplete-popper]').should('be.visible'); - }); - }); - - visualTests((mount) => { - describe('Accessibility checks', () => { - describe('Single select', () => { - it('passes aXe check when menu is closed without an item selected', () => { - mount(); - - checkComponentA11y(); - }); - - it('passes aXe check when menu is closed with an item selected', () => { - mount( - - ); - - checkComponentA11y(); - }); - - it('passes aXe check when menu is open with an item selected', () => { - mount( - - ); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - checkComponentA11y(); - }); - }); - - describe('MultiSelect', () => { - it('passes aXe check when menu is closed without an item selected', () => { - mount( - - ); - - checkComponentA11y(); - }); - - it('passes aXe check when menu is closed with an item selected', () => { - mount( - - ); - - checkComponentA11y(); - }); - - it('passes aXe check when menu is open with an item selected', () => { - mount( - - ); - - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); - - checkComponentA11y(); - }); - }); - }); - }); - }); -}); diff --git a/packages/manager/cypress/component/components/select.spec.tsx b/packages/manager/cypress/component/components/select.spec.tsx deleted file mode 100644 index bf49abb3415..00000000000 --- a/packages/manager/cypress/component/components/select.spec.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import { Box, Select, Typography } from '@linode/ui'; -import * as React from 'react'; -import { ui } from 'support/ui'; -import { createSpy } from 'support/util/components'; -import { componentTests } from 'support/util/components'; - -import type { SelectOptionType, SelectProps } from '@linode/ui'; - -const options = [ - { label: 'Option 1', value: 'option-1' }, - { label: 'Option 2', value: 'option-2' }, - { label: 'Option 3', value: 'option-3' }, -]; - -const openAutocompletePopper = () => { - ui.button - .findByAttribute('title', 'Open') - .should('be.visible') - .should('be.enabled') - .click(); -}; - -componentTests('Select', (mount) => { - describe('Basics', () => { - describe('Open menu', () => { - it('can open drop-down menu by clicking drop-down arrow', () => { - mount(); - - openAutocompletePopper(); - - ui.autocompletePopper - .find() - .should('be.visible') - .within(() => { - cy.contains('No options available').should('be.visible'); - }); - }); - - it('can close menu with ESC key', () => { - mount( - - ); - - openAutocompletePopper(); - - ui.autocompletePopper - .findByTitle(`${options[0].label}`) - .should('be.visible'); - - cy.get('#other-element').click(); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - }); - - describe('Selection', () => { - it('can select an option initially', () => { - mount(); - - cy.get('input').should('have.attr', 'placeholder', 'Select an option'); - cy.findByText('My Select').should('be.visible').click(); - cy.focused().type(options[0].label[0]); - - ui.autocompletePopper - .findByTitle(`${options[0].label}`) - .should('be.visible') - .click(); - - cy.get('input').should('have.attr', 'value', `${options[0].label}`); - cy.get('[data-qa-autocomplete-popper]').should('not.exist'); - }); - - it('can change region selection', () => { - mount( - - ); - - cy.get('input').should('have.attr', 'value', `${options[1].label}`); - - cy.findByLabelText('Clear') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.get('input').should('have.attr', 'value', ''); - cy.get('input').should('have.attr', 'placeholder', 'Select an option'); - }); - - it('cannot clear region selection when clearable is disabled', () => { - mount( - ); - - cy.get('input').should('have.attr', 'value', ''); - cy.get('input').should('have.attr', 'placeholder', 'Select an option'); - - cy.findByLabelText('Clear').should('not.exist'); - }); - - it('calls `onChange` callback when region is initially selected', () => { - const spyFn = createSpy(() => {}, 'changeSpy'); - mount( - ); - - cy.findByLabelText('Clear') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.get('@changeSpy').should('have.been.calledOnce'); - }); - }); - }); - - describe('Creatable', () => { - it('can create a new option', () => { - mount( - setValue({ - label: newValue?.label ?? '', - value: newValue?.value.replace(' ', '-').toLowerCase() ?? '', - }) - } - textFieldProps={{ - onChange: (e) => - setValue({ - label: e.target.value, - value: e.target.value.replace(' ', '-').toLowerCase(), - }), - }} - value={value} - {...props} - /> - - - {JSON.stringify(value)} - - - - ); - }; - - it('renders the value for an existing option', () => { - mount(); - - cy.get('[data-qa-selected-value]').should('have.text', 'null'); - - options.forEach((option) => { - openAutocompletePopper(); - ui.autocompletePopper - .findByTitle(`${option.label}`) - .should('be.visible') - .click(); - - cy.get('[data-qa-selected-value]').should( - 'have.text', - `{"label":"${option.label}","value":"${option.value}"}` - ); - }); - }); - - it('renders the value for a new option', () => { - mount(); - const newOption = 'New Option'; - - cy.get('[data-qa-selected-value]').should('have.text', 'null'); - - openAutocompletePopper(); - cy.focused().type(newOption); - - ui.autocompletePopper - .find() - .within(() => { - cy.contains(`Create "${newOption}"`).should('be.visible'); - }) - .click(); - - cy.get('[data-qa-selected-value]').should( - 'have.text', - `{"label":"${newOption}","value":"${newOption - .replace(' ', '-') - .toLowerCase()}"}` - ); - }); - }); -}); diff --git a/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx b/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx deleted file mode 100644 index f686299fcde..00000000000 --- a/packages/manager/cypress/component/features/firewalls/firewall-rule-table.spec.tsx +++ /dev/null @@ -1,604 +0,0 @@ -/* eslint-disable sonarjs/no-duplicate-string */ -import '@4tw/cypress-drag-drop'; // Using this lib only for mouse drag-and-drop interactions -import * as React from 'react'; -import { ui } from 'support/ui'; -import { componentTests } from 'support/util/components'; -import { - randomItem, - randomLabel, - randomNumber, - randomString, -} from 'support/util/random'; - -import { firewallRuleFactory } from 'src/factories'; -import { FirewallRulesLanding } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding'; - -import type { FirewallPolicyType, FirewallRuleType } from '@linode/api-v4'; - -interface MoveFocusedElementViaKeyboard { - direction: 'DOWN' | 'UP'; - times: number; -} - -const portPresetMap = { - '22': 'SSH', - '53': 'DNS', - '80': 'HTTP', - '443': 'HTTPS', - '3306': 'MySQL', -}; - -const mockInboundRules = Array.from({ length: 3 }, () => - firewallRuleFactory.build({ - action: 'ACCEPT', - description: randomString(), - label: randomLabel(), - ports: randomItem(Object.keys(portPresetMap)), - }) -); - -const mockOutboundRules = Array.from({ length: 3 }, () => - firewallRuleFactory.build({ - action: 'DROP', - description: randomString(), - label: randomLabel(), - ports: randomItem(Object.keys(portPresetMap)), - }) -); - -const inboundRule1 = mockInboundRules[0]; -const inboundRule2 = mockInboundRules[1]; -const inboundRule3 = mockInboundRules[2]; - -const outboundRule1 = mockOutboundRules[0]; -const outboundRule2 = mockOutboundRules[1]; -const outboundRule3 = mockOutboundRules[2]; - -const inboundAriaLabel = 'inbound Rules List'; -const outboundAriaLabel = 'outbound Rules List'; -const buttonText = 'Save Changes'; - -/** - * Returns the formatted label for the given firewall rule action. - * - * @param ruleAction - */ -const getRuleActionLabel = (ruleAction: FirewallPolicyType): string => { - return `${ruleAction.charAt(0).toUpperCase()}${ruleAction - .slice(1) - .toLowerCase()}`; -}; - -/** - * Move the focused element either up or down, N times via Keyboard. - * - * note: Cypress automatically focuses the element when you use .type() or .type(' '). - * - * @param options.direction - Direction to move the element (row) "UP" or "DOWN". - * @param options.times - Number of times to move the element. - */ -const moveFocusedElementViaKeyboard = ({ - direction, - times, -}: MoveFocusedElementViaKeyboard) => { - // `direction` is either "UP" or "DOWN" - const arrowKey = direction === 'DOWN' ? '{downarrow}' : '{uparrow}'; - - const repeatedArrowKey = arrowKey.repeat(times); - - // Focused element will receive the repeated arrow key presses - cy.focused().type(repeatedArrowKey); -}; - -/** - * Verifies that the firewall landing page correctly lists the specified inbound - * and outbound rules in the firewall table, based on the provided options. - * - * @param options.includeInbound - Boolean flag to specify whether inbound rules should be included. - * @param options.includeOutbound - Boolean flag to specify whether outbound rules should be included. - * @param options.isSmallViewport - Boolean flag to specify whether the viewport is considered small (default is false). - */ -const verifyFirewallWithRules = ({ - includeInbound, - includeOutbound, - isSmallViewport = false, -}: { - includeInbound: boolean; - includeOutbound: boolean; - isSmallViewport?: boolean; -}) => { - // Verify that the Firewall Landing page displays the "Inbound Rules" and "Outbound Rules" headers. - cy.findByText('Inbound Rules').should('be.visible'); - cy.findByText('Outbound Rules').should('be.visible'); - - const inboundRules = includeInbound ? mockInboundRules : []; - const outboundRules = includeOutbound ? mockOutboundRules : []; - - // Confirm the appropriate rules are listed with correct details. - [...inboundRules, ...outboundRules].forEach((rule) => { - cy.findByText(rule.label!) - .should('be.visible') - .closest('tr') - .within(() => { - if (isSmallViewport) { - // Column 'Protocol' is not visible for smaller screens. - cy.findByText(rule.protocol).should('not.exist'); - } else { - cy.findByText(rule.protocol).should('be.visible'); - } - - cy.findByText(rule.ports!).should('be.visible'); - cy.findByText(getRuleActionLabel(rule.action)).should('be.visible'); - }); - }); -}; - -/** - * Verifies that the rows in a table are in the expected order based on the - * provided list of rules and the specified aria-label. - * - * @param ariaLabel - The `aria-label` of the table (either inbound or outbound rule table). - * @param expectedOrder - The expected order of rules (Array of FirewallRuleType objects). - * - * @example - * // Verifies that the inbound rule table rows are in the expected order of rule1, rule2, rule3. - * verifyTableRowOrder('inbound Rules List', [rule1, rule2, rule3]); - */ -const verifyTableRowOrder = ( - ariaLabel: string, - expectedOrder: FirewallRuleType[] -) => { - cy.get(`[aria-label="${ariaLabel}"]`).within(() => { - cy.get('tbody tr').then((rows) => { - expectedOrder.forEach((rule, index) => { - expect(rows[index]).to.contain(rule.label); - }); - }); - }); -}; - -/** - * Drags a row from one position to another within a table using mouse interaction. - * - * Note: this utility uses '@4tw/cypress-drag-drop lib. - * - * @param ariaLabel - The `aria-label` of the table containing the rows. - * @param sourceRowPosition - The position (1-based index) of the row to be moved. - * @param targetRowPosition - The position (1-based index) to drop the moved row. - * - * @example - * dragRowToPositionViaMouse('inbound Rules List', 1, 2); // Moves the first row to second position - */ -const dragRowToPositionViaMouse = ( - ariaLabel: string, - sourceRowPosition: number, - targetRowPosition: number -) => { - const sourceRow = `div[aria-label="${ariaLabel}"] tbody tr:nth-child(${sourceRowPosition})`; - const targetRow = `div[aria-label="${ariaLabel}"] tbody tr:nth-child(${targetRowPosition})`; - cy.get(sourceRow).drag(targetRow); -}; - -/** - * Test scenario for moving inbound rule rows using keyboard interactions. - * - * This test verifies that the keyboard-based drag-and-drop functionality - * works as expected for inbound rules: - * - Ensuring the `Save Changes` button is initially disabled. - * - Activating the row drag mode via `Space/Enter` key. - * - Moving the rule rows up and down with arrow keys. - * - Dropping the row and verifying the updated row order. - * - Enabling the `Save Changes` button after the operation. - */ -const testMoveInboundRuleRowsViaKeyboard = () => { - // Verify 'Save Changes' button is initially disabled. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'true'); - - // Activate keyboard drag mode using `Space/Enter` key on the first row - inboundRule1. - cy.findByText(inboundRule1.label!).should('be.visible'); - cy.findByText(inboundRule1.label!).closest('tr').type(' '); - cy.findByText(inboundRule1.label!) - .closest('tr') - .should('have.attr', 'aria-pressed', 'true'); - - // Move `inboundRule1` down two rows. - moveFocusedElementViaKeyboard({ direction: 'DOWN', times: 2 }); - - // Drop row with the keyboard `Space/Enter` key. - cy.focused().type(' '); - - // Verify that "inboundRule2" is in the 1st row, - // "inboundRule3" is in the 2nd row, and "inboundRule1" is in the 3rd row. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule2, - inboundRule3, - inboundRule1, - ]); - - // Activate keyboard drag mode using `Space/Enter` key on the 2nd row - inboundRule3. - cy.findByText(inboundRule3.label!).should('be.visible'); - cy.findByText(inboundRule3.label!).closest('tr').type(' '); - cy.findByText(inboundRule3.label!) - .closest('tr') - .should('have.attr', 'aria-pressed', 'true'); - - // Move `inboundRule3` up one row. - moveFocusedElementViaKeyboard({ direction: 'UP', times: 1 }); - - // Drop row with the keyboard `Space/Enter` key. - cy.focused().type(' '); - - // Verify that "inboundRule3" is in the 1st row, - // "inboundRule2" is in the 2nd row, and "inboundRule1" is in the 3rd row. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule3, - inboundRule2, - inboundRule1, - ]); - - // Verify 'Save Changes' button is enabled after row is moved. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'false'); -}; - -/** - * Test scenario for canceling the inbound rule drag-and-drop operation using the keyboard `Esc` key. - * - * This test checks that when the `Esc` key is pressed during a row drag operation, - * the row returns to its original position and the `Save Changes` button remains disabled. - */ -const testDiscardInboundRuleDragViaKeyboard = () => { - // Verify 'Save Changes' button is initially disabled. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'true'); - - // Activate keyboard drag mode using `Space/Enter` key on the first row - inboundRule1. - cy.findByText(inboundRule1.label!).should('be.visible'); - cy.findByText(inboundRule1.label!).closest('tr').type(' '); - cy.findByText(inboundRule1.label!) - .closest('tr') - .should('have.attr', 'aria-pressed', 'true'); - - // Move `inboundRule1` down two rows. - moveFocusedElementViaKeyboard({ direction: 'DOWN', times: 2 }); - - // Cancel with the keyboard `Esc` key. - cy.focused().type('{esc}'); - - // Ensure row remains in its original position. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule1, - inboundRule2, - inboundRule3, - ]); - - // Verify 'Save Changes' button remains disabled after discarding with the keyboard `Esc` key. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'true'); -}; - -/** - * Test scenario for moving outbound rule rows using keyboard interactions. - * - * This test verifies that the keyboard-based drag-and-drop functionality - * works as expected for outbound rules: - * - Ensuring the `Save Changes` button is initially disabled. - * - Activating the row drag mode via `Space/Enter` key. - * - Moving the rule rows up and down with arrow keys. - * - Dropping the row and verifying the updated row order. - * - Enabling the `Save Changes` button after the operation. - */ -const testMoveOutboundRulesViaKeyboard = () => { - // Verify 'Save Changes' button is initially disabled. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'true'); - - // Activate keyboard drag mode using `Space/Enter` key on the first row - outboundRule1. - cy.findByText(outboundRule1.label!).should('be.visible'); - cy.findByText(outboundRule1.label!).closest('tr').type(' '); - cy.findByText(outboundRule1.label!) - .closest('tr') - .should('have.attr', 'aria-pressed', 'true'); - - // Move `outboundRule1` down two rows - moveFocusedElementViaKeyboard({ direction: 'DOWN', times: 2 }); - - // Drop row with the keyboard `Space/Enter` key - cy.focused().type(' '); - - // Verify that "outboundRule2" is in the 1st row, - // "outboundRule3" is in the 2nd row, and "outboundRule1" is in the 3rd row. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule2, - outboundRule3, - outboundRule1, - ]); - - // Activate keyboard drag mode using `Space/Enter` key on the 2nd row - outboundRule3. - cy.findByText(outboundRule3.label!).should('be.visible'); - cy.findByText(outboundRule3.label!).closest('tr').type(' '); - cy.findByText(outboundRule3.label!) - .closest('tr') - .should('have.attr', 'aria-pressed', 'true'); - - // Move `outboundRule3` up one row. - moveFocusedElementViaKeyboard({ direction: 'UP', times: 1 }); - - // Drop row with the keyboard `Space/Enter` key. - cy.focused().type(' '); - - // Verify that "outboundRule3" is in the 1st row, - // "outboundRule2" is in the 2nd row, and "outboundRule1" is in the 3rd row. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule3, - outboundRule2, - outboundRule1, - ]); - - // Verify 'Save Changes' button is enabled after row is moved. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'false'); -}; - -/** - * Test scenario for canceling the outbound rule drag-and-drop operation using the keyboard `Esc` key. - * - * This test checks that when the `Esc` key is pressed during a row drag operation, - * the row returns to its original position and the `Save Changes` button remains disabled. - */ -const testDiscardOutboundRuleDragViaKeyboard = () => { - // Verify 'Save Changes' button is initially disabled. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'true'); - - // Activate keyboard drag mode using `Space/Enter` key on the first row - outboundRule1. - cy.findByText(outboundRule1.label!).should('be.visible'); - cy.findByText(outboundRule1.label!).closest('tr').type(' '); - cy.findByText(outboundRule1.label!) - .closest('tr') - .should('have.attr', 'aria-pressed', 'true'); - - // Move `outboundRule1` down two rows. - moveFocusedElementViaKeyboard({ direction: 'DOWN', times: 2 }); - - // Cancel with the keyboard `Esc` key. - cy.focused().type('{esc}'); - - // Ensure row remains in its original position. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule1, - outboundRule2, - outboundRule3, - ]); - - // Verify 'Save Changes' button remains disabled after discarding with the keyboard `Esc` key. - ui.button - .findByTitle(buttonText) - .should('be.visible') - .should('have.attr', 'aria-disabled', 'true'); -}; - -componentTests('Firewall Rules Table', (mount) => { - /** - * Keyboard keys used to perform interactions with rows in the Firewall Rules table: - * - Press `Space/Enter` key once to activate keyboard sensor on the selected row. - * - Use `Up/Down` arrow keys to move the row up or down. - * - Press `Space/Enter` key again to drop the focused row. - * - Press `Esc` key to discard drag and drop operation. - * - * Confirms: - * - All keyboard interactions on Firewall Rules table rows work as expected for - * both normal (no vertical scrollbar) and smaller window sizes (with vertical scrollbar). - * - `CustomKeyboardSensor` works as expected. - * - All Mouse interactions on Firewall Rules table rows work as expected. - */ - describe('Keyboard and Mouse Drag and Drop Interactions', () => { - describe('Normal window (no vertical scrollbar)', () => { - beforeEach(() => { - cy.viewport(1536, 960); - }); - - describe('Inbound Rules:', () => { - beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: true, - includeOutbound: false, - }); - }); - - it('should move Inbound rule rows using keyboard interaction', () => { - testMoveInboundRuleRowsViaKeyboard(); - }); - - it('should cancel the Inbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardInboundRuleDragViaKeyboard(); - }); - - it('should move Inbound rules rows using mouse interaction', () => { - // Drag the 1st row rule to 2nd row position. - dragRowToPositionViaMouse(inboundAriaLabel, 1, 2); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule2, - inboundRule1, - inboundRule3, - ]); - - // Drag the 3rd row rule to 2nd row position. - dragRowToPositionViaMouse(inboundAriaLabel, 3, 2); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule2, - inboundRule3, - inboundRule1, - ]); - - // Drag the 3rd row rule to 1st position. - dragRowToPositionViaMouse(inboundAriaLabel, 3, 1); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(inboundAriaLabel, [ - inboundRule1, - inboundRule2, - inboundRule3, - ]); - }); - }); - - describe('Outbound Rules:', () => { - beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: false, - includeOutbound: true, - }); - }); - - it('should move Outbound rule rows using keyboard interaction', () => { - testMoveOutboundRulesViaKeyboard(); - }); - - it('should cancel the Outbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardOutboundRuleDragViaKeyboard(); - }); - - it('should move Outbound rules rows using mouse interaction', () => { - // Drag the 1st row rule to 2nd row position. - dragRowToPositionViaMouse(outboundAriaLabel, 1, 2); - - // Verify the labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule2, - outboundRule1, - outboundRule3, - ]); - - // Drag the 3rd row rule to 2nd row position. - dragRowToPositionViaMouse(outboundAriaLabel, 3, 2); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule2, - outboundRule3, - outboundRule1, - ]); - - // Drag the 3rd row rule to 1st position. - dragRowToPositionViaMouse(outboundAriaLabel, 3, 1); - - // Verify the order and labels in the 1st, 2nd, and 3rd rows. - verifyTableRowOrder(outboundAriaLabel, [ - outboundRule1, - outboundRule2, - outboundRule3, - ]); - }); - }); - }); - - describe('Window with vertical scrollbar', () => { - beforeEach(() => { - // Browser window with vertical scroll bar enabled (smaller screens). - cy.viewport(800, 400); - cy.window().should('have.property', 'innerWidth', 800); - cy.window().should('have.property', 'innerHeight', 400); - }); - - describe('Inbound Rules:', () => { - beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: true, - includeOutbound: false, - isSmallViewport: true, - }); - }); - - it('should move Inbound rule rows using keyboard interaction', () => { - testMoveInboundRuleRowsViaKeyboard(); - }); - - it('should cancel the Inbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardInboundRuleDragViaKeyboard(); - }); - }); - - describe('Outbound Rules:', () => { - beforeEach(() => { - mount( - - ); - verifyFirewallWithRules({ - includeInbound: false, - includeOutbound: true, - isSmallViewport: true, - }); - }); - - it('should move Outbound rule rows using keyboard interaction', () => { - testMoveOutboundRulesViaKeyboard(); - }); - - it('should cancel the Outbound rules drag operation with the keyboard `Esc` key', () => { - testDiscardOutboundRuleDragViaKeyboard(); - }); - }); - }); - }); -}); diff --git a/packages/manager/cypress/component/components/beta-chip.spec.tsx b/packages/manager/cypress/component/poc/beta-chip.spec.tsx similarity index 99% rename from packages/manager/cypress/component/components/beta-chip.spec.tsx rename to packages/manager/cypress/component/poc/beta-chip.spec.tsx index 58641a6e63b..962e9ccf2b4 100644 --- a/packages/manager/cypress/component/components/beta-chip.spec.tsx +++ b/packages/manager/cypress/component/poc/beta-chip.spec.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { checkComponentA11y } from 'support/util/accessibility'; -import { componentTests, visualTests } from 'support/util/components'; - import { BetaChip } from 'src/components/BetaChip/BetaChip'; +import { componentTests, visualTests } from 'support/util/components'; +import { checkComponentA11y } from 'support/util/accessibility'; componentTests('BetaChip', () => { visualTests((mount) => { diff --git a/packages/manager/cypress/component/components/region-select.spec.tsx b/packages/manager/cypress/component/poc/region-select.spec.tsx similarity index 99% rename from packages/manager/cypress/component/components/region-select.spec.tsx rename to packages/manager/cypress/component/poc/region-select.spec.tsx index 656d5dacaf3..e15411dece2 100644 --- a/packages/manager/cypress/component/components/region-select.spec.tsx +++ b/packages/manager/cypress/component/poc/region-select.spec.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; -import { mockGetAccountAvailability } from 'support/intercepts/account'; -import { ui } from 'support/ui'; -import { checkComponentA11y } from 'support/util/accessibility'; -import { createSpy } from 'support/util/components'; -import { componentTests, visualTests } from 'support/util/components'; - import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { componentTests, visualTests } from 'support/util/components'; +import { checkComponentA11y } from 'support/util/accessibility'; import { accountAvailabilityFactory, regionFactory } from 'src/factories'; +import { ui } from 'support/ui'; +import { mockGetAccountAvailability } from 'support/intercepts/account'; +import { createSpy } from 'support/util/components'; componentTests('RegionSelect', (mount) => { beforeEach(() => { @@ -25,10 +24,10 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} + currentCapability="Object Storage" value={undefined} + onChange={() => {}} /> ); @@ -53,10 +52,10 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} + currentCapability="Object Storage" value={undefined} + onChange={() => {}} /> ); @@ -82,10 +81,10 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} + currentCapability="Object Storage" value={undefined} + onChange={() => {}} /> ); @@ -111,10 +110,10 @@ componentTests('RegionSelect', (mount) => { <> Other Element {}} regions={[region]} + currentCapability="Object Storage" value={undefined} + onChange={() => {}} /> ); @@ -144,10 +143,10 @@ componentTests('RegionSelect', (mount) => { it('can select a region initially', () => { mount( {}} regions={regions} + currentCapability={undefined} value={undefined} + onChange={() => {}} /> ); @@ -178,10 +177,10 @@ componentTests('RegionSelect', (mount) => { it('can change region selection', () => { mount( {}} regions={regions} + currentCapability={undefined} value={regionToPreselect.id} + onChange={() => {}} /> ); @@ -213,10 +212,10 @@ componentTests('RegionSelect', (mount) => { it('can clear region selection', () => { mount( {}} regions={regions} + currentCapability={undefined} value={regionToSelect.id} + onChange={() => {}} /> ); @@ -239,11 +238,11 @@ componentTests('RegionSelect', (mount) => { it('cannot clear region selection when clearable is disabled', () => { mount( {}} - regions={regions} - value={regionToSelect.id} /> ); @@ -259,10 +258,10 @@ componentTests('RegionSelect', (mount) => { it('cannot clear region selection when no region is selected', () => { mount( {}} regions={regions} + currentCapability={undefined} value={undefined} + onChange={() => {}} /> ); @@ -276,10 +275,10 @@ componentTests('RegionSelect', (mount) => { const spyFn = createSpy(() => {}, 'changeSpy'); mount( ); @@ -300,10 +299,10 @@ componentTests('RegionSelect', (mount) => { const spyFn = createSpy(() => {}, 'changeSpy'); mount( ); @@ -344,10 +343,10 @@ componentTests('RegionSelect', (mount) => { // TODO Remove `dcGetWell` flag override when feature flag is removed from codebase. mount( {}} regions={regions} + currentCapability="Object Storage" value={undefined} + onChange={() => {}} />, { dcGetWell: true, @@ -378,10 +377,10 @@ componentTests('RegionSelect', (mount) => { it('only lists regions with the specified capability', () => { mount( {}} regions={regions} + currentCapability="Object Storage" value={undefined} + onChange={() => {}} /> ); @@ -407,10 +406,10 @@ componentTests('RegionSelect', (mount) => { it('lists all regions when no capability is specified', () => { mount( {}} regions={regions} + currentCapability={undefined} value={undefined} + onChange={() => {}} /> ); @@ -437,10 +436,10 @@ componentTests('RegionSelect', (mount) => { it('passes aXe check when menu is closed without an item selected', () => { mount( {}} regions={regions} + currentCapability={undefined} value={undefined} + onChange={() => {}} /> ); checkComponentA11y(); @@ -449,10 +448,10 @@ componentTests('RegionSelect', (mount) => { it('passes aXe check when menu is closed with an item selected', () => { mount( {}} regions={regions} + currentCapability={undefined} value={selectedRegion.id} + onChange={() => {}} /> ); checkComponentA11y(); @@ -461,10 +460,10 @@ componentTests('RegionSelect', (mount) => { it('passes aXe check when menu is open', () => { mount( {}} regions={regions} + currentCapability={undefined} value={selectedRegion.id} + onChange={() => {}} /> ); diff --git a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts index c984bf30ec1..40a4b5761ef 100644 --- a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts @@ -12,7 +12,6 @@ import { import { cancellationDataLossWarning, cancellationPaymentErrorMessage, - cancellationDialogTitle, } from 'support/constants/account'; import { CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, @@ -40,7 +39,7 @@ describe('Account cancellation', () => { it('users can cancel account', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - email: 'mock-user@linode.com', + username: 'mock-user', restricted: false, }); const mockCancellationResponse: CancelAccount = { @@ -75,7 +74,9 @@ describe('Account cancellation', () => { }); ui.dialog - .findByTitle(cancellationDialogTitle) + .findByTitle( + 'Are you sure you want to close your cloud computing services account?' + ) .should('be.visible') .within(() => { cy.findByText(cancellationDataLossWarning, { exact: false }).should( @@ -88,30 +89,14 @@ describe('Account cancellation', () => { .should('be.visible') .should('be.disabled'); - // Verify checkboxes are present with correct labels - cy.get('[data-qa-checkbox="deleteAccountServices"]') - .should('be.visible') - .should('not.be.checked'); - - cy.get('[data-qa-checkbox="deleteAccountUsers"]') - .should('be.visible') - .should('not.be.checked'); - - // Check both boxes but verify submit remains disabled without email - cy.get('[data-qa-checkbox="deleteAccountServices"]').click(); - cy.get('[data-qa-checkbox="deleteAccountUsers"]').click(); - - ui.button - .findByTitle('Close Account') - .should('be.visible') - .should('be.disabled'); - - // Enter email, confirm that submit button becomes enabled, and click + // Enter username, confirm that submit button becomes enabled, and click // the submit button. - cy.findByLabelText(`Enter your email address (${mockProfile.email})`) + cy.findByLabelText( + `Please enter your Username (${mockProfile.username}) to confirm.` + ) .should('be.visible') .should('be.enabled') - .type(mockProfile.email); + .type(mockProfile.username); ui.button .findByTitle('Close Account') @@ -166,7 +151,7 @@ describe('Account cancellation', () => { it('restricted users cannot cancel account', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - email: 'mock-user@linode.com', + username: 'mock-restricted-user', restricted: true, }); @@ -191,22 +176,17 @@ describe('Account cancellation', () => { // Fill out cancellation dialog and attempt submission. ui.dialog - .findByTitle(cancellationDialogTitle) + .findByTitle( + 'Are you sure you want to close your cloud computing services account?' + ) .should('be.visible') .within(() => { - // Check both boxes but verify submit remains disabled without email - cy.get('[data-qa-checkbox="deleteAccountServices"]').click(); - cy.get('[data-qa-checkbox="deleteAccountUsers"]').click(); - - ui.button - .findByTitle('Close Account') - .should('be.visible') - .should('be.disabled'); - - cy.findByLabelText(`Enter your email address (${mockProfile.email})`) + cy.findByLabelText( + `Please enter your Username (${mockProfile.username}) to confirm.` + ) .should('be.visible') .should('be.enabled') - .type(mockProfile.email); + .type(mockProfile.username); ui.button .findByTitle('Close Account') @@ -228,7 +208,7 @@ describe('Parent/Child account cancellation', () => { it('disables the "Close Account" button for a child user', () => { const mockAccount = accountFactory.build({}); const mockProfile = profileFactory.build({ - email: 'mock-user@linode.com', + username: 'mock-child-user', restricted: false, user_type: 'child', }); @@ -262,7 +242,7 @@ describe('Parent/Child account cancellation', () => { it('disables "Close Account" button for proxy users', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - email: 'mock-user@linode.com', + username: 'proxy-user', restricted: false, user_type: 'proxy', }); @@ -296,7 +276,7 @@ describe('Parent/Child account cancellation', () => { it('disables "Close Account" button for parent users', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - email: 'mock-user@linode.com', + username: 'parent-user', restricted: false, user_type: 'parent', }); @@ -330,7 +310,7 @@ describe('Parent/Child account cancellation', () => { it('allows a default account with no active child accounts to close the account', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - email: 'mock-user@linode.com', + username: 'default-user', restricted: false, user_type: 'default', }); @@ -366,7 +346,9 @@ describe('Parent/Child account cancellation', () => { }); ui.dialog - .findByTitle(cancellationDialogTitle) + .findByTitle( + 'Are you sure you want to close your cloud computing services account?' + ) .should('be.visible') .within(() => { cy.findByText(cancellationDataLossWarning, { exact: false }).should( @@ -379,21 +361,14 @@ describe('Parent/Child account cancellation', () => { .should('be.visible') .should('be.disabled'); - // Check both boxes but verify submit remains disabled without email - cy.get('[data-qa-checkbox="deleteAccountServices"]').click(); - cy.get('[data-qa-checkbox="deleteAccountUsers"]').click(); - - ui.button - .findByTitle('Close Account') - .should('be.visible') - .should('be.disabled'); - - // Enter email, confirm that submit button becomes enabled, and click + // Enter username, confirm that submit button becomes enabled, and click // the submit button. - cy.findByLabelText(`Enter your email address (${mockProfile.email})`) + cy.findByLabelText( + `Please enter your Username (${mockProfile.username}) to confirm.` + ) .should('be.visible') .should('be.enabled') - .type(mockProfile.email); + .type(mockProfile.username); ui.button .findByTitle('Close Account') diff --git a/packages/manager/cypress/e2e/core/account/account-logout.spec.ts b/packages/manager/cypress/e2e/core/account/account-logout.spec.ts index 8b3eac4866a..550c4b39c98 100644 --- a/packages/manager/cypress/e2e/core/account/account-logout.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-logout.spec.ts @@ -1,4 +1,4 @@ -import { loginBaseUrl } from 'support/constants/login'; +import { LOGIN_ROOT } from 'src/constants'; import { interceptGetAccount } from 'support/intercepts/account'; import { ui } from 'support/ui'; @@ -26,9 +26,9 @@ describe('Logout Test', () => { cy.findByText('Log Out').should('be.visible').click(); }); // Upon clicking "Log Out", the user is redirected to the login endpoint at /login - cy.url().should('equal', `${loginBaseUrl}/login`); + cy.url().should('equal', `${LOGIN_ROOT}/login`); // Using cy.visit to navigate back to Cloud results in another redirect to the login page cy.visit('/'); - cy.url().should('startWith', `${loginBaseUrl}/login`); + cy.url().should('startWith', `${LOGIN_ROOT}/login`); }); }); 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 deleted file mode 100644 index 148d8c270bd..00000000000 --- a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { accountFactory } from 'src/factories'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetUserPreferences } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; - -const creditCardExpiredBannerNotice = - 'Your credit card has expired! Please update your payment details.'; - -describe('Credit Card Expired Banner', () => { - beforeEach(() => { - mockGetUserPreferences({ dismissed_notifications: {} }); - }); - - it('appears when the expiration date is in the past', () => { - mockGetAccount( - accountFactory.build({ credit_card: { expiry: '01/2000' } }) - ).as('getAccount'); - cy.visitWithLogin('/'); - cy.wait('@getAccount'); - cy.findByText(creditCardExpiredBannerNotice).should('be.visible'); - ui.button.findByTitle('Update Card').should('be.visible').click(); - - // clicking on the link navigates to /account/billing - cy.url().should('endWith', '/account/billing'); - }); - - it('does not appear when the expiration date is in the future', () => { - mockGetAccount( - accountFactory.build({ credit_card: { expiry: '01/2999' } }) - ).as('getAccount'); - cy.visitWithLogin('/account/billing'); - cy.wait('@getAccount'); - cy.findByText('Payment Methods').should('be.visible'); - cy.findByText(creditCardExpiredBannerNotice).should('not.exist'); - }); -}); 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 4a1f57eeb9b..4780133b78f 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 @@ -329,14 +329,11 @@ describe('Billing Activity Feed', () => { // Time zones against which to verify invoice and payment dates. const timeZonesList = [ { key: 'America/New_York', human: 'Eastern Time - New York' }, - { key: 'UTC', human: 'Coordinated Universal Time' }, + { key: 'GMT', human: 'Coordinated Universal Time' }, { key: 'Asia/Hong_Kong', human: 'Hong Kong Standard Time' }, ]; - const mockProfile = profileFactory.build({ - timezone: 'Pacific/Honolulu', - }); - + const mockProfile = profileFactory.build(); const mockInvoice = invoiceFactory.build({ date: DateTime.now().minus({ days: 2 }).toISO(), }); @@ -352,16 +349,11 @@ describe('Billing Activity Feed', () => { cy.visitWithLogin('/profile/display'); cy.wait('@getProfile'); - // Verify the user's initial timezone is selected by default - cy.findByLabelText('Timezone') - .should('be.visible') - .should('contain.value', 'Hawaii-Aleutian Standard Time'); - // Iterate through each timezone and confirm that payment and invoice dates // reflect each timezone. timeZonesList.forEach((timezone) => { const timezoneId = timezone.key; - const timezoneLabel = timezone.human; + const humanReadable = timezone.human; mockUpdateProfile({ ...mockProfile, @@ -375,7 +367,7 @@ describe('Billing Activity Feed', () => { cy.findByText('Timezone') .should('be.visible') .click() - .type(`${timezoneLabel}{enter}`); + .type(`${humanReadable}{enter}`); ui.button .findByTitle('Update Timezone') @@ -385,11 +377,6 @@ describe('Billing Activity Feed', () => { cy.wait('@updateProfile'); - // Verify the new timezone remains selected after clicking "Update Timezone" - cy.findByLabelText('Timezone') - .should('be.visible') - .should('contain.value', timezoneLabel); - // Navigate back to Billing & Contact Information page to confirm that // invoice and payment data correctly reflects updated timezone. navigateToBilling(); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts index 0b1fa8258d7..f2fbcdc66a7 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts @@ -35,7 +35,6 @@ import { } from 'support/intercepts/databases'; import { Database } from '@linode/api-v4'; import { mockGetAccount } from 'support/intercepts/account'; -import { Flags } from 'src/featureFlags'; /** * Verifies the presence and values of specific properties within the aclpPreference object @@ -45,24 +44,6 @@ import { Flags } from 'src/featureFlags'; * @param requestPayload - The payload received from the request, containing the aclpPreference object. * @param expectedValues - An object containing the expected values for properties to validate against the requestPayload. */ - -const flags: Partial = { - aclp: { enabled: true, beta: true }, - aclpResourceTypeMap: [ - { - dimensionKey: 'LINODE_ID', - maxResourceSelections: 10, - serviceType: 'linode', - supportedRegionIds: 'us-ord', - }, - { - dimensionKey: 'cluster_id', - maxResourceSelections: 10, - serviceType: 'dbaas', - supportedRegionIds: 'us-ord', - }, - ], -}; const { metrics, id, @@ -86,18 +67,21 @@ const dashboard = dashboardFactory.build({ }), }); -const metricDefinitions = metrics.map(({ title, name, unit }) => - dashboardMetricFactory.build({ - label: title, - metric: name, - unit, - }) -); +const metricDefinitions = { + data: metrics.map(({ title, name, unit }) => + dashboardMetricFactory.build({ + label: title, + metric: name, + unit, + }) + ), +}; const mockRegion = regionFactory.build({ - capabilities: ['Managed Databases'], + capabilities: ['Linodes'], id: 'us-ord', label: 'Chicago, IL', + country: 'us', }); const databaseMock: Database = databaseFactory.build({ @@ -113,7 +97,9 @@ const mockAccount = accountFactory.build(); describe('Tests for API error handling', () => { beforeEach(() => { - mockAppendFeatureFlags(flags); + mockAppendFeatureFlags({ + aclp: { beta: true, enabled: true }, + }); mockGetAccount(mockAccount); mockGetCloudPulseMetricDefinitions(serviceType, metricDefinitions); mockGetCloudPulseDashboards(serviceType, [dashboard]).as('fetchDashboard'); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index 5bed5edc465..1c63c435993 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -27,6 +27,7 @@ import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { mockGetRegions } from 'support/intercepts/regions'; +import { extendRegion } from 'support/util/regions'; import { CloudPulseMetricsResponse, Database } from '@linode/api-v4'; import { Interception } from 'cypress/types/net-stubbing'; import { generateRandomMetricsData } from 'support/util/cloudpulse'; @@ -48,29 +49,14 @@ import { formatToolTip } from 'src/features/CloudPulse/Utils/unitConversion'; const expectedGranularityArray = ['Auto', '1 day', '1 hr', '5 min']; const timeDurationToSelect = 'Last 24 Hours'; -const flags: Partial = { - aclp: { enabled: true, beta: true }, - aclpResourceTypeMap: [ - { - dimensionKey: 'LINODE_ID', - maxResourceSelections: 10, - serviceType: 'linode', - supportedRegionIds: '', - }, - { - dimensionKey: 'cluster_id', - maxResourceSelections: 10, - serviceType: 'dbaas', - supportedRegionIds: 'us-ord', - }, - ], -}; +const flags: Partial = { aclp: { enabled: true, beta: true } }; const { metrics, id, serviceType, dashboardName, + region, engine, clusterName, nodeType, @@ -89,13 +75,15 @@ const dashboard = dashboardFactory.build({ }), }); -const metricDefinitions = metrics.map(({ title, name, unit }) => - dashboardMetricFactory.build({ - label: title, - metric: name, - unit, - }) -); +const metricDefinitions = { + data: metrics.map(({ title, name, unit }) => + dashboardMetricFactory.build({ + label: title, + metric: name, + unit, + }) + ), +}; const mockLinode = linodeFactory.build({ label: clusterName, @@ -103,18 +91,14 @@ const mockLinode = linodeFactory.build({ }); const mockAccount = accountFactory.build(); - -const mockRegion = regionFactory.build({ - capabilities: ['Managed Databases'], - id: 'us-ord', - label: 'Chicago, IL', -}); - -const extendedMockRegion = regionFactory.build({ - capabilities: ['Managed Databases'], - id: 'us-east', - label: 'Newark,NL', -}); +const mockRegion = extendRegion( + regionFactory.build({ + capabilities: ['Linodes'], + id: 'us-ord', + label: 'Chicago, IL', + country: 'us', + }) +); const metricsAPIResponsePayload = cloudPulseMetricsResponseFactory.build({ data: generateRandomMetricsData(timeDurationToSelect, '5 min'), }); @@ -167,9 +151,9 @@ const getWidgetLegendRowValuesFromResponse = ( }; const databaseMock: Database = databaseFactory.build({ - label: clusterName, - type: engine, - region: mockRegion.label, + label: widgetDetails.dbaas.clusterName, + type: widgetDetails.dbaas.engine, + region: widgetDetails.dbaas.region, version: '1', status: 'provisioning', cluster_size: 1, @@ -193,7 +177,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => { mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( 'getMetrics' ); - mockGetRegions([mockRegion, extendedMockRegion]); + mockGetRegions([mockRegion]); mockGetUserPreferences({}); mockGetDatabases([databaseMock]).as('getDatabases'); @@ -207,60 +191,35 @@ describe('Integration Tests for DBaaS Dashboard ', () => { ui.autocomplete .findByLabel('Dashboard') .should('be.visible') - .type(dashboardName); - - ui.autocompletePopper - .findByTitle(dashboardName) - .should('be.visible') - .click(); + .type(`${dashboardName}{enter}`) + .should('be.visible'); // Select a time duration from the autocomplete input. ui.autocomplete .findByLabel('Time Range') .should('be.visible') - .type(timeDurationToSelect); - - ui.autocompletePopper - .findByTitle(timeDurationToSelect) - .should('be.visible') - .click(); + .type(`${timeDurationToSelect}{enter}`) + .should('be.visible'); - //Select a Database Engine from the autocomplete input. + //Select a Engine from the autocomplete input. ui.autocomplete .findByLabel('Database Engine') .should('be.visible') - .type(engine); - - ui.autocompletePopper.findByTitle(engine).should('be.visible').click(); - - // Select a region from the dropdown. - ui.regionSelect.find().click(); - - ui.regionSelect.find().type(extendedMockRegion.label); + .type(`${engine}{enter}`) + .should('be.visible'); - // Since DBaaS does not support this region, we expect it to not be in the dropdown. + // Select a region from the dropdown. + ui.regionSelect.find().click().type(`${region}{enter}`); - ui.autocompletePopper.find().within(() => { - cy.findByText( - `${extendedMockRegion.label} (${extendedMockRegion.id})` - ).should('not.exist'); - }); - - ui.regionSelect.find().click().clear(); - ui.regionSelect - .findItemByRegionId(mockRegion.id, [mockRegion]) - .should('be.visible') - .click(); - - // Select a resource (Database Clusters) from the autocomplete input. + // Select a resource from the autocomplete input. ui.autocomplete .findByLabel('Database Clusters') .should('be.visible') - .type(clusterName); - - ui.autocompletePopper.findByTitle(clusterName).should('be.visible').click(); + .type(`${clusterName}{enter}`) + .click(); + cy.findByText(clusterName).should('be.visible'); - // Select a Node from the autocomplete input. + //Select a Node from the autocomplete input. ui.autocomplete .findByLabel('Node Type') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index 70a6ef1c615..303748e957a 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -26,6 +26,7 @@ import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { mockGetRegions } from 'support/intercepts/regions'; +import { extendRegion } from 'support/util/regions'; import { CloudPulseMetricsResponse } from '@linode/api-v4'; import { generateRandomMetricsData } from 'support/util/cloudpulse'; import { Interception } from 'cypress/types/net-stubbing'; @@ -45,23 +46,7 @@ import { formatToolTip } from 'src/features/CloudPulse/Utils/unitConversion'; */ const expectedGranularityArray = ['Auto', '1 day', '1 hr', '5 min']; const timeDurationToSelect = 'Last 24 Hours'; -const flags: Partial = { - aclp: { enabled: true, beta: true }, - aclpResourceTypeMap: [ - { - dimensionKey: 'LINODE_ID', - maxResourceSelections: 10, - serviceType: 'linode', - supportedRegionIds: 'us-ord', - }, - { - dimensionKey: 'cluster_id', - maxResourceSelections: 10, - serviceType: 'dbaas', - supportedRegionIds: '', - }, - ], -}; +const flags: Partial = { aclp: { enabled: true, beta: true } }; const { metrics, id, @@ -84,13 +69,15 @@ const dashboard = dashboardFactory.build({ }), }); -const metricDefinitions = metrics.map(({ title, name, unit }) => - dashboardMetricFactory.build({ - label: title, - metric: name, - unit, - }) -); +const metricDefinitions = { + data: metrics.map(({ title, name, unit }) => + dashboardMetricFactory.build({ + label: title, + metric: name, + unit, + }) + ), +}; const mockLinode = linodeFactory.build({ label: resource, @@ -98,18 +85,14 @@ const mockLinode = linodeFactory.build({ }); const mockAccount = accountFactory.build(); - -const mockRegion = regionFactory.build({ - capabilities: ['Linodes'], - id: 'us-ord', - label: 'Chicago, IL', -}); - -const extendedMockRegion = regionFactory.build({ - capabilities: ['Managed Databases'], - id: 'us-east', - label: 'Newark,NL', -}); +const mockRegion = extendRegion( + regionFactory.build({ + capabilities: ['Linodes'], + id: 'us-ord', + label: 'Chicago, IL', + country: 'us', + }) +); const metricsAPIResponsePayload = cloudPulseMetricsResponseFactory.build({ data: generateRandomMetricsData(timeDurationToSelect, '5 min'), }); @@ -187,41 +170,18 @@ describe('Integration Tests for Linode Dashboard ', () => { ui.autocomplete .findByLabel('Dashboard') .should('be.visible') - .type(dashboardName); - - ui.autocompletePopper - .findByTitle(dashboardName) - .should('be.visible') - .click(); + .type(`${dashboardName}{enter}`) + .should('be.visible'); // Select a time duration from the autocomplete input. ui.autocomplete .findByLabel('Time Range') .should('be.visible') - .type(timeDurationToSelect); - - ui.autocompletePopper - .findByTitle(timeDurationToSelect) - .should('be.visible') - .click(); - - ui.regionSelect.find().click(); - - // Select a region from the dropdown. - ui.regionSelect.find().click(); - - ui.regionSelect.find().type(extendedMockRegion.label); - - // Since Linode does not support this region, we expect it to not be in the dropdown. - - ui.autocompletePopper.find().within(() => { - cy.findByText( - `${extendedMockRegion.label} (${extendedMockRegion.id})` - ).should('not.exist'); - }); + .type(`${timeDurationToSelect}{enter}`) + .should('be.visible'); // Select a region from the dropdown. - ui.regionSelect.find().click().clear().type(`${region}{enter}`); + ui.regionSelect.find().click().type(`${region}{enter}`); // Select a resource from the autocomplete input. ui.autocomplete @@ -231,7 +191,6 @@ describe('Integration Tests for Linode Dashboard ', () => { .click(); cy.findByText(resource).should('be.visible'); - // Wait for all metrics query requests to resolve. cy.wait(['@getMetrics', '@getMetrics', '@getMetrics', '@getMetrics']); }); diff --git a/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts index f379f8c35a5..4e9b28fb0b9 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts @@ -60,7 +60,7 @@ describe('Clone a Domain', () => { .closest('tr') .within(() => { ui.actionMenu - .findByTitle(`Action menu for Domain ${domain.domain}`) + .findByTitle(`Action menu for Domain ${domain}`) .should('be.visible') .click(); }); @@ -84,7 +84,7 @@ describe('Clone a Domain', () => { .closest('tr') .within(() => { ui.actionMenu - .findByTitle(`Action menu for Domain ${domain.domain}`) + .findByTitle(`Action menu for Domain ${domain}`) .should('be.visible') .click(); }); @@ -118,7 +118,7 @@ describe('Clone a Domain', () => { .click(); }); // After cloning a Domain, the user is redirected to the new Domain's details page - cy.url().should('match', /\/domains\/\d+$/); + cy.url().should('endWith', 'domains'); // Confirm that domain is cloned and cloned domains contain the same records as the original Domain. cy.visitWithLogin('/domains'); diff --git a/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts index 22fd28b798a..950719ce399 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts @@ -2,83 +2,22 @@ import { authenticate } from 'support/api/authentication'; import { createDomain } from 'support/api/domains'; import { interceptCreateDomainRecord } from 'support/intercepts/domains'; import { createDomainRecords } from 'support/constants/domains'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; - -const createCaaRecord = ( - name: string, - tag: string, - value: string, - ttl: string -) => { - cy.findByText('Add a CAA Record').click(); - - // Fill in the form fields - cy.findByLabelText('Name').type(name); - - ui.autocomplete.findByLabel('Tag').click(); - ui.autocompletePopper.findByTitle(tag).click(); - - cy.findByLabelText('Value').type(value); - - ui.autocomplete.findByLabel('TTL').click(); - ui.autocompletePopper.findByTitle(ttl).click(); - - // Save the record - ui.button - .findByTitle('Save') - .should('be.visible') - .should('be.enabled') - .click(); -}; - -// Reusable function to edit a CAA record -const editCaaRecord = (name: string, newValue: string) => { - ui.actionMenu - .findByTitle(`Action menu for Record ${name}`) - .should('be.visible') - .click(); - - ui.actionMenuItem.findByTitle('Edit').should('be.visible').click(); - - // Edit the value field - cy.findByLabelText('Value').clear().type(newValue); - ui.button.findByTitle('Save').click(); -}; - -// Reusable function to verify record details in the table -const verifyRecordInTable = ( - name: string, - tag: string, - value: string, - ttl: string -) => { - cy.get('[aria-label="List of Domains CAA Record"]') // Target table by aria-label - .should('contain', name) - .and('contain', tag) - .and('contain', value) - .and('contain', ttl); -}; authenticate(); - -before(() => { - cleanUp('domains'); -}); - beforeEach(() => { cy.tag('method:e2e'); - createDomain().then((domain) => { - // intercept create API record request - interceptCreateDomainRecord().as('apiCreateRecord'); - const url = `/domains/${domain.id}`; - cy.visitWithLogin(url); - cy.url().should('contain', url); - }); }); describe('Creates Domains records with Form', () => { it('Adds domain records to a newly created Domain', () => { + createDomain().then((domain) => { + // intercept create api record request + interceptCreateDomainRecord().as('apiCreateRecord'); + const url = `/domains/${domain.id}`; + cy.visitWithLogin(url); + cy.url().should('contain', url); + }); + createDomainRecords().forEach((rec) => { cy.findByText(rec.name).click(); rec.fields.forEach((field) => { @@ -97,37 +36,3 @@ describe('Creates Domains records with Form', () => { }); }); }); - -describe('Tests for Editable Domain CAA Records', () => { - beforeEach(() => { - // Create the initial record with a valid email - createCaaRecord( - 'securitytest', - 'iodef', - 'mailto:security@example.com', - '5 minutes' - ); - - // Verify the initial record is in the table - verifyRecordInTable( - 'securitytest', - 'iodef', - 'mailto:security@example.com', - '5 minutes' - ); - }); - - it('Validates that "iodef" domain records can be edited with valid record', () => { - // Edit the record with a valid email and verify the updated record - editCaaRecord('securitytest', 'mailto:secdef@example.com'); - cy.get('table').should('contain', 'mailto:secdef@example.com'); - }); - - it('Validates that "iodef" domain records returns error with invalid record', () => { - // Edit the record with invalid email and validate form validation - editCaaRecord('securitytest', 'invalid-email-format'); - cy.get('p[role="alert"][data-qa-textfield-error-text="Value"]') - .should('exist') - .and('have.text', 'You have entered an invalid target'); - }); -}); diff --git a/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts index 0e4710ec621..4e9223cca14 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts @@ -33,7 +33,7 @@ describe('Delete a Domain', () => { .closest('tr') .within(() => { ui.actionMenu - .findByTitle(`Action menu for Domain ${domain.domain}`) + .findByTitle(`Action menu for Domain ${domain}`) .should('be.visible') .click(); }); @@ -57,7 +57,7 @@ describe('Delete a Domain', () => { .closest('tr') .within(() => { ui.actionMenu - .findByTitle(`Action menu for Domain ${domain.domain}`) + .findByTitle(`Action menu for Domain ${domain}`) .should('be.visible') .click(); }); diff --git a/packages/manager/cypress/e2e/core/general/account-login-redirect.spec.ts b/packages/manager/cypress/e2e/core/general/account-login-redirect.spec.ts index a7c3d3a465a..79c7ad08d48 100644 --- a/packages/manager/cypress/e2e/core/general/account-login-redirect.spec.ts +++ b/packages/manager/cypress/e2e/core/general/account-login-redirect.spec.ts @@ -1,5 +1,5 @@ import { mockApiRequestWithError } from 'support/intercepts/general'; -import { loginBaseUrl } from 'support/constants/login'; +import { LOGIN_ROOT } from 'src/constants'; describe('account login redirect', () => { /** @@ -14,7 +14,7 @@ describe('account login redirect', () => { cy.visitWithLogin('/linodes/create'); - cy.url().should('contain', `${loginBaseUrl}/login?`, { exact: false }); + cy.url().should('contain', `${LOGIN_ROOT}/login?`, { exact: false }); cy.findByText('Please log in to continue.').should('be.visible'); }); diff --git a/packages/manager/cypress/e2e/core/images/create-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-image.spec.ts index 06cf3fa0e47..2b9c9d017d4 100644 --- a/packages/manager/cypress/e2e/core/images/create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-image.spec.ts @@ -1,9 +1,46 @@ -import type { Linode } from '@linode/api-v4'; +import type { Linode, Region } from '@linode/api-v4'; +import { accountFactory, linodeFactory, regionFactory } from 'src/factories'; import { authenticate } from 'support/api/authentication'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomPhrase } from 'support/util/random'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { + mockGetLinodeDetails, + mockGetLinodes, +} from 'support/intercepts/linodes'; + +const mockRegions: Region[] = [ + regionFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + id: 'us-east', + label: 'Newark, NJ', + site_type: 'core', + }), + regionFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + id: 'us-den-1', + label: 'Distributed - Denver, CO', + site_type: 'distributed', + }), +]; + +const mockLinodes: Linode[] = [ + linodeFactory.build({ + label: 'core-region-linode', + region: mockRegions[0].id, + }), + linodeFactory.build({ + label: 'distributed-region-linode', + region: mockRegions[1].id, + }), +]; + +const DISK_ENCRYPTION_IMAGES_CAVEAT_COPY = + 'Virtual Machine Images are not encrypted.'; authenticate(); describe('create image (e2e)', () => { @@ -16,9 +53,9 @@ describe('create image (e2e)', () => { const label = randomLabel(); const description = randomPhrase(); - // When Alpine 3.20 becomes deprecated, we will have to update these values for the test to pass. - const image = 'linode/alpine3.20'; - const disk = 'Alpine 3.20 Disk'; + // When Alpine 3.19 becomes deprecated, we will have to update these values for the test to pass. + const image = 'linode/alpine3.19'; + const disk = 'Alpine 3.19 Disk'; cy.defer( () => createTestLinode({ image }, { waitForDisks: true }), @@ -86,4 +123,116 @@ describe('create image (e2e)', () => { }); }); }); + + it('displays notice informing user that Images are not encrypted, provided the LDE feature is enabled and the selected linode is not in a distributed region', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: true, + }).as('getFeatureFlags'); + + // Mock responses + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + mockGetLinodes(mockLinodes).as('getLinodes'); + + // intercept request + cy.visitWithLogin('/images/create'); + cy.wait(['@getFeatureFlags', '@getAccount', '@getLinodes', '@getRegions']); + + // Find the Linode select and open it + cy.findByLabelText('Linode') + .should('be.visible') + .should('be.enabled') + .should('have.attr', 'placeholder', 'Select a Linode') + .click(); + + // Select the Linode + ui.autocompletePopper + .findByTitle(mockLinodes[0].label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Check if notice is visible + cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('be.visible'); + }); + + it('does not display a notice informing user that Images are not encrypted if the LDE feature is disabled', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: false, + }).as('getFeatureFlags'); + + // Mock responses + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + mockGetLinodes(mockLinodes).as('getLinodes'); + + // intercept request + cy.visitWithLogin('/images/create'); + cy.wait(['@getFeatureFlags', '@getAccount', '@getLinodes', '@getRegions']); + + // Find the Linode select and open it + cy.findByLabelText('Linode') + .should('be.visible') + .should('be.enabled') + .should('have.attr', 'placeholder', 'Select a Linode') + .click(); + + // Select the Linode + ui.autocompletePopper + .findByTitle(mockLinodes[0].label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Check if notice is visible + cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('not.exist'); + }); + + it('does not display a notice informing user that Images are not encrypted if the selected linode is in a distributed region', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: true, + }).as('getFeatureFlags'); + + // Mock responses + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + mockGetLinodes(mockLinodes).as('getLinodes'); + mockGetLinodeDetails(mockLinodes[1].id, mockLinodes[1]); + + // intercept request + cy.visitWithLogin('/images/create'); + cy.wait(['@getFeatureFlags', '@getAccount', '@getRegions', '@getLinodes']); + + // Find the Linode select and open it + cy.findByLabelText('Linode') + .should('be.visible') + .should('be.enabled') + .should('have.attr', 'placeholder', 'Select a Linode') + .click(); + + // Select the Linode + ui.autocompletePopper + .findByTitle(mockLinodes[1].label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Check if notice is visible + cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('not.exist'); + }); }); diff --git a/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts deleted file mode 100644 index 5154138d6f8..00000000000 --- a/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { imageFactory } from 'src/factories'; -import { ui } from 'support/ui'; -import { mockGetAllImages } from 'support/intercepts/images'; -import { profileFactory } from 'src/factories'; -import { randomLabel } from 'support/util/random'; -import { grantsFactory } from 'src/factories'; -import { accountUserFactory } from 'src/factories'; -import { mockGetUser } from 'support/intercepts/account'; -import { - mockGetProfile, - mockGetProfileGrants, -} from 'support/intercepts/profile'; -import { Image } from '@linode/api-v4'; - -function checkActionMenu(tableAlias: string, mockImages: any[]) { - mockImages.forEach((image) => { - cy.get(tableAlias) - .find('tbody tr') - .should('contain', image.label) - .then(($row) => { - // If the row contains the label, proceed with clicking the action menu - const actionButton = $row.find( - `button[aria-label="Action menu for Image ${image.label}"]` - ); - if (actionButton) { - cy.wrap(actionButton).click(); - - // Check that the item with text 'Deploy to New Linode' is active - cy.get('ul[role="menu"]') - .contains('Deploy to New Linode') - .should('be.visible') - .and('be.enabled'); - - // Check that all other items are disabled - cy.get('ul[role="menu"]') - .find('li') - .not(':contains("Deploy to New Linode")') - .each(($li) => { - cy.wrap($li).should('be.visible').and('be.disabled'); - }); - - // Close the action menu by clicking on Custom Image Title of the screen - cy.get('body').click(0, 0); - } - }); - }); -} - -describe('image landing checks for non-empty state with restricted user', () => { - beforeEach(() => { - const mockImages: Image[] = new Array(3).fill(null).map( - (_item: null, index: number): Image => { - return imageFactory.build({ - label: `Image ${index}`, - tags: [index % 2 == 0 ? 'even' : 'odd', 'nums'], - }); - } - ); - - // Mock setup to display the Image landing page in an non-empty state - mockGetAllImages(mockImages).as('getImages'); - - // Alias the mockImages array - cy.wrap(mockImages).as('mockImages'); - }); - - it('checks restricted user with read access has no access to create image and can see existing images', () => { - // Mock setup for user profile, account user, and user grants with restricted permissions, - const mockProfile = profileFactory.build({ - username: randomLabel(), - restricted: true, - }); - - const mockUser = accountUserFactory.build({ - username: mockProfile.username, - restricted: true, - user_type: 'default', - }); - - const mockGrants = grantsFactory.build({ - global: { - add_images: false, - }, - }); - - mockGetProfile(mockProfile); - mockGetProfileGrants(mockGrants); - mockGetUser(mockUser); - - // Login and wait for application to load - cy.visitWithLogin('/images'); - cy.wait('@getImages'); - cy.url().should('endWith', '/images'); - - cy.contains('h3', 'Custom Images') - .closest('div[data-qa-paper="true"]') - .find('[role="table"]') - .should('exist') - .as('customImageTable'); - - cy.contains('h3', 'Recovery Images') - .closest('div[data-qa-paper="true"]') - .find('[role="table"]') - .should('exist') - .as('recoveryImageTable'); - - // Assert that Create Image button is visible and disabled - ui.button - .findByTitle('Create Image') - .should('be.visible') - .and('be.disabled') - .trigger('mouseover'); - - // Assert that tooltip is visible with message - ui.tooltip - .findByText( - "You don't have permissions to create Images. Please contact your account administrator to request the necessary permissions." - ) - .should('be.visible'); - - cy.get('@mockImages').then((mockImages) => { - // Assert that the correct number of Image entries are present in the customImageTable - cy.get('@customImageTable') - .find('tbody tr') - .should('have.length', mockImages.length); - - // Assert that the correct number of Image entries are present in the recoveryImageTable - cy.get('@recoveryImageTable') - .find('tbody tr') - .should('have.length', mockImages.length); - - checkActionMenu('@customImageTable', mockImages); // For the custom image table - checkActionMenu('@recoveryImageTable', mockImages); // For the recovery image table - }); - }); -}); diff --git a/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts b/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts index 582af41ebc1..929ed932684 100644 --- a/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts +++ b/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts @@ -320,11 +320,12 @@ describe('machine image', () => { const label = randomLabel(); const status = 'failed'; const message = 'Upload window expired'; + const expiredDate = DateTime.local().minus({ days: 1 }).toISO(); uploadImage(label); cy.wait('@imageUpload').then((xhr) => { const imageId = xhr.response?.body.image.id; assertProcessing(label, imageId); - eventIntercept(label, imageId, status, message); + eventIntercept(label, imageId, status, message, expiredDate); cy.wait('@getEvent'); assertFailed(label, imageId, message); }); diff --git a/packages/manager/cypress/e2e/core/images/search-images.spec.ts b/packages/manager/cypress/e2e/core/images/search-images.spec.ts index 69a637ff321..8da716b8d33 100644 --- a/packages/manager/cypress/e2e/core/images/search-images.spec.ts +++ b/packages/manager/cypress/e2e/core/images/search-images.spec.ts @@ -24,7 +24,7 @@ describe('Search Images', () => { cy.defer( () => createTestLinode( - { image: 'linode/debian12', region: 'us-east' }, + { image: 'linode/debian10', region: 'us-east' }, { waitForDisks: true } ), 'create linode' diff --git a/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts b/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts index 4c981b2f3ce..1ee83565388 100644 --- a/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts @@ -10,7 +10,7 @@ import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; describe('create image (using mocks)', () => { it('create image from a linode', () => { const mockDisks = [ - linodeDiskFactory.build({ label: 'Debian 12 Disk', filesystem: 'ext4' }), + linodeDiskFactory.build({ label: 'Debian 10 Disk', filesystem: 'ext4' }), linodeDiskFactory.build({ label: '512 MB Swap Image', filesystem: 'swap', diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 1f0e42c010a..6c93301d30b 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -3,38 +3,32 @@ */ import { accountFactory, - dedicatedTypeFactory, kubernetesClusterFactory, kubernetesControlPlaneACLFactory, kubernetesControlPlaneACLOptionsFactory, linodeTypeFactory, regionFactory, - nodePoolFactory, - kubeLinodeFactory, - lkeHighAvailabilityTypeFactory, } from 'src/factories'; import { mockCreateCluster, mockGetCluster, mockCreateClusterError, mockGetControlPlaneACL, - mockGetClusterPools, - mockGetDashboardUrl, - mockGetApiEndpoints, - mockGetClusters, - mockGetLKEClusterTypes, - mockGetTieredKubernetesVersions, - mockGetKubernetesVersions, } from 'support/intercepts/lke'; -import { mockGetAccountBeta } from 'support/intercepts/betas'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetRegions, mockGetRegionAvailability, } from 'support/intercepts/regions'; -import { getRegionById } from 'support/util/regions'; +import { KubernetesCluster } from '@linode/api-v4'; +import { LkePlanDescription } from 'support/api/lke'; +import { lkeClusterPlans } from 'support/constants/lke'; +import { chooseRegion, getRegionById } from 'support/util/regions'; +import { interceptCreateCluster } from 'support/intercepts/lke'; import { ui } from 'support/ui'; import { randomLabel, randomNumber, randomItem } from 'support/util/random'; +import { cleanUp } from 'support/util/cleanup'; +import { authenticate } from 'support/api/authentication'; import { dcPricingLkeCheckoutSummaryPlaceholder, dcPricingLkeHAPlaceholder, @@ -46,151 +40,77 @@ import { } from 'support/constants/dc-specific-pricing'; import { mockGetLinodeTypes } from 'support/intercepts/linodes'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { chooseRegion } from 'support/util/regions'; -import { getTotalClusterMemoryCPUAndStorage } from 'src/features/Kubernetes/kubeUtils'; -import { getTotalClusterPrice } from 'src/utilities/pricing/kubernetes'; -import type { ExtendedType } from 'src/utilities/extendType'; -import type { LkePlanDescription } from 'support/api/lke'; -import { PriceType } from '@linode/api-v4/lib/types'; -import { - latestEnterpriseTierKubernetesVersion, - latestKubernetesVersion, -} from 'support/constants/lke'; -import { lkeEnterpriseTypeFactory } from 'src/factories'; +/** + * Gets the label for an LKE plan as shown in creation plan table. + * + * @param clusterPlan - Cluster plan from which to determine Cloud Manager LKE plan name. + * + * @returns LKE plan name for plan. + */ +const getLkePlanName = (clusterPlan: LkePlanDescription) => { + return `${clusterPlan.type} ${clusterPlan.size} GB`; +}; -const dedicatedNodeCount = 4; -const nanodeNodeCount = 3; +/** + * Gets the label for an LKE plan as shown in the creation checkout bar. + * + * @param clusterPlan - Cluster plan from which to determine Cloud Manager LKE checkout name. + * + * @returns LKE checkout plan name for plan. + */ +const getLkePlanCheckoutName = (clusterPlan: LkePlanDescription) => { + return `${clusterPlan.type} ${clusterPlan.size} GB Plan`; +}; -const clusterRegion = chooseRegion({ - capabilities: ['Kubernetes'], -}); -const dedicatedCpuPool = nodePoolFactory.build({ - count: dedicatedNodeCount, - nodes: kubeLinodeFactory.buildList(dedicatedNodeCount), - type: 'g6-dedicated-2', -}); -const nanodeMemoryPool = nodePoolFactory.build({ - count: nanodeNodeCount, - nodes: kubeLinodeFactory.buildList(nanodeNodeCount), - type: 'g6-nanode-1', -}); -const dedicatedType = dedicatedTypeFactory.build({ - disk: 81920, - id: 'g6-dedicated-2', - label: 'Dedicated 4 GB', - memory: 4096, - price: { - hourly: 0.054, - monthly: 36.0, - }, - region_prices: dcPricingMockLinodeTypes.find( - (type) => type.id === 'g6-dedicated-2' - )?.region_prices, - vcpus: 2, -}) as ExtendedType; -const nanodeType = linodeTypeFactory.build({ - disk: 25600, - id: 'g6-nanode-1', - label: 'Linode 2 GB', - memory: 2048, - price: { - hourly: 0.0075, - monthly: 5.0, - }, - region_prices: dcPricingMockLinodeTypes.find( - (type) => type.id === 'g6-nanode-1' - )?.region_prices, - vcpus: 1, -}) as ExtendedType; -const mockedLKEClusterPrices: PriceType[] = [ - { - id: 'lke-sa', - label: 'LKE Standard Availability', - price: { - hourly: 0.0, - monthly: 0.0, - }, - region_prices: [], - transfer: 0, - }, -]; -const mockedLKEHAClusterPrices: PriceType[] = [ - { - id: 'lke-ha', - label: 'LKE High Availability', - price: { - hourly: 0.09, - monthly: 60.0, - }, - region_prices: [], - transfer: 0, - }, -]; -const mockedLKEEnterprisePrices = [ - lkeHighAvailabilityTypeFactory.build(), - lkeEnterpriseTypeFactory.build(), -]; -const clusterPlans: LkePlanDescription[] = [ - { - nodeCount: dedicatedNodeCount, - planName: 'Dedicated 4 GB', - size: 4, - tab: 'Dedicated CPU', - type: 'dedicated', - }, - { - nodeCount: nanodeNodeCount, - planName: 'Linode 2 GB', - size: 24, - tab: 'Shared CPU', - type: 'nanode', - }, -]; -const mockedLKEClusterTypes = [dedicatedType, nanodeType]; +/** + * Returns each plan in an array which is similar to the given plan. + * + * Plans are considered similar if they have identical type and size. + * + * @param clusterPlan - Cluster plan with which to compare similarity. + * @param clusterPlans - Array from which to find similar cluster plans. + * + * @returns Array of similar cluster plans. + */ +const getSimilarPlans = ( + clusterPlan: LkePlanDescription, + clusterPlans: LkePlanDescription[] +) => { + return clusterPlans.filter((otherClusterPlan: any) => { + return ( + clusterPlan.type === otherClusterPlan.type && + clusterPlan.size === otherClusterPlan.size + ); + }); +}; +authenticate(); describe('LKE Cluster Creation', () => { + before(() => { + cleanUp(['linodes', 'lke-clusters']); + }); + /* * - Confirms that users can create a cluster by completing the LKE create form. * - Confirms that LKE cluster is created. * - Confirms that user is redirected to new LKE cluster summary page. - * - Confirms that correct information is shown on the LKE cluster summary page * - Confirms that new LKE cluster summary page shows expected node pools. * - Confirms that new LKE cluster is shown on LKE clusters landing page. + * - Confirms that correct information is shown on the LKE cluster summary page */ - const clusterLabel = randomLabel(); - const clusterVersion = '1.31'; - const mockedLKECluster = kubernetesClusterFactory.build({ - label: clusterLabel, - region: clusterRegion.id, - }); - const mockedLKEClusterPools = [nanodeMemoryPool, dedicatedCpuPool]; - const mockedLKEClusterControlPlane = kubernetesControlPlaneACLFactory.build(); - const { - CPU: totalCpu, - RAM: totalMemory, - Storage: totalStorage, - } = getTotalClusterMemoryCPUAndStorage( - mockedLKEClusterPools, - mockedLKEClusterTypes - ); - it('can create an LKE cluster', () => { - mockCreateCluster(mockedLKECluster).as('createCluster'); - mockGetCluster(mockedLKECluster).as('getCluster'); - mockGetClusterPools(mockedLKECluster.id, mockedLKEClusterPools).as( - 'getClusterPools' - ); - mockGetDashboardUrl(mockedLKECluster.id).as('getDashboardUrl'); - mockGetControlPlaneACL( - mockedLKECluster.id, - mockedLKEClusterControlPlane - ).as('getControlPlaneACL'); - mockGetApiEndpoints(mockedLKECluster.id).as('getApiEndpoints'); - mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); - mockGetLKEClusterTypes(mockedLKEClusterPrices).as('getLKEClusterTypes'); - mockGetClusters([mockedLKECluster]).as('getClusters'); - mockGetKubernetesVersions([clusterVersion]).as('getKubernetesVersions'); + cy.tag('method:e2e', 'purpose:dcTesting'); + const clusterLabel = randomLabel(); + const clusterRegion = chooseRegion({ + capabilities: ['Kubernetes'], + }); + const clusterVersion = '1.27'; + const clusterPlans = new Array(2) + .fill(null) + .map(() => randomItem(lkeClusterPlans)); + + interceptCreateCluster().as('createCluster'); cy.visitWithLogin('/kubernetes/clusters'); @@ -203,40 +123,44 @@ describe('LKE Cluster Creation', () => { cy.url().should('endWith', '/kubernetes/create'); // Fill out LKE creation form label, region, and Kubernetes version fields. - cy.get('[data-qa-textfield-label="Cluster Label"]') + cy.findByLabelText('Cluster Label') .should('be.visible') - .click(); - cy.focused().type(`${clusterLabel}{enter}`); + .click() + .type(`${clusterLabel}{enter}`); ui.regionSelect.find().click().type(`${clusterRegion.label}{enter}`); - ui.autocomplete - .findByLabel('Kubernetes Version') + cy.findByText('Kubernetes Version') + .should('be.visible') .click() .type(`${clusterVersion}{enter}`); - cy.get('[data-testid="ha-radio-button-no"]').should('be.visible').click(); + cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); + let totalCpu = 0; + let totalMemory = 0; + let totalStorage = 0; let monthPrice = 0; - // Add a node pool for each selected plan, and confirm that the + // Add a node pool for each randomly selected plan, and confirm that the // selected node pool plan is added to the checkout bar. clusterPlans.forEach((clusterPlan) => { - const nodeCount = clusterPlan.nodeCount; - const planName = clusterPlan.planName; + const nodeCount = randomNumber(1, 3); + const planName = getLkePlanName(clusterPlan); + const checkoutName = getLkePlanCheckoutName(clusterPlan); - cy.log(`Adding ${nodeCount}x ${planName} node(s)`); + cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); // Click the right tab for the plan, and add a node pool with the desired // number of nodes. cy.findByText(clusterPlan.tab).should('be.visible').click(); - const quantityInput = '[name="Quantity"]'; cy.findByText(planName) .should('be.visible') .closest('tr') .within(() => { - cy.get(quantityInput).should('be.visible'); - cy.get(quantityInput).click(); - cy.get(quantityInput).type(`{selectall}${nodeCount}`); + cy.get('[name="Quantity"]') + .should('be.visible') + .click() + .type(`{selectall}${nodeCount}`); ui.button .findByTitle('Add') @@ -252,16 +176,31 @@ describe('LKE Cluster Creation', () => { // It's possible that multiple pools of the same type get added. // We're taking a naive approach here by confirming that at least one // instance of the pool appears in the checkout bar. - cy.findAllByText(`${planName} Plan`).first().should('be.visible'); + cy.findAllByText(checkoutName).first().should('be.visible'); }); + // Expected information on the LKE cluster summary page. - monthPrice = getTotalClusterPrice({ - highAvailabilityPrice: 0, - pools: [nanodeMemoryPool, dedicatedCpuPool], - region: clusterRegion.id, - types: mockedLKEClusterTypes, - }); + if (clusterPlan.size == 2 && clusterPlan.type == 'Linode') { + totalCpu = totalCpu + nodeCount * 1; + totalMemory = totalMemory + nodeCount * 2; + totalStorage = totalStorage + nodeCount * 50; + monthPrice = monthPrice + nodeCount * 12; + } + if (clusterPlan.size == 4 && clusterPlan.type == 'Linode') { + totalCpu = totalCpu + nodeCount * 2; + totalMemory = totalMemory + nodeCount * 4; + totalStorage = totalStorage + nodeCount * 80; + monthPrice = monthPrice + nodeCount * 24; + } + if (clusterPlan.size == 4 && clusterPlan.type == 'Dedicated') { + totalCpu = totalCpu + nodeCount * 2; + totalMemory = totalMemory + nodeCount * 4; + totalStorage = totalStorage + nodeCount * 80; + monthPrice = monthPrice + nodeCount * 36; + } }); + // $60.00/month for enabling HA control plane + const totalPrice = monthPrice + 60; // Create LKE cluster. cy.get('[data-testid="kube-checkout-bar"]') @@ -276,40 +215,31 @@ describe('LKE Cluster Creation', () => { // Wait for LKE cluster to be created and confirm that we are redirected // to the cluster summary page. - cy.wait([ - '@getCluster', - '@getClusterPools', - '@createCluster', - '@getLKEClusterTypes', - '@getLinodeTypes', - '@getDashboardUrl', - '@getControlPlaneACL', - '@getApiEndpoints', - ]); - cy.url().should( - 'endWith', - `/kubernetes/clusters/${mockedLKECluster.id}/summary` - ); + cy.wait('@createCluster').then(({ response }) => { + if (!response) { + throw new Error( + `Error creating LKE cluster ${clusterLabel}; API request failed` + ); + } + const cluster: KubernetesCluster = response.body; + cy.url().should('endWith', `/kubernetes/clusters/${cluster.id}/summary`); + }); // Confirm that each node pool is shown. clusterPlans.forEach((clusterPlan) => { // Because multiple node pools may have identical labels, we figure out // how many identical labels for each plan will exist and confirm that // the expected number is present. - const nodePoolLabel = clusterPlan.planName; + const nodePoolLabel = getLkePlanName(clusterPlan); const similarNodePoolCount = getSimilarPlans(clusterPlan, clusterPlans) .length; - // Confirm that the cluster created with the expected parameters. + //Confirm that the cluster created with the expected parameters. cy.findAllByText(`${clusterRegion.label}`).should('be.visible'); cy.findAllByText(`${totalCpu} CPU Cores`).should('be.visible'); - cy.findAllByText(`${Math.round(totalStorage / 1024)} GB Storage`).should( - 'be.visible' - ); - cy.findAllByText(`${Math.round(totalMemory / 1024)} GB RAM`).should( - 'be.visible' - ); - cy.findAllByText(`$${monthPrice.toFixed(2)}/month`).should('be.visible'); + cy.findAllByText(`${totalMemory} GB RAM`).should('be.visible'); + cy.findAllByText(`${totalStorage} GB Storage`).should('be.visible'); + cy.findAllByText(`$${totalPrice}.00/month`).should('be.visible'); cy.contains('Kubernetes API Endpoint').should('be.visible'); cy.contains('linodelke.net:443').should('be.visible'); @@ -319,169 +249,26 @@ describe('LKE Cluster Creation', () => { .should('be.visible'); }); + // Navigate to the LKE landing page and confirm that new cluster is shown. ui.breadcrumb .find() .should('be.visible') .within(() => { cy.findByText(clusterLabel).should('be.visible'); - }); - }); -}); - -describe('LKE Cluster Creation with APL enabled', () => { - it('can create an LKE cluster with APL flag enabled', () => { - const clusterLabel = randomLabel(); - const mockedLKECluster = kubernetesClusterFactory.build({ - label: clusterLabel, - region: clusterRegion.id, - }); - const mockedLKEClusterPools = [nanodeMemoryPool, dedicatedCpuPool]; - const mockedLKEClusterControlPlane = kubernetesControlPlaneACLFactory.build(); - const dedicated4Type = dedicatedTypeFactory.build({ - disk: 163840, - id: 'g6-dedicated-4', - label: 'Dedicated 8GB', - memory: 8192, - price: { - hourly: 0.108, - monthly: 72.0, - }, - region_prices: dcPricingMockLinodeTypes.find( - (type) => type.id === 'g6-dedicated-8' - )?.region_prices, - vcpus: 4, - }); - const dedicated8Type = dedicatedTypeFactory.build({ - disk: 327680, - id: 'g6-dedicated-8', - label: 'Dedicated 16GB', - memory: 16384, - price: { - hourly: 0.216, - monthly: 144.0, - }, - region_prices: dcPricingMockLinodeTypes.find( - (type) => type.id === 'g6-dedicated-8' - )?.region_prices, - vcpus: 8, - }); - const mockedLKEClusterTypes = [ - dedicatedType, - dedicated4Type, - dedicated8Type, - nanodeType, - ]; - mockAppendFeatureFlags({ - apl: { - enabled: true, - }, - }).as('getFeatureFlags'); - mockGetAccountBeta({ - id: 'apl', - label: 'Akamai App Platform Beta', - enrolled: '2024-11-04T21:39:41', - description: - 'Akamai App Platform is a platform that combines developer and operations-centric tools, automation and self-service to streamline the application lifecycle when using Kubernetes. This process will pre-register you for an upcoming beta.', - started: '2024-10-31T18:00:00', - ended: null, - }).as('getAccountBeta'); - mockCreateCluster(mockedLKECluster).as('createCluster'); - mockGetCluster(mockedLKECluster).as('getCluster'); - mockGetClusterPools(mockedLKECluster.id, mockedLKEClusterPools).as( - 'getClusterPools' - ); - mockGetDashboardUrl(mockedLKECluster.id).as('getDashboardUrl'); - mockGetControlPlaneACL( - mockedLKECluster.id, - mockedLKEClusterControlPlane - ).as('getControlPlaneACL'); - mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); - mockGetLKEClusterTypes(mockedLKEHAClusterPrices).as('getLKEClusterTypes'); - mockGetApiEndpoints(mockedLKECluster.id).as('getApiEndpoints'); - - cy.visitWithLogin('/kubernetes/create'); - - cy.wait([ - '@getFeatureFlags', - '@getAccountBeta', - '@getLinodeTypes', - '@getLKEClusterTypes', - ]); - - // Enter cluster details - cy.get('[data-qa-textfield-label="Cluster Label"]') - .should('be.visible') - .click(); - cy.focused().type(`${clusterLabel}{enter}`); - - ui.regionSelect.find().click().type(`${clusterRegion.label}{enter}`); - - cy.findByTestId('apl-label').should('have.text', 'Akamai App Platform'); - cy.findByTestId('apl-radio-button-yes').should('be.visible').click(); - cy.findByTestId('ha-radio-button-yes').should('be.disabled'); - cy.get( - '[aria-label="Enabled by default when Akamai App Platform is enabled."]' - ).should('be.visible'); - - // Check that Shared CPU plans are disabled - ui.tabList.findTabByTitle('Shared CPU').click(); - cy.findByText( - 'Shared CPU instances are currently not available for Akamai App Platform.' - ).should('be.visible'); - cy.get('[data-qa-plan-row="Linode 2 GB"]').should('have.attr', 'disabled'); - - // Check that Dedicated CPU plans are available if greater than 8GB - ui.tabList.findTabByTitle('Dedicated CPU').click(); - cy.get('[data-qa-plan-row="Dedicated 4 GB"]').should( - 'have.attr', - 'disabled' - ); - cy.get('[data-qa-plan-row="Dedicated 8 GB"]').should( - 'not.have.attr', - 'disabled' - ); - cy.get('[data-qa-plan-row="Dedicated 16 GB"]').within(() => { - cy.get('[name="Quantity"]').click(); - cy.get('[name="Quantity"]').type('{selectall}3'); - ui.button - .findByTitle('Add') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Check that the checkout bar displays the correct information - cy.get('[data-testid="kube-checkout-bar"]') - .should('be.visible') - .within(() => { - cy.findByText(`Dedicated 16 GB Plan`).should('be.visible'); - cy.findByText('$432.00').should('be.visible'); - cy.findByText('High Availability (HA) Control Plane').should( - 'be.visible' - ); - cy.findByText('$60.00/month').should('be.visible'); - cy.findByText('$492.00').should('be.visible'); - - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.enabled') - .click(); + cy.findByText('kubernetes').should('be.visible').click(); }); - cy.wait([ - '@createCluster', - '@getCluster', - '@getClusterPools', - '@getDashboardUrl', - '@getControlPlaneACL', - '@getApiEndpoints', - ]); + cy.url().should('endWith', '/kubernetes/clusters'); + cy.findByText(clusterLabel).should('be.visible'); }); }); describe('LKE Cluster Creation with DC-specific pricing', () => { + before(() => { + cleanUp('lke-clusters'); + }); + /* * - Confirms that DC-specific prices are present in the LKE create form. * - Confirms that pricing docs link is shown in "Region" section. @@ -490,9 +277,9 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { * - Confirms that HA helper text updates dynamically to display pricing when a region is selected. */ it('can dynamically update prices when creating an LKE cluster based on region', () => { - // In staging API, only the Dallas region is available for LKE creation - const dcSpecificPricingRegion = getRegionById('us-central'); + const dcSpecificPricingRegion = getRegionById('us-east'); const clusterLabel = randomLabel(); + const clusterVersion = '1.27'; const clusterPlans = new Array(2) .fill(null) .map(() => randomItem(dcPricingLkeClusterPlans)); @@ -535,16 +322,18 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { .click() .type(`${clusterLabel}{enter}`); - ui.regionSelect - .find() - .click() - .type(`${dcSpecificPricingRegion.label}{enter}`); + ui.regionSelect.find().type(`${dcSpecificPricingRegion.label}{enter}`); // Confirm that HA price updates dynamically once region selection is made. cy.contains(/\$.*\/month/).should('be.visible'); cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); + cy.findByText('Kubernetes Version') + .should('be.visible') + .click() + .type(`${clusterVersion}{enter}`); + // Confirm that with region and HA selections, create button is still disabled until plan selection is made. cy.get('[data-qa-deploy-linode]') .should('contain.text', 'Create Cluster') @@ -554,9 +343,10 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { // selected node pool plan is added to the checkout bar. clusterPlans.forEach((clusterPlan) => { const nodeCount = randomNumber(1, 3); - const planName = clusterPlan.planName; + const planName = getLkePlanName(clusterPlan); + const checkoutName = getLkePlanCheckoutName(clusterPlan); - cy.log(`Adding ${nodeCount}x ${clusterPlan.planName} node(s)`); + cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); // Click the right tab for the plan, and add a node pool with the desired // number of nodes. cy.findByText(clusterPlan.tab).should('be.visible').click(); @@ -583,7 +373,7 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { // It's possible that multiple pools of the same type get added. // We're taking a naive approach here by confirming that at least one // instance of the pool appears in the checkout bar. - cy.findAllByText(`${planName} Plan`).first().should('be.visible'); + cy.findAllByText(checkoutName).first().should('be.visible'); }); }); @@ -722,6 +512,7 @@ describe('LKE Cluster Creation with ACL', () => { .should('be.visible'); // Add a node pool + cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); cy.findByText(clusterPlan.tab).should('be.visible').click(); cy.findByText(planName) .should('be.visible') @@ -856,6 +647,7 @@ describe('LKE Cluster Creation with ACL', () => { .click(); // Add a node pool + cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); cy.findByText(clusterPlan.tab).should('be.visible').click(); cy.findByText(planName) .should('be.visible') @@ -993,6 +785,7 @@ describe('LKE Cluster Creation with ACL', () => { cy.contains('Must be a valid IPv6 address.').should('not.exist'); // Add a node pool + cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); cy.findByText(clusterPlan.tab).should('be.visible').click(); cy.findByText(planName) .should('be.visible') @@ -1054,7 +847,7 @@ describe('LKE Cluster Creation with LKE-E', () => { cy.url().should('endWith', '/kubernetes/create'); - cy.contains('Cluster Tier').should('not.exist'); + cy.contains('Cluster Type').should('not.exist'); }); describe('shows the LKE-E flow with the feature flag on', () => { @@ -1067,39 +860,16 @@ describe('LKE Cluster Creation with LKE-E', () => { /** * - Mocks the LKE-E capability - * - Confirms the Cluster Tier selection can be made + * - Confirms the Cluster Type selection can be made * - Confirms that HA is enabled by default with LKE-E selection - * - Confirms an LKE-E supported region can be selected - * - Confirms an LKE-E supported k8 version can be selected - * - Confirms the checkout bar displays the correct LKE-E info - * - Confirms an enterprise cluster can be created with the correct chip, version, and price + * @todo LKE-E: Add onto this test as the LKE-E changes to the Create flow are built out */ it('creates an LKE-E cluster with the account capability', () => { - const clusterLabel = randomLabel(); - const mockedEnterpriseCluster = kubernetesClusterFactory.build({ - label: clusterLabel, - region: 'us-iad', - tier: 'enterprise', - k8s_version: latestEnterpriseTierKubernetesVersion.id, - }); - const mockedEnterpriseClusterPools = [nanodeMemoryPool, dedicatedCpuPool]; - const mockedLKEClusterTypes = [dedicatedType, nanodeType]; - mockGetAccount( accountFactory.build({ capabilities: ['Kubernetes Enterprise'], }) ).as('getAccount'); - mockGetTieredKubernetesVersions('enterprise', [ - latestEnterpriseTierKubernetesVersion, - ]).as('getTieredKubernetesVersions'); - mockGetKubernetesVersions([latestKubernetesVersion]).as( - 'getKubernetesVersions' - ); - mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); - mockGetLKEClusterTypes(mockedLKEEnterprisePrices).as( - 'getLKEEnterpriseClusterTypes' - ); mockGetRegions([ regionFactory.build({ capabilities: ['Linodes', 'Kubernetes'], @@ -1112,18 +882,8 @@ describe('LKE Cluster Creation with LKE-E', () => { label: 'Washington, DC', }), ]).as('getRegions'); - mockGetCluster(mockedEnterpriseCluster).as('getCluster'); - mockCreateCluster(mockedEnterpriseCluster).as('createCluster'); - mockGetClusters([mockedEnterpriseCluster]).as('getClusters'); - mockGetClusterPools( - mockedEnterpriseCluster.id, - mockedEnterpriseClusterPools - ).as('getClusterPools'); - mockGetDashboardUrl(mockedEnterpriseCluster.id).as('getDashboardUrl'); - mockGetApiEndpoints(mockedEnterpriseCluster.id).as('getApiEndpoints'); cy.visitWithLogin('/kubernetes/clusters'); - cy.wait(['@getAccount']); ui.button .findByTitle('Create Cluster') @@ -1132,16 +892,10 @@ describe('LKE Cluster Creation with LKE-E', () => { .click(); cy.url().should('endWith', '/kubernetes/create'); - cy.wait(['@getKubernetesVersions', '@getTieredKubernetesVersions']); - cy.findByLabelText('Cluster Label') - .should('be.visible') - .click() - .type(`${clusterLabel}{enter}`); - - cy.findByText('Cluster Tier').should('be.visible'); + cy.findByText('Cluster Type').should('be.visible'); - // Confirm both Cluster Tiers exist and the LKE card is selected by default + // Confirm both cluster types exist and the LKE card is selected by default cy.get(`[data-qa-select-card-heading="LKE"]`) .closest('[data-qa-selection-card]') .should('be.visible') @@ -1153,13 +907,16 @@ describe('LKE Cluster Creation with LKE-E', () => { .should('have.attr', 'data-qa-selection-card-checked', 'false') .click(); - // Select LKE-E as the Cluster Tier + // Select LKE-E as the cluster type cy.get(`[data-qa-select-card-heading="LKE Enterprise"]`) .closest('[data-qa-selection-card]') .should('be.visible') .should('have.attr', 'data-qa-selection-card-checked', 'true'); - cy.wait(['@getLKEEnterpriseClusterTypes', '@getRegions']); + // Confirm HA section is hidden since LKE-E includes HA by default + cy.findByText('HA Control Plane').should('not.exist'); + + cy.wait(['@getRegions']); // Confirm unsupported regions are not displayed ui.regionSelect.find().click().type('Newark, NJ'); @@ -1177,107 +934,11 @@ describe('LKE Cluster Creation with LKE-E', () => { ) .should('be.visible'); - // Selects an enterprise version - ui.autocomplete - .findByLabel('Kubernetes Version') - .should('be.visible') - .click(); - - ui.autocompletePopper - .findByTitle(latestEnterpriseTierKubernetesVersion.id) - .should('be.visible') - .should('be.enabled') - .click(); - - // Add a node pool for each selected plan, and confirm that the - // selected node pool plan is added to the checkout bar. - clusterPlans.forEach((clusterPlan) => { - const nodeCount = clusterPlan.nodeCount; - const planName = clusterPlan.planName; - - cy.log(`Adding ${nodeCount}x ${planName} node(s)`); - // Click the right tab for the plan, and add a node pool with the desired - // number of nodes. - cy.findByText(clusterPlan.tab).should('be.visible').click(); - const quantityInput = '[name="Quantity"]'; - cy.findByText(planName) - .should('be.visible') - .closest('tr') - .within(() => { - cy.get(quantityInput).should('be.visible'); - cy.get(quantityInput).click(); - cy.get(quantityInput).type(`{selectall}${nodeCount}`); - - ui.button - .findByTitle('Add') - .should('be.visible') - .should('be.enabled') - .click(); - }); - }); - - // Check that the checkout bar displays the correct information - cy.get('[data-testid="kube-checkout-bar"]') - .should('be.visible') - .within(() => { - // Confirm HA section is hidden since LKE-E includes HA by default - cy.findByText('High Availability (HA) Control Plane').should( - 'not.exist' - ); - - // Confirm LKE-E section is shown - cy.findByText('LKE Enterprise').should('be.visible'); - cy.findByText('HA control plane, Dedicated control plane').should( - 'be.visible' - ); - cy.findByText('$300.00/month').should('be.visible'); - - cy.findByText(`Dedicated 4 GB Plan`).should('be.visible'); - cy.findByText('$144.00').should('be.visible'); - cy.findByText(`Linode 2 GB Plan`).should('be.visible'); - cy.findByText('$15.00').should('be.visible'); - cy.findByText('$459.00').should('be.visible'); - - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Wait for LKE cluster to be created and confirm that we are redirected - // to the cluster summary page. - cy.wait([ - '@getCluster', - '@getClusterPools', - '@createCluster', - '@getLKEEnterpriseClusterTypes', - '@getLinodeTypes', - '@getDashboardUrl', - '@getApiEndpoints', - ]); - - cy.url().should( - 'endWith', - `/kubernetes/clusters/${mockedEnterpriseCluster.id}/summary` - ); - - // Confirm the LKE-E cluster has the correct enterprise chip, version, and pricing. - cy.findByText('ENTERPRISE').should('be.visible'); - cy.findByText( - `Version ${latestEnterpriseTierKubernetesVersion.id}` - ).should('be.visible'); - cy.findByText('$459.00/month').should('be.visible'); + // TODO: finish the rest of this test in subsequent PRs }); it('disables the Cluster Type selection without the LKE-E account capability', () => { - mockGetAccount( - accountFactory.build({ - capabilities: [], - }) - ).as('getAccount'); cy.visitWithLogin('/kubernetes/clusters'); - cy.wait(['@getAccount']); ui.button .findByTitle('Create Cluster') @@ -1287,8 +948,8 @@ describe('LKE Cluster Creation with LKE-E', () => { cy.url().should('endWith', '/kubernetes/create'); - // Confirm the Cluster Tier selection can be made when the LKE-E feature is enabled - cy.findByText('Cluster Tier').should('be.visible'); + // Confirm the Cluster Type selection can be made when the LKE-E feature is enabled + cy.findByText('Cluster Type').should('be.visible'); // Confirm both tiers exist and the LKE card is selected by default cy.get(`[data-qa-select-card-heading="LKE"]`) @@ -1303,25 +964,3 @@ describe('LKE Cluster Creation with LKE-E', () => { }); }); }); - -/** - * Returns each plan in an array which is similar to the given plan. - * - * Plans are considered similar if they have identical type and size. - * - * @param clusterPlan - Cluster plan with which to compare similarity. - * @param clusterPlans - Array from which to find similar cluster plans. - * - * @returns Array of similar cluster plans. - */ -const getSimilarPlans = ( - clusterPlan: LkePlanDescription, - clusterPlans: LkePlanDescription[] -) => { - return clusterPlans.filter((otherClusterPlan) => { - return ( - clusterPlan.type === otherClusterPlan.type && - clusterPlan.size === otherClusterPlan.size - ); - }); -}; diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts index e4eaa8c155b..b97a4b77baf 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts @@ -3,10 +3,6 @@ import { mockGetClusters, mockGetClusterPools, mockGetKubeconfig, - mockGetKubernetesVersions, - mockGetTieredKubernetesVersions, - mockRecycleAllNodes, - mockUpdateCluster, } from 'support/intercepts/lke'; import { accountFactory, @@ -169,186 +165,4 @@ describe('LKE landing page', () => { cy.wait('@getKubeconfig'); readDownload(mockKubeconfigFilename).should('eq', mockKubeconfigContents); }); - - it('does not show an Upgrade chip when there is no new kubernetes standard version', () => { - const oldVersion = '1.25'; - const newVersion = '1.26'; - - const cluster = kubernetesClusterFactory.build({ - k8s_version: newVersion, - }); - - mockGetClusters([cluster]).as('getClusters'); - mockGetKubernetesVersions([newVersion, oldVersion]).as('getVersions'); - - cy.visitWithLogin(`/kubernetes/clusters`); - - cy.wait(['@getClusters', '@getVersions']); - - cy.findByText(newVersion).should('be.visible'); - - cy.findByText('UPGRADE').should('not.exist'); - }); - - it('does not show an Upgrade chip when there is no new kubernetes enterprise version', () => { - const oldVersion = '1.31.1+lke1'; - const newVersion = '1.32.1+lke2'; - - mockGetAccount( - accountFactory.build({ - capabilities: ['Kubernetes Enterprise'], - }) - ).as('getAccount'); - - // TODO LKE-E: Remove once feature is in GA - mockAppendFeatureFlags({ - lkeEnterprise: { enabled: true, la: true }, - }); - - const cluster = kubernetesClusterFactory.build({ - k8s_version: newVersion, - tier: 'enterprise', - }); - - mockGetClusters([cluster]).as('getClusters'); - mockGetTieredKubernetesVersions('enterprise', [ - { id: newVersion, tier: 'enterprise' }, - { id: oldVersion, tier: 'enterprise' }, - ]).as('getTieredVersions'); - - cy.visitWithLogin(`/kubernetes/clusters`); - - cy.wait(['@getAccount', '@getClusters', '@getTieredVersions']); - - cy.findByText(newVersion).should('be.visible'); - - cy.findByText('UPGRADE').should('not.exist'); - }); - - it('can upgrade the standard kubernetes version from the landing page', () => { - const oldVersion = '1.25'; - const newVersion = '1.26'; - - const cluster = kubernetesClusterFactory.build({ - k8s_version: oldVersion, - }); - - const updatedCluster = { ...cluster, k8s_version: newVersion }; - - mockGetClusters([cluster]).as('getClusters'); - mockGetKubernetesVersions([newVersion, oldVersion]).as('getVersions'); - mockUpdateCluster(cluster.id, updatedCluster).as('updateCluster'); - mockRecycleAllNodes(cluster.id).as('recycleAllNodes'); - - cy.visitWithLogin(`/kubernetes/clusters`); - - cy.wait(['@getClusters', '@getVersions']); - - cy.findByText(oldVersion).should('be.visible'); - - cy.findByText('UPGRADE').should('be.visible').should('be.enabled').click(); - - ui.dialog - .findByTitle( - `Step 1: Upgrade ${cluster.label} to Kubernetes ${newVersion}` - ) - .should('be.visible'); - - mockGetClusters([updatedCluster]).as('getClusters'); - - ui.button - .findByTitle('Upgrade Version') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.wait(['@updateCluster', '@getClusters']); - - ui.dialog - .findByTitle('Step 2: Recycle All Cluster Nodes') - .should('be.visible'); - - ui.button - .findByTitle('Recycle All Nodes') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.wait('@recycleAllNodes'); - - ui.toast.assertMessage('Recycle started successfully.'); - - cy.findByText(newVersion).should('be.visible'); - }); - - it('can upgrade the enterprise kubernetes version from the landing page', () => { - const oldVersion = '1.31.1+lke1'; - const newVersion = '1.32.1+lke2'; - - mockGetAccount( - accountFactory.build({ - capabilities: ['Kubernetes Enterprise'], - }) - ).as('getAccount'); - - // TODO LKE-E: Remove once feature is in GA - mockAppendFeatureFlags({ - lkeEnterprise: { enabled: true, la: true }, - }); - - const cluster = kubernetesClusterFactory.build({ - k8s_version: oldVersion, - tier: 'enterprise', - }); - - const updatedCluster = { ...cluster, k8s_version: newVersion }; - - mockGetClusters([cluster]).as('getClusters'); - mockGetTieredKubernetesVersions('enterprise', [ - { id: newVersion, tier: 'enterprise' }, - { id: oldVersion, tier: 'enterprise' }, - ]).as('getTieredVersions'); - mockUpdateCluster(cluster.id, updatedCluster).as('updateCluster'); - mockRecycleAllNodes(cluster.id).as('recycleAllNodes'); - - cy.visitWithLogin(`/kubernetes/clusters`); - - cy.wait(['@getAccount', '@getClusters', '@getTieredVersions']); - - cy.findByText(oldVersion).should('be.visible'); - - cy.findByText('UPGRADE').should('be.visible').should('be.enabled').click(); - - ui.dialog - .findByTitle( - `Step 1: Upgrade ${cluster.label} to Kubernetes ${newVersion}` - ) - .should('be.visible'); - - mockGetClusters([updatedCluster]).as('getClusters'); - - ui.button - .findByTitle('Upgrade Version') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.wait(['@updateCluster', '@getClusters']); - - ui.dialog - .findByTitle('Step 2: Recycle All Cluster Nodes') - .should('be.visible'); - - ui.button - .findByTitle('Recycle All Nodes') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.wait('@recycleAllNodes'); - - ui.toast.assertMessage('Recycle started successfully.'); - - cy.findByText(newVersion).should('be.visible'); - }); }); 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 788f19e2d31..4d4f89c60cb 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -6,7 +6,6 @@ import { linodeFactory, kubernetesControlPlaneACLFactory, kubernetesControlPlaneACLOptionsFactory, - linodeTypeFactory, } from 'src/factories'; import { extendType } from 'src/utilities/extendType'; import { mockGetAccount } from 'support/intercepts/account'; @@ -25,12 +24,11 @@ import { mockRecycleAllNodes, mockGetDashboardUrl, mockGetApiEndpoints, + mockGetClusters, mockUpdateControlPlaneACL, mockGetControlPlaneACL, mockUpdateControlPlaneACLError, mockGetControlPlaneACLError, - mockGetTieredKubernetesVersions, - mockUpdateClusterError, } from 'support/intercepts/lke'; import { mockGetLinodeType, @@ -44,7 +42,6 @@ import { getRegionById } from 'support/util/regions'; import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { randomString } from 'support/util/random'; -import { buildArray } from 'support/util/arrays'; const mockNodePools = nodePoolFactory.buildList(2); @@ -136,7 +133,7 @@ describe('LKE cluster updates', () => { * - Confirms that Kubernetes upgrade prompt is shown when not up-to-date. * - Confirms that Kubernetes upgrade prompt is hidden when up-to-date. */ - it('can upgrade standard kubernetes version from the details page', () => { + it('can upgrade kubernetes version from the details page', () => { const oldVersion = '1.25'; const newVersion = '1.26'; @@ -238,132 +235,63 @@ describe('LKE cluster updates', () => { ui.toast.findByMessage('Recycle started successfully.'); }); - /* - * - Confirms UI flow of upgrading Kubernetes enterprise version using mocked API requests. - * - Confirms that Kubernetes upgrade prompt is shown when not up-to-date. - * - Confirms that Kubernetes upgrade prompt is hidden when up-to-date. - */ - it('can upgrade enterprise kubernetes version from the details page', () => { - const oldVersion = '1.31.1+lke1'; - const newVersion = '1.31.1+lke2'; - - mockGetAccount( - accountFactory.build({ - capabilities: ['Kubernetes Enterprise'], - }) - ).as('getAccount'); - - // TODO LKE-E: Remove once feature is in GA - mockAppendFeatureFlags({ - lkeEnterprise: { enabled: true, la: true }, - }); + it('can upgrade the kubernetes version from the landing page', () => { + const oldVersion = '1.25'; + const newVersion = '1.26'; - const mockCluster = kubernetesClusterFactory.build({ + const cluster = kubernetesClusterFactory.build({ k8s_version: oldVersion, - tier: 'enterprise', }); - const mockClusterUpdated = { - ...mockCluster, - k8s_version: newVersion, - }; + const updatedCluster = { ...cluster, k8s_version: newVersion }; - const upgradePrompt = - 'A new version of Kubernetes is available (1.31.1+lke2).'; + mockGetClusters([cluster]).as('getClusters'); + mockGetKubernetesVersions([newVersion, oldVersion]).as('getVersions'); + mockUpdateCluster(cluster.id, updatedCluster).as('updateCluster'); + mockRecycleAllNodes(cluster.id).as('recycleAllNodes'); - const upgradeNotes = [ - 'Once the upgrade is complete you will need to recycle all nodes in your cluster', - // Confirm that the old version and new version are both shown. - oldVersion, - newVersion, - ]; + cy.visitWithLogin(`/kubernetes/clusters`); - mockGetCluster(mockCluster).as('getCluster'); - mockGetTieredKubernetesVersions('enterprise', [ - { id: newVersion, tier: 'enterprise' }, - { id: oldVersion, tier: 'enterprise' }, - ]).as('getTieredVersions'); - mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); - mockUpdateCluster(mockCluster.id, mockClusterUpdated).as('updateCluster'); - mockGetDashboardUrl(mockCluster.id); - mockGetApiEndpoints(mockCluster.id); + cy.wait(['@getClusters', '@getVersions']); - cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); - cy.wait([ - '@getAccount', - '@getCluster', - '@getNodePools', - '@getTieredVersions', - ]); + cy.findByText(oldVersion).should('be.visible'); - // Confirm that upgrade prompt is shown. - cy.findByText(upgradePrompt).should('be.visible'); - ui.button - .findByTitle('Upgrade Version') + cy.findByText('UPGRADE') .should('be.visible') .should('be.enabled') .click(); ui.dialog .findByTitle( - `Step 1: Upgrade ${mockCluster.label} to Kubernetes ${newVersion}` + `Step 1: Upgrade ${cluster.label} to Kubernetes ${newVersion}` ) - .should('be.visible') - .within(() => { - upgradeNotes.forEach((note: string) => { - cy.findAllByText(note, { exact: false }).should('be.visible'); - }); + .should('be.visible'); - ui.button - .findByTitle('Upgrade Version') - .should('be.visible') - .should('be.enabled') - .click(); - }); + mockGetClusters([updatedCluster]).as('getClusters'); - // Wait for API response and assert toast message is shown. - cy.wait('@updateCluster'); - - // Verify the banner goes away because the version update has happened - cy.findByText(upgradePrompt).should('not.exist'); - - mockRecycleAllNodes(mockCluster.id).as('recycleAllNodes'); + ui.button + .findByTitle('Upgrade Version') + .should('be.visible') + .should('be.enabled') + .click(); - const stepTwoDialogTitle = 'Step 2: Recycle All Cluster Nodes'; + cy.wait(['@updateCluster', '@getClusters']); ui.dialog - .findByTitle(stepTwoDialogTitle) - .should('be.visible') - .within(() => { - cy.findByText('Kubernetes version has been updated successfully.', { - exact: false, - }).should('be.visible'); + .findByTitle('Step 2: Recycle All Cluster Nodes') + .should('be.visible'); - cy.findByText( - 'For the changes to take full effect you must recycle the nodes in your cluster.', - { exact: false } - ).should('be.visible'); - - ui.button - .findByTitle('Recycle All Nodes') - .should('be.visible') - .should('be.enabled') - .click(); - }); + ui.button + .findByTitle('Recycle All Nodes') + .should('be.visible') + .should('be.enabled') + .click(); - // Verify clicking the "Recycle All Nodes" makes an API call cy.wait('@recycleAllNodes'); - // Verify the upgrade dialog closed - cy.findByText(stepTwoDialogTitle).should('not.exist'); + ui.toast.assertMessage('Recycle started successfully.'); - // Verify the banner is still gone after the flow - cy.findByText(upgradePrompt).should('not.exist'); - - // Verify the version is correct after the update - cy.findByText(`Version ${newVersion}`); - - ui.toast.findByMessage('Recycle started successfully.'); + cy.findByText(newVersion).should('be.visible'); }); /* @@ -930,179 +858,6 @@ describe('LKE cluster updates', () => { .should('be.visible') .should('be.disabled'); }); - - /* - * - Confirms LKE summary page updates to reflect new cluster name. - */ - it('can rename cluster', () => { - const mockCluster = kubernetesClusterFactory.build({ - k8s_version: latestKubernetesVersion, - }); - const mockNewCluster = kubernetesClusterFactory.build({ - label: 'newClusterName', - }); - - mockGetCluster(mockCluster).as('getCluster'); - mockGetKubernetesVersions().as('getVersions'); - mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); - mockUpdateCluster(mockCluster.id, mockNewCluster).as('updateCluster'); - - cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}/summary`); - cy.wait(['@getCluster', '@getNodePools', '@getVersions']); - - // LKE clusters can be renamed by clicking on the cluster's name in the breadcrumbs towards the top of the page. - cy.get('[data-testid="editable-text"] > [data-testid="button"]').click(); - cy.findByTestId('textfield-input') - .should('be.visible') - .should('have.value', mockCluster.label) - .clear() - .type(`${mockNewCluster.label}{enter}`); - - cy.wait('@updateCluster'); - - cy.findAllByText(mockNewCluster.label).should('be.visible'); - cy.findAllByText(mockCluster.label).should('not.exist'); - }); - - /* - * - Confirms error message shows when the API request fails. - */ - it('can handle API errors when renaming cluster', () => { - const mockCluster = kubernetesClusterFactory.build({ - k8s_version: latestKubernetesVersion, - }); - const mockErrorCluster = kubernetesClusterFactory.build({ - label: 'errorClusterName', - }); - const mockErrorMessage = 'API request fails'; - - mockGetCluster(mockCluster).as('getCluster'); - mockGetKubernetesVersions().as('getVersions'); - mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); - mockUpdateClusterError(mockCluster.id, mockErrorMessage).as( - 'updateClusterError' - ); - - cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}/summary`); - cy.wait(['@getCluster', '@getNodePools', '@getVersions']); - - // LKE cluster can be renamed by clicking on the cluster's name in the breadcrumbs towards the top of the page. - cy.get('[data-testid="editable-text"] > [data-testid="button"]').click(); - cy.findByTestId('textfield-input') - .should('be.visible') - .should('have.value', mockCluster.label) - .clear() - .type(`${mockErrorCluster.label}{enter}`); - - // Error message shows when API request fails. - cy.wait('@updateClusterError'); - cy.findAllByText(mockErrorMessage).should('be.visible'); - }); - }); - - it('can add and delete node pool tags', () => { - const mockCluster = kubernetesClusterFactory.build({ - k8s_version: latestKubernetesVersion, - }); - - const mockType = linodeTypeFactory.build(); - - const mockNodePoolInstances = buildArray(3, () => - linodeFactory.build({ label: randomLabel() }) - ); - - const mockNodes = mockNodePoolInstances.map((linode, i) => - kubeLinodeFactory.build({ - id: `id-${i * 5000}`, - instance_id: linode.id, - status: 'ready', - }) - ); - - const mockNodePoolNoTags = nodePoolFactory.build({ - id: 1, - type: mockType.id, - nodes: mockNodes, - }); - - const mockNodePoolWithTags = { - ...mockNodePoolNoTags, - tags: ['test-tag'], - }; - - mockGetLinodes(mockNodePoolInstances); - mockGetLinodeType(linodeTypeFactory.build({ id: mockType.id })).as( - 'getType' - ); - mockGetCluster(mockCluster).as('getCluster'); - mockGetClusterPools(mockCluster.id, [mockNodePoolNoTags]).as( - 'getNodePoolsNoTags' - ); - mockGetKubernetesVersions().as('getVersions'); - mockGetControlPlaneACL(mockCluster.id, { acl: { enabled: false } }).as( - 'getControlPlaneAcl' - ); - mockUpdateNodePool(mockCluster.id, mockNodePoolWithTags).as('addTag'); - mockGetDashboardUrl(mockCluster.id); - mockGetApiEndpoints(mockCluster.id); - - cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); - cy.wait([ - '@getCluster', - '@getNodePoolsNoTags', - '@getVersions', - '@getType', - '@getControlPlaneAcl', - ]); - - // Confirm that Linode instance info has finished loading before attempting - // to interact with the tag button. - mockNodePoolInstances.forEach((linode) => { - cy.findByText(linode.label) - .should('be.visible') - .closest('tr') - .within(() => { - cy.findByText('Running').should('be.visible'); - }); - }); - - cy.get(`[data-qa-node-pool-id="${mockNodePoolNoTags.id}"]`).within(() => { - ui.button.findByTitle('Add a tag').should('be.visible').click(); - - cy.findByLabelText('Create or Select a Tag') - .should('be.visible') - .type(`${mockNodePoolWithTags.tags[0]}`); - - ui.autocompletePopper - .findByTitle(`Create "${mockNodePoolWithTags.tags[0]}"`) - .scrollIntoView() - .should('be.visible') - .click(); - }); - - mockGetClusterPools(mockCluster.id, [mockNodePoolWithTags]).as( - 'getNodePoolsWithTags' - ); - - cy.wait(['@addTag', '@getNodePoolsWithTags']); - - mockUpdateNodePool(mockCluster.id, mockNodePoolNoTags).as('deleteTag'); - mockGetClusterPools(mockCluster.id, [mockNodePoolNoTags]).as( - 'getNodePoolsNoTags' - ); - - // Delete the newly added node pool tag. - cy.get(`[data-qa-tag="${mockNodePoolWithTags.tags[0]}"]`) - .should('be.visible') - .within(() => { - cy.get('[data-qa-delete-tag="true"]').should('be.visible').click(); - }); - - cy.wait(['@deleteTag', '@getNodePoolsNoTags']); - - cy.get(`[data-qa-tag="${mockNodePoolWithTags.tags[0]}"]`).should( - 'not.exist' - ); }); describe('LKE cluster updates for DC-specific prices', () => { diff --git a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts index a93a904c248..3f6518e2bec 100644 --- a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts @@ -28,7 +28,6 @@ import { dcPricingMockLinodeTypesForBackups } from 'support/constants/dc-specifi import { chooseRegion } from 'support/util/regions'; import { expectManagedDisabled } from 'support/api/managed'; import { createTestLinode } from 'support/util/linodes'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; const BackupsCancellationNote = 'Once backups for this Linode have been canceled, you cannot re-enable them for 24 hours.'; @@ -77,9 +76,7 @@ describe('linode backups', () => { cy.wait('@getLinode'); // Wait for Linode to finish provisioning. - cy.findByText('OFFLINE', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.findByText('OFFLINE').should('be.visible'); // Confirm that enable backups prompt is shown. cy.contains( @@ -187,9 +184,7 @@ describe('linode backups', () => { cy.wait('@getLinode'); // Wait for the Linode to finish provisioning. - cy.findByText('OFFLINE', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.findByText('OFFLINE').should('be.visible'); cy.findByText('Manual Snapshot') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts index e15151274c0..0ae05e81ecc 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts @@ -20,7 +20,7 @@ describe('Create Linode flow to validate code snippet modal', () => { // Set Linode label, distribution, plan type, password, etc. linodeCreatePage.setLabel(linodeLabel); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById('us-east'); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(rootPass); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts index 2b6399144be..c6d8befe082 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts @@ -29,7 +29,7 @@ describe('Create Linode with Add-ons', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -83,7 +83,7 @@ describe('Create Linode with Add-ons', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); 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 daf52a7b707..70a759c140a 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 @@ -45,7 +45,7 @@ describe('Create Linode with Firewall', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -111,7 +111,7 @@ describe('Create Linode with Firewall', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -218,7 +218,7 @@ describe('Create Linode with Firewall', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts index dd9b1fa1153..b613f8cf384 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts @@ -41,7 +41,7 @@ describe('Create Linode with SSH Key', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -107,7 +107,7 @@ describe('Create Linode with SSH Key', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index 009aa74305d..8951ed66e9b 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -34,7 +34,7 @@ describe('Create Linode with user data', () => { // Fill out create form, selecting a region and image that both have // cloud-init capabilities. linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -88,7 +88,7 @@ describe('Create Linode with user data', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(mockLinodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); 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 270b9de1072..d80d2a963b9 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 @@ -42,7 +42,7 @@ describe('Create Linode with VLANs', () => { // Fill out necessary Linode create fields. linodeCreatePage.selectRegionById(mockLinodeRegion.id); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.setLabel(mockLinode.label); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -130,7 +130,7 @@ describe('Create Linode with VLANs', () => { // Fill out necessary Linode create fields. linodeCreatePage.selectRegionById(mockLinodeRegion.id); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.setLabel(mockLinode.label); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); 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 08842e8f196..f0746146d43 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 @@ -3,10 +3,7 @@ import { regionFactory, subnetFactory, vpcFactory, - linodeConfigFactory, - LinodeConfigInterfaceFactoryWithVPC, } from 'src/factories'; -import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockCreateLinode, mockGetLinodeDetails, @@ -15,7 +12,6 @@ import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreateVPC, mockCreateVPCError, - mockGetSubnets, mockGetVPC, mockGetVPCs, } from 'support/intercepts/vpc'; @@ -29,14 +25,12 @@ import { randomString, } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; describe('Create Linode with VPCs', () => { /* * - Confirms UI flow to create a Linode with an existing VPC assigned using mock API data. * - Confirms that VPC assignment is reflected in create summary section. * - Confirms that outgoing API request contains expected VPC interface data. - * - Confirms newly assigned Linode does not have an unrecommended config notice inside VPC */ it('can assign existing VPCs during Linode Create flow', () => { const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); @@ -61,27 +55,6 @@ describe('Create Linode with VPCs', () => { region: linodeRegion.id, }); - const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ - vpc_id: mockVPC.id, - subnet_id: mockSubnet.id, - primary: true, - active: true, - }); - - const mockLinodeConfig = linodeConfigFactory.build({ - interfaces: [mockInterface], - }); - - const mockUpdatedSubnet = { - ...mockSubnet, - linodes: [ - { - id: mockLinode.id, - interfaces: [{ id: mockInterface.id, active: true }], - }, - ], - }; - mockGetVPCs([mockVPC]).as('getVPCs'); mockGetVPC(mockVPC).as('getVPC'); mockCreateLinode(mockLinode).as('createLinode'); @@ -90,7 +63,7 @@ describe('Create Linode with VPCs', () => { cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -132,27 +105,12 @@ describe('Create Linode with VPCs', () => { expect(expectedVpcInterface['ipv4']).to.be.an('object').that.is.empty; expect(expectedVpcInterface['subnet_id']).to.equal(mockSubnet.id); expect(expectedVpcInterface['purpose']).to.equal('vpc'); - // Confirm that VPC interfaces are always marked as the primary interface - expect(expectedVpcInterface['primary']).to.equal(true); }); // Confirm redirect to new Linode. cy.url().should('endWith', `/linodes/${mockLinode.id}`); // Confirm toast notification should appear on Linode create. ui.toast.assertMessage(`Your Linode ${mockLinode.label} is being created.`); - - // Confirm newly created Linode does not have unrecommended configuration notice - mockGetVPC(mockVPC).as('getVPC'); - mockGetSubnets(mockVPC.id, [mockUpdatedSubnet]).as('getSubnets'); - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( - 'getLinodeConfigs' - ); - - cy.visit(`/vpcs/${mockVPC.id}`); - cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfigs'); - cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); }); /* @@ -160,7 +118,6 @@ describe('Create Linode with VPCs', () => { * - Creates a VPC and a subnet from within the Linode Create flow. * - Confirms that Cloud responds gracefully when VPC create API request fails. * - Confirms that outgoing API request contains correct VPC interface data. - * - Confirms newly assigned Linode does not have an unrecommended config notice inside VPC */ it('can assign new VPCs during Linode Create flow', () => { const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); @@ -188,33 +145,12 @@ describe('Create Linode with VPCs', () => { region: linodeRegion.id, }); - const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ - vpc_id: mockVPC.id, - subnet_id: mockSubnet.id, - primary: true, - active: true, - }); - - const mockLinodeConfig = linodeConfigFactory.build({ - interfaces: [mockInterface], - }); - - const mockUpdatedSubnet = { - ...mockSubnet, - linodes: [ - { - id: mockLinode.id, - interfaces: [{ id: mockInterface.id, active: true }], - }, - ], - }; - mockGetVPCs([]); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -296,26 +232,11 @@ describe('Create Linode with VPCs', () => { expect(expectedVpcInterface['ipv4']).to.deep.equal({ nat_1_1: 'any' }); expect(expectedVpcInterface['subnet_id']).to.equal(mockSubnet.id); expect(expectedVpcInterface['purpose']).to.equal('vpc'); - // Confirm that VPC interfaces are always marked as the primary interface - expect(expectedVpcInterface['primary']).to.equal(true); }); cy.url().should('endWith', `/linodes/${mockLinode.id}`); // Confirm toast notification should appear on Linode create. ui.toast.assertMessage(`Your Linode ${mockLinode.label} is being created.`); - - // Confirm newly created Linode does not have unrecommended configuration notice - mockGetVPC(mockVPC).as('getVPC'); - mockGetSubnets(mockVPC.id, [mockUpdatedSubnet]).as('getSubnets'); - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( - 'getLinodeConfigs' - ); - - cy.visit(`/vpcs/${mockVPC.id}`); - cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfigs'); - cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); }); /* 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 37e5309715d..fbe81f6b9c6 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -104,7 +104,7 @@ describe('Create Linode', () => { // Set Linode label, OS, plan type, password, etc. linodeCreatePage.setLabel(linodeLabel); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan( planConfig.planType, @@ -116,7 +116,7 @@ describe('Create Linode', () => { cy.get('[data-qa-linode-create-summary]') .scrollIntoView() .within(() => { - cy.findByText('Debian 12').should('be.visible'); + cy.findByText('Debian 11').should('be.visible'); cy.findByText(linodeRegion.label).should('be.visible'); cy.findByText(planConfig.planLabel).should('be.visible'); }); @@ -230,7 +230,7 @@ describe('Create Linode', () => { // Set Linode label, OS, plan type, password, etc. linodeCreatePage.setLabel(linodeLabel); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Accelerated', mockAcceleratedType[0].label); linodeCreatePage.setRootPassword(randomString(32)); @@ -239,7 +239,7 @@ describe('Create Linode', () => { cy.get('[data-qa-linode-create-summary]') .scrollIntoView() .within(() => { - cy.findByText('Debian 12').should('be.visible'); + cy.findByText('Debian 11').should('be.visible'); cy.findByText(`US, ${linodeRegion.label}`).should('be.visible'); cy.findByText(mockAcceleratedType[0].label).should('be.visible'); }); @@ -363,17 +363,13 @@ describe('Create Linode', () => { // intercept request cy.visitWithLogin('/linodes/create'); - cy.wait('@getLinodeTypes'); + cy.wait(['@getLinodeTypes', '@getVPCs']); cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); // Check the 'Backups' add on cy.get('[data-testid="backups"]').should('be.visible').click(); ui.regionSelect.find().click().type(`${region.label} {enter}`); - - // Verify VPCs get fetched once a region is selected - cy.wait('@getVPCs'); - fbtClick('Shared CPU'); getClick(`[id="${dcPricingMockLinodeTypes[0].id}"]`); @@ -462,7 +458,7 @@ describe('Create Linode', () => { // Set Linode label, OS, plan type, password, etc. linodeCreatePage.setLabel(linodeLabel); - linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); @@ -491,25 +487,4 @@ describe('Create Linode', () => { // Confirm the createLinodeErrorMessage disappears. cy.findByText(`${createLinodeErrorMessage}`).should('not.exist'); }); - - it('shows correct validation errors if no backup or plan is selected', () => { - cy.visitWithLogin('/linodes/create'); - - // Navigate to Linode Create page "Backups" tab - cy.get('[role="tablist"]') - .should('be.visible') - .findByText('Backups') - .click(); - - // Submit without selecting any options - ui.button - .findByTitle('Create Linode') - .should('be.visible') - .should('be.enabled') - .click(); - - // Confirm the correct validation errors show up on the page. - cy.findByText('You must select a Backup.').should('be.visible'); - cy.findByText('Plan is required.').should('be.visible'); - }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts index 465afc41c42..d17336e1110 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts @@ -1,7 +1,5 @@ import { linodeFactory, ipAddressFactory } from '@src/factories'; -import type { IPRange } from '@linode/api-v4'; - import { mockGetLinodeDetails, mockGetLinodeIPAddresses, @@ -11,31 +9,19 @@ import { mockUpdateIPAddress } from 'support/intercepts/networking'; import { ui } from 'support/ui'; describe('linode networking', () => { - const mockLinode = linodeFactory.build(); - const linodeIPv4 = mockLinode.ipv4[0]; - const mockRDNS = `${linodeIPv4}.ip.linodeusercontent.com`; - const ipAddress = ipAddressFactory.build({ - address: linodeIPv4, - linode_id: mockLinode.id, - rdns: mockRDNS, - }); - const _ipv6Range: IPRange = { - prefix: 64, - range: '2fff:db08:e003:1::', - region: 'us-east', - route_target: '2600:3c02::f03c:92ff:fe9d:0f25', - }; - const ipv6Range = `${_ipv6Range.range}/${_ipv6Range.prefix}`; - const ipv6Address = ipAddressFactory.build({ - address: mockLinode.ipv6 ?? '2600:3c00::f03c:92ff:fee2:6c40/64', - gateway: 'fe80::1', - linode_id: mockLinode.id, - prefix: 64, - subnet_mask: 'ffff:ffff:ffff:ffff::', - type: 'ipv6', - }); + /** + * - Confirms the success toast message after editing RDNS + */ + it('checks for the toast message upon editing an RDNS', () => { + const mockLinode = linodeFactory.build(); + const linodeIPv4 = mockLinode.ipv4[0]; + const mockRDNS = `${linodeIPv4}.ip.linodeusercontent.com`; + const ipAddress = ipAddressFactory.build({ + address: linodeIPv4, + linode_id: mockLinode.id, + rdns: mockRDNS, + }); - beforeEach(() => { mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); mockGetLinodeFirewalls(mockLinode.id, []).as('getLinodeFirewalls'); mockGetLinodeIPAddresses(mockLinode.id, { @@ -45,22 +31,12 @@ describe('linode networking', () => { shared: [], reserved: [], }, - ipv6: { - global: [_ipv6Range], - link_local: ipv6Address, - slaac: ipv6Address, - }, }).as('getLinodeIPAddresses'); mockUpdateIPAddress(linodeIPv4, mockRDNS).as('updateIPAddress'); cy.visitWithLogin(`linodes/${mockLinode.id}/networking`); cy.wait(['@getLinode', '@getLinodeFirewalls', '@getLinodeIPAddresses']); - }); - /** - * - Confirms the success toast message after editing RDNS - */ - it('checks for the toast message upon editing an RDNS', () => { cy.findByLabelText('IPv4 Addresses') .should('be.visible') .within(() => { @@ -104,31 +80,4 @@ describe('linode networking', () => { // confirm RDNS toast message ui.toast.assertMessage(`Successfully updated RDNS for ${linodeIPv4}`); }); - - it('validates the action menu title (aria-label) for the IP address in the table row', () => { - // Set the viewport to 1279px x 800px (width < 1280px) to ensure the Action menu is visible. - cy.viewport(1279, 800); - - // Ensure the action menu has the correct aria-label for the IP address. - cy.get(`[data-qa-ip="${linodeIPv4}"]`) - .should('be.visible') - .closest('tr') - .within(() => { - cy.findByText('IPv4 – Public').should('be.visible'); - ui.actionMenu - .findByTitle(`Action menu for IP Address ${linodeIPv4}`) - .should('be.visible'); - }); - - // Ensure the action menu has the correct aria-label for the IP Range. - cy.get(`[data-qa-ip="${ipv6Range}"]`) - .should('be.visible') - .closest('tr') - .within(() => { - cy.findByText('IPv6 – Range').should('be.visible'); - ui.actionMenu - .findByTitle(`Action menu for IP Address ${_ipv6Range.range}`) - .should('be.visible'); - }); - }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts index 79268f5d555..88ff834b419 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -208,7 +208,7 @@ describe('linode storage tab', () => { * - Confirms that Cloud Manager UI automatically updates to reflect resize. */ it('resize disk', () => { - const diskName = 'Debian 12 Disk'; + const diskName = 'Debian 10 Disk'; cy.defer(() => createTestLinode({ image: null }, { securityMethod: 'powered_off' }) ).then((linode: Linode) => { diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index 296ac8ae6b3..157e43dda05 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -13,7 +13,6 @@ import { mockRebuildLinodeError, } from 'support/intercepts/linodes'; import { createTestLinode } from 'support/util/linodes'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; /** * Creates a Linode and StackScript. @@ -127,9 +126,7 @@ describe('rebuild linode', () => { interceptRebuildLinode(linode.id).as('linodeRebuild'); cy.visitWithLogin(`/linodes/${linode.id}`); - cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.findByText('RUNNING').should('be.visible'); openRebuildDialog(linode.label); findRebuildDialog(linode.label).within(() => { @@ -185,9 +182,7 @@ describe('rebuild linode', () => { interceptRebuildLinode(linode.id).as('linodeRebuild'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin(`/linodes/${linode.id}`); - cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.findByText('RUNNING').should('be.visible'); openRebuildDialog(linode.label); findRebuildDialog(linode.label).within(() => { @@ -199,7 +194,6 @@ describe('rebuild linode', () => { cy.wait('@getStackScripts'); cy.findByLabelText('Search by Label, Username, or Description') - .scrollIntoView() .should('be.visible') .type(`${stackScriptName}`); @@ -261,9 +255,7 @@ describe('rebuild linode', () => { ).then(([stackScript, linode]) => { interceptRebuildLinode(linode.id).as('linodeRebuild'); cy.visitWithLogin(`/linodes/${linode.id}`); - cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.findByText('RUNNING').should('be.visible'); openRebuildDialog(linode.label); findRebuildDialog(linode.label).within(() => { @@ -274,7 +266,6 @@ describe('rebuild linode', () => { .click(); cy.findByLabelText('Search by Label, Username, or Description') - .scrollIntoView() .should('be.visible') .type(`${stackScript.label}`); diff --git a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts index 636dc15a9c9..07e1c262ff0 100644 --- a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts @@ -11,7 +11,6 @@ import { } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; @@ -64,9 +63,7 @@ describe('Rescue Linodes', () => { cy.wait('@getLinode'); // Wait for Linode to boot. - cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.findByText('RUNNING').should('be.visible'); // Open rescue dialog using action menu.. ui.actionMenu diff --git a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts index 4242ba01f9d..59137de9022 100644 --- a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts @@ -1,9 +1,9 @@ import { createTestLinode } from 'support/util/linodes'; +import { containsVisible, fbtVisible, getClick } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { authenticate } from 'support/api/authentication'; import { interceptLinodeResize } from 'support/intercepts/linodes'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; authenticate(); describe('resize linode', () => { @@ -22,34 +22,15 @@ describe('resize linode', () => { ).then((linode) => { interceptLinodeResize(linode.id).as('linodeResize'); cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); - - ui.dialog - .findByTitle(`Resize Linode ${linode.label}`) - .should('be.visible') - .within(() => { - // Click "Shared CPU" plan tab, and select 8 GB plan. - ui.tabList.findTabByTitle('Shared CPU').should('be.visible').click(); - - cy.contains('Linode 8 GB').should('be.visible').click(); - - // Select warm resize option, and enter Linode label in type-to-confirm field. - cy.findByText('Warm resize') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.findByLabelText('Linode Label').type(linode.label); - - // Click "Resize Linode". - // The Resize Linode button remains disabled while the Linode is provisioning, - // so we have to wait for that to complete before the button becomes enabled. - ui.button - .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) - .click(); - }); - + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 8 GB'); + getClick('[id="g6-standard-4"]'); + cy.get('[data-qa-radio="warm"]').find('input').should('be.checked'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); cy.wait('@linodeResize'); + + // TODO: Unified Migration: [M3-7115] - Replace with copy from API '../notifications.py' cy.contains( "Your linode will be warm resized and will automatically attempt to power off and restore to it's previous state." ).should('be.visible'); @@ -66,32 +47,16 @@ describe('resize linode', () => { ).then((linode) => { interceptLinodeResize(linode.id).as('linodeResize'); cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); - - ui.dialog - .findByTitle(`Resize Linode ${linode.label}`) - .should('be.visible') - .within(() => { - ui.tabList.findTabByTitle('Shared CPU').should('be.visible').click(); - - cy.contains('Linode 8 GB').should('be.visible').click(); - - cy.findByText('Cold resize') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.findByLabelText('Linode Label').type(linode.label); - - // Click "Resize Linode". - // The Resize Linode button remains disabled while the Linode is provisioning, - // so we have to wait for that to complete before the button becomes enabled. - ui.button - .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) - .click(); - }); - + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 8 GB'); + getClick('[id="g6-standard-4"]'); + cy.get('[data-qa-radio="cold"]').click(); + cy.get('[data-qa-radio="cold"]').find('input').should('be.checked'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); cy.wait('@linodeResize'); + + // TODO: Unified Migration: [M3-7115] - Replace with copy from API '../notifications.py' cy.contains( 'Your Linode will soon be automatically powered off, migrated, and restored to its previous state (booted or powered off).' ).should('be.visible'); @@ -104,58 +69,49 @@ describe('resize linode', () => { // when attempting to interact with it shortly after booting up when the // Linode is attached to a Cloud Firewall. cy.defer(() => - createTestLinode( - { booted: false }, - { securityMethod: 'vlan_no_internet' } - ) + createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) ).then((linode) => { - interceptLinodeResize(linode.id).as('linodeResize'); cy.visitWithLogin(`/linodes/${linode.id}`); - cy.findByText('OFFLINE', { timeout: LINODE_CREATE_TIMEOUT }); - - ui.actionMenu - .findByTitle(`Action menu for Linode ${linode.label}`) - .click(); - ui.actionMenuItem - .findByTitle('Resize') - .should('be.visible') - .should('be.enabled') - .click(); + // Turn off the linode to resize the disk + ui.button.findByTitle('Power Off').should('be.visible').click(); ui.dialog - .findByTitle(`Resize Linode ${linode.label}`) + .findByTitle(`Power Off Linode ${linode.label}?`) .should('be.visible') - .within(() => { - ui.tabList.findTabByTitle('Shared CPU').should('be.visible').click(); - - cy.contains('Linode 8 GB').should('be.visible').click(); - - // When a Linode is powered off, only cold resizes are available. - // Confirm that the UI reflects this by ensuring the cold resize - // option is checked and both radio buttons are disabled. - cy.findByLabelText('Warm resize', { exact: false }) - .should('be.disabled') - .should('not.be.checked'); - - cy.findByLabelText('Cold resize') - .should('be.disabled') - .should('be.checked'); - - // Enter Linode label in type-to-confirm field and proceed with resize. - cy.findByLabelText('Linode Label').type(linode.label); - - ui.button.findByTitle('Resize Linode').should('be.enabled').click(); + .then(() => { + ui.button + .findByTitle(`Power Off Linode`) + .should('be.visible') + .click(); }); + containsVisible('OFFLINE'); + + interceptLinodeResize(linode.id).as('linodeResize'); + cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 8 GB'); + getClick('[id="g6-standard-4"]'); + // We disable the options if the linode is offline, and proceed with a + // cold migration even though warm is selected by default. + cy.get('[data-qa-radio="warm"]').find('input').should('be.disabled'); + cy.get('[data-qa-radio="cold"]') + .find('input') + .should('be.checked') + .should('be.disabled'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); cy.wait('@linodeResize'); + + // TODO: Unified Migration: [M3-7115] - Replace with copy from API '../notifications.py' cy.contains( 'Your Linode will soon be automatically powered off, migrated, and restored to its previous state (booted or powered off).' ).should('be.visible'); }); }); - it.only('resizes a linode by decreasing size', () => { + it('resizes a linode by decreasing size', () => { // Use `vlan_no_internet` security method. // This works around an issue where the Linode API responds with a 400 // when attempting to interact with it shortly after booting up when the @@ -173,28 +129,13 @@ describe('resize linode', () => { // resizing the disk to the requested size first. interceptLinodeResize(linode.id).as('linodeResize'); cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); - - ui.dialog - .findByTitle(`Resize Linode ${linode.label}`) - .should('be.visible') - .within(() => { - ui.tabList.findTabByTitle('Shared CPU').should('be.visible').click(); - - cy.contains('Linode 2 GB').should('be.visible').click(); - cy.findByLabelText('Linode Label').type(linode.label); - - // Click "Resize Linode". - // The Resize Linode button remains disabled while the Linode is provisioning, - // so we have to wait for that to complete before the button becomes enabled. - ui.button - .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) - .click(); - }); - - // Confirm that API responds with an error message when attempting to - // decrease the size of the Linode while its disk is too large. + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 2 GB'); + getClick('[id="g6-standard-1"]'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); cy.wait('@linodeResize'); + // Failed to reduce the size of the linode cy.contains( 'The current disk size of your Linode is too large for the new service plan. Please resize your disk to accommodate the new plan. You can read our Resize Your Linode guide for more detailed instructions.' ) @@ -203,9 +144,9 @@ describe('resize linode', () => { // Normal flow when resizing a linode to a smaller size after first resizing // its disk. - cy.visitWithLogin(`/linodes/${linode.id}/storage`); + cy.visitWithLogin(`/linodes/${linode.id}`); - // Power off the Linode to resize the disk + // Turn off the linode to resize the disk ui.button.findByTitle('Power Off').should('be.visible').click(); ui.dialog @@ -218,28 +159,20 @@ describe('resize linode', () => { .click(); }); - // Wait for Linode to power off, then resize the disk to 50 GB. - cy.findByText('OFFLINE', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); - cy.findByText(diskName) - .should('be.visible') - .closest('tr') - .within(() => { - ui.button - .findByTitle('Resize') - .should('be.visible') - .should('be.enabled') - .click(); - }); + containsVisible('OFFLINE'); + + cy.visitWithLogin(`linodes/${linode.id}/storage`); + fbtVisible(diskName); + + cy.get(`[data-qa-disk="${diskName}"]`).within(() => { + cy.contains('Resize').should('be.enabled').click(); + }); ui.drawer .findByTitle(`Resize ${diskName}`) .should('be.visible') .within(() => { - cy.contains('Size (required)').should('be.visible').click(); - - cy.focused().clear().type(size); + cy.get('[id="size"]').should('be.visible').click().clear().type(size); ui.buttonGroup .findButtonByTitle('Resize') @@ -248,41 +181,18 @@ describe('resize linode', () => { .click(); }); - // Wait until the disk resize is done, then initiate another resize attempt. + // Wait until the disk resize is done. ui.toast.assertMessage( `Disk ${diskName} on Linode ${linode.label} has been resized.` ); - ui.actionMenu - .findByTitle(`Action menu for Linode ${linode.label}`) - .click(); - - ui.actionMenuItem - .findByTitle('Resize') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.drawer - .findByTitle(`Resize Linode ${linode.label}`) - .should('be.visible') - .within(() => { - ui.tabList.findTabByTitle('Shared CPU').should('be.visible').click(); - - cy.contains('Linode 2 GB').should('be.visible').click(); - cy.findByLabelText('Linode Label').type(linode.label); - - // Click "Resize Linode". - // The Resize Linode button remains disabled while the Linode is provisioning, - // so we have to wait for that to complete before the button becomes enabled. - ui.button - .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) - .click(); - }); - - // Confirm that the resize API request succeeds now that the Linode's disk - // size has been decreased. + interceptLinodeResize(linode.id).as('linodeResize'); + cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 2 GB'); + getClick('[id="g6-standard-1"]'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); cy.wait('@linodeResize'); cy.contains( 'Your Linode will soon be automatically powered off, migrated, and restored to its previous state (booted or powered off).' diff --git a/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts b/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts index fff6fce7576..dcc0b7c133a 100644 --- a/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts @@ -15,30 +15,30 @@ describe('Search Linodes', () => { * - Confirm that linodes are searchable and filtered in the UI. */ it('create a linode and make sure it shows up in the table and is searchable in main search tool', () => { - cy.defer(() => - createTestLinode({ booted: true }, { waitForBoot: true }) - ).then((linode: Linode) => { - cy.visitWithLogin('/linodes'); - cy.get(`[data-qa-linode="${linode.label}"]`) - .should('be.visible') - .within(() => { - cy.contains('Running').should('be.visible'); - }); + cy.defer(() => createTestLinode({ booted: true })).then( + (linode: Linode) => { + cy.visitWithLogin('/linodes'); + cy.get(`[data-qa-linode="${linode.label}"]`) + .should('be.visible') + .within(() => { + cy.contains('Running').should('be.visible'); + }); - // Confirm that linode is listed on the landing page. - cy.findByText(linode.label).should('be.visible'); + // Confirm that linode is listed on the landing page. + cy.findByText(linode.label).should('be.visible'); - // Use the main search bar to search and filter linode by label - cy.get('[id="main-search"').type(linode.label); - ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); + // Use the main search bar to search and filter linode by label + cy.get('[id="main-search"').type(linode.label); + ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); - // Use the main search bar to search and filter linode by id value - cy.get('[id="main-search"').clear().type(`${linode.id}`); - ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); + // Use the main search bar to search and filter linode by id value + cy.get('[id="main-search"').clear().type(`${linode.id}`); + ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); - // Use the main search bar to search and filter linode by id: pattern - cy.get('[id="main-search"').clear().type(`id:${linode.id}`); - ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); - }); + // Use the main search bar to search and filter linode by id: pattern + cy.get('[id="main-search"').clear().type(`id:${linode.id}`); + ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); + } + ); }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts index 173a1fe4ec3..c0e03a30ea6 100644 --- a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts @@ -1,13 +1,12 @@ import { ui } from 'support/ui'; -import { authenticate } from 'support/api/authentication'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { cleanUp } from 'support/util/cleanup'; +import { authenticate } from 'support/api/authentication'; import { createTestLinode } from 'support/util/linodes'; import type { Linode } from '@linode/api-v4'; authenticate(); describe('switch linode state', () => { - before(() => { + beforeEach(() => { cleanUp(['linodes']); cy.tag('method:e2e'); }); @@ -30,9 +29,7 @@ describe('switch linode state', () => { cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - cy.contains('Running', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('Running').should('be.visible'); }); ui.actionMenu @@ -76,9 +73,7 @@ describe('switch linode state', () => { createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) ).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); - cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('RUNNING').should('be.visible'); cy.findByText(linode.label).should('be.visible'); cy.findByText('Power Off').should('be.visible').click(); @@ -110,9 +105,7 @@ describe('switch linode state', () => { cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - cy.contains('Offline', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('Offline').should('be.visible'); }); ui.actionMenu @@ -137,9 +130,7 @@ describe('switch linode state', () => { .should('be.visible') .within(() => { cy.contains('Booting').should('be.visible'); - cy.contains('Running', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('Running', { timeout: 300000 }).should('be.visible'); }); } ); @@ -155,9 +146,7 @@ describe('switch linode state', () => { cy.defer(() => createTestLinode({ booted: false })).then( (linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); - cy.contains('OFFLINE', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('OFFLINE').should('be.visible'); cy.findByText(linode.label).should('be.visible'); cy.findByText('Power On').should('be.visible').click(); @@ -195,9 +184,7 @@ describe('switch linode state', () => { cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - cy.contains('Running', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('Running').should('be.visible'); }); ui.actionMenu @@ -241,9 +228,7 @@ describe('switch linode state', () => { createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) ).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); - cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('RUNNING').should('be.visible'); cy.findByText(linode.label).should('be.visible'); cy.findByText('Reboot').should('be.visible').click(); diff --git a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts index 648b6516da4..08c14da2a33 100644 --- a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts @@ -1,7 +1,6 @@ import { createTestLinode } from 'support/util/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { authenticate } from 'support/api/authentication'; import { randomLabel } from 'support/util/random'; @@ -16,9 +15,7 @@ describe('update linode label', () => { cy.defer(() => createTestLinode({ booted: true })).then((linode) => { const newLinodeLabel = randomLabel(); cy.visitWithLogin(`/linodes/${linode.id}`); - cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('RUNNING').should('be.visible'); cy.get(`[aria-label="Edit ${linode.label}"]`).click(); cy.get(`[id="edit-${linode.label}-label"]`) @@ -35,9 +32,7 @@ describe('update linode label', () => { cy.defer(() => createTestLinode({ booted: true })).then((linode) => { const newLinodeLabel = randomLabel(); cy.visitWithLogin(`/linodes/${linode.id}`); - cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); + cy.contains('RUNNING').should('be.visible'); cy.visitWithLogin(`/linodes/${linode.id}/settings`); cy.get('[id="label"]').click().clear().type(`${newLinodeLabel}{enter}`); diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts index a8bf39db270..0c29d1aecd8 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts @@ -5,6 +5,8 @@ import { mockGetObjectStorageEndpoints, mockGetBucketAccess, } from 'support/intercepts/object-storage'; +import { checkRateLimitsTable } from 'support/util/object-storage-gen2'; +import { ui } from 'support/ui'; import { accountFactory, objectStorageBucketFactoryGen2, @@ -141,4 +143,96 @@ describe('Object Storage Gen 2 bucket details tabs', () => { }); }); }); + + describe('Properties tab', () => { + ['E0', 'E1'].forEach((endpoint: ObjectStorageEndpointTypes) => { + /** + * Parameterized test for buckets with endpoint types of E0 and E1 + * - Confirms the Properties tab is visible + * - Confirms there is no bucket rate limits table + */ + it(`confirms the Properties tab does not have a rate limits table for buckets with endpoint type ${endpoint}`, () => { + const { mockBucket, mockEndpoint } = createMocksBasedOnEndpointType( + endpoint + ); + const { cluster, label } = mockBucket; + + mockGetBucketsForRegion(mockRegion.id, [mockBucket]).as( + 'getBucketsForRegion' + ); + mockGetObjectStorageEndpoints([mockEndpoint]).as( + 'getObjectStorageEndpoints' + ); + + cy.visitWithLogin( + `/object-storage/buckets/${cluster}/${label}/properties` + ); + cy.wait([ + '@getFeatureFlags', + '@getAccount', + '@getObjectStorageEndpoints', + '@getBucketsForRegion', + ]); + + cy.findByText('Bucket Rate Limits').should('be.visible'); + // confirms helper text + cy.contains( + 'This endpoint type supports up to 750 Requests Per Second (RPS). Understand bucket rate limits' + ).should('be.visible'); + + // confirm bucket rate limit table should not exist + cy.get('[data-testid="bucket-rate-limit-table"]').should('not.exist'); + + // confirm 'Save' button is disabled (for now) + ui.button + .findByAttribute('label', 'Save') + .should('be.visible') + .should('be.disabled'); + }); + }); + + ['E2', 'E3'].forEach((endpoint: ObjectStorageEndpointTypes) => { + /** + * Parameterized test for buckets with endpoint types of E2 and E3 + * - Confirms the Properties tab is visible + * - Confirms bucket rates limit table exists + */ + it(`confirms the Properties tab and rate limits table for buckets with endpoint type ${endpoint}`, () => { + const { mockBucket, mockEndpoint } = createMocksBasedOnEndpointType( + endpoint + ); + const { cluster, label } = mockBucket; + + mockGetBucketsForRegion(mockRegion.id, [mockBucket]).as( + 'getBucketsForRegion' + ); + mockGetObjectStorageEndpoints([mockEndpoint]).as( + 'getObjectStorageEndpoints' + ); + + cy.visitWithLogin( + `/object-storage/buckets/${cluster}/${label}/properties` + ); + cy.wait([ + '@getFeatureFlags', + '@getAccount', + '@getObjectStorageEndpoints', + '@getBucketsForRegion', + ]); + + cy.findByText('Bucket Rate Limits'); + cy.contains( + 'Specifies the maximum Requests Per Second (RPS) for a bucket. To increase it to High, open a support ticket. Understand bucket rate limits.' + ).should('be.visible'); + // Confirm bucket rate limits table exists with correct values + checkRateLimitsTable(endpoint); + + // confirm 'Save' button is disabled (for now) + ui.button + .findByAttribute('label', 'Save') + .should('be.visible') + .should('be.disabled'); + }); + }); + }); }); 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 7d3b490d032..5cfce4ac762 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -128,9 +128,9 @@ const fillOutLinodeForm = (label: string, regionName: string) => { * @returns Promise that resolves to the new Image. */ const createLinodeAndImage = async () => { - // 2GB - // Shout out to Debian for fitting on a 2GB disk. - const resizedDiskSize = 2048; + // 1.5GB + // Shout out to Debian for fitting on a 1.5GB disk. + const resizedDiskSize = 1536; const linode = await createTestLinode( createLinodeRequestFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts index 146233cae6c..e65a582243b 100644 --- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts @@ -1,11 +1,14 @@ +import type { VolumeRequestPayload } from '@linode/api-v4'; +import { createVolume } from '@linode/api-v4/lib/volumes'; import { Volume } from '@linode/api-v4'; import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; import { interceptCloneVolume } from 'support/intercepts/volumes'; +import { SimpleBackoffMethod } from 'support/util/backoff'; import { cleanUp } from 'support/util/cleanup'; +import { pollVolumeStatus } from 'support/util/polling'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { createActiveVolume } from 'support/api/volumes'; // Local storage override to force volume table to list up to 100 items. // This is a workaround while we wait to get stuck volumes removed. @@ -14,6 +17,19 @@ const pageSizeOverride = { PAGE_SIZE: 100, }; +/** + * Creates a Volume and waits for it to become active. + * + * @param volumeRequest - Volume create request payload. + * + * @returns Promise that resolves to created Volume. + */ +const createActiveVolume = async (volumeRequest: VolumeRequestPayload) => { + const volume = await createVolume(volumeRequest); + await pollVolumeStatus(volume.id, 'active', new SimpleBackoffMethod(10000)); + return volume; +}; + authenticate(); describe('volume clone flow', () => { before(() => { diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts index 0ca5c8ce4e2..3c645ace145 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts @@ -11,7 +11,6 @@ import { } from 'support/intercepts/linodes'; import { mockCreateVolume, - mockGetVolume, mockGetVolumes, mockDetachVolume, mockGetVolumeTypesError, @@ -86,7 +85,6 @@ describe('volumes', () => { mockGetVolumes([]).as('getVolumes'); mockCreateVolume(mockVolume).as('createVolume'); - mockGetVolume(mockVolume).as('getVolume'); mockGetVolumeTypes(mockVolumeTypes).as('getVolumeTypes'); cy.visitWithLogin('/volumes', { @@ -116,7 +114,7 @@ describe('volumes', () => { mockGetVolumes([mockVolume]).as('getVolumes'); ui.button.findByTitle('Create Volume').should('be.visible').click(); - cy.wait(['@createVolume', '@getVolume', '@getVolumes']); + cy.wait(['@createVolume', '@getVolumes']); validateBasicVolume(mockVolume.label); ui.actionMenu @@ -195,7 +193,6 @@ describe('volumes', () => { mockDetachVolume(mockAttachedVolume.id).as('detachVolume'); mockGetVolumes([mockAttachedVolume]).as('getAttachedVolumes'); - mockGetVolume(mockAttachedVolume).as('getVolume'); cy.visitWithLogin('/volumes', { preferenceOverrides, localStorageOverrides, @@ -212,8 +209,6 @@ describe('volumes', () => { ui.actionMenuItem.findByTitle('Detach').click(); - cy.wait('@getVolume'); - ui.dialog .findByTitle(`Detach Volume ${mockAttachedVolume.label}?`) .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts index be6d7f8dec1..6828618fb70 100644 --- a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts @@ -1,4 +1,4 @@ -import { createVolume, VolumeRequestPayload } from '@linode/api-v4/lib/volumes'; +import { createVolume } from '@linode/api-v4/lib/volumes'; import { Volume } from '@linode/api-v4'; import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; @@ -7,8 +7,6 @@ import { randomLabel } from 'support/util/random'; import { ui } from 'support/ui'; import { chooseRegion } from 'support/util/regions'; import { cleanUp } from 'support/util/cleanup'; -import { SimpleBackoffMethod } from 'support/util/backoff'; -import { pollVolumeStatus } from 'support/util/polling'; // Local storage override to force volume table to list up to 100 items. // This is a workaround while we wait to get stuck volumes removed. @@ -17,19 +15,6 @@ const pageSizeOverride = { PAGE_SIZE: 100, }; -/** - * Creates a Volume and waits for it to become active. - * - * @param volumeRequest - Volume create request payload. - * - * @returns Promise that resolves to created Volume. - */ -const createActiveVolume = async (volumeRequest: VolumeRequestPayload) => { - const volume = await createVolume(volumeRequest); - await pollVolumeStatus(volume.id, 'active', new SimpleBackoffMethod(10000)); - return volume; -}; - authenticate(); describe('volume delete flow', () => { before(() => { @@ -52,7 +37,7 @@ describe('volume delete flow', () => { region: chooseRegion().id, }); - cy.defer(() => createActiveVolume(volumeRequest), 'creating volume').then( + cy.defer(() => createVolume(volumeRequest), 'creating volume').then( (volume: Volume) => { interceptDeleteVolume(volume.id).as('deleteVolume'); cy.visitWithLogin('/volumes', { diff --git a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts index 25b5e128405..70f4840bbad 100644 --- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts @@ -1,12 +1,10 @@ +import { createVolume } from '@linode/api-v4/lib/volumes'; import { Volume } from '@linode/api-v4'; - import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { cleanUp } from 'support/util/cleanup'; -import { ui } from 'support/ui'; -import { createActiveVolume } from 'support/api/volumes'; authenticate(); describe('volume update flow', () => { @@ -18,17 +16,18 @@ describe('volume update flow', () => { }); /* - * - Confirms that volume label can be changed from the Volumes landing page. + * - Confirms that volume label and tags can be changed from the Volumes landing page. */ - it("updates a volume's label", () => { + it("updates a volume's label and tags", () => { const volumeRequest = volumeRequestPayloadFactory.build({ label: randomLabel(), region: chooseRegion().id, }); const newLabel = randomLabel(); + const newTags = [randomLabel(5), randomLabel(5), randomLabel(5)]; - cy.defer(() => createActiveVolume(volumeRequest), 'creating volume').then( + cy.defer(() => createVolume(volumeRequest), 'creating volume').then( (volume: Volume) => { cy.visitWithLogin('/volumes', { // Temporarily force volume table to show up to 100 results per page. @@ -44,15 +43,10 @@ describe('volume update flow', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('active').should('be.visible'); + cy.findByText('Edit').click(); }); - ui.actionMenu - .findByTitle(`Action menu for Volume ${volume.label}`) - .should('be.visible') - .click(); - cy.get('[data-testid="Edit"]').click(); - // Enter new label, click "Save Changes". + // Enter new label and add tags, click "Save Changes". cy.get('[data-qa-drawer="true"]').within(() => { cy.findByText('Edit Volume').should('be.visible'); cy.findByDisplayValue(volume.label) @@ -60,67 +54,6 @@ describe('volume update flow', () => { .click() .type(`{selectall}{backspace}${newLabel}`); - cy.findByText('Save Changes').should('be.visible').click(); - }); - - // Confirm new label is applied, click "Edit" to re-open drawer. - cy.findByText(newLabel).should('be.visible'); - ui.actionMenu - .findByTitle(`Action menu for Volume ${newLabel}`) - .should('be.visible') - .click(); - cy.get('[data-testid="Edit"]').click(); - - // Confirm new label is shown. - cy.get('[data-qa-drawer="true"]').within(() => { - cy.findByText('Edit Volume').should('be.visible'); - cy.findByDisplayValue(newLabel).should('be.visible'); - }); - } - ); - }); - - /* - * - Confirms that volume tags can be changed from the Volumes landing page. - */ - it("updates volume's tags", () => { - const volumeRequest = volumeRequestPayloadFactory.build({ - label: randomLabel(), - region: chooseRegion().id, - }); - - const newTags = [randomLabel(5), randomLabel(5), randomLabel(5)]; - - cy.defer(() => createActiveVolume(volumeRequest), 'creating volume').then( - (volume: Volume) => { - cy.visitWithLogin('/volumes', { - // Temporarily force volume table to show up to 100 results per page. - // This is a workaround while we wait to get stuck volumes removed. - // @TODO Remove local storage override when stuck volumes are removed from test accounts. - localStorageOverrides: { - PAGE_SIZE: 100, - }, - }); - - // Confirm that volume is listed on landing page, click "Edit" to open drawer. - cy.findByText(volume.label) - .should('be.visible') - .closest('tr') - .within(() => { - cy.findByText('active').should('be.visible'); - }); - - ui.actionMenu - .findByTitle(`Action menu for Volume ${volume.label}`) - .should('be.visible') - .click(); - - cy.get('[data-testid="Manage Tags"]').click(); - - // Add tags, click "Save Changes". - cy.get('[data-qa-drawer="true"]').within(() => { - cy.findByText('Manage Volume Tags').should('be.visible'); - cy.findByPlaceholderText('Type to choose or create a tag.') .should('be.visible') .click() @@ -129,18 +62,18 @@ describe('volume update flow', () => { cy.findByText('Save Changes').should('be.visible').click(); }); - // Confirm new tags are shown, click "Manage Volume Tags" to re-open drawer. - cy.findByText(volumeRequest.label).should('be.visible'); - - ui.actionMenu - .findByTitle(`Action menu for Volume ${volume.label}`) + // Confirm new label is applied, click "Edit" to re-open drawer. + cy.findByText(newLabel) .should('be.visible') - .click(); - - cy.get('[data-testid="Manage Tags"]').click(); + .closest('tr') + .within(() => { + cy.findByText('Edit').click(); + }); + // Confirm new label and tags are shown. cy.get('[data-qa-drawer="true"]').within(() => { - cy.findByText('Manage Volume Tags').should('be.visible'); + cy.findByText('Edit Volume').should('be.visible'); + cy.findByDisplayValue(newLabel).should('be.visible'); // Click the tags input field to see all the selected tags cy.findByRole('combobox').should('be.visible').click(); @@ -152,8 +85,4 @@ describe('volume update flow', () => { } ); }); - - after(() => { - cleanUp(['tags', 'volumes']); - }); }); diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts index 27f5e379b84..f6d807e4c15 100644 --- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -10,11 +10,7 @@ import { mockGetLinodeDisks, mockGetLinodeVolumes, } from 'support/intercepts/linodes'; -import { - mockMigrateVolumes, - mockGetVolumes, - mockGetVolume, -} from 'support/intercepts/volumes'; +import { mockMigrateVolumes, mockGetVolumes } from 'support/intercepts/volumes'; import { ui } from 'support/ui'; describe('volume upgrade/migration', () => { @@ -27,7 +23,6 @@ describe('volume upgrade/migration', () => { }); mockGetVolumes([volume]).as('getVolumes'); - mockGetVolume(volume).as('getVolume'); mockMigrateVolumes().as('migrateVolumes'); mockGetNotifications([migrationScheduledNotification]).as( 'getNotifications' @@ -58,7 +53,7 @@ describe('volume upgrade/migration', () => { .click(); }); - cy.wait(['@migrateVolumes', '@getVolume', '@getNotifications']); + cy.wait(['@migrateVolumes', '@getNotifications']); cy.findByText('UPGRADE PENDING').should('be.visible'); diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index 31c9522f730..86601275d26 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -71,8 +71,7 @@ describe('VPC create flow', () => { subnets: mockSubnets, }); - const ipValidationErrorMessage1 = 'A subnet must have an IPv4 range.'; - const ipValidationErrorMessage2 = 'The IPv4 range must be in CIDR format.'; + const ipValidationErrorMessage = 'The IPv4 range must be in CIDR format'; const vpcCreationErrorMessage = 'An unknown error has occurred.'; const totalSubnetUniqueLinodes = getUniqueLinodesFromSubnets(mockSubnets); @@ -112,7 +111,7 @@ describe('VPC create flow', () => { .should('be.enabled') .click(); - cy.findByText(ipValidationErrorMessage1).should('be.visible'); + cy.findByText(ipValidationErrorMessage).should('be.visible'); // Enter a random non-IP address string to further test client side validation. cy.findByText('Subnet IP Address Range') @@ -127,7 +126,7 @@ describe('VPC create flow', () => { .should('be.enabled') .click(); - cy.findByText(ipValidationErrorMessage2).should('be.visible'); + cy.findByText(ipValidationErrorMessage).should('be.visible'); // Enter a valid IP address with an invalid network prefix to further test client side validation. cy.findByText('Subnet IP Address Range') @@ -142,7 +141,7 @@ describe('VPC create flow', () => { .should('be.enabled') .click(); - cy.findByText(ipValidationErrorMessage2).should('be.visible'); + cy.findByText(ipValidationErrorMessage).should('be.visible'); // Replace invalid IP address range with valid range. cy.findByText('Subnet IP Address Range') @@ -181,12 +180,10 @@ describe('VPC create flow', () => { getSubnetNodeSection(1) .should('be.visible') .within(() => { - cy.findByText('Label must be between 1 and 64 characters.').should( - 'be.visible' - ); + cy.findByText('Label is required').should('be.visible'); // Delete subnet. - cy.findByLabelText('Remove Subnet 1') + cy.findByLabelText('Remove Subnet') .should('be.visible') .should('be.enabled') .click(); @@ -303,7 +300,7 @@ describe('VPC create flow', () => { getSubnetNodeSection(0) .should('be.visible') .within(() => { - cy.findByLabelText('Remove Subnet 0') + cy.findByLabelText('Remove Subnet') .should('be.visible') .should('be.enabled') .click(); diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts index f82c9d4650c..dd7bd443c96 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts @@ -8,21 +8,11 @@ import { mockEditSubnet, mockGetSubnets, } from 'support/intercepts/vpc'; -import { mockGetLinodeConfigs } from 'support/intercepts/configs'; -import { mockGetLinodeDetails } from 'support/intercepts/linodes'; -import { - linodeFactory, - linodeConfigFactory, - LinodeConfigInterfaceFactoryWithVPC, - subnetFactory, - vpcFactory, -} from '@src/factories'; +import { subnetFactory, vpcFactory } from '@src/factories'; import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; import type { VPC } from '@linode/api-v4'; import { getRegionById } from 'support/util/regions'; import { ui } from 'support/ui'; -import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; describe('VPC details page', () => { /** @@ -276,117 +266,4 @@ describe('VPC details page', () => { cy.findByText('No Subnets are assigned.'); cy.findByText(mockEditedSubnet.label).should('not.exist'); }); - - /** - * - Confirms UI for Linode with a recommended config (no notice displayed) - */ - it('does not display an unrecommended config notice for a Linode', () => { - const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); - - const mockInterfaceId = randomNumber(); - const mockLinode = linodeFactory.build({ - id: randomNumber(), - label: randomLabel(), - region: linodeRegion.id, - }); - - const mockSubnet = subnetFactory.build({ - id: randomNumber(), - label: randomLabel(), - linodes: [ - { - id: mockLinode.id, - interfaces: [{ id: mockInterfaceId, active: true }], - }, - ], - ipv4: '10.0.0.0/24', - }); - - const mockVPC = vpcFactory.build({ - id: randomNumber(), - label: randomLabel(), - region: linodeRegion.id, - subnets: [mockSubnet], - }); - - const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ - vpc_id: mockVPC.id, - subnet_id: mockSubnet.id, - primary: true, - active: true, - }); - - const mockLinodeConfig = linodeConfigFactory.build({ - interfaces: [mockInterface], - }); - - mockGetVPC(mockVPC).as('getVPC'); - mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( - 'getLinodeConfigs' - ); - - cy.visitWithLogin(`/vpcs/${mockVPC.id}`); - cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfigs'); - cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); - }); - - /** - * - Confirms UI for Linode with an unrecommended config (notice displayed) - */ - it('displays an unrecommended config notice for a Linode', () => { - const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); - - const mockInterfaceId = randomNumber(); - const mockLinode = linodeFactory.build({ - id: randomNumber(), - label: randomLabel(), - region: linodeRegion.id, - }); - - const mockSubnet = subnetFactory.build({ - id: randomNumber(), - label: randomLabel(), - linodes: [ - { - id: mockLinode.id, - interfaces: [{ id: mockInterfaceId, active: true }], - }, - ], - ipv4: '10.0.0.0/24', - }); - - const mockVPC = vpcFactory.build({ - id: randomNumber(), - label: randomLabel(), - region: linodeRegion.id, - subnets: [mockSubnet], - }); - - const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ - id: mockInterfaceId, - vpc_id: mockVPC.id, - subnet_id: mockSubnet.id, - primary: false, - active: true, - }); - - const mockLinodeConfig = linodeConfigFactory.build({ - interfaces: [mockInterface], - }); - - mockGetVPC(mockVPC).as('getVPC'); - mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( - 'getLinodeConfigs' - ); - - cy.visitWithLogin(`/vpcs/${mockVPC.id}`); - cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfigs'); - cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('exist'); - }); }); diff --git a/packages/manager/cypress/support/api/lke.ts b/packages/manager/cypress/support/api/lke.ts index fa99f1eed71..14ed8339749 100644 --- a/packages/manager/cypress/support/api/lke.ts +++ b/packages/manager/cypress/support/api/lke.ts @@ -1,39 +1,29 @@ import { + KubeNodePoolResponse, + KubernetesCluster, + PoolNodeResponse, deleteKubernetesCluster, getKubernetesClusters, getNodePools, } from '@linode/api-v4'; -import { DateTime } from 'luxon'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; +import { DateTime } from 'luxon'; import { isTestLabel } from './common'; -import type { - KubeNodePoolResponse, - KubernetesCluster, - PoolNodeResponse, -} from '@linode/api-v4'; -import type { LinodeTypeClass } from '@linode/api-v4/lib/linodes/types'; - /** * Describes an LKE plan as shown in Cloud Manager. */ export interface LkePlanDescription { - /** Number of nodes in the plan. */ - nodeCount: number; - /** Name of the plan. */ - planName: string; - /** Plan size, GB. */ + // / Plan size, GB. size: number; - /** Label for tab containing the plan in creation screen. */ + + // / Label for tab containing the plan in creation screen. tab: string; - /** Type of plan. */ - type: LinodeTypeClass; -} -export interface LkePlanDescriptionAPL extends LkePlanDescription { - disabled: boolean; + // / Type of plan. + type: string; } /* diff --git a/packages/manager/cypress/support/api/volumes.ts b/packages/manager/cypress/support/api/volumes.ts index 0ef03c9d816..a52e4784f13 100644 --- a/packages/manager/cypress/support/api/volumes.ts +++ b/packages/manager/cypress/support/api/volumes.ts @@ -1,17 +1,8 @@ -import { - createVolume, - deleteVolume, - detachVolume, - getVolumes, -} from '@linode/api-v4'; +import { Volume, deleteVolume, detachVolume, getVolumes } from '@linode/api-v4'; import { pageSize } from 'support/constants/api'; -import { SimpleBackoffMethod, attemptWithBackoff } from 'support/util/backoff'; import { depaginate } from 'support/util/paginate'; -import { pollVolumeStatus } from 'support/util/polling'; - import { isTestLabel } from './common'; - -import type { Volume, VolumeRequestPayload } from '@linode/api-v4'; +import { attemptWithBackoff, SimpleBackoffMethod } from 'support/util/backoff'; /** * Delete all Volumes whose labels are prefixed "cy-test-". @@ -54,18 +45,3 @@ export const deleteAllTestVolumes = async (): Promise => { await Promise.all(detachDeletePromises); }; - -/** - * Creates a Volume and waits for it to become active. - * - * @param volumeRequest - Volume create request payload. - * - * @returns Promise that resolves to created Volume. - */ -export const createActiveVolume = async ( - volumeRequest: VolumeRequestPayload -) => { - const volume = await createVolume(volumeRequest); - await pollVolumeStatus(volume.id, 'active', new SimpleBackoffMethod(10000)); - return volume; -}; diff --git a/packages/manager/cypress/support/constants/account.ts b/packages/manager/cypress/support/constants/account.ts index c8030ac4cbc..2ef3525eaf7 100644 --- a/packages/manager/cypress/support/constants/account.ts +++ b/packages/manager/cypress/support/constants/account.ts @@ -2,15 +2,9 @@ * Data loss warning which is displayed in the account cancellation dialog. */ export const cancellationDataLossWarning = - 'This is an extremely destructive action. All services, Linodes, volumes, \ -DNS records, and user accounts will be permanently lost.'; - -/** - * Title text displayed in the account cancellation confirmation dialog. - */ -export const cancellationDialogTitle = - 'Are you sure you want to close your Akamai cloud \ -computing services account?'; + 'Please note this is an extremely destructive action. Closing your account \ +means that all services Linodes, Volumes, DNS Records, etc will be lost and \ +may not be able be restored.'; /** * Error message that appears when a payment failure occurs upon cancellation attempt. diff --git a/packages/manager/cypress/support/constants/dc-specific-pricing.ts b/packages/manager/cypress/support/constants/dc-specific-pricing.ts index 584fee1378f..3843a35aceb 100644 --- a/packages/manager/cypress/support/constants/dc-specific-pricing.ts +++ b/packages/manager/cypress/support/constants/dc-specific-pricing.ts @@ -3,8 +3,7 @@ */ import { linodeTypeFactory } from '@src/factories'; - -import type { LkePlanDescription } from 'support/api/lke'; +import { LkePlanDescription } from 'support/api/lke'; /** Notice shown to users when selecting a region with a different price structure. */ export const dcPricingRegionDifferenceNotice = @@ -128,11 +127,9 @@ export const dcPricingMockLinodeTypesForBackups = linodeTypeFactory.buildList( export const dcPricingLkeClusterPlans: LkePlanDescription[] = dcPricingMockLinodeTypes.map( (type) => { return { - nodeCount: 1, - planName: 'Linode 2 GB', size: parseInt(type.id.split('-')[2], 10), tab: 'Shared CPU', - type: 'nanode', + type: 'Linode', }; } ); diff --git a/packages/manager/cypress/support/constants/linodes.ts b/packages/manager/cypress/support/constants/linodes.ts index 35d6762f810..c1985c9036f 100644 --- a/packages/manager/cypress/support/constants/linodes.ts +++ b/packages/manager/cypress/support/constants/linodes.ts @@ -5,6 +5,6 @@ /** * Length of time to wait for a Linode to be created. * - * Equals 5 minutes. + * Equals 4 minutes. */ -export const LINODE_CREATE_TIMEOUT = 300_000; +export const LINODE_CREATE_TIMEOUT = 240_000; diff --git a/packages/manager/cypress/support/constants/lke.ts b/packages/manager/cypress/support/constants/lke.ts index 50e125e1159..1a4ca20aded 100644 --- a/packages/manager/cypress/support/constants/lke.ts +++ b/packages/manager/cypress/support/constants/lke.ts @@ -1,36 +1,20 @@ -import { getLatestKubernetesVersion } from 'support/util/lke'; - -import type { KubernetesTieredVersion } from '@linode/api-v4'; +import { LkePlanDescription } from 'support/api/lke'; /** - * Kubernetes versions available for cluster creation via Cloud Manager. + * Subset of LKE cluster plans as shown on Cloud Manager. */ -export const kubernetesVersions = ['1.31', '1.30']; +export const lkeClusterPlans: LkePlanDescription[] = [ + { size: 4, tab: 'Dedicated CPU', type: 'Dedicated' }, + { size: 2, tab: 'Shared CPU', type: 'Linode' }, + { size: 4, tab: 'Shared CPU', type: 'Linode' }, +]; /** - * Enterprise kubernetes versions available for cluster creation via Cloud Manager. + * Kubernetes versions available for cluster creation via Cloud Manager. */ -export const enterpriseKubernetesVersions = ['v1.31.1+lke1']; +export const kubernetesVersions = ['1.25', '1.24']; /** * The latest Kubernetes version available for cluster creation via Cloud Manager. */ -export const latestKubernetesVersion = getLatestKubernetesVersion( - kubernetesVersions -); - -/** - * The latest standard tier Kubernetes version available for cluster creation via Cloud Manager. - */ -export const latestStandardTierKubernetesVersion: KubernetesTieredVersion = { - id: latestKubernetesVersion, - tier: 'standard', -}; - -/** - * The latest enterprise tier Kubernetes version available for cluster creation via Cloud Manager. - */ -export const latestEnterpriseTierKubernetesVersion: KubernetesTieredVersion = { - id: getLatestKubernetesVersion(enterpriseKubernetesVersions), - tier: 'enterprise', -}; +export const latestKubernetesVersion = kubernetesVersions[0]; diff --git a/packages/manager/cypress/support/constants/login.ts b/packages/manager/cypress/support/constants/login.ts deleted file mode 100644 index cd3b364f68f..00000000000 --- a/packages/manager/cypress/support/constants/login.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Constants related to Cloud Manager login/logout flows. - */ - -/** - * Login base URL for Cloud Manager. - */ -export const loginBaseUrl = Cypress.env('REACT_APP_LOGIN_ROOT'); diff --git a/packages/manager/cypress/support/intercepts/betas.ts b/packages/manager/cypress/support/intercepts/betas.ts index 4874620ec77..384961da39e 100644 --- a/packages/manager/cypress/support/intercepts/betas.ts +++ b/packages/manager/cypress/support/intercepts/betas.ts @@ -6,7 +6,7 @@ import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; -import type { AccountBeta, Beta } from '@linode/api-v4'; +import type { Beta } from '@linode/api-v4'; /** * Intercepts GET request to fetch account betas (the ones the user has opted into) and mocks response. @@ -15,9 +15,7 @@ import type { AccountBeta, Beta } from '@linode/api-v4'; * * @returns Cypress chainable. */ -export const mockGetAccountBetas = ( - betas: AccountBeta[] -): Cypress.Chainable => { +export const mockGetAccountBetas = (betas: Beta[]): Cypress.Chainable => { return cy.intercept( 'GET', apiMatcher('account/betas'), @@ -25,21 +23,6 @@ export const mockGetAccountBetas = ( ); }; -/** - * Intercepts GET request to fetch a beta and mocks response. - * - * @param beta - Beta with which to mock response. - * - * @returns Cypress chainable. - */ -export const mockGetAccountBeta = (beta: AccountBeta): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`account/betas/${beta.id}`), - makeResponse(beta) - ); -}; - /** * Intercepts GET request to fetch available betas (all betas available to the user). * diff --git a/packages/manager/cypress/support/intercepts/cloudpulse.ts b/packages/manager/cypress/support/intercepts/cloudpulse.ts index f17e884787a..e3d472a67c6 100644 --- a/packages/manager/cypress/support/intercepts/cloudpulse.ts +++ b/packages/manager/cypress/support/intercepts/cloudpulse.ts @@ -13,7 +13,7 @@ import { makeResponse } from 'support/util/response'; import type { CloudPulseMetricsResponse, Dashboard, - MetricDefinition, + MetricDefinitions, } from '@linode/api-v4'; /** @@ -27,12 +27,12 @@ import type { export const mockGetCloudPulseMetricDefinitions = ( serviceType: string, - metricDefinitions: MetricDefinition[] + metricDefinitions: MetricDefinitions ): Cypress.Chainable => { return cy.intercept( 'GET', apiMatcher(`/monitor/services/${serviceType}/metric-definitions`), - paginateResponse(metricDefinitions) + makeResponse(metricDefinitions) ); }; diff --git a/packages/manager/cypress/support/intercepts/lke.ts b/packages/manager/cypress/support/intercepts/lke.ts index 88905b33b38..5f646730a96 100644 --- a/packages/manager/cypress/support/intercepts/lke.ts +++ b/packages/manager/cypress/support/intercepts/lke.ts @@ -6,11 +6,7 @@ import { kubeEndpointFactory, kubernetesDashboardUrlFactory, } from '@src/factories'; -import { - kubernetesVersions, - latestEnterpriseTierKubernetesVersion, - latestStandardTierKubernetesVersion, -} from 'support/constants/lke'; +import { kubernetesVersions } from 'support/constants/lke'; import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; @@ -22,10 +18,7 @@ import type { KubeNodePoolResponse, KubernetesCluster, KubernetesControlPlaneACLPayload, - KubernetesTier, - KubernetesTieredVersion, KubernetesVersion, - PriceType, } from '@linode/api-v4'; /** @@ -49,39 +42,6 @@ export const mockGetKubernetesVersions = (versions?: string[] | undefined) => { ); }; -/** - * Intercepts GET request to retrieve tiered Kubernetes versions and mocks response. - * - * @param tier - Standard or enterprise Kubernetes tier. - * @param versions - Optional array of strings containing mocked tiered versions. - * - * @returns Cypress chainable. - */ -export const mockGetTieredKubernetesVersions = ( - tier: KubernetesTier, - versions?: KubernetesTieredVersion[] -) => { - const defaultTieredVersions = - tier === 'enterprise' - ? [latestEnterpriseTierKubernetesVersion] - : [latestStandardTierKubernetesVersion]; - - const versionObjects = (versions ? versions : defaultTieredVersions).map( - (kubernetesTieredVersion): KubernetesTieredVersion => { - return { - id: kubernetesTieredVersion.id, - tier: kubernetesTieredVersion.tier, - }; - } - ); - - return cy.intercept( - 'GET', - apiMatcher(`lke/versions/${tier}*`), - paginateResponse(versionObjects) - ); -}; - /** * Intercepts GET request to retrieve LKE clusters and mocks response. * @@ -495,37 +455,3 @@ export const mockUpdateControlPlaneACLError = ( makeErrorResponse(errorMessage, statusCode) ); }; - -/** - * Intercepts GET request for LKE cluster types and mocks the response - * - * @param types - LKE cluster types with which to mock response - * - * @returns Cypress chainable - */ -export const mockGetLKEClusterTypes = ( - types: PriceType[] -): Cypress.Chainable => { - return cy.intercept('GET', apiMatcher('lke/types*'), paginateResponse(types)); -}; - -/** - * Intercepts PUT request to update an LKE cluster and mocks an error response. - * - * @param clusterId - ID of cluster for which to intercept PUT request. - * @param errorMessage - Optional error message with which to mock response. - * @param statusCode - HTTP status code with which to mock response. - * - * @returns Cypress chainable. - */ -export const mockUpdateClusterError = ( - clusterId: number, - errorMessage: string = 'An unknown error occurred.', - statusCode: number = 500 -): Cypress.Chainable => { - return cy.intercept( - 'PUT', - apiMatcher(`lke/clusters/${clusterId}`), - makeErrorResponse(errorMessage, statusCode) - ); -}; diff --git a/packages/manager/cypress/support/setup/defer-command.ts b/packages/manager/cypress/support/setup/defer-command.ts index bb695ef47a0..a667d505030 100644 --- a/packages/manager/cypress/support/setup/defer-command.ts +++ b/packages/manager/cypress/support/setup/defer-command.ts @@ -1,7 +1,6 @@ import type { APIError } from '@linode/api-v4'; import type { AxiosError } from 'axios'; import { timeout } from 'support/util/backoff'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; type LinodeApiV4Error = { errors: APIError[]; @@ -189,9 +188,9 @@ Cypress.Commands.add( const timeoutLength = (() => { if (typeof labelOrOptions !== 'string') { - return labelOrOptions?.timeout ?? LINODE_CREATE_TIMEOUT; + return labelOrOptions?.timeout; } - return LINODE_CREATE_TIMEOUT; + return undefined; })(); const commandLog = Cypress.log({ diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index da303ff0bb0..2c6bca8da83 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -158,7 +158,15 @@ export const createTestLinode = async ( // Wait for Linode status to be 'running' if `waitForBoot` is true. if (resolvedOptions.waitForBoot) { - await pollLinodeStatus(linode.id, 'running'); + // Wait 15 seconds before initial check, then poll again every 5 seconds. + await pollLinodeStatus( + linode.id, + 'running', + new SimpleBackoffMethod(5000, { + initialDelay: 15000, + maxAttempts: 25, + }) + ); } Cypress.log({ diff --git a/packages/manager/cypress/support/util/lke.ts b/packages/manager/cypress/support/util/lke.ts deleted file mode 100644 index e1e65848532..00000000000 --- a/packages/manager/cypress/support/util/lke.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { sortByVersion } from 'src/utilities/sort-by'; - -/** - * Returns the string of the highest semantic version. - */ -export const getLatestKubernetesVersion = (versions: string[]) => { - const sortedVersions = versions.sort((a, b) => { - return sortByVersion(a, b, 'asc'); - }); - - const latestVersion = sortedVersions.pop(); - - if (!latestVersion) { - // Return an empty string if sorting does not yield latest version - return ''; - } - return latestVersion; -}; diff --git a/packages/manager/cypress/support/util/polling.ts b/packages/manager/cypress/support/util/polling.ts index e10bc0467af..d188b2b1ba1 100644 --- a/packages/manager/cypress/support/util/polling.ts +++ b/packages/manager/cypress/support/util/polling.ts @@ -22,11 +22,9 @@ import { BackoffMethod, BackoffOptions, FibonacciBackoffMethod, - SimpleBackoffMethod, attemptWithBackoff, } from './backoff'; import { depaginate } from './paginate'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; /** * Describes a backoff configuration for a poll. This may be a partial BackoffOptions object, @@ -105,13 +103,9 @@ export const poll = async ( /** * Polls a Linode with the given ID until it has the given status. * - * By default, polling will occur after 15 seconds have passed, and reattempts - * occur on a 5-second interval until the default Linode create timeout is reached. - * This behavior can be customized by passing an alternative `backoffMethod`. - * * @param linodeId - ID of Linode to poll. * @param desiredStatus - Desired status of Linode that is being polled. - * @param backoffMethod - Optional backoff method for reattempts. + * @param backoffMethod - Backoff method implementation to manage re-attempts. * @param label - Optional label to assign to poll for logging and troubleshooting. * * @returns A Promise that resolves to the polled Linode's status or rejects on timeout. @@ -127,24 +121,10 @@ export const pollLinodeStatus = async ( return linode.status; }; - // By default, wait 15 seconds before initial check then poll again every 5 - // seconds until default Linode create timeout is reached. - const initialDelay = 15_000; - const interval = 5_000; - const maxAttempts = Math.ceil( - (LINODE_CREATE_TIMEOUT - initialDelay) / interval - ); - const defaultBackoffMethod = new SimpleBackoffMethod(interval, { - initialDelay, - maxAttempts, - }); - - const backoff = backoffOptions ? backoffOptions : defaultBackoffMethod; - const checkLinodeStatus = (status: LinodeStatus): boolean => status === desiredStatus; - return poll(getLinodeStatus, checkLinodeStatus, backoff, label); + return poll(getLinodeStatus, checkLinodeStatus, backoffOptions, label); }; /** diff --git a/packages/manager/cypress/tsconfig.json b/packages/manager/cypress/tsconfig.json index bedcfa42811..0331e9038b2 100644 --- a/packages/manager/cypress/tsconfig.json +++ b/packages/manager/cypress/tsconfig.json @@ -12,8 +12,7 @@ "cypress-file-upload", "@testing-library/cypress", "cypress-real-events", - "vite/client", - "@4tw/cypress-drag-drop" + "vite/client" ] }, "include": [ diff --git a/packages/manager/index.html b/packages/manager/index.html index 926711c776d..3ce95341cf1 100644 --- a/packages/manager/index.html +++ b/packages/manager/index.html @@ -11,4 +11,4 @@
- + \ No newline at end of file diff --git a/packages/manager/package.json b/packages/manager/package.json index b84877cb298..9331fc23cee 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.134.0", + "version": "1.133.2", "private": true, "type": "module", "bugs": { @@ -19,7 +19,7 @@ "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@hookform/resolvers": "3.9.1", + "@hookform/resolvers": "2.9.11", "@linode/api-v4": "*", "@linode/design-language-system": "^2.6.1", "@linode/search": "*", @@ -73,12 +73,13 @@ "react-router-dom": "~5.3.4", "react-router-hash-link": "^2.3.1", "react-select": "~3.1.0", - "react-vnc": "^2.0.2", + "react-vnc": "^0.5.3", "react-waypoint": "^10.3.0", "recharts": "^2.14.1", "recompose": "^0.30.0", "redux": "^4.0.4", "redux-thunk": "^2.3.0", + "reselect": "^4.0.0", "search-string": "^3.1.0", "throttle-debounce": "^2.0.0", "tss-react": "^4.8.2", @@ -96,7 +97,6 @@ "build:analyze": "bunx vite-bundle-visualizer", "precommit": "lint-staged && yarn typecheck", "test": "vitest run", - "test:ui": "vitest --ui", "test:debug": "node --inspect-brk scripts/test.js --runInBand", "storybook": "NODE_OPTIONS='--max-old-space-size=4096' storybook dev -p 6006", "storybook-static": "storybook build -c .storybook -o .out", @@ -169,11 +169,12 @@ "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", - "@4tw/cypress-drag-drop": "^2.2.5", "@vitejs/plugin-react-swc": "^3.7.0", "@vitest/coverage-v8": "^2.1.1", "@vitest/ui": "^2.1.1", "chai-string": "^1.5.0", + "chalk": "^5.2.0", + "commander": "^6.2.1", "css-mediaquery": "^0.1.2", "cypress": "13.11.0", "cypress-axe": "^1.5.0", @@ -198,13 +199,16 @@ "factory.ts": "^0.5.1", "glob": "^10.3.1", "jsdom": "^24.1.1", + "junit2json": "^3.1.4", "lint-staged": "^15.2.9", "mocha-junit-reporter": "^2.2.1", "msw": "^2.2.3", "prettier": "~2.2.1", "redux-mock-store": "^1.5.3", + "simple-git": "^3.19.0", "storybook": "^8.3.0", "storybook-dark-mode": "4.0.1", + "tsx": "^4.19.1", "vite": "^5.4.6", "vite-plugin-svgr": "^3.2.0" }, diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index dcdae2ec607..e0bfc2f2af3 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -1,6 +1,5 @@ import { Box } from '@linode/ui'; import Grid from '@mui/material/Unstable_Grid2'; -import { useQueryClient } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; @@ -31,7 +30,6 @@ import { sessionExpirationContext } from './context/sessionExpirationContext'; import { switchAccountSessionContext } from './context/switchAccountSessionContext'; import { useIsACLPEnabled } from './features/CloudPulse/Utils/utils'; import { useIsDatabasesEnabled } from './features/Databases/utilities'; -import { useIsIAMEnabled } from './features/IAM/Shared/utilities'; import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils'; import { useGlobalErrors } from './hooks/useGlobalErrors'; import { useAccountSettings } from './queries/account/settings'; @@ -40,6 +38,7 @@ import { migrationRouter } from './routes'; import type { Theme } from '@mui/material/styles'; import type { AnyRouter } from '@tanstack/react-router'; +import { useIsIAMEnabled } from './features/IAM/Shared/utilities'; const useStyles = makeStyles()((theme: Theme) => ({ activationWrapper: { @@ -131,6 +130,12 @@ const LinodesRoutes = React.lazy(() => default: module.LinodesRoutes, })) ); +const Volumes = React.lazy(() => import('src/features/Volumes')); +const Domains = React.lazy(() => + import('src/features/Domains').then((module) => ({ + default: module.DomainsRoutes, + })) +); const Images = React.lazy(() => import('src/features/Images')); const Kubernetes = React.lazy(() => import('src/features/Kubernetes').then((module) => ({ @@ -200,11 +205,8 @@ const IAM = React.lazy(() => export const MainContent = () => { const { classes, cx } = useStyles(); - const { data: isDesktopSidebarOpenPreference } = usePreferences( - (preferences) => preferences?.desktop_sidebar_open - ); + const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); - const queryClient = useQueryClient(); const globalErrors = useGlobalErrors(); @@ -284,11 +286,11 @@ export const MainContent = () => { return ; } - const desktopMenuIsOpen = isDesktopSidebarOpenPreference ?? false; + const desktopMenuIsOpen = preferences?.desktop_sidebar_open ?? false; const desktopMenuToggle = () => { updatePreferences({ - desktop_sidebar_open: !isDesktopSidebarOpenPreference, + desktop_sidebar_open: !preferences?.desktop_sidebar_open, }); }; @@ -334,10 +336,13 @@ export const MainContent = () => { path="/placement-groups" /> )} + + + @@ -377,7 +382,6 @@ export const MainContent = () => { */} diff --git a/packages/manager/src/Root.tsx b/packages/manager/src/Root.tsx index 64089d9c47e..3df94224ecb 100644 --- a/packages/manager/src/Root.tsx +++ b/packages/manager/src/Root.tsx @@ -31,9 +31,7 @@ import { useStyles } from './Root.styles'; export const Root = () => { const { classes, cx } = useStyles(); - const { data: isDesktopSidebarOpenPreference } = usePreferences( - (preferences) => preferences?.desktop_sidebar_open - ); + const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); const globalErrors = useGlobalErrors(); @@ -59,11 +57,11 @@ export const Root = () => { const { data: profile } = useProfile(); const username = profile?.username || ''; - const desktopMenuIsOpen = isDesktopSidebarOpenPreference ?? false; + const desktopMenuIsOpen = preferences?.desktop_sidebar_open ?? false; const desktopMenuToggle = () => { updatePreferences({ - desktop_sidebar_open: !isDesktopSidebarOpenPreference, + desktop_sidebar_open: !preferences?.desktop_sidebar_open, }); }; diff --git a/packages/manager/src/Router.tsx b/packages/manager/src/Router.tsx index a8e12fd54f3..7d89f4fa297 100644 --- a/packages/manager/src/Router.tsx +++ b/packages/manager/src/Router.tsx @@ -1,4 +1,3 @@ -import { QueryClient } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import * as React from 'react'; @@ -25,7 +24,6 @@ export const Router = () => { isACLPEnabled, isDatabasesEnabled, isPlacementGroupsEnabled, - queryClient: new QueryClient(), }, }); diff --git a/packages/manager/src/__data__/linodes.ts b/packages/manager/src/__data__/linodes.ts index 8cf0cac9de2..cf156461fa9 100644 --- a/packages/manager/src/__data__/linodes.ts +++ b/packages/manager/src/__data__/linodes.ts @@ -16,7 +16,6 @@ export const linode1: Linode = { window: 'W2', }, }, - capabilities: [], created: '2017-12-07T19:12:58', group: 'active', hypervisor: 'kvm', @@ -66,7 +65,6 @@ export const linode2: Linode = { window: 'Scheduling', }, }, - capabilities: [], created: '2018-02-22T16:11:07', group: 'inactive', hypervisor: 'kvm', @@ -116,7 +114,6 @@ export const linode3: Linode = { window: 'Scheduling', }, }, - capabilities: [], created: '2018-02-22T16:11:07', group: 'inactive', hypervisor: 'kvm', @@ -166,7 +163,6 @@ export const linode4: Linode = { window: 'Scheduling', }, }, - capabilities: [], created: '2018-02-22T16:11:07', group: 'inactive', hypervisor: 'kvm', diff --git a/packages/manager/src/assets/icons/emptynotification.svg b/packages/manager/src/assets/icons/emptynotification.svg deleted file mode 100644 index 5181cecf317..00000000000 --- a/packages/manager/src/assets/icons/emptynotification.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/packages/manager/src/assets/icons/entityIcons/alerts.svg b/packages/manager/src/assets/icons/entityIcons/alerts.svg deleted file mode 100644 index 69a49e63d18..00000000000 --- a/packages/manager/src/assets/icons/entityIcons/alerts.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/packages/manager/src/assets/icons/uploadPending.svg b/packages/manager/src/assets/icons/uploadPending.svg new file mode 100644 index 00000000000..07895a6311c --- /dev/null +++ b/packages/manager/src/assets/icons/uploadPending.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx b/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx index eddee90c914..457dc086e6f 100644 --- a/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx +++ b/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx @@ -1,11 +1,11 @@ -import { Box, Button, omittedProps } from '@linode/ui'; +import { Box, Button } from '@linode/ui'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { useStyles } from 'tss-react/mui'; import type { BoxProps, ButtonProps } from '@linode/ui'; -export interface ActionButtonsProps extends ButtonProps { +interface ActionButtonsProps extends ButtonProps { 'data-node-idx'?: number; 'data-qa-form-data-loading'?: boolean; 'data-testid'?: string; @@ -17,10 +17,6 @@ export interface ActionPanelProps extends BoxProps { * primary type actionable button custom aria descripton. */ primaryButtonProps?: ActionButtonsProps; - /** - * Determines the position of the primary button within the actions panel. - */ - reversePrimaryButtonPosition?: boolean; /** * secondary type actionable button custom aria descripton. */ @@ -35,7 +31,6 @@ export const ActionsPanel = (props: ActionPanelProps) => { const { className, primaryButtonProps, - reversePrimaryButtonPosition = false, secondaryButtonProps, ...rest } = props; @@ -49,7 +44,6 @@ export const ActionsPanel = (props: ActionPanelProps) => { {secondaryButtonProps ? ( @@ -75,13 +69,14 @@ export const ActionsPanel = (props: ActionPanelProps) => { ); }; -const StyledBox = styled(Box, { - label: 'StyledActionsPanel', - shouldForwardProp: omittedProps(['reversePrimaryButtonPosition']), -})(({ theme: { spacing }, ...props }) => ({ - display: 'flex', - flexDirection: props.reversePrimaryButtonPosition ? 'row-reverse' : 'row', - gap: spacing(), +const StyledBox = styled(Box)(({ theme: { spacing } }) => ({ + '& > :first-of-type': { + marginLeft: 0, + marginRight: spacing(), + }, + '& > :only-child': { + marginRight: 0, + }, justifyContent: 'flex-end', marginTop: spacing(1), paddingBottom: spacing(1), diff --git a/packages/manager/src/components/Avatar/Avatar.tsx b/packages/manager/src/components/Avatar/Avatar.tsx index 097f03ef9d3..d84a53f170b 100644 --- a/packages/manager/src/components/Avatar/Avatar.tsx +++ b/packages/manager/src/components/Avatar/Avatar.tsx @@ -50,9 +50,7 @@ export const Avatar = (props: AvatarProps) => { const theme = useTheme(); - const { data: avatarColorPreference } = usePreferences( - (preferences) => preferences?.avatarColor - ); + const { data: preferences } = usePreferences(); const { data: profile } = useProfile(); const _username = username ?? profile?.username ?? ''; @@ -60,10 +58,9 @@ export const Avatar = (props: AvatarProps) => { _username === 'Akamai' || _username.startsWith('lke-service-account'); const savedAvatarColor = - isAkamai || !avatarColorPreference + isAkamai || !preferences?.avatarColor ? theme.palette.primary.dark - : avatarColorPreference; - + : preferences.avatarColor; const avatarLetter = _username[0]?.toUpperCase() ?? ''; return ( diff --git a/packages/manager/src/components/BarPercent/BarPercent.tsx b/packages/manager/src/components/BarPercent/BarPercent.tsx index 58b76e006e2..81a9d6e4ccf 100644 --- a/packages/manager/src/components/BarPercent/BarPercent.tsx +++ b/packages/manager/src/components/BarPercent/BarPercent.tsx @@ -73,13 +73,11 @@ const StyledLinearProgress = styled(LinearProgress, { shouldForwardProp: omittedProps(['rounded', 'narrow']), })>(({ theme, ...props }) => ({ '& .MuiLinearProgress-bar2Buffer': { - backgroundColor: theme.tokens.color.Green[60], + backgroundColor: '#5ad865', }, '& .MuiLinearProgress-barColorPrimary': { // Increase contrast if we have a buffer bar - backgroundColor: props.valueBuffer - ? theme.tokens.color.Green[70] - : theme.tokens.color.Green[60], + backgroundColor: props.valueBuffer ? '#1CB35C' : '#5ad865', }, '& .MuiLinearProgress-dashed': { display: 'none', diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx index af43289b40f..57c84f428bb 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx @@ -1,8 +1,6 @@ import { EditableText, H1Header } from '@linode/ui'; import { styled } from '@mui/material'; -import type { EditableTextProps } from '@linode/ui'; - export const StyledDiv = styled('div', { label: 'StyledDiv' })({ display: 'flex', flexDirection: 'column', @@ -10,7 +8,7 @@ export const StyledDiv = styled('div', { label: 'StyledDiv' })({ export const StyledEditableText = styled(EditableText, { label: 'StyledEditableText', -})(({ theme }) => ({ +})(({ theme }) => ({ '& > div': { width: 250, }, diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx index 729f1222230..5cb20414d84 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx @@ -1,13 +1,11 @@ import * as React from 'react'; -import { Link } from '../Link'; import { StyledDiv, StyledEditableText, StyledH1Header, } from './FinalCrumb.styles'; - -import type { EditableProps, LabelProps } from './types'; +import { EditableProps, LabelProps } from './types'; interface Props { crumb: string; @@ -24,13 +22,6 @@ export const FinalCrumb = React.memo((props: Props) => { onEditHandlers, } = props; - const linkProps = labelOptions?.linkTo - ? { - LinkComponent: Link, - labelLink: labelOptions.linkTo, - } - : {}; - if (onEditHandlers) { return ( { disabledBreadcrumbEditButton={disabledBreadcrumbEditButton} errorText={onEditHandlers.errorText} handleAnalyticsEvent={onEditHandlers.handleAnalyticsEvent} + labelLink={labelOptions && labelOptions.linkTo} onCancel={onEditHandlers.onCancel} onEdit={onEditHandlers.onEdit} text={onEditHandlers.editableTextTitle} - {...linkProps} /> ); } diff --git a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx index 9ba6994f259..d06759879f8 100644 --- a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx +++ b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx @@ -81,7 +81,7 @@ const StyledSummary = styled(Grid2)(({ theme }) => ({ '&:last-child': { borderRight: 'none', }, - borderRight: `solid 1px ${theme.tokens.color.Neutrals[50]}`, + borderRight: 'solid 1px #9DA4A6', }, }, })); diff --git a/packages/manager/src/components/CodeBlock/CodeBlock.styles.ts b/packages/manager/src/components/CodeBlock/CodeBlock.styles.ts index fd2a2a325de..2308b4c5abf 100644 --- a/packages/manager/src/components/CodeBlock/CodeBlock.styles.ts +++ b/packages/manager/src/components/CodeBlock/CodeBlock.styles.ts @@ -18,20 +18,20 @@ export const StyledHighlightedMarkdown = styled(HighlightedMarkdown, { })(({ theme }) => ({ '& .hljs': { '& .hljs-literal, .hljs-built_in': { - color: theme.tokens.color.Yellow[5], + color: '#f8f8f2', }, '& .hljs-string': { - color: theme.tokens.color.Yellow[50], + color: '#e6db74', }, '& .hljs-symbol': { - color: theme.tokens.color.Yellow[5], + color: '#f8f8f2', }, '& .hljs-variable': { color: 'teal', }, - backgroundColor: theme.tokens.color.Neutrals[100], - color: theme.tokens.color.Yellow[5], + backgroundColor: '#32363b', + color: '#f8f8f2', padding: `${theme.spacing(4)} ${theme.spacing(2)}`, }, })); @@ -40,10 +40,10 @@ export const StyledCopyTooltip = styled(CopyTooltip, { label: 'StyledCopyTooltip', })(({ theme }) => ({ '& svg': { - color: theme.tokens.color.Green[60], + color: '#17CF73', }, '& svg:hover': { - color: theme.tokens.color.Green[70], + color: '#00B159', }, position: 'absolute', right: `${theme.spacing(1.5)}`, diff --git a/packages/manager/src/components/ColorPalette/ColorPalette.tsx b/packages/manager/src/components/ColorPalette/ColorPalette.tsx index 5d3501de3bf..9805c9a7867 100644 --- a/packages/manager/src/components/ColorPalette/ColorPalette.tsx +++ b/packages/manager/src/components/ColorPalette/ColorPalette.tsx @@ -13,22 +13,22 @@ interface Color { const useStyles = makeStyles()((theme: Theme) => ({ alias: { - color: theme.tokens.color.Neutrals[90], + color: '#32363c', fontFamily: '"UbuntuMono", monospace, sans-serif', fontSize: '0.875rem', }, color: { - color: theme.tokens.color.Neutrals[60], + color: '#888f91', fontFamily: '"UbuntuMono", monospace, sans-serif', fontSize: '0.875rem', }, root: { '& h2': { - color: theme.tokens.color.Neutrals[90], + color: '#32363c', }, }, swatch: { - border: `1px solid ${theme.tokens.color.Neutrals[60]}`, + border: '1px solid #888f91', borderRadius: 3, height: theme.spacing(4.5), margin: '0px 16px', diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx deleted file mode 100644 index 640fae75510..00000000000 --- a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import * as React from 'react'; - -import { DatePicker } from './DatePicker'; - -import type { Meta, StoryObj } from '@storybook/react'; -import type { DateTime } from 'luxon'; - -type Story = StoryObj; - -export const Default: Story = { - argTypes: { - errorText: { - control: 'text', - description: 'Error text to display below the input', - }, - format: { - control: 'text', - description: 'Format of the date when rendered in the input field', - }, - helperText: { - control: 'text', - description: 'Helper text to display below the input', - }, - label: { - control: 'text', - description: 'Label to display for the date picker input', - }, - onChange: { - action: 'date-changed', - description: 'Callback function fired when the value changes', - }, - placeholder: { - control: 'text', - description: 'Placeholder text for the date picker input', - }, - textFieldProps: { - control: 'object', - description: - 'Additional props to pass to the underlying TextField component', - }, - value: { - control: 'date', - description: 'The currently selected date', - }, - }, - args: { - errorText: '', - format: 'yyyy-MM-dd', - label: 'Select a Date', - onChange: action('date-changed'), - placeholder: 'yyyy-MM-dd', - textFieldProps: { label: 'Select a Date' }, - value: null, - }, -}; - -export const ControlledExample: Story = { - args: { - errorText: '', - format: 'yyyy-MM-dd', - helperText: 'This is a controlled DatePicker', - label: 'Controlled Date Picker', - placeholder: 'yyyy-MM-dd', - value: null, - }, - render: (args) => { - const ControlledDatePicker = () => { - const [selectedDate, setSelectedDate] = React.useState(); - - const handleChange = (newDate: DateTime | null) => { - setSelectedDate(newDate); - action('Controlled date change')(newDate?.toISO()); - }; - - return ( - - ); - }; - - return ; - }, -}; - -const meta: Meta = { - argTypes: { - errorText: { - control: 'text', - }, - format: { - control: 'text', - }, - helperText: { - control: 'text', - }, - label: { - control: 'text', - }, - onChange: { - action: 'date-changed', - }, - placeholder: { - control: 'text', - }, - textFieldProps: { - control: 'object', - }, - value: { - control: 'date', - }, - }, - args: { - errorText: '', - format: 'yyyy-MM-dd', - helperText: '', - label: 'Select a Date', - placeholder: 'yyyy-MM-dd', - value: null, - }, - component: DatePicker, - title: 'Components/DatePicker/DatePicker', -}; - -export default meta; diff --git a/packages/manager/src/components/DatePicker/DatePicker.test.tsx b/packages/manager/src/components/DatePicker/DatePicker.test.tsx deleted file mode 100644 index e051d160ec5..00000000000 --- a/packages/manager/src/components/DatePicker/DatePicker.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { DateTime } from 'luxon'; -import * as React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { DatePicker } from './DatePicker'; - -import type { DatePickerProps } from './DatePicker'; - -const props: DatePickerProps = { - onChange: vi.fn(), - placeholder: 'Pick a date', - textFieldProps: { errorText: 'Invalid date', label: 'Select a date' }, - value: null, -}; - -describe('DatePicker', () => { - it('should render the DatePicker component', () => { - renderWithTheme(); - const DatePickerField = screen.getByRole('textbox', { - name: 'Select a date', - }); - - expect(DatePickerField).toBeVisible(); - }); - - it('should handle value changes', async () => { - renderWithTheme(); - - const calendarButton = screen.getByRole('button', { name: 'Choose date' }); - - // Click the calendar button to open the date picker - await userEvent.click(calendarButton); - - // Find a date button to click (e.g., the 15th of the month) - const dateToSelect = screen.getByRole('gridcell', { name: '15' }); - await userEvent.click(dateToSelect); - - // Check if onChange was called after selecting a date - expect(props.onChange).toHaveBeenCalled(); - }); - - it('should display the error text when provided', () => { - renderWithTheme(); - const errorMessage = screen.getByText('Invalid date'); - expect(errorMessage).toBeVisible(); - }); - - it('should display the helper text when provided', () => { - renderWithTheme(); - const helperText = screen.getByText('Choose a valid date'); - expect(helperText).toBeVisible(); - }); - - it('should use the default format when no format is specified', () => { - renderWithTheme( - - ); - const datePickerField = screen.getByRole('textbox', { - name: 'Select a date', - }); - expect(datePickerField).toHaveValue('2024-10-25'); - }); - - it('should handle the custom format correctly', () => { - renderWithTheme( - - ); - const datePickerField = screen.getByRole('textbox', { - name: 'Select a date', - }); - expect(datePickerField).toHaveValue('25/10/2024'); - }); -}); diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx deleted file mode 100644 index 25fb95ff048..00000000000 --- a/packages/manager/src/components/DatePicker/DatePicker.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { TextField } from '@linode/ui'; -import { useTheme } from '@mui/material/styles'; -import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; -import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import React from 'react'; - -import type { TextFieldProps } from '@linode/ui'; -import type { DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers/DatePicker'; -import type { DateTime } from 'luxon'; - -export interface DatePickerProps - extends Omit, 'onChange' | 'value'> { - /** Error text to display below the input */ - errorText?: string; - /** Format of the date when rendered in the input field. */ - format?: string; - /** Helper text to display below the input */ - helperText?: string; - /** Label to display for the date picker input */ - label?: string; - /** Callback function fired when the value changes */ - onChange: (newDate: DateTime | null) => void; - /** Placeholder text for the date picker input */ - placeholder?: string; - /** Additional props to pass to the underlying TextField component */ - textFieldProps?: Omit; - /** The currently selected date */ - value?: DateTime | null; -} - -export const DatePicker = ({ - format = 'yyyy-MM-dd', - helperText = '', - label = 'Select a date', - onChange, - placeholder = 'Pick a date', - textFieldProps, - value = null, - ...props -}: DatePickerProps) => { - const theme = useTheme(); - - const onChangeHandler = (newDate: DateTime | null) => { - onChange(newDate); - }; - - return ( - - - - ); -}; diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx deleted file mode 100644 index 7df04ed26cd..00000000000 --- a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import * as React from 'react'; - -import { DateTimePicker } from './DateTimePicker'; - -import type { Meta, StoryObj } from '@storybook/react'; -import type { DateTime } from 'luxon'; - -type Story = StoryObj; - -export const ControlledExample: Story = { - args: { - label: 'Controlled Date-Time Picker', - onApply: action('Apply clicked'), - onCancel: action('Cancel clicked'), - placeholder: 'yyyy-MM-dd HH:mm', - showTime: true, - showTimeZone: true, - timeSelectProps: { - label: 'Select Time', - }, - timeZoneSelectProps: { - label: 'Timezone', - onChange: action('Timezone changed'), - }, - }, - render: (args) => { - const ControlledDateTimePicker = () => { - const [ - selectedDateTime, - setSelectedDateTime, - ] = React.useState(args.value || null); - - const handleChange = (newDateTime: DateTime | null) => { - setSelectedDateTime(newDateTime); - action('Controlled dateTime change')(newDateTime?.toISO()); - }; - - return ( - - ); - }; - - return ; - }, -}; - -export const DefaultExample: Story = { - args: { - label: 'Default Date-Time Picker', - onApply: action('Apply clicked'), - onCancel: action('Cancel clicked'), - onChange: action('Date-Time selected'), - placeholder: 'yyyy-MM-dd HH:mm', - showTime: true, - showTimeZone: true, - }, -}; - -export const WithErrorText: Story = { - args: { - errorText: 'This field is required', - label: 'Date-Time Picker with Error', - onApply: action('Apply clicked with error'), - onCancel: action('Cancel clicked with error'), - onChange: action('Date-Time selected with error'), - placeholder: 'yyyy-MM-dd HH:mm', - showTime: true, - showTimeZone: true, - }, -}; - -const meta: Meta = { - argTypes: { - dateCalendarProps: { - control: { type: 'object' }, - description: 'Additional props for the DateCalendar component.', - }, - errorText: { - control: { type: 'text' }, - description: 'Error text for the date picker field.', - }, - format: { - control: { type: 'text' }, - description: 'Format for displaying the date-time.', - }, - label: { - control: { type: 'text' }, - description: 'Label for the input field.', - }, - onApply: { - action: 'applyClicked', - description: 'Callback when the "Apply" button is clicked.', - }, - onCancel: { - action: 'cancelClicked', - description: 'Callback when the "Cancel" button is clicked.', - }, - onChange: { - action: 'dateTimeChanged', - description: 'Callback when the date-time changes.', - }, - placeholder: { - control: { type: 'text' }, - description: 'Placeholder text for the input field.', - }, - showTime: { - control: { type: 'boolean' }, - description: 'Whether to show the time selector.', - }, - showTimeZone: { - control: { type: 'boolean' }, - description: 'Whether to show the timezone selector.', - }, - sx: { - control: { type: 'object' }, - description: 'Styles to apply to the root element.', - }, - timeSelectProps: { - control: { type: 'object' }, - description: 'Props for customizing the TimePicker component.', - }, - timeZoneSelectProps: { - control: { type: 'object' }, - description: 'Props for customizing the TimeZoneSelect component.', - }, - value: { - control: { type: 'date' }, - description: 'Initial or controlled dateTime value.', - }, - }, - args: { - format: 'yyyy-MM-dd HH:mm', - label: 'Date-Time Picker', - placeholder: 'Select a date and time', - }, - component: DateTimePicker, - title: 'Components/DatePicker/DateTimePicker', -}; - -export default meta; diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx deleted file mode 100644 index 12f1795a747..00000000000 --- a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { DateTime } from 'luxon'; -import * as React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { DateTimePicker } from './DateTimePicker'; - -import type { DateTimePickerProps } from './DateTimePicker'; - -const defaultProps: DateTimePickerProps = { - label: 'Select Date and Time', - onApply: vi.fn(), - onCancel: vi.fn(), - onChange: vi.fn(), - placeholder: 'yyyy-MM-dd HH:mm', - value: DateTime.fromISO('2024-10-25T15:30:00'), -}; - -describe('DateTimePicker Component', () => { - it('should render the DateTimePicker component with the correct label and placeholder', () => { - renderWithTheme(); - const textField = screen.getByRole('textbox', { - name: 'Select Date and Time', - }); - expect(textField).toBeVisible(); - expect(textField).toHaveAttribute('placeholder', 'yyyy-MM-dd HH:mm'); - }); - - it('should open the Popover when the TextField is clicked', async () => { - renderWithTheme(); - const textField = screen.getByRole('textbox', { - name: 'Select Date and Time', - }); - await userEvent.click(textField); - expect(screen.getByRole('dialog')).toBeVisible(); // Verifying the Popover is open - }); - - it('should call onCancel when the Cancel button is clicked', async () => { - renderWithTheme(); - await userEvent.click( - screen.getByRole('textbox', { name: 'Select Date and Time' }) - ); - const cancelButton = screen.getByRole('button', { name: /Cancel/i }); - await userEvent.click(cancelButton); - expect(defaultProps.onCancel).toHaveBeenCalled(); - }); - - it('should call onApply when the Apply button is clicked', async () => { - renderWithTheme(); - await userEvent.click( - screen.getByRole('textbox', { name: 'Select Date and Time' }) - ); - const applyButton = screen.getByRole('button', { name: /Apply/i }); - await userEvent.click(applyButton); - expect(defaultProps.onApply).toHaveBeenCalled(); - expect(defaultProps.onChange).toHaveBeenCalledWith(expect.any(DateTime)); // Ensuring onChange was called with a DateTime object - }); - - it('should handle date changes correctly', async () => { - renderWithTheme(); - await userEvent.click( - screen.getByRole('textbox', { name: 'Select Date and Time' }) - ); - - // Simulate selecting a date (e.g., 15th of the month) - const dateButton = screen.getByRole('gridcell', { name: '15' }); - await userEvent.click(dateButton); - - // Check that the displayed value has been updated correctly (this assumes the date format) - expect(defaultProps.onChange).toHaveBeenCalled(); - }); - - it('should handle timezone changes correctly', async () => { - const timezoneChangeMock = vi.fn(); // Create a mock function - - const updatedProps = { - ...defaultProps, - timeZoneSelectProps: { onChange: timezoneChangeMock, value: 'UTC' }, - }; - - renderWithTheme(); - - await userEvent.click( - screen.getByRole('textbox', { name: 'Select Date and Time' }) - ); - - // Simulate selecting a timezone from the TimeZoneSelect - const timezoneInput = screen.getByPlaceholderText(/Choose a Timezone/i); - await userEvent.click(timezoneInput); - - // Select a timezone from the dropdown options - await userEvent.click( - screen.getByRole('option', { name: '(GMT -11:00) Niue Time' }) - ); - - // Click the Apply button to trigger the change - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Verify that the onChange function was called with the expected value - expect(timezoneChangeMock).toHaveBeenCalledWith('Pacific/Niue'); - }); - - it('should display the error text when provided', () => { - renderWithTheme( - - ); - expect(screen.getByText(/Invalid date-time/i)).toBeVisible(); - }); - - it('should format the date-time correctly when a custom format is provided', () => { - renderWithTheme( - - ); - const textField = screen.getByRole('textbox', { - name: 'Select Date and Time', - }); - - expect(textField).toHaveValue('25/10/2024 15:30'); - }); - it('should not render the time selector when showTime is false', async () => { - renderWithTheme(); - await userEvent.click( - screen.getByRole('textbox', { name: 'Select Date and Time' }) - ); - const timePicker = screen.queryByLabelText(/Select Time/i); // Label from timeSelectProps - expect(timePicker).not.toBeInTheDocument(); - }); - - it('should not render the timezone selector when showTimeZone is false', async () => { - renderWithTheme(); - await userEvent.click( - screen.getByRole('textbox', { name: 'Select Date and Time' }) - ); - const timeZoneSelect = screen.queryByLabelText(/Timezone/i); // Label from timeZoneSelectProps - expect(timeZoneSelect).not.toBeInTheDocument(); - }); -}); diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx deleted file mode 100644 index 86c66ee834a..00000000000 --- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { Divider } from '@linode/ui'; -import { InputAdornment, TextField } from '@linode/ui'; -import { Box } from '@linode/ui'; -import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; -import { Grid, Popover } from '@mui/material'; -import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; -import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { TimePicker } from '@mui/x-date-pickers/TimePicker'; -import React, { useEffect, useState } from 'react'; - -import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; - -import { TimeZoneSelect } from './TimeZoneSelect'; - -import type { TextFieldProps } from '@linode/ui'; -import type { SxProps, Theme } from '@mui/material/styles'; -import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar'; -import type { DateTime } from 'luxon'; - -export interface DateTimePickerProps { - /** Additional props for the DateCalendar */ - dateCalendarProps?: Partial>; - /** Error text for the date picker field */ - errorText?: string; - /** Format for displaying the date-time */ - format?: string; - /** Label for the input field */ - label?: string; - /** Callback when the "Apply" button is clicked */ - onApply?: () => void; - /** Callback when the "Cancel" button is clicked */ - onCancel?: () => void; - /** Callback when date-time changes */ - onChange: (dateTime: DateTime | null) => void; - /** Placeholder text for the input field */ - placeholder?: string; - /** Whether to show the time selector */ - showTime?: boolean; - /** Whether to show the timezone selector */ - showTimeZone?: boolean; - /** - * Any additional styles to apply to the root element. - */ - sx?: SxProps; - /** Props for customizing the TimePicker component */ - timeSelectProps?: { - label?: string; - onChange?: (time: null | string) => void; - value?: null | string; - }; - /** Props for customizing the TimeZoneSelect component */ - timeZoneSelectProps?: { - label?: string; - onChange?: (timezone: string) => void; - value?: null | string; - }; - /** Initial or controlled dateTime value */ - value?: DateTime | null; -} - -export const DateTimePicker = ({ - dateCalendarProps = {}, - errorText = '', - format = 'yyyy-MM-dd HH:mm', - label = 'Select Date and Time', - onApply, - onCancel, - onChange, - placeholder = 'Select Date', - showTime = true, - showTimeZone = true, - sx, - timeSelectProps = {}, - timeZoneSelectProps = {}, - value = null, -}: DateTimePickerProps) => { - const [anchorEl, setAnchorEl] = useState(null); - - // Current and original states - const [selectedDateTime, setSelectedDateTime] = useState( - value - ); - const [selectedTimeZone, setSelectedTimeZone] = useState( - timeZoneSelectProps.value || null - ); - - const [originalDateTime, setOriginalDateTime] = useState( - value - ); - const [originalTimeZone, setOriginalTimeZone] = useState( - timeZoneSelectProps.value || null - ); - - const TimePickerFieldProps: TextFieldProps = { - label: timeSelectProps?.label ?? 'Select Time', - noMarginTop: true, - }; - - const handleDateChange = (newDate: DateTime | null) => { - setSelectedDateTime((prev) => - newDate - ? newDate.set({ - hour: prev?.hour || 0, - minute: prev?.minute || 0, - }) - : null - ); - }; - - const handleTimeChange = (newTime: DateTime | null) => { - if (newTime) { - setSelectedDateTime((prev) => - prev ? prev.set({ hour: newTime.hour, minute: newTime.minute }) : prev - ); - } - }; - - const handleTimeZoneChange = (newTimeZone: string) => { - setSelectedTimeZone(newTimeZone); - if (timeZoneSelectProps.onChange) { - timeZoneSelectProps.onChange(newTimeZone); - } - }; - - const handleApply = () => { - setAnchorEl(null); - setOriginalDateTime(selectedDateTime); - setOriginalTimeZone(selectedTimeZone); - onChange(selectedDateTime); - - if (onApply) { - onApply(); - } - }; - - const handleClose = () => { - setAnchorEl(null); - setSelectedDateTime(originalDateTime); - setSelectedTimeZone(originalTimeZone); - - if (onCancel) { - onCancel(); - } - }; - - useEffect(() => { - if (timeZoneSelectProps.value) { - setSelectedTimeZone(timeZoneSelectProps.value); - } - }, [timeZoneSelectProps.value]); - - return ( - - - - - - ), - sx: { paddingLeft: '32px' }, - }} - value={ - selectedDateTime - ? `${selectedDateTime.toFormat(format)}${ - selectedTimeZone ? ` (${selectedTimeZone})` : '' - }` - : '' - } - errorText={errorText} - label={label} - noMarginTop - onClick={(event) => setAnchorEl(event.currentTarget)} - placeholder={placeholder} - /> - - - - ({ - '& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': { - justifyContent: 'space-between', - }, - '& .MuiDayCalendar-weekDayLabel': { - fontSize: '0.875rem', - }, - '& .MuiPickersCalendarHeader-label': { - fontFamily: theme.font.bold, - }, - '& .MuiPickersCalendarHeader-root': { - borderBottom: `1px solid ${theme.borderColors.divider}`, - fontSize: '0.875rem', - paddingBottom: theme.spacing(1), - }, - '& .MuiPickersDay-root': { - fontSize: '0.875rem', - margin: `${theme.spacing(0.5)}px`, - }, - borderRadius: `${theme.spacing(2)}`, - borderWidth: '0px', - })} - /> - - {showTime && ( - - ({ - justifyContent: 'center', - marginBottom: theme.spacing(1 / 2), - marginTop: theme.spacing(1 / 2), - padding: 0, - }), - }, - layout: { - sx: (theme: Theme) => ({ - '& .MuiPickersLayout-contentWrapper': { - borderBottom: `1px solid ${theme.borderColors.divider}`, - }, - border: `1px solid ${theme.borderColors.divider}`, - }), - }, - openPickerButton: { - sx: { padding: 0 }, - }, - popper: { - sx: (theme: Theme) => ({ - ul: { - borderColor: `${theme.borderColors.divider} !important`, - }, - }), - }, - textField: TimePickerFieldProps, - }} - onChange={handleTimeChange} - slots={{ textField: TextField }} - value={selectedDateTime || null} - /> - - )} - {showTimeZone && ( - - - - )} - - - - - ({ - marginBottom: theme.spacing(1), - marginRight: theme.spacing(2), - })} - primaryButtonProps={{ label: 'Apply', onClick: handleApply }} - /> - - - - ); -}; diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx deleted file mode 100644 index aeaecd516d0..00000000000 --- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { DateTime } from 'luxon'; -import * as React from 'react'; - -import { DateTimeRangePicker } from './DateTimeRangePicker'; - -import type { Meta, StoryObj } from '@storybook/react'; - -type Story = StoryObj; - -export const Default: Story = { - args: { - endDateProps: { - errorMessage: '', - label: 'End Date and Time', - placeholder: '', - showTimeZone: false, - value: null, - }, - format: 'yyyy-MM-dd HH:mm', - onChange: action('DateTime range changed'), - presetsProps: { - defaultValue: { label: '', value: '' }, - enablePresets: true, - label: '', - placeholder: '', - }, - startDateProps: { - errorMessage: '', - label: 'Start Date and Time', - placeholder: '', - showTimeZone: true, - timeZoneValue: null, - value: null, - }, - sx: {}, - }, - render: (args) => , -}; - -export const WithInitialValues: Story = { - args: { - endDateProps: { - label: 'End Date and Time', - showTimeZone: true, - value: DateTime.now(), - }, - format: 'yyyy-MM-dd HH:mm', - onChange: action('DateTime range changed'), - presetsProps: { - defaultValue: { label: 'Last 7 Days', value: '7days' }, - enablePresets: true, - label: 'Time Range', - placeholder: 'Select Range', - }, - startDateProps: { - label: 'Start Date and Time', - showTimeZone: true, - timeZoneValue: 'America/New_York', - value: DateTime.now().minus({ days: 1 }), - }, - sx: {}, - }, -}; - -export const WithCustomErrors: Story = { - args: { - endDateProps: { - errorMessage: 'End date must be after the start date.', - label: 'Custom End Label', - placeholder: '', - showTimeZone: false, - value: DateTime.now().minus({ days: 1 }), - }, - format: 'yyyy-MM-dd HH:mm', - onChange: action('DateTime range changed'), - presetsProps: { - defaultValue: { label: '', value: '' }, - enablePresets: true, - label: '', - placeholder: '', - }, - startDateProps: { - errorMessage: 'Start date must be before the end date.', - label: 'Start Date and Time', - placeholder: '', - showTimeZone: true, - timeZoneValue: null, - value: DateTime.now().minus({ days: 2 }), - }, - }, -}; - -const meta: Meta = { - argTypes: { - endDateProps: { - errorMessage: { - control: 'text', - description: 'Custom error message for invalid end date', - }, - label: { - control: 'text', - description: 'Custom label for the end date-time picker', - }, - placeholder: { - control: 'text', - description: 'Placeholder for the end date-time', - }, - showTimeZone: { - control: 'boolean', - description: - 'Whether to show the timezone selector for the end date picker', - }, - value: { - control: 'date', - description: 'Initial or controlled value for the end date-time', - }, - }, - format: { - control: 'text', - description: 'Format for displaying the date-time', - }, - onChange: { - action: 'DateTime range changed', - description: 'Callback when the date-time range changes', - }, - presetsProps: { - defaultValue: { - label: { - control: 'text', - description: 'Default value label for the presets field', - }, - value: { - control: 'text', - description: 'Default value for the presets field', - }, - }, - enablePresets: { - control: 'boolean', - description: - 'If true, shows the date presets field instead of the date pickers', - }, - label: { - control: 'text', - description: 'Label for the presets dropdown', - }, - placeholder: { - control: 'text', - description: 'Placeholder for the presets dropdown', - }, - }, - startDateProps: { - errorMessage: { - control: 'text', - description: 'Custom error message for invalid start date', - }, - placeholder: { - control: 'text', - description: 'Placeholder for the start date-time', - }, - showTimeZone: { - control: 'boolean', - description: - 'Whether to show the timezone selector for the start date picker', - }, - startLabel: { - control: 'text', - description: 'Custom label for the start date-time picker', - }, - timeZoneValue: { - control: 'text', - description: 'Initial or controlled value for the start timezone', - }, - value: { - control: 'date', - description: 'Initial or controlled value for the start date-time', - }, - }, - sx: { - control: 'object', - description: 'Styles to apply to the root element', - }, - }, - args: { - endDateProps: { label: 'End Date and Time' }, - format: 'yyyy-MM-dd HH:mm', - startDateProps: { label: 'Start Date and Time' }, - }, - component: DateTimeRangePicker, - title: 'Components/DatePicker/DateTimeRangePicker', -}; - -export default meta; diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx deleted file mode 100644 index 3dc542f4c36..00000000000 --- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx +++ /dev/null @@ -1,354 +0,0 @@ -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { DateTime } from 'luxon'; -import * as React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { DateTimeRangePicker } from './DateTimeRangePicker'; - -import type { DateTimeRangePickerProps } from './DateTimeRangePicker'; - -const onChangeMock = vi.fn(); - -const Props: DateTimeRangePickerProps = { - endDateProps: { - label: 'End Date and Time', - }, - onChange: onChangeMock, - presetsProps: { - enablePresets: true, - label: 'Date Presets', - }, - - startDateProps: { - label: 'Start Date and Time', - }, -}; - -describe('DateTimeRangePicker Component', () => { - beforeEach(() => { - // Mock DateTime.now to return a fixed datetime - const fixedNow = DateTime.fromISO( - '2024-12-18T00:28:27.071-06:00' - ).toUTC() as DateTime; - vi.setSystemTime(fixedNow.toJSDate()); - }); - - afterEach(() => { - // Restore the original DateTime.now implementation after each test - vi.restoreAllMocks(); - vi.clearAllMocks(); - }); - - it('should render start and end DateTimePickers with correct labels', () => { - renderWithTheme(); - - expect(screen.getByLabelText('Start Date and Time')).toBeVisible(); - expect(screen.getByLabelText('End Date and Time')).toBeVisible(); - }); - - it('should call onChange when start date is changed', async () => { - vi.setSystemTime(vi.getRealSystemTime()); - - renderWithTheme(); - - // Open start date picker - await userEvent.click(screen.getByLabelText('Start Date and Time')); - - await userEvent.click(screen.getByRole('gridcell', { name: '10' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - const expectedStartTime = DateTime.fromObject({ - day: 10, - month: DateTime.now().month, - year: DateTime.now().year, - }).toISO(); - - // Check if the onChange function is called with the expected value - expect(onChangeMock).toHaveBeenCalledWith({ - end: null, - preset: 'custom_range', - start: expectedStartTime, - timeZone: null, - }); - }); - - it('should show error when end date-time is before start date-time', async () => { - renderWithTheme(); - - // Set start date-time to the 15th - const startDateField = screen.getByLabelText('Start Date and Time'); - await userEvent.click(startDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '15' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Open the end date picker - const endDateField = screen.getByLabelText('End Date and Time'); - await userEvent.click(endDateField); - - // Set start date-time to the 10th - await userEvent.click(screen.getByRole('gridcell', { name: '10' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Confirm error message is displayed - expect( - screen.getByText('End date/time cannot be before the start date/time.') - ).toBeInTheDocument(); - }); - - it('should show error when start date-time is after end date-time', async () => { - const updateProps = { - ...Props, - presetsProps: { ...Props.presetsProps, enablePresets: false }, - }; - renderWithTheme(); - - // Set the end date-time to the 15th - const endDateField = screen.getByLabelText('End Date and Time'); - await userEvent.click(endDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '15' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Set the start date-time to the 10th (which is earlier than the end date-time) - const startDateField = screen.getByLabelText('Start Date and Time'); - await userEvent.click(startDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Confirm the error message is displayed - expect( - screen.getByText('Start date/time cannot be after the end date/time.') - ).toBeInTheDocument(); - }); - - it('should display custom error messages when start date-time is after end date-time', async () => { - const updatedProps = { - ...Props, - endDateProps: { - ...Props.endDateProps, - errorMessage: 'Custom end date error', - label: 'End Date and Time', - }, - presetsProps: {}, - startDateProps: { - ...Props.startDateProps, - errorMessage: 'Custom start date error', - label: 'Start Date and Time', - }, - }; - renderWithTheme(); - - // Set the end date-time to the 15th - const endDateField = screen.getByLabelText('End Date and Time'); - await userEvent.click(endDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '15' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Set the start date-time to the 20th (which is earlier than the end date-time) - const startDateField = screen.getByLabelText('Start Date and Time'); - await userEvent.click(startDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Confirm the custom error message is displayed for the start date - expect(screen.getByText('Custom start date error')).toBeInTheDocument(); - }); - - it('should set the date range for the last 24 hours when the "Last 24 Hours" preset is selected', async () => { - renderWithTheme(); - - // Open the presets dropdown - const presetsDropdown = screen.getByLabelText('Date Presets'); - await userEvent.click(presetsDropdown); - - // Select the "Last 24 Hours" option - const last24HoursOption = screen.getByText('Last 24 Hours'); - await userEvent.click(last24HoursOption); - - // Expected start and end dates in ISO format - const expectedStartDateISO = DateTime.now().minus({ hours: 24 }).toISO(); // 2024-12-17T00:28:27.071-06:00 - const expectedEndDateISO = DateTime.now().toISO(); // 2024-12-18T00:28:27.071-06:00 - - // Verify onChangeMock was called with correct ISO strings - expect(onChangeMock).toHaveBeenCalledWith({ - end: expectedEndDateISO, - preset: '24hours', - start: expectedStartDateISO, - timeZone: null, - }); - expect( - screen.queryByRole('button', { name: 'Presets' }) - ).not.toBeInTheDocument(); - }); - - it('should set the date range for the last 7 days when the "Last 7 Days" preset is selected', async () => { - renderWithTheme(); - - // Open the presets dropdown - const presetsDropdown = screen.getByLabelText('Date Presets'); - await userEvent.click(presetsDropdown); - - // Select the "Last 7 Days" option - const last7DaysOption = screen.getByText('Last 7 Days'); - await userEvent.click(last7DaysOption); - - // Expected start and end dates in ISO format - const expectedStartDateISO = DateTime.now().minus({ days: 7 }).toISO(); - const expectedEndDateISO = DateTime.now().toISO(); - - // Verify that onChange is called with the correct date range - expect(onChangeMock).toHaveBeenCalledWith({ - end: expectedEndDateISO, - preset: '7days', - start: expectedStartDateISO, - timeZone: null, - }); - expect( - screen.queryByRole('button', { name: 'Presets' }) - ).not.toBeInTheDocument(); - }); - - it('should set the date range for the last 30 days when the "Last 30 Days" preset is selected', async () => { - renderWithTheme(); - - // Open the presets dropdown - const presetsDropdown = screen.getByLabelText('Date Presets'); - await userEvent.click(presetsDropdown); - - // Select the "Last 30 Days" option - const last30DaysOption = screen.getByText('Last 30 Days'); - await userEvent.click(last30DaysOption); - - // Expected start and end dates in ISO format - const expectedStartDateISO = DateTime.now().minus({ days: 30 }).toISO(); - const expectedEndDateISO = DateTime.now().toISO(); - - // Verify that onChange is called with the correct date range - expect(onChangeMock).toHaveBeenCalledWith({ - end: expectedEndDateISO, - preset: '30days', - start: expectedStartDateISO, - timeZone: null, - }); - expect( - screen.queryByRole('button', { name: 'Presets' }) - ).not.toBeInTheDocument(); - }); - - it('should set the date range for this month when the "This Month" preset is selected', async () => { - renderWithTheme(); - - // Open the presets dropdown - const presetsDropdown = screen.getByLabelText('Date Presets'); - await userEvent.click(presetsDropdown); - - // Select the "This Month" option - const thisMonthOption = screen.getByText('This Month'); - await userEvent.click(thisMonthOption); - - // Expected start and end dates in ISO format - const expectedStartDateISO = DateTime.now().startOf('month').toISO(); - const expectedEndDateISO = DateTime.now().endOf('month').toISO(); - - // Verify that onChange is called with the correct date range - expect(onChangeMock).toHaveBeenCalledWith({ - end: expectedEndDateISO, - preset: 'this_month', - start: expectedStartDateISO, - timeZone: null, - }); - expect( - screen.queryByRole('button', { name: 'Presets' }) - ).not.toBeInTheDocument(); - }); - - it('should set the date range for last month when the "Last Month" preset is selected', async () => { - renderWithTheme(); - - // Open the presets dropdown - const presetsDropdown = screen.getByLabelText('Date Presets'); - await userEvent.click(presetsDropdown); - - // Select the "Last Month" option - const lastMonthOption = screen.getByText('Last Month'); - await userEvent.click(lastMonthOption); - - const lastMonth = DateTime.now().minus({ months: 1 }); - - // Expected start and end dates in ISO format - const expectedStartDateISO = lastMonth.startOf('month').toISO(); - const expectedEndDateISO = lastMonth.endOf('month').toISO(); - - // Verify that onChange is called with the correct date range - expect(onChangeMock).toHaveBeenCalledWith({ - end: expectedEndDateISO, - preset: 'last_month', - start: expectedStartDateISO, - timeZone: null, - }); - expect( - screen.queryByRole('button', { name: 'Presets' }) - ).not.toBeInTheDocument(); - }); - - it('should display the date range fields with empty values when the "Custom Range" preset is selected', async () => { - renderWithTheme(); - - // Open the presets dropdown - const presetsDropdown = screen.getByLabelText('Date Presets'); - await userEvent.click(presetsDropdown); - - // Select the "Custom Range" option - const customRange = screen.getByText('Custom'); - await userEvent.click(customRange); - - // Verify the input fields display the correct values - expect( - screen.getByRole('textbox', { name: 'Start Date and Time' }) - ).toHaveValue(''); - expect( - screen.getByRole('textbox', { name: 'End Date and Time' }) - ).toHaveValue(''); - expect(screen.getByRole('button', { name: 'Presets' })).toBeInTheDocument(); - - // Set start date-time to the 15th - const startDateField = screen.getByLabelText('Start Date and Time'); - await userEvent.click(startDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '15' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Open the end date picker - const endDateField = screen.getByLabelText('End Date and Time'); - await userEvent.click(endDateField); - - // Set start date-time to the 12th - await userEvent.click(screen.getByRole('gridcell', { name: '12' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Confirm error message is shown since the click was blocked - expect( - screen.getByText('End date/time cannot be before the start date/time.') - ).toBeInTheDocument(); - - // Set start date-time to the 11th - await userEvent.click(startDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '11' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Confirm error message is not displayed - expect( - screen.queryByText('End date/time cannot be before the start date/time.') - ).not.toBeInTheDocument(); - - // Set start date-time to the 20th - await userEvent.click(startDateField); - await userEvent.click(screen.getByRole('gridcell', { name: '20' })); - await userEvent.click(screen.getByRole('button', { name: 'Apply' })); - - // Confirm error message is not displayed - expect( - screen.queryByText('Start date/time cannot be after the end date/time.') - ).toBeInTheDocument(); - }); -}); diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx deleted file mode 100644 index 7083ba7bee8..00000000000 --- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { Autocomplete, Box, StyledActionButton } from '@linode/ui'; -import { useTheme } from '@mui/material'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { DateTime } from 'luxon'; -import React, { useState } from 'react'; - -import { DateTimePicker } from './DateTimePicker'; - -import type { SxProps, Theme } from '@mui/material/styles'; - -export interface DateTimeRangePickerProps { - /** Properties for the end date field */ - endDateProps?: { - /** Custom error message for invalid end date */ - errorMessage?: string; - /** Label for the end date field */ - label?: string; - /** placeholder for the end date field */ - placeholder?: string; - /** Whether to show the timezone selector for the end date */ - showTimeZone?: boolean; - /** Initial or controlled value for the end date-time */ - value?: DateTime | null; - }; - - /** Format for displaying the date-time */ - format?: string; - - /** Callback when the date-time range changes, - * this returns start date, end date in ISO formate, - * preset value and timezone - * */ - onChange?: (params: { - end: null | string; - preset?: string; - start: null | string; - timeZone?: null | string; - }) => void; - - /** Additional settings for the presets dropdown */ - presetsProps?: { - /** Default value for the presets field */ - defaultValue?: { label: string; value: string }; - /** If true, shows the date presets field instead of the date pickers */ - enablePresets?: boolean; - /** Label for the presets field */ - label?: string; - /** placeholder for the presets field */ - placeholder?: string; - }; - - /** Properties for the start date field */ - startDateProps?: { - /** Custom error message for invalid start date */ - errorMessage?: string; - /** Label for the start date field */ - label?: string; - /** placeholder for the start date field */ - placeholder?: string; - /** Whether to show the timezone selector for the start date */ - showTimeZone?: boolean; - /** Initial or controlled value for the start timezone */ - timeZoneValue?: null | string; - /** Initial or controlled value for the start date-time */ - value?: DateTime | null; - }; - - /** Any additional styles to apply to the root element */ - sx?: SxProps; -} - -type DatePresetType = - | '7days' - | '24hours' - | '30days' - | 'custom_range' - | 'last_month' - | 'this_month'; - -const presetsOptions: { label: string; value: DatePresetType }[] = [ - { label: 'Last 24 Hours', value: '24hours' }, - { label: 'Last 7 Days', value: '7days' }, - { label: 'Last 30 Days', value: '30days' }, - { label: 'This Month', value: 'this_month' }, - { label: 'Last Month', value: 'last_month' }, - { label: 'Custom', value: 'custom_range' }, -]; - -export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => { - const { - endDateProps: { - errorMessage: endDateErrorMessage = 'End date/time cannot be before the start date/time.', - label: endLabel = 'End Date and Time', - placeholder: endDatePlaceholder, - showTimeZone: showEndTimeZone = false, - value: endDateTimeValue = null, - } = {}, - - format = 'yyyy-MM-dd HH:mm', - - onChange, - - presetsProps: { - defaultValue: presetsDefaultValue = { label: '', value: '' }, - enablePresets = false, - label: presetsLabel = 'Time Range', - placeholder: presetsPlaceholder = 'Select a preset', - } = {}, - startDateProps: { - errorMessage: startDateErrorMessage = 'Start date/time cannot be after the end date/time.', - label: startLabel = 'Start Date and Time', - placeholder: startDatePlaceholder, - showTimeZone: showStartTimeZone = false, - timeZoneValue: startTimeZoneValue = null, - value: startDateTimeValue = null, - } = {}, - sx, - } = props; - - const [startDateTime, setStartDateTime] = useState( - startDateTimeValue - ); - const [endDateTime, setEndDateTime] = useState( - endDateTimeValue - ); - const [presetValue, setPresetValue] = useState<{ - label: string; - value: string; - }>(presetsDefaultValue); - const [startTimeZone, setStartTimeZone] = useState( - startTimeZoneValue - ); - const [startDateError, setStartDateError] = useState(null); - const [endDateError, setEndDateError] = useState(null); - const [showPresets, setShowPresets] = useState(enablePresets); - - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - - const validateDates = ( - start: DateTime | null, - end: DateTime | null, - source: 'end' | 'start' - ) => { - if (start && end) { - if (source === 'start' && start > end) { - setStartDateError(startDateErrorMessage); - return; - } - if (source === 'end' && end < start) { - setEndDateError(endDateErrorMessage); - return; - } - } - // Reset validation errors - setStartDateError(null); - setEndDateError(null); - }; - - const handlePresetSelection = (value: DatePresetType) => { - const now = DateTime.now(); - let newStartDateTime: DateTime | null = null; - let newEndDateTime: DateTime | null = null; - - switch (value) { - case '24hours': - newStartDateTime = now.minus({ hours: 24 }); - newEndDateTime = now; - break; - case '7days': - newStartDateTime = now.minus({ days: 7 }); - newEndDateTime = now; - break; - case '30days': - newStartDateTime = now.minus({ days: 30 }); - newEndDateTime = now; - break; - case 'this_month': - newStartDateTime = now.startOf('month'); - newEndDateTime = now.endOf('month'); - break; - case 'last_month': - const lastMonth = now.minus({ months: 1 }); - newStartDateTime = lastMonth.startOf('month'); - newEndDateTime = lastMonth.endOf('month'); - break; - case 'custom_range': - newStartDateTime = null; - newEndDateTime = null; - break; - default: - return; - } - - setStartDateTime(newStartDateTime); - setEndDateTime(newEndDateTime); - setPresetValue( - presetsOptions.find((option) => option.value === value) ?? - presetsDefaultValue - ); - - if (onChange) { - onChange({ - end: newEndDateTime?.toISO() ?? null, - preset: value, - start: newStartDateTime?.toISO() ?? null, - timeZone: startTimeZone, - }); - } - - setShowPresets(value !== 'custom_range'); - }; - - const handleStartDateTimeChange = (newStart: DateTime | null) => { - setStartDateTime(newStart); - validateDates(newStart, endDateTime, 'start'); - - if (onChange) { - onChange({ - end: endDateTime?.toISO() ?? null, - preset: 'custom_range', - start: newStart?.toISO() ?? null, - timeZone: startTimeZone, - }); - } - }; - - const handleEndDateTimeChange = (newEnd: DateTime | null) => { - setEndDateTime(newEnd); - validateDates(startDateTime, newEnd, 'end'); - - if (onChange) { - onChange({ - end: newEnd?.toISO() ?? null, - preset: 'custom_range', - start: startDateTime?.toISO() ?? null, - timeZone: startTimeZone, - }); - } - }; - - return ( - - {showPresets ? ( - { - if (selection) { - handlePresetSelection(selection.value as DatePresetType); - } - }} - defaultValue={presetsDefaultValue} - disableClearable - fullWidth - label={presetsLabel} - noMarginTop - options={presetsOptions} - placeholder={presetsPlaceholder} - value={presetValue} - /> - ) : ( - - setStartTimeZone(value), - value: startTimeZone, - }} - errorText={startDateError ?? undefined} - format={format} - label={startLabel} - onChange={handleStartDateTimeChange} - placeholder={startDatePlaceholder} - showTimeZone={showStartTimeZone} - timeSelectProps={{ label: 'Start Time' }} - value={startDateTime} - /> - - - { - setShowPresets(true); - setPresetValue(presetsDefaultValue); - }} - variant="text" - > - Presets - - - - )} - - ); -}; diff --git a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx deleted file mode 100644 index f4bd68c97a3..00000000000 --- a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Autocomplete } from '@linode/ui'; -import { DateTime } from 'luxon'; -import React from 'react'; - -import { timezones } from 'src/assets/timezones/timezones'; - -type Timezone = typeof timezones[number]; - -interface TimeZoneSelectProps { - disabled?: boolean; - errorText?: string; - label?: string; - noMarginTop?: boolean; - onChange: (timezone: string) => void; - value: null | string; -} - -const getOptionLabel = ({ label, offset }: Timezone) => { - const minutes = (Math.abs(offset) % 60).toLocaleString(undefined, { - minimumIntegerDigits: 2, - useGrouping: false, - }); - const hours = Math.floor(Math.abs(offset) / 60); - const isPositive = Math.abs(offset) === offset ? '+' : '-'; - - return `(GMT ${isPositive}${hours}:${minutes}) ${label}`; -}; - -const getTimezoneOptions = () => { - return timezones - .map((tz) => { - const offset = DateTime.now().setZone(tz.name).offset; - const label = getOptionLabel({ ...tz, offset }); - return { label, offset, value: tz.name }; - }) - .sort((a, b) => a.offset - b.offset); -}; - -const timezoneOptions = getTimezoneOptions(); - -export const TimeZoneSelect = ({ - disabled = false, - errorText, - label = 'Timezone', - noMarginTop = false, - onChange, - value, -}: TimeZoneSelectProps) => { - return ( - option.value === value) ?? undefined - } - autoHighlight - disabled={disabled} - errorText={errorText} - label={label} - noMarginTop={noMarginTop} - onChange={(e, option) => onChange(option?.value || '')} - options={timezoneOptions} - placeholder="Choose a Timezone" - /> - ); -}; diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx index 278748ecc43..e9372ce1eea 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx @@ -33,6 +33,9 @@ const meta: Meta = { open: { description: 'Is the modal open?', }, + typeToConfirm: { + description: `Whether or not a user is required to type the enity's label to delete.`, + }, }, args: { disableAutoFocus: true, @@ -49,6 +52,7 @@ const meta: Meta = { onDelete: action('onDelete'), open: true, style: { position: 'unset' }, + typeToConfirm: true, }, component: DeletionDialog, title: 'Components/Dialog/DeletionDialog', diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx index ad5747dda1d..c1385003852 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx @@ -6,25 +6,6 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { DeletionDialog } from './DeletionDialog'; import type { DeletionDialogProps } from './DeletionDialog'; -import type { ManagerPreferences } from 'src/types/ManagerPreferences'; - -const preference: ManagerPreferences['type_to_confirm'] = true; - -const queryMocks = vi.hoisted(() => ({ - usePreferences: vi.fn().mockReturnValue({}), -})); - -vi.mock('src/queries/profile/preferences', async () => { - const actual = await vi.importActual('src/queries/profile/preferences'); - return { - ...actual, - usePreferences: queryMocks.usePreferences, - }; -}); - -queryMocks.usePreferences.mockReturnValue({ - data: preference, -}); describe('DeletionDialog', () => { const defaultArgs: DeletionDialogProps = { @@ -91,21 +72,12 @@ describe('DeletionDialog', () => { }); it('should call onDelete when the DeletionDialog delete button is clicked', () => { - queryMocks.usePreferences.mockReturnValue({ - data: preference, - }); const { getByTestId } = renderWithTheme( ); const deleteButton = getByTestId('confirm'); - expect(deleteButton).toBeDisabled(); - - const input = getByTestId('textfield-input'); - fireEvent.change(input, { target: { value: defaultArgs.label } }); - - expect(deleteButton).toBeEnabled(); - + expect(deleteButton).not.toBeDisabled(); fireEvent.click(deleteButton); expect(defaultArgs.onDelete).toHaveBeenCalled(); @@ -156,12 +128,12 @@ describe('DeletionDialog', () => { ])( 'should %s input field with label when typeToConfirm is %s', (_, typeToConfirm) => { - queryMocks.usePreferences.mockReturnValue({ - data: typeToConfirm, - }); - const { queryByTestId } = renderWithTheme( - + ); if (typeToConfirm) { diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx index efcedc42a6f..93f1c2bcdf9 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx @@ -19,8 +19,15 @@ export interface DeletionDialogProps extends Omit { onClose: () => void; onDelete: () => void; open: boolean; + typeToConfirm?: boolean; } +/** + * A Deletion Dialog is used for deleting entities such as Linodes, NodeBalancers, Volumes, or other entities. + * + * Require `typeToConfirm` when an action would have a significant negative impact if done in error, consider requiring the user to enter a unique identifier such as entity label before activating the action button. + * If a user has opted out of type-to-confirm this will be ignored + */ export const DeletionDialog = React.memo((props: DeletionDialogProps) => { const theme = useTheme(); const { @@ -31,21 +38,18 @@ export const DeletionDialog = React.memo((props: DeletionDialogProps) => { onClose, onDelete, open, + typeToConfirm, ...rest } = props; - - const { data: typeToConfirmPreference } = usePreferences( - (preferences) => preferences?.type_to_confirm ?? true - ); - + const { data: preferences } = usePreferences(); const [confirmationText, setConfirmationText] = React.useState(''); - + const typeToConfirmRequired = + typeToConfirm && preferences?.type_to_confirm !== false; const renderActions = () => ( { onChange={(input) => { setConfirmationText(input); }} - expand label={`${capitalize(entity)} Name:`} placeholder={label} value={confirmationText} - visible={Boolean(typeToConfirmPreference)} + visible={typeToConfirmRequired} /> ); diff --git a/packages/manager/src/components/Encryption/constants.tsx b/packages/manager/src/components/Encryption/constants.tsx index 7224e491364..0c41c74573c 100644 --- a/packages/manager/src/components/Encryption/constants.tsx +++ b/packages/manager/src/components/Encryption/constants.tsx @@ -49,6 +49,9 @@ export const DISK_ENCRYPTION_DESCRIPTION_NODE_POOL_REBUILD_CAVEAT = export const DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY = 'Virtual Machine Backups are not encrypted.'; +export const DISK_ENCRYPTION_IMAGES_CAVEAT_COPY = + 'Virtual Machine Images are not encrypted.'; + export const ENCRYPT_DISK_DISABLED_REBUILD_LKE_REASON = 'The Encrypt Disk setting cannot be changed for a Linode attached to a node pool.'; diff --git a/packages/manager/src/components/EnhancedSelect/Select.styles.ts b/packages/manager/src/components/EnhancedSelect/Select.styles.ts index 99a45b34250..3d1b2d4260f 100644 --- a/packages/manager/src/components/EnhancedSelect/Select.styles.ts +++ b/packages/manager/src/components/EnhancedSelect/Select.styles.ts @@ -90,7 +90,7 @@ export const useStyles = makeStyles()((theme: Theme) => ({ cursor: 'text', }, '&--is-focused, &--is-focused:hover': { - border: `1px dotted ${theme.tokens.color.Neutrals[50]}`, + border: `1px dotted #999`, }, backgroundColor: theme.bg.white, border: `1px solid transparent`, @@ -294,7 +294,7 @@ export const reactSelectStyles = (theme: Theme) => ({ cursor: 'text', }, '&--is-focused, &--is-focused:hover': { - border: `1px dotted ${theme.tokens.color.Neutrals[50]}`, + border: `1px dotted #999`, }, backgroundColor: theme.bg.white, border: `1px solid transparent`, diff --git a/packages/manager/src/components/IconTextLink/IconTextLink.tsx b/packages/manager/src/components/IconTextLink/IconTextLink.tsx index 0a19f86edae..8a2f79ebf7a 100644 --- a/packages/manager/src/components/IconTextLink/IconTextLink.tsx +++ b/packages/manager/src/components/IconTextLink/IconTextLink.tsx @@ -8,14 +8,14 @@ import type { SvgIcon } from 'src/components/SvgIcon'; const useStyles = makeStyles()((theme: Theme) => ({ active: { - color: theme.tokens.color.Ultramarine[80], + color: '#1f64b6', }, disabled: { '& $icon': { - borderColor: theme.tokens.color.Neutrals[50], - color: theme.tokens.color.Neutrals[50], + borderColor: '#939598', + color: '#939598', }, - color: theme.tokens.color.Neutrals[50], + color: '#939598', pointerEvents: 'none', }, icon: { @@ -43,7 +43,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ justifyContent: 'center', }, root: { - '&:focus': { outline: `1px dotted ${theme.tokens.color.Neutrals[50]}` }, + '&:focus': { outline: '1px dotted #999' }, '&:hover': { '& .border': { color: theme.palette.primary.light, diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index 62127aa16c8..1e09b698713 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -1,10 +1,8 @@ -import { Autocomplete, Box, Notice, Stack, Typography } from '@linode/ui'; -import { DateTime } from 'luxon'; +import { Autocomplete } from '@linode/ui'; import React, { useMemo } from 'react'; import { imageFactory } from 'src/factories/images'; import { useAllImagesQuery } from 'src/queries/images'; -import { formatDate } from 'src/utilities/formatDate'; import { OSIcon } from '../OSIcon'; import { ImageOption } from './ImageOption'; @@ -12,7 +10,6 @@ import { getAPIFilterForImageSelect, getDisabledImages, getFilteredImagesForImageSelect, - isImageDeprecated, } from './utilities'; import type { Image, RegionSite } from '@linode/api-v4'; @@ -134,114 +131,69 @@ export const ImageSelect = (props: Props) => { return options.find((option) => option.id === selected) ?? null; }, [multiple, options, selected]); - const selectedDeprecatedImages = useMemo(() => { - if (!value) { - return false; - } - if (Array.isArray(value)) { - return value.filter((img) => isImageDeprecated(img)); - } - return isImageDeprecated(value) && [value]; - }, [value]); - if (options.length === 1 && onChange && selectIfOnlyOneOption && !multiple) { onChange(options[0]); } return ( - - { - if (option.id === 'any/all') { - return ''; - } - if (!option.is_public) { - return 'My Images'; - } - - return option.vendor ?? ''; - }} - renderOption={(props, option, state) => { - const { key, ...rest } = props; - - return ( - - ); - }} - textFieldProps={{ - InputProps: { - startAdornment: - !multiple && value && !Array.isArray(value) ? ( - - ) : null, - }, - }} - clearOnBlur - disableSelectAll - label={label || 'Images'} - loading={isLoading} - options={sortedOptions} - placeholder={placeholder || 'Choose an image'} - {...rest} - disableClearable={ - rest.disableClearable ?? - (selectIfOnlyOneOption && options.length === 1 && !multiple) + { + if (option.id === 'any/all') { + return ''; } - onChange={(_, value) => - multiple && Array.isArray(value) - ? onChange(value) - : !multiple && !Array.isArray(value) && onChange(value) + if (!option.is_public) { + return 'My Images'; } - errorText={rest.errorText ?? error?.[0].reason} - getOptionDisabled={(option) => Boolean(disabledImages[option.id])} - multiple={multiple} - value={value} - /> - - - {selectedDeprecatedImages && - selectedDeprecatedImages.map((image) => ( - - {image.eol && DateTime.fromISO(image.eol) > DateTime.now() ? ( - theme.font.bold}> - {image.label} will reach its end-of-life on{' '} - {formatDate(image.eol ?? '', { format: 'MM/dd/yyyy' })}. After - this date, this OS distribution will no longer receive - security updates or technical support. We recommend selecting - a newer supported version to ensure continued security and - stability for your linodes. - - ) : ( - theme.font.bold}> - {image.label} reached its end-of-life on{' '} - {formatDate(image.eol ?? '', { format: 'MM/dd/yyyy' })}. This - OS distribution will no longer receive security updates or - technical support. We recommend selecting a newer supported - version to ensure continued security and stability for your - linodes. - - )} - - ))} - - + + return option.vendor ?? ''; + }} + renderOption={(props, option, state) => { + const { key, ...rest } = props; + + return ( + + ); + }} + textFieldProps={{ + InputProps: { + startAdornment: + !multiple && value && !Array.isArray(value) ? ( + + ) : null, + }, + }} + clearOnBlur + disableSelectAll + label={label || 'Images'} + loading={isLoading} + options={sortedOptions} + placeholder={placeholder || 'Choose an image'} + {...rest} + disableClearable={ + rest.disableClearable ?? + (selectIfOnlyOneOption && options.length === 1 && !multiple) + } + onChange={(_, value) => + multiple && Array.isArray(value) + ? onChange(value) + : !multiple && !Array.isArray(value) && onChange(value) + } + errorText={rest.errorText ?? error?.[0].reason} + getOptionDisabled={(option) => Boolean(disabledImages[option.id])} + multiple={multiple} + value={value} + /> ); }; diff --git a/packages/manager/src/components/LineGraph/LineGraph.tsx b/packages/manager/src/components/LineGraph/LineGraph.tsx index 5563f67b180..3cf464e8199 100644 --- a/packages/manager/src/components/LineGraph/LineGraph.tsx +++ b/packages/manager/src/components/LineGraph/LineGraph.tsx @@ -271,9 +271,9 @@ export const LineGraph = (props: LineGraphProps) => { ], }, tooltips: { - backgroundColor: theme.tokens.color.Neutrals[5], - bodyFontColor: theme.tokens.color.Neutrals[90], - borderColor: theme.tokens.color.Neutrals[50], + backgroundColor: '#fbfbfb', + bodyFontColor: '#32363C', + borderColor: '#999', borderWidth: 0.5, callbacks: { label: _formatTooltip(data, formatTooltip, _tooltipUnit), diff --git a/packages/manager/src/components/Link.tsx b/packages/manager/src/components/Link.tsx index ccdd26198a9..8242c791db1 100644 --- a/packages/manager/src/components/Link.tsx +++ b/packages/manager/src/components/Link.tsx @@ -46,7 +46,7 @@ export interface LinkProps extends Omit<_LinkProps, 'to'> { * @example "/profile/display" * @example "https://linode.com" */ - to: Exclude; + to: TanStackLinkProps['to'] | (string & {}); } /** diff --git a/packages/manager/src/components/MainContentBanner.tsx b/packages/manager/src/components/MainContentBanner.tsx index 239389e033c..60678184e50 100644 --- a/packages/manager/src/components/MainContentBanner.tsx +++ b/packages/manager/src/components/MainContentBanner.tsx @@ -27,14 +27,12 @@ export const MainContentBanner = React.memo(() => { const flags = useFlags(); - const { data: mainContentBannerPreferences } = usePreferences( - (preferences) => preferences?.main_content_banner_dismissal - ); + const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); const handleDismiss = (key: string) => { const existingMainContentBannerDismissal = - mainContentBannerPreferences ?? {}; + preferences?.main_content_banner_dismissal ?? {}; updatePreferences({ main_content_banner_dismissal: { @@ -46,7 +44,7 @@ export const MainContentBanner = React.memo(() => { const hasDismissedBanner = flags.mainContentBanner?.key !== undefined && - mainContentBannerPreferences?.[flags.mainContentBanner.key]; + preferences?.main_content_banner_dismissal?.[flags.mainContentBanner.key]; if ( !flags.mainContentBanner || diff --git a/packages/manager/src/components/MaskableText/MaskableText.test.tsx b/packages/manager/src/components/MaskableText/MaskableText.test.tsx index 38c101a9230..8c910d491bf 100644 --- a/packages/manager/src/components/MaskableText/MaskableText.test.tsx +++ b/packages/manager/src/components/MaskableText/MaskableText.test.tsx @@ -18,7 +18,9 @@ describe('MaskableText', () => { text: plainText, }; - const preference: ManagerPreferences['maskSensitiveData'] = true; + const preferences: ManagerPreferences = { + maskSensitiveData: true, + }; const queryMocks = vi.hoisted(() => ({ usePreferences: vi.fn().mockReturnValue({}), @@ -33,12 +35,12 @@ describe('MaskableText', () => { }); queryMocks.usePreferences.mockReturnValue({ - data: preference, + data: preferences, }); it('should render masked text if the maskSensitiveData preference is enabled', () => { queryMocks.usePreferences.mockReturnValue({ - data: preference, + data: preferences, }); const { getByText, queryByText } = renderWithTheme( @@ -52,7 +54,9 @@ describe('MaskableText', () => { it('should not render masked text if the maskSensitiveData preference is disabled', () => { queryMocks.usePreferences.mockReturnValue({ - data: false, + data: { + maskSensitiveData: false, + }, }); const { getByText, queryByText } = renderWithTheme( @@ -66,7 +70,9 @@ describe('MaskableText', () => { it("should render MaskableText's children if the maskSensitiveData preference is disabled and children are provided", () => { queryMocks.usePreferences.mockReturnValue({ - data: false, + data: { + maskSensitiveData: false, + }, }); const plainTextElement =
{plainText}
; @@ -81,7 +87,7 @@ describe('MaskableText', () => { it('should render a toggleable VisibilityTooltip if isToggleable is provided', async () => { queryMocks.usePreferences.mockReturnValue({ - data: preference, + data: preferences, }); const { getByTestId, getByText } = renderWithTheme( diff --git a/packages/manager/src/components/MaskableText/MaskableText.tsx b/packages/manager/src/components/MaskableText/MaskableText.tsx index dbeaedb1988..7f9b395fc8e 100644 --- a/packages/manager/src/components/MaskableText/MaskableText.tsx +++ b/packages/manager/src/components/MaskableText/MaskableText.tsx @@ -5,8 +5,6 @@ import * as React from 'react'; import { usePreferences } from 'src/queries/profile/preferences'; import { createMaskedText } from 'src/utilities/createMaskedText'; -import type { SxProps, Theme } from '@mui/material'; - export type MaskableTextLength = 'ipv4' | 'ipv6' | 'plaintext'; export interface MaskableTextProps { @@ -14,28 +12,14 @@ export interface MaskableTextProps { * (Optional) original JSX element to render if the text is not masked. */ children?: JSX.Element | JSX.Element[]; - /** - * Optionally specifies the position of the VisibilityTooltip icon either before or after the text. - * @default end - */ - iconPosition?: 'end' | 'start'; /** * If true, displays a VisibilityTooltip icon to toggle the masked and unmasked text. - * @default false */ isToggleable?: boolean; /** * Optionally specifies the length of the masked text to depending on data type (e.g. 'ipv4', 'ipv6', 'plaintext'); if not provided, will use a default length. */ length?: MaskableTextLength; - /** - * Optional styling for the masked and unmasked Typography - */ - sxTypography?: SxProps; - /** - * Optional styling for VisibilityTooltip icon - */ - sxVisibilityTooltip?: SxProps; /** * The original, maskable text; if the text is not masked, render this text or the styled text via children. */ @@ -43,27 +27,14 @@ export interface MaskableTextProps { } export const MaskableText = (props: MaskableTextProps) => { - const { - children, - iconPosition = 'end', - isToggleable = false, - length, - sxTypography, - sxVisibilityTooltip, - text, - } = props; + const { children, isToggleable = false, text, length } = props; - const { data: maskedPreferenceSetting } = usePreferences( - (preferences) => preferences?.maskSensitiveData - ); + const { data: preferences } = usePreferences(); + const maskedPreferenceSetting = preferences?.maskSensitiveData; const [isMasked, setIsMasked] = React.useState(maskedPreferenceSetting); - const unmaskedText = children ? ( - children - ) : ( - {text} - ); + const unmaskedText = children ? children : {text}; // Return early based on the preference setting and the original text. @@ -82,25 +53,14 @@ export const MaskableText = (props: MaskableTextProps) => { flexDirection="row" justifyContent="flex-start" > - {iconPosition === 'start' && isToggleable && ( - setIsMasked(!isMasked)} - isVisible={!isMasked} - /> - )} {isMasked ? ( - + {createMaskedText(text, length)} ) : ( unmaskedText )} - {iconPosition === 'end' && isToggleable && ( + {isToggleable && ( setIsMasked(!isMasked)} isVisible={!isMasked} diff --git a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx deleted file mode 100644 index abbeb85697a..00000000000 --- a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Typography } from '@linode/ui'; -import React from 'react'; - -import { Link } from '../Link'; - -/** - * This copy is intended to display where a larger area of data is masked. - * Example: Billing Contact info, rather than masking many individual fields - */ -export const MaskableTextAreaCopy = () => { - return ( - - This data is sensitive and hidden for privacy. To unmask all sensitive - data by default, go to{' '} - profile settings. - - ); -}; diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.stories.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.stories.tsx deleted file mode 100644 index 7bf2157e428..00000000000 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.stories.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState } from 'react'; - -import { MultipleIPInput } from './MultipleIPInput'; - -import type { MultipeIPInputProps } from './MultipleIPInput'; -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; - -type Story = StoryObj; - -const mockTitle = 'IP Address'; - -const defaultArgs = { - buttonText: 'Add An IP', - ips: [{ address: '192.0.2.1/01' }, { address: '192.0.2.1/02' }], - title: mockTitle, -}; - -const meta: Meta = { - component: MultipleIPInput, - decorators: [ - (Story: StoryFn) => ( -
- -
- ), - ], - title: 'Components/MultipleIPInput', -}; - -export default meta; - -const MultipleIPInputWithState = ({ ...args }: MultipeIPInputProps) => { - const [ips, setIps] = useState(args.ips); - - const handleChange = (newIps: typeof ips) => { - setIps(newIps); - }; - - return ; -}; - -export const Default: Story = { - args: defaultArgs, - render: (args) => { - return ; - }, -}; - -export const Disabled: Story = { - args: { - ...defaultArgs, - disabled: true, - }, -}; - -export const HelperText: Story = { - args: { - ...defaultArgs, - helperText: 'helperText', - }, - render: (args) => { - return ; - }, -}; - -export const Placeholder: Story = { - args: { - ips: [{ address: '' }], - placeholder: 'placeholder', - title: mockTitle, - }, - render: (args) => { - return ; - }, -}; diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 1f936b8ee8a..2080d9a92ba 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -61,89 +61,21 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); export interface MultipeIPInputProps { - /** - * Text displayed on the button. - */ buttonText?: string; - - /** - * Custom CSS class for additional styling. - */ className?: string; - - /** - * Disables the component (non-interactive). - * @default false - */ disabled?: boolean; - - /** - * Error message for invalid input. - */ error?: string; - - /** - * Indicates if the input relates to database access controls. - * @default false - */ forDatabaseAccessControls?: boolean; - - /** - * Indicates if the input is for VPC IPv4 ranges. - * @default false - */ forVPCIPv4Ranges?: boolean; - - /** - * Helper text for additional guidance. - */ helperText?: string; - - /** - * Custom input properties passed to the underlying input component. - */ inputProps?: InputBaseProps; - - /** - * Array of `ExtendedIP` objects representing managed IPs. - */ ips: ExtendedIP[]; - - /** - * Styles the button as a link. - * @default false - */ isLinkStyled?: boolean; - - /** - * Callback triggered when the input loses focus, passing updated `ips`. - */ onBlur?: (ips: ExtendedIP[]) => void; - - /** - * Callback triggered when IPs change, passing updated `ips`. - */ onChange: (ips: ExtendedIP[]) => void; - - /** - * Placeholder text for an empty input field. - */ placeholder?: string; - - /** - * Indicates if the input is required for form submission. - * @default false - */ required?: boolean; - - /** - * Title or label for the input field. - */ title: string; - - /** - * Tooltip text for extra info on hover. - */ tooltip?: string; } diff --git a/packages/manager/src/components/OrderBy.test.tsx b/packages/manager/src/components/OrderBy.test.tsx index 4d3e189c65a..273c83e107a 100644 --- a/packages/manager/src/components/OrderBy.test.tsx +++ b/packages/manager/src/components/OrderBy.test.tsx @@ -9,8 +9,6 @@ import { sortData, } from './OrderBy'; -import type { ManagerPreferences } from 'src/types/ManagerPreferences'; - const a = { age: 43, hobbies: ['this', 'that', 'the other'], @@ -63,13 +61,14 @@ describe('OrderBy', () => { }); describe('getInitialValuesFromUserPreferences', () => { - const preferences: ManagerPreferences['sortKeys'] = { - ['listening-services']: { - order: 'desc', - orderBy: 'test-order', + const preferences = { + sortKeys: { + ['listening-services']: { + order: 'desc' as any, + orderBy: 'test-order', + }, }, }; - it('should return values from query params if available', () => { expect( getInitialValuesFromUserPreferences( diff --git a/packages/manager/src/components/OrderBy.tsx b/packages/manager/src/components/OrderBy.tsx index 9ffcba5454a..0d8f2bab051 100644 --- a/packages/manager/src/components/OrderBy.tsx +++ b/packages/manager/src/components/OrderBy.tsx @@ -58,7 +58,7 @@ export type CombinedProps = Props; */ export const getInitialValuesFromUserPreferences = ( preferenceKey: string, - preferences: ManagerPreferences['sortKeys'], + preferences: ManagerPreferences, params: Record, defaultOrderBy?: string, defaultOrder?: Order, @@ -91,7 +91,7 @@ export const getInitialValuesFromUserPreferences = ( }; } return ( - preferences?.[preferenceKey] ?? { + preferences?.sortKeys?.[preferenceKey] ?? { order: defaultOrder, orderBy: defaultOrderBy, } @@ -156,9 +156,7 @@ export const sortData = (orderBy: string, order: Order) => { }; export const OrderBy = (props: CombinedProps) => { - const { data: sortPreferences } = usePreferences( - (preferences) => preferences?.sortKeys - ); + const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); const location = useLocation(); const history = useHistory(); @@ -166,7 +164,7 @@ export const OrderBy = (props: CombinedProps) => { const initialValues = getInitialValuesFromUserPreferences( props.preferenceKey ?? '', - sortPreferences ?? {}, + preferences ?? {}, params, props.orderBy, props.order @@ -212,7 +210,7 @@ export const OrderBy = (props: CombinedProps) => { if (props.preferenceKey) { updatePreferences({ sortKeys: { - ...(sortPreferences ?? {}), + ...(preferences?.sortKeys ?? {}), [props.preferenceKey]: { order, orderBy }, }, }); diff --git a/packages/manager/src/components/PreferenceToggle/PreferenceToggle.tsx b/packages/manager/src/components/PreferenceToggle/PreferenceToggle.tsx index 64afe3886c3..b4494ec9a26 100644 --- a/packages/manager/src/components/PreferenceToggle/PreferenceToggle.tsx +++ b/packages/manager/src/components/PreferenceToggle/PreferenceToggle.tsx @@ -3,30 +3,28 @@ import { usePreferences, } from 'src/queries/profile/preferences'; -import type { ManagerPreferences } from 'src/types/ManagerPreferences'; +export interface PreferenceToggleProps { + preference: T; + togglePreference: () => T; +} interface RenderChildrenProps { - preference: NonNullable; - togglePreference: () => NonNullable; + preference: T; + togglePreference: () => T; } type RenderChildren = (props: RenderChildrenProps) => JSX.Element; -interface Props { - children: RenderChildren; - initialSetCallbackFn?: (value: ManagerPreferences[Key]) => void; - preferenceKey: Key; - preferenceOptions: [ManagerPreferences[Key], ManagerPreferences[Key]]; - toggleCallbackFn?: (value: ManagerPreferences[Key]) => void; - value?: ManagerPreferences[Key]; +interface Props { + children: RenderChildren; + initialSetCallbackFn?: (value: T) => void; + preferenceKey: string; + preferenceOptions: [T, T]; + toggleCallbackFn?: (value: T) => void; + value?: T; } -/** - * @deprecated There are more simple ways to use preferences. Look into using `usePreferences` directly. - */ -export const PreferenceToggle = ( - props: Props -) => { +export const PreferenceToggle = (props: Props) => { const { children, preferenceKey, @@ -35,19 +33,17 @@ export const PreferenceToggle = ( value, } = props; - const { data: preference } = usePreferences( - (preferences) => preferences?.[preferenceKey] - ); + const { data: preferences } = usePreferences(); const { mutateAsync: updateUserPreferences } = useMutatePreferences(); const togglePreference = () => { - let newPreferenceToSet: ManagerPreferences[Key]; + let newPreferenceToSet: T; - if (preference === undefined) { + if (preferences?.[preferenceKey] === undefined) { // Because we default to preferenceOptions[0], toggling with no preference should pick preferenceOptions[1] newPreferenceToSet = preferenceOptions[1]; - } else if (preference === preferenceOptions[0]) { + } else if (preferences[preferenceKey] === preferenceOptions[0]) { newPreferenceToSet = preferenceOptions[1]; } else { newPreferenceToSet = preferenceOptions[0]; @@ -62,11 +58,11 @@ export const PreferenceToggle = ( toggleCallbackFn(newPreferenceToSet); } - return newPreferenceToSet!; + return newPreferenceToSet; }; return children({ - preference: value ?? preference ?? preferenceOptions[0]!, + preference: value ?? preferences?.[preferenceKey] ?? preferenceOptions[0], togglePreference, }); }; diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts b/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts index c551daa3c10..5de34fcc7c1 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts @@ -108,7 +108,7 @@ export const StyledAccordion = styled(Accordion, { ({ theme, ...props }) => ({ '& h3': { '& p': { - color: theme.tokens.color.Neutrals[50], + color: '#B8B8B8', transition: theme.transitions.create(['opacity']), ...(props.isCollapsed && { opacity: 0, @@ -116,9 +116,7 @@ export const StyledAccordion = styled(Accordion, { }, // product family icon '& svg': { - color: props.isActiveProductFamily - ? theme.tokens.color.Green[70] - : theme.color.grey4, + color: props.isActiveProductFamily ? '#00B159' : theme.color.grey4, height: 20, marginRight: 14, transition: theme.transitions.create(['color']), diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 6cac9b49d01..f968da8fd05 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -93,12 +93,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { const { isIAMEnabled, isIAMBeta } = useIsIAMEnabled(); - const { data: collapsedSideNavPreference } = usePreferences( - (preferences) => preferences?.collapsedSideNavProductFamilies - ); - - const collapsedAccordions = collapsedSideNavPreference ?? []; - + const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); const productFamilyLinkGroups: ProductFamilyLinkGroup< @@ -258,6 +253,10 @@ export const PrimaryNav = (props: PrimaryNavProps) => { ] ); + const [collapsedAccordions, setCollapsedAccordions] = React.useState< + number[] + >(preferences?.collapsedSideNavProductFamilies ?? []); + const accordionClicked = (index: number) => { let updatedCollapsedAccordions; if (collapsedAccordions.includes(index)) { @@ -267,11 +266,13 @@ export const PrimaryNav = (props: PrimaryNavProps) => { updatePreferences({ collapsedSideNavProductFamilies: updatedCollapsedAccordions, }); + setCollapsedAccordions(updatedCollapsedAccordions); } else { updatedCollapsedAccordions = [...collapsedAccordions, index]; updatePreferences({ collapsedSideNavProductFamilies: updatedCollapsedAccordions, }); + setCollapsedAccordions(updatedCollapsedAccordions); } }; diff --git a/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx b/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx index 27ce8c76b2c..1bbb41870a3 100644 --- a/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx +++ b/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx @@ -17,10 +17,10 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, button: { '&:hover, &:focus': { - backgroundColor: theme.tokens.color.Green[70], + backgroundColor: '#3f8a4e', color: theme.tokens.color.Neutrals.White, }, - backgroundColor: theme.tokens.color.Green[60], + backgroundColor: '#4FAD62', color: theme.tokens.color.Neutrals.White, marginLeft: theme.spacing(2), marginRight: theme.spacing(2), @@ -29,13 +29,13 @@ const useStyles = makeStyles()((theme: Theme) => ({ buttonSecondary: { '&:hover, &:focus': { backgroundColor: 'inherit', - borderColor: theme.tokens.color.Green[50], - color: theme.tokens.color.Green[50], + borderColor: '#72BD81', + color: '#72BD81', }, backgroundColor: 'inherit', border: '1px solid transparent', - borderColor: theme.tokens.color.Green[60], - color: theme.tokens.color.Green[60], + borderColor: '#4FAD62', + color: '#4FAD62', transition: theme.transitions.create(['color', 'border-color']), }, buttonSection: { diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts b/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts index ee1cd833005..1f274abd608 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts @@ -48,9 +48,9 @@ export const StyledChip = styled(Chip)(({ theme }) => ({ '& .MuiChip-deleteIcon.MuiSvgIcon-root': { '&:hover': { backgroundColor: theme.tokens.color.Neutrals.White, - color: theme.tokens.color.Ultramarine[70], + color: '#3683dc', }, - backgroundColor: theme.tokens.color.Ultramarine[70], + backgroundColor: '#3683dc', color: theme.tokens.color.Neutrals.White, }, })); diff --git a/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx b/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx index 33beaff933a..ae81fb10c9f 100644 --- a/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx +++ b/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx @@ -63,7 +63,10 @@ export const SelectFirewallPanel = (props: Props) => { : null; return ( - + ({ marginTop: theme.spacing(3) })} + > ({ marginBottom: theme.spacing(2) })} variant="h2" diff --git a/packages/manager/src/components/SelectRegionPanel/RegionHelperText.tsx b/packages/manager/src/components/SelectRegionPanel/RegionHelperText.tsx index a2e273543d3..b677cba9286 100644 --- a/packages/manager/src/components/SelectRegionPanel/RegionHelperText.tsx +++ b/packages/manager/src/components/SelectRegionPanel/RegionHelperText.tsx @@ -11,15 +11,11 @@ interface RegionHelperTextProps extends BoxProps { } export const RegionHelperText = (props: RegionHelperTextProps) => { - const { onClick, showCoreHelperText, sx, ...rest } = props; + const { onClick, showCoreHelperText, ...rest } = props; return ( - - + + {showCoreHelperText && `Data centers in central locations support a robust set of cloud computing services. `} You can use diff --git a/packages/manager/src/components/SelectionCard/CardBase.styles.ts b/packages/manager/src/components/SelectionCard/CardBase.styles.ts index 4e7f6ebe07a..8c06a76beee 100644 --- a/packages/manager/src/components/SelectionCard/CardBase.styles.ts +++ b/packages/manager/src/components/SelectionCard/CardBase.styles.ts @@ -51,13 +51,13 @@ export const CardBaseGrid = styled(Grid, { export const CardBaseIcon = styled(Grid, { label: 'CardBaseIcon', -})(({ theme }) => ({ +})(() => ({ '& img': { maxHeight: 32, maxWidth: 32, }, '& svg, & span': { - color: theme.tokens.color.Neutrals[50], + color: '#939598', fontSize: 32, }, alignItems: 'flex-end', diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index 511119cb740..4991bf1e32b 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -191,12 +191,12 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => { const StyledGrid = styled(Grid, { label: 'SelectionCardGrid', -})>(({ theme, ...props }) => ({ +})>(({ ...props }) => ({ '& [class^="fl-"]': { transition: 'color 225ms ease-in-out', }, '&:focus': { - outline: `1px dotted ${theme.tokens.color.Neutrals[50]}`, + outline: '1px dotted #999', }, ...(props.onClick && !props.disabled && { diff --git a/packages/manager/src/components/ShowMore/ShowMore.tsx b/packages/manager/src/components/ShowMore/ShowMore.tsx index 1e3f11e2c91..d9a8640d420 100644 --- a/packages/manager/src/components/ShowMore/ShowMore.tsx +++ b/packages/manager/src/components/ShowMore/ShowMore.tsx @@ -69,7 +69,7 @@ const StyledChip = styled(Chip)(({ theme }) => ({ }, '&:focus': { backgroundColor: theme.bg.lightBlue1, - outline: `1px dotted ${theme.tokens.color.Neutrals[50]}`, + outline: '1px dotted #999', }, '&:hover': { backgroundColor: theme.palette.primary.main, diff --git a/packages/manager/src/components/TanstackLink.stories.tsx b/packages/manager/src/components/TanstackLink.stories.tsx deleted file mode 100644 index badc132c03f..00000000000 --- a/packages/manager/src/components/TanstackLink.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import { TanstackLink } from './TanstackLinks'; - -import type { TanstackLinkComponentProps } from './TanstackLinks'; -import type { Meta, StoryObj } from '@storybook/react'; - -export const AsButtonPrimary: StoryObj = { - render: () => ( - - Home - - ), -}; - -export const AsButtonSecondary: StoryObj = { - render: () => ( - - Home - - ), -}; - -export const AsLink: StoryObj = { - render: () => ( - - Home - - ), -}; - -const meta: Meta = { - parameters: { - tanStackRouter: true, - }, - title: 'Foundations/Link/Tanstack Link', -}; -export default meta; diff --git a/packages/manager/src/components/TanstackLinks.tsx b/packages/manager/src/components/TanstackLinks.tsx deleted file mode 100644 index bea4ddd332d..00000000000 --- a/packages/manager/src/components/TanstackLinks.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Button } from '@linode/ui'; -import { omitProps } from '@linode/ui'; -import { LinkComponent } from '@tanstack/react-router'; -import { createLink } from '@tanstack/react-router'; -import * as React from 'react'; - -import { MenuItem } from 'src/components/MenuItem'; - -import type { ButtonProps, ButtonType } from '@linode/ui'; -import type { LinkProps as TanStackLinkProps } from '@tanstack/react-router'; - -export interface TanstackLinkComponentProps - extends Omit { - linkType: 'link' | ButtonType; - tooltipAnalyticsEvent?: (() => void) | undefined; - tooltipText?: string; -} - -export interface TanStackLinkRoutingProps { - linkType: TanstackLinkComponentProps['linkType']; - params?: TanStackLinkProps['params']; - preload?: TanStackLinkProps['preload']; - search?: TanStackLinkProps['search']; - to: TanStackLinkProps['to']; -} - -const LinkComponent = React.forwardRef< - HTMLButtonElement, - TanstackLinkComponentProps ->((props, ref) => { - const _props = omitProps(props, ['linkType']); - - return + + -
-
- +
+
+
+
+ Seeds (CRUD preset only) + +
+
+
+ +
-
-
-
Presets
-
-
- +
+
Presets
+
+
+ +
-
+
-
- )} -
- {isEnabled && isEditingCustomAccount && ( - setIsEditingCustomAccount(false)} - open={isEditingCustomAccount} - title="Edit Custom Account" - > -
{ - e.preventDefault(); - setIsEditingCustomAccount(false); - }} - className="dev-tools__modal-form" - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- )} - - ); -}; - -const FieldWrapper = ({ children }: { children: React.ReactNode }) => { - return
{children}
; -}; diff --git a/packages/manager/src/dev-tools/components/ExtraPresetOptionCheckbox.tsx b/packages/manager/src/dev-tools/components/ExtraPresetOptionCheckbox.tsx index 60dc6c8a22c..9eedf8d709b 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetOptionCheckbox.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetOptionCheckbox.tsx @@ -13,6 +13,7 @@ export const ExtraPresetOptionCheckbox = ( props: ExtraPresetOptionCheckboxProps ) => { const { + disabled, group, handlers, onPresetCountChange, @@ -31,14 +32,18 @@ export const ExtraPresetOptionCheckbox = ( style={{ display: 'flex', justifyContent: 'space-between' }} >
- +
{extraMockPreset.canUpdateCount && (
{ - const { group, handlers, onSelectChange } = props; + const { disabled, group, handlers, onSelectChange } = props; return (
@@ -28,6 +28,7 @@ export const ExtraPresetOptionSelect = ( ) || '' } className="dev-tools__select thin" + disabled={disabled} onChange={(e) => onSelectChange(e, group)} style={{ width: 125 }} > diff --git a/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx b/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx index 333fba7f4e5..6a56f0f650c 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx @@ -3,19 +3,12 @@ import * as React from 'react'; import { getMockPresetGroups } from 'src/mocks/mockPreset'; import { extraMockPresets } from 'src/mocks/presets'; -import { ExtraPresetAccount } from './ExtraPresetAccount'; import { ExtraPresetOptionCheckbox } from './ExtraPresetOptionCheckbox'; import { ExtraPresetOptionSelect } from './ExtraPresetOptionSelect'; -import { ExtraPresetProfile } from './ExtraPresetProfile'; - -import type { Account, Profile } from '@linode/api-v4'; export interface ExtraPresetOptionsProps { - customAccountData?: Account | null; - customProfileData?: Profile | null; + disabled: boolean; handlers: string[]; - onCustomAccountChange?: (data: Account | null | undefined) => void; - onCustomProfileChange?: (data: Profile | null | undefined) => void; onPresetCountChange: (e: React.ChangeEvent, presetId: string) => void; onSelectChange: (e: React.ChangeEvent, presetId: string) => void; onTogglePreset: (e: React.ChangeEvent, presetId: string) => void; @@ -26,11 +19,8 @@ export interface ExtraPresetOptionsProps { * Renders a list of extra presets with an optional count. */ export const ExtraPresetOptions = ({ - customAccountData, - customProfileData, + disabled, handlers, - onCustomAccountChange, - onCustomProfileChange, onPresetCountChange, onSelectChange, onTogglePreset, @@ -56,6 +46,7 @@ export const ExtraPresetOptions = ({ {group}{' '} {currentGroupType === 'select' && ( {currentGroupType === 'checkbox' && ( )} - {currentGroupType === 'account' && ( - - )} - {currentGroupType === 'profile' && ( - - )}
); })} diff --git a/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx b/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx deleted file mode 100644 index 6701c5f3d3e..00000000000 --- a/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import * as React from 'react'; - -import { Dialog } from 'src/components/Dialog/Dialog'; -import { profileFactory } from 'src/factories'; -import { extraMockPresets } from 'src/mocks/presets'; -import { setCustomProfileData } from 'src/mocks/presets/extra/account/customProfile'; - -import { saveCustomProfileData } from '../utils'; -import { JsonTextArea } from './JsonTextArea'; - -import type { Profile } from '@linode/api-v4'; - -const profilePreset = extraMockPresets.find((p) => p.id === 'profile:custom'); - -interface ExtraPresetProfileProps { - customProfileData: Profile | null | undefined; - handlers: string[]; - onFormChange?: (data: Profile | null | undefined) => void; - onTogglePreset: ( - e: React.ChangeEvent, - presetId: string - ) => void; -} - -export const ExtraPresetProfile = ({ - customProfileData, - handlers, - onFormChange, - onTogglePreset, -}: ExtraPresetProfileProps) => { - const isEnabled = handlers.includes('profile:custom'); - const [formData, setFormData] = React.useState(() => ({ - ...profileFactory.build({ - restricted: false, - }), - ...customProfileData, - })); - const [isEditingCustomProfile, setIsEditingCustomProfile] = React.useState( - false - ); - - const handleInputChange = ( - e: React.ChangeEvent< - HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement - > - ) => { - // radios - const { name, value } = e.target; - const isRadioToggleField = [ - 'email_notifications', - 'restricted', - 'two_factor_auth', - ].includes(name); - - const newValue = isRadioToggleField ? value === 'true' : value; - const newFormData = { - ...formData, - [name]: newValue, - }; - - setFormData(newFormData); - - if (isEnabled) { - onFormChange?.(newFormData); - } - }; - - const handleTogglePreset = (e: React.ChangeEvent) => { - if (!e.target.checked) { - saveCustomProfileData(null); - } else { - saveCustomProfileData(formData); - } - onTogglePreset(e, 'profile:custom'); - }; - - React.useEffect(() => { - if (!isEnabled) { - setFormData({ - ...profileFactory.build(), - }); - setCustomProfileData(null); - } else if (isEnabled && customProfileData) { - setFormData((prev) => ({ - ...prev, - ...customProfileData, - })); - setCustomProfileData(customProfileData); - } - }, [isEnabled, customProfileData]); - - if (!profilePreset) { - return null; - } - - return ( -
  • -
    -
    - -
    - {isEnabled && ( -
    - -
    - )} -
    - {isEnabled && isEditingCustomProfile && ( - setIsEditingCustomProfile(false)} - open={isEditingCustomProfile} - title="Edit Custom Profile" - > -
    setIsEditingCustomProfile(false)} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - )} -
  • - ); -}; - -const FieldWrapper = ({ children }: { children: React.ReactNode }) => { - return
    {children}
    ; -}; diff --git a/packages/manager/src/dev-tools/components/JsonTextArea.tsx b/packages/manager/src/dev-tools/components/JsonTextArea.tsx deleted file mode 100644 index dbccbe44fce..00000000000 --- a/packages/manager/src/dev-tools/components/JsonTextArea.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; - -interface JsonTextAreaProps { - height?: number; - label?: string; - name: string; - onChange: ( - e: React.ChangeEvent< - HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement - > - ) => void; - value: unknown; -} - -export const JsonTextArea = ({ - height, - label, - name, - onChange, - value, -}: JsonTextAreaProps) => { - const [rawText, setRawText] = React.useState(JSON.stringify(value, null, 2)); - - const debouncedUpdate = React.useMemo( - () => - debounce((text: string) => { - try { - const parsedJson = JSON.parse(text); - const event = { - currentTarget: { - name, - value: parsedJson, - }, - target: { - name, - value: parsedJson, - }, - } as React.ChangeEvent; - - onChange(event); - } catch (err) { - // Only warn if the text isn't empty and isn't in the middle of editing - if (text.trim()) { - // eslint-disable-next-line no-console - console.warn(`Invalid JSON in ${name}, error: ${err}`); - } - } - }, 500), - [name, onChange] - ); - - React.useEffect(() => { - const newText = JSON.stringify(value, null, 2); - if (newText !== rawText) { - setRawText(newText); - } - }, [value]); - - const handleChange = (e: React.ChangeEvent) => { - const newText = e.target.value; - setRawText(newText); - debouncedUpdate(newText); - }; - - return ( -