From c5505e5c78720dc847233609c6da3b9980d9b470 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Mon, 15 May 2023 12:56:43 -0400 Subject: [PATCH] =?UTF-8?q?Release=20v1.93.0=20=E2=86=92=20Staging=20(#911?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Co-authored-by: ecarrill Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> Co-authored-by: Jaalah Ramos Co-authored-by: carrillo-erik <119514965+carrillo-erik@users.noreply.github.com> Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Co-authored-by: Banks Nussman Co-authored-by: cliu-akamai <126020611+cliu-akamai@users.noreply.github.com> Co-authored-by: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Co-authored-by: rmcintosh Co-authored-by: mjac0bs Co-authored-by: Richie McIntosh <93939013+richardmcintosh@users.noreply.github.com> Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> --- .gitignore | 1 + CHANGELOG.md | 44 + packages/api-v4/CHANGELOG.md | 11 + packages/api-v4/package.json | 2 +- packages/api-v4/src/account/payments.ts | 49 - packages/api-v4/src/account/types.ts | 15 - packages/api-v4/src/domains/domains.ts | 12 + packages/api-v4/src/domains/types.ts | 4 + packages/api-v4/src/linodes/types.ts | 3 +- packages/manager/.storybook/main.ts | 7 +- packages/manager/.storybook/manager-head.html | 5 + packages/manager/.storybook/preview-head.html | 4 +- packages/manager/.storybook/preview.tsx | 97 +- packages/manager/Dockerfile | 2 +- packages/manager/cypress.config.ts | 1 + .../smoke-create-domain-records.spec.ts | 6 +- .../e2e/domains/smoke-create-domain.spec.ts | 29 +- .../images/create-linode-from-image.spec.ts | 5 +- .../e2e/images/machine-image-upload.spec.ts | 24 +- .../e2e/images/smoke-create-image.spec.ts | 64 +- .../cypress/e2e/kubernetes/lke-create.spec.ts | 2 +- .../cypress/e2e/kubernetes/lke-delete.spec.ts | 106 +++ .../e2e/kubernetes/lke-landing-page.spec.ts | 108 +++ .../cypress/e2e/kubernetes/lke-update.spec.ts | 731 +++++++++++++++ .../cypress/e2e/linodes/backup-linode.spec.ts | 2 +- .../smoke-linode-landing-table.spec.ts | 33 +- .../cypress/support/intercepts/domains.ts | 36 + .../cypress/support/intercepts/images.ts | 57 ++ .../cypress/support/intercepts/linodes.ts | 21 +- .../manager/cypress/support/intercepts/lke.ts | 324 ++++++- .../manager/cypress/support/util/downloads.ts | 24 + packages/manager/package.json | 6 +- packages/manager/src/App.tsx | 20 +- packages/manager/src/cachedData/kernels.json | 2 +- packages/manager/src/cachedData/regions.json | 2 +- .../manager/src/cachedData/typesLegacy.json | 2 +- .../components/AccessPanel/AccessPanel.tsx | 2 +- .../AccessPanel/UserSSHKeyPanel.tsx | 12 +- .../src/components/Accordion/Accordion.tsx | 2 +- .../BarPercent/BarPercent.stories.mdx | 47 - .../BarPercent/BarPercent.stories.tsx | 21 + .../src/components/BarPercent/BarPercent.tsx | 24 +- .../manager/src/components/Button/Button.tsx | 4 +- .../CheckoutSummary/CheckoutSummary.tsx | 68 +- .../CheckoutSummary/SummaryItem.tsx | 27 +- .../ConfirmationDialog/ConfirmationDialog.tsx | 8 +- .../CopyableTextField/CopyableTextField.tsx | 71 +- .../src/components/CopyableTextField/index.ts | 1 - .../DeletionDialog/DeletionDialog.tsx | 2 +- .../manager/src/components/Dialog/Dialog.tsx | 65 +- .../components/DialogTitle/DialogTitle.tsx | 93 +- .../src/components/DialogTitle/index.tsx | 2 - .../DismissibleBanner/DismissibleBanner.tsx | 8 +- .../components/DownloadCSV/DownloadCSV.tsx | 80 +- .../src/components/DownloadCSV/index.ts | 1 - .../DrawerContent/DrawerContent.tsx | 2 +- .../ResourcesLinkIcon.tsx | 34 + .../ResourcesLinks.tsx | 33 + .../ResourcesLinksSection.tsx | 39 + .../ResourcesLinksSubSection.tsx | 62 ++ .../ResourcesLinksTypes.ts | 27 + .../ResourcesMoreLink.tsx | 16 + .../ResourcesSection.tsx | 158 ++++ .../EntityDetail/.EntityDetail.stories.mdx | 33 +- .../EntityHeader/EntityHeader.stories.tsx | 12 +- .../EntityTable/APIPaginatedTable.tsx | 131 --- .../components/EntityTable/EntityTable.tsx | 105 --- .../EntityTable/EntityTableHeader.tsx | 205 ---- .../EntityTable/GroupedEntitiesByTag.tsx | 156 --- .../components/EntityTable/ListEntities.tsx | 95 -- .../src/components/EntityTable/index.tsx | 7 - .../src/components/EntityTable/types.ts | 50 - .../src/components/IconButton/IconButton.tsx | 4 + .../LabelAndTagsPanel/LabelAndTagsPanel.tsx | 2 +- .../LineGraph/AccessibleGraphData.test.tsx | 92 ++ .../LineGraph/AccessibleGraphData.tsx | 82 ++ .../LineGraph/LineGraph.stories.mdx | 4 + .../src/components/LineGraph/LineGraph.tsx | 49 +- .../LineGraph/MetricsDisplay.test.tsx | 2 +- .../components/LineGraph/MetricsDisplay.tsx | 10 +- .../LineGraph/NewMetricDisplay.styles.ts | 7 +- .../LinodeMultiSelect/LinodeMultiSelect.tsx | 2 +- .../MaintenanceBanner/MaintenanceBanner.tsx | 12 +- .../MultipleIPInput/MultipleIPInput.tsx | 2 +- .../src/components/Notice/Notice.stories.mdx | 1 - .../manager/src/components/Notice/Notice.tsx | 12 +- .../manager/src/components/Notice/index.tsx | 4 - .../PaginationFooter/PaginationFooter.tsx | 4 +- .../src/components/PaginationFooter/index.ts | 7 - .../components/Placeholder/Placeholder.tsx | 18 +- .../PrimaryNav/AdditionalMenuItems.tsx | 11 +- .../src/components/PrimaryNav/NavItem.tsx | 9 +- .../PrimaryNav/PrimaryNav.styles.ts | 270 +++--- .../src/components/PrimaryNav/PrimaryNav.tsx | 53 +- .../src/components/PrimaryNav/index.ts | 2 - .../ProductInformationBanner.tsx | 2 +- .../ProductNotification.tsx | 2 +- .../SelectRegionPanel/SelectRegionPanel.tsx | 2 +- .../SelectableTableRow/SelectableTableRow.tsx | 4 +- .../ShowMoreExpansion/ShowMoreExpansion.tsx | 63 +- .../SingleTextFieldForm.tsx | 2 +- .../components/TabbedPanel/TabbedPanel.tsx | 2 +- .../src/components/Table/Table.stories.mdx | 98 +- .../manager/src/components/Table/Table.tsx | 33 +- .../manager/src/components/Table/index.ts | 6 +- packages/manager/src/components/TableBody.tsx | 2 + .../src/components/TableCell/TableCell.tsx | 47 +- .../manager/src/components/TableCell/index.ts | 6 +- packages/manager/src/components/TableHead.tsx | 2 + .../src/components/TableRow/TableRow.test.tsx | 36 - .../src/components/TableRow/TableRow.tsx | 38 +- .../manager/src/components/TableRow/index.ts | 6 +- .../TableRowEmptyState.stories.mdx | 6 +- .../TableRowEmptyState/TableRowEmptyState.tsx | 4 +- .../TableRowError/TableRowError.stories.mdx | 6 +- .../TableRowError/TableRowError.tsx | 4 +- .../TableRowLoading.stories.mdx | 6 +- .../TableRowLoading/TableRowLoading.tsx | 4 +- .../TableSortCell/TableSortCell.tsx | 24 +- .../src/components/TableSortCell/index.ts | 6 +- packages/manager/src/components/Tile/Tile.tsx | 2 +- .../TypeToConfirmDialog.tsx | 2 +- packages/manager/src/components/core/Table.ts | 6 - .../manager/src/components/core/TableBody.ts | 8 - .../manager/src/components/core/TableCell.ts | 8 - .../manager/src/components/core/TableHead.ts | 8 - .../manager/src/components/core/TableRow.ts | 8 - .../src/components/core/TableSortLabel.ts | 8 - packages/manager/src/factories/billing.ts | 5 - .../src/factories/kubernetesCluster.ts | 7 + packages/manager/src/factories/linodes.ts | 26 + packages/manager/src/featureFlags.ts | 1 + .../src/features/Account/AccountLogins.tsx | 14 +- .../Account/AccountLoginsTableRow.tsx | 4 +- .../src/features/Account/AutoBackups.tsx | 2 +- .../features/Account/CloseAccountDialog.tsx | 2 +- .../features/Account/EnableObjectStorage.tsx | 2 +- .../Account/Maintenance/MaintenanceTable.tsx | 50 +- .../Maintenance/MaintenanceTableRow.tsx | 4 +- .../src/features/Backups/AutoEnroll.test.tsx | 2 +- .../src/features/Backups/AutoEnroll.tsx | 2 +- .../src/features/Backups/BackupDrawer.tsx | 2 +- .../src/features/Backups/BackupLinodes.tsx | 4 +- .../src/features/Backups/BackupsTable.tsx | 10 +- .../BillingActivityPanel.tsx | 12 +- .../PaymentDrawer/PayPalButton.tsx | 1 + .../PaymentBits/CreditCardDialog.tsx | 2 +- .../PaymentBits/PaypalDialog.tsx | 64 -- .../PaymentBits/PaypalDialogActionButtons.tsx | 67 -- .../PaymentDrawer/PaymentDrawer.tsx | 2 +- .../BillingSummary/PromoDialog.tsx | 2 +- .../UpdateContactInformationForm.tsx | 2 +- .../AddCreditCardForm.tsx | 2 +- .../AddPaymentMethodDrawer.tsx | 3 +- .../PaymentInfoPanel/PayPalChip.tsx | 10 +- .../Billing/InvoiceDetail/InvoiceDetail.tsx | 19 +- .../Billing/InvoiceDetail/InvoiceTable.tsx | 12 +- .../Billing/PdfGenerator/PdfGenerator.ts | 4 +- .../DatabaseCreate/DatabaseCreate.tsx | 2 +- .../DatabaseDetail/AccessControls.tsx | 10 +- .../DatabaseDetail/AddAccessControlDrawer.tsx | 2 +- .../DatabaseBackupTableRow.tsx | 4 +- .../DatabaseBackups/DatabaseBackups.tsx | 12 +- .../RestoreFromBackupDialog.tsx | 2 +- .../DatabaseSettingsDeleteClusterDialog.tsx | 2 +- .../DatabaseSettingsResetPasswordDialog.tsx | 2 +- .../DatabaseSettings/MaintenanceWindow.tsx | 2 +- .../DatabaseLanding/DatabaseEmptyState.tsx | 180 +--- .../DatabaseLanding/DatabaseLanding.test.tsx | 2 +- .../DatabaseLanding/DatabaseLanding.tsx | 30 +- .../DatabaseLandingEmptyStateData.ts | 73 ++ .../Databases/DatabaseLanding/DatabaseRow.tsx | 4 +- .../features/Domains/CloneDomainDrawer.tsx | 2 +- .../Domains/CreateDomain/CreateDomain.tsx | 2 +- .../Domains/DomainDetail/DomainDetail.tsx | 9 +- .../features/Domains/DomainRecordDrawer.tsx | 2 +- .../src/features/Domains/DomainRecords.tsx | 12 +- .../src/features/Domains/DomainTableRow.tsx | 4 +- .../Domains/DomainZoneImportDrawer.tsx | 2 +- .../Domains/DomainsEmptyLandingPage.tsx | 53 ++ .../Domains/DomainsEmptyResourcesData.ts | 71 ++ .../src/features/Domains/DomainsLanding.tsx | 56 +- .../DownloadDNSZoneFileButton.test.tsx | 38 + .../Domains/DownloadDNSZoneFileButton.tsx | 25 + .../src/features/Domains/EditDomainDrawer.tsx | 2 +- .../features/Domains/SortableTableHead.tsx | 75 -- .../EntityTransfersCreate.tsx | 2 +- .../LinodeTransferTable.tsx | 17 +- .../EntityTransfersCreate/TransferTable.tsx | 12 +- .../ConfirmTransferCancelDialog.tsx | 2 +- .../ConfirmTransferDialog.tsx | 2 +- .../CreateTransferSuccessDialog.tsx | 2 +- .../EntityTransfers/RenderTransferRow.tsx | 4 +- .../EntityTransfers/TransfersTable.tsx | 12 +- .../manager/src/features/Events/EventRow.tsx | 4 +- .../src/features/Events/EventsLanding.tsx | 10 +- .../Devices/AddDeviceDrawer.tsx | 2 +- .../Devices/FirewallDeviceRow.tsx | 4 +- .../Devices/FirewallDevicesTable.tsx | 12 +- .../Devices/FirewallLinodesLanding.tsx | 2 +- .../Rules/FirewallRuleDrawer.tsx | 2 +- .../Rules/FirewallRulesLanding.tsx | 2 +- .../FirewallLanding/CreateFirewallDrawer.tsx | 2 +- .../FirewallLanding/FirewallEmptyState.tsx | 35 - .../FirewallLanding/FirewallLanding.tsx | 16 +- .../FirewallLandingEmptyResourcesData.ts | 69 ++ .../FirewallLandingEmptyState.tsx | 41 + .../Firewalls/FirewallLanding/FirewallRow.tsx | 4 +- .../GlobalNotifications/EmailBounce.tsx | 2 +- .../RegionStatusBanner.tsx | 2 +- .../features/Help/Panels/AlgoliaSearchBar.tsx | 2 +- .../SupportSearchLanding.tsx | 2 +- .../manager/src/features/Images/ImageRow.tsx | 4 +- .../src/features/Images/ImageUpload.tsx | 2 +- .../Images/ImagesCreate/CreateImageTab.tsx | 2 +- .../src/features/Images/ImagesDrawer.tsx | 2 +- .../src/features/Images/ImagesLanding.tsx | 16 +- .../ClusterList/KubernetesClusterRow.tsx | 4 +- .../CreateCluster/CreateCluster.tsx | 2 +- .../KubeCheckoutBar/KubeCheckoutBar.tsx | 2 +- .../DeleteKubernetesClusterDialog.tsx | 2 +- .../NodePoolsDisplay/AddNodePoolDrawer.tsx | 2 +- .../NodePoolsDisplay/AutoscalePoolDialog.tsx | 2 +- .../NodePoolsDisplay/NodePool.tsx | 2 + .../NodePoolsDisplay/NodeTable.tsx | 20 +- .../NodePoolsDisplay/RecycleNodeDialog.tsx | 1 + .../NodePoolsDisplay/ResizeNodePoolDrawer.tsx | 2 +- .../UpgradeClusterDialog.tsx | 2 +- .../KubernetesLanding/KubernetesLanding.tsx | 31 +- .../KubernetesLandingEmptyState.tsx | 183 +--- .../KubernetesLandingEmptyStateData.ts | 82 ++ .../LinodeConfigSelectionDrawer.tsx | 2 +- packages/manager/src/features/Lish/Lish.tsx | 5 +- .../ActiveConnections/ActiveConnections.tsx | 12 +- .../ActiveConnections/ConnectionRow.tsx | 4 +- .../DetailTabs/Apache/Apache.tsx | 2 +- .../ListeningServices/ListeningServices.tsx | 12 +- .../ListeningServices/LongviewServiceRow.tsx | 4 +- .../DetailTabs/MySQL/MySQLLanding.tsx | 2 +- .../LongviewDetail/DetailTabs/NGINX/NGINX.tsx | 2 +- .../DetailTabs/Processes/ProcessesTable.tsx | 12 +- .../DetailTabs/TopProcesses.tsx | 12 +- .../LongviewDetail/LongviewDetail.tsx | 2 +- .../Longview/LongviewLanding/LongviewList.tsx | 2 +- .../LongviewLanding/LongviewPlans.tsx | 12 +- .../Longview/LongviewPackageDrawer.tsx | 10 +- .../features/Longview/LongviewPackageRow.tsx | 4 +- .../features/Managed/Contacts/Contacts.tsx | 14 +- .../Managed/Contacts/ContactsDrawer.tsx | 2 +- .../features/Managed/Contacts/ContactsRow.tsx | 4 +- .../Credentials/AddCredentialDrawer.tsx | 2 +- .../Managed/Credentials/CredentialList.tsx | 14 +- .../Managed/Credentials/CredentialRow.tsx | 4 +- .../Credentials/UpdateCredentialDrawer.tsx | 2 +- .../ManagedChartPanel.tsx | 3 + .../src/features/Managed/MonitorDrawer.tsx | 2 +- .../features/Managed/Monitors/MonitorRow.tsx | 4 +- .../Managed/Monitors/MonitorTable.tsx | 14 +- .../Managed/SSHAccess/EditSSHAccessDrawer.tsx | 2 +- .../Managed/SSHAccess/SSHAccessRow.tsx | 4 +- .../Managed/SSHAccess/SSHAccessTable.tsx | 14 +- .../NodeBalancers/NodeBalancerConfigNode.tsx | 2 +- .../NodeBalancers/NodeBalancerConfigPanel.tsx | 2 +- .../NodeBalancers/NodeBalancerCreate.tsx | 2 +- .../NodeBalancerDeleteDialog.tsx | 2 +- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 2 +- .../NodeBalancerSummary/TablesPanel.tsx | 2 + .../NodeBalancerTableRow.tsx | 4 +- .../NodeBalancersLanding.tsx | 12 +- .../AccessKeyLanding/AccessKeyDrawer.tsx | 2 +- .../AccessKeyLanding/AccessKeyLanding.tsx | 2 +- .../AccessKeyLanding/AccessKeyTable.tsx | 10 +- .../LimitedAccessControls.tsx | 10 +- .../BucketDetail/AccessSelect.tsx | 2 +- .../BucketDetail/BucketDetail.tsx | 10 +- .../ObjectStorage/BucketDetail/BucketSSL.tsx | 2 +- .../BucketDetail/FolderTableRow.tsx | 4 +- .../BucketDetail/ObjectTableRow.tsx | 4 +- .../BucketLanding/BucketLanding.tsx | 2 +- .../BucketLanding/BucketTable.tsx | 14 +- .../BucketLanding/BucketTableRow.tsx | 4 +- .../BucketLanding/CreateBucketDrawer.test.tsx | 81 ++ .../BucketLanding/CreateBucketDrawer.tsx | 13 +- .../src/features/OneClickApps/FakeSpec.ts | 2 +- .../Profile/APITokens/APITokenTable.tsx | 14 +- .../APITokens/CreateAPITokenDrawer.tsx | 12 +- .../Profile/APITokens/EditAPITokenDrawer.tsx | 2 +- .../Profile/APITokens/ViewAPITokenDrawer.tsx | 10 +- .../PhoneVerification/PhoneVerification.tsx | 5 +- .../AuthenticationSettings/SMSMessaging.tsx | 2 +- .../AuthenticationSettings/TPAProviders.tsx | 2 +- .../AuthenticationSettings/TrustedDevices.tsx | 14 +- .../TwoFactor/ConfirmToken.tsx | 2 +- .../TwoFactor/EnableTwoFactorForm.tsx | 2 +- .../TwoFactor/QRCodeForm.tsx | 2 +- .../TwoFactor/TwoFactor.tsx | 2 +- .../Profile/LishSettings/LishSettings.tsx | 2 +- .../OAuthClients/CreateOAuthClientDrawer.tsx | 2 +- .../OAuthClients/EditOAuthClientDrawer.tsx | 2 +- .../Profile/OAuthClients/OAuthClients.tsx | 14 +- .../features/Profile/Referrals/Referrals.tsx | 4 +- .../Profile/SSHKeys/CreateSSHKeyDrawer.tsx | 2 +- .../Profile/SSHKeys/EditSSHKeyDrawer.tsx | 2 +- .../src/features/Profile/SSHKeys/SSHKeys.tsx | 12 +- .../SecretTokenDialog/SecretTokenDialog.tsx | 2 +- .../Profile/Settings/PreferenceEditor.tsx | 2 +- .../src/features/Search/ResultGroup.tsx | 10 +- .../manager/src/features/Search/ResultRow.tsx | 4 +- .../src/features/Search/SearchLanding.tsx | 2 +- .../src/features/Search/refinedSearch.test.ts | 48 +- .../src/features/Search/refinedSearch.ts | 21 +- .../Partials/StackScriptTableHead.tsx | 8 +- .../SelectStackScriptPanel.tsx | 4 +- .../SelectStackScriptsSection.tsx | 6 +- .../StackScriptSelectionRow.tsx | 4 +- .../StackScriptBase/StackScriptBase.styles.ts | 1 - .../StackScriptBase/StackScriptBase.tsx | 91 +- .../StackScriptsEmptyLandingPage.tsx | 41 + .../StackScriptsEmptyResourcesData.ts | 72 ++ .../StackScriptCreate/StackScriptCreate.tsx | 2 +- .../StackScripts/StackScriptDialog.tsx | 2 +- .../StackScriptForm/StackScriptForm.tsx | 2 +- .../StackScriptPanel/StackScriptRow.tsx | 4 +- .../StackScriptPanel/StackScriptsSection.tsx | 6 +- .../StackScripts/StackScriptsLanding.tsx | 2 +- .../FieldTypes/UserDefinedMultiSelect.tsx | 2 +- .../FieldTypes/UserDefinedSelect.tsx | 2 +- .../UserDefinedFieldsPanel.tsx | 27 +- .../SupportTicketDetail/AttachmentError.tsx | 2 +- .../SupportTicketDetail/CloseTicketLink.tsx | 2 +- .../SupportTicketDetail.tsx | 2 +- .../TabbedReply/ReplyContainer.tsx | 2 +- .../SupportTickets/SupportTicketDrawer.tsx | 2 +- .../Support/SupportTickets/TicketList.tsx | 14 +- .../Support/SupportTickets/TicketRow.tsx | 4 +- .../src/features/TheApplicationIsOnFire.tsx | 2 +- .../src/features/Users/CreateUserDrawer.tsx | 2 +- .../manager/src/features/Users/UserDetail.tsx | 2 +- .../src/features/Users/UserPermissions.tsx | 2 +- .../Users/UserPermissionsEntitySection.tsx | 12 +- .../src/features/Users/UserProfile.tsx | 2 +- .../src/features/Users/UsersLanding.tsx | 14 +- .../Volumes/DestructiveVolumeDialog.tsx | 2 +- .../Volumes/VolumeAttachmentDrawer.tsx | 4 +- .../Volumes/VolumeCreate/CreateVolumeForm.tsx | 2 +- .../VolumeDrawer/AttachVolumeToLinodeForm.tsx | 2 +- .../Volumes/VolumeDrawer/EditVolumeForm.tsx | 2 +- .../Volumes/VolumeDrawer/NoticePanel.tsx | 2 +- .../Volumes/VolumeDrawer/ResizeVolumeForm.tsx | 2 +- .../VolumeDrawer/ResizeVolumesInstruction.tsx | 2 +- .../Volumes/VolumeDrawer/VolumeConfigForm.tsx | 2 +- .../src/features/Volumes/VolumeTableRow.tsx | 4 +- .../src/features/Volumes/VolumesLanding.tsx | 58 +- .../VolumesLandingEmptyState.styles.ts | 8 + .../Volumes/VolumesLandingEmptyState.tsx | 38 + .../Volumes/VolumesLandingEmptyStateData.ts | 72 ++ .../features/linodes/CloneLanding/Configs.tsx | 10 +- .../features/linodes/CloneLanding/Details.tsx | 2 +- .../features/linodes/CloneLanding/Disks.tsx | 12 +- .../features/linodes/LinodeEntityDetail.tsx | 93 +- .../LinodesCreate/ApiAwarenessModal/index.tsx | 2 +- .../linodes/LinodesCreate/LinodeCreate.tsx | 2 +- .../features/linodes/LinodesCreate/Panel.tsx | 2 +- .../linodes/LinodesCreate/PasswordPanel.tsx | 2 +- .../PremiumPlansAvailabilityNotice.tsx | 15 + .../linodes/LinodesCreate/SelectAppPanel.tsx | 2 +- .../LinodesCreate/SelectBackupPanel.tsx | 16 +- .../LinodesCreate/SelectLinodePanel.tsx | 4 +- .../linodes/LinodesCreate/SelectPlanPanel.tsx | 22 +- .../LinodesCreate/SelectPlanQuantityPanel.tsx | 17 +- .../TabbedContent/ImageEmptyState.tsx | 2 +- .../UserDataAccordion/UserDataAccordion.tsx | 2 +- .../UserDataAccordionHeading.tsx | 2 +- .../LinodesDetail/HostMaintenanceError.tsx | 2 +- .../LinodeAdvanced/ConfigRow.tsx | 4 +- .../LinodeAdvanced/LinodeConfigs.tsx | 14 +- .../LinodeAdvanced/LinodeDiskDrawer.tsx | 2 +- .../LinodeAdvanced/LinodeDiskRow.tsx | 4 +- .../LinodeAdvanced/LinodeDisks.tsx | 16 +- .../LinodeAdvanced/LinodeVolumes.tsx | 14 +- .../LinodeBackup/BackupTableRow.tsx | 26 +- .../LinodeBackup/BackupsPlaceholder.tsx | 8 +- .../LinodeBackup/CancelBackupsDialog.tsx | 74 ++ .../LinodeBackup/CaptureSnapshot.test.tsx | 50 + .../LinodeBackup/CaptureSnapshot.tsx | 114 +++ .../CaptureSnapshotConfirmationDialog.tsx | 48 + .../DestructiveSnapshotDialog.tsx | 72 -- .../LinodeBackup/EnableBackupsDialog.tsx | 119 ++- .../LinodeBackup/LinodeBackup.tsx | 886 ------------------ .../LinodeBackup/LinodeBackupActionMenu.tsx | 19 +- .../LinodeBackup/LinodeBackups.test.tsx | 71 ++ .../LinodeBackup/LinodeBackups.tsx | 216 +++++ .../RestoreToLinodeDrawer.test.tsx | 39 +- .../LinodeBackup/RestoreToLinodeDrawer.tsx | 280 +++--- .../LinodeBackup/ScheduleSettings.test.tsx | 62 ++ .../LinodeBackup/ScheduleSettings.tsx | 174 ++++ .../LinodesDetail/LinodeBackup/index.ts | 3 - .../LinodeNetworking/AddIPDrawer.tsx | 2 +- .../LinodeNetworking/CreateIPv4Drawer.tsx | 2 +- .../LinodeNetworking/IPSharing.tsx | 4 +- .../LinodeNetworking/IPTransfer.tsx | 4 +- .../LinodeNetworking/LinodeNetworking.tsx | 12 +- .../NetworkTransfer.tsx | 2 +- .../TransferHistory.tsx | 3 +- .../LinodesDetail/LinodePermissionsError.tsx | 2 +- .../LinodePowerControl/LinodePowerControl.tsx | 12 +- .../LinodeRebuild/LinodeRebuildDialog.tsx | 48 +- .../LinodeRebuild/RebuildDialog.tsx | 47 - .../LinodeRebuild/RebuildFromImage.styles.ts | 2 +- .../LinodesDetail/LinodeRebuild/index.ts | 3 - .../LinodeRescue/BareMetalRescue.tsx | 20 +- .../LinodeRescue/RescueContainer.tsx | 38 - .../LinodeRescue/RescueDescription.tsx | 4 +- ...ntainer.test.tsx => RescueDialog.test.tsx} | 64 +- .../LinodeRescue/RescueDialog.tsx | 281 +----- .../LinodeRescue/StandardRescueDialog.tsx | 257 +++++ .../LinodesDetail/LinodeRescue/index.ts | 1 - .../LinodeResize/LinodeResize.tsx | 2 +- .../LinodesDetail/LinodeResize/index.ts | 2 - .../LinodeSettings/LinodeConfigDialog.tsx | 2 +- .../LinodeSettingsAlertsPanel.tsx | 2 +- .../LinodeSettingsDeletePanel.tsx | 2 +- .../LinodeSettingsLabelPanel.tsx | 2 +- .../LinodeSettingsPasswordPanel.tsx | 2 +- .../LinodeSettings/LinodeWatchdogPanel.tsx | 2 +- .../LinodeSummary/LinodeSummary.tsx | 4 +- .../LinodeSummary/NetworkGraphs.tsx | 1 + .../LinodesDetailHeader/HostMaintenance.tsx | 2 +- .../LinodeDetailHeader.tsx | 302 ++---- .../MigrationNotification.tsx | 2 +- .../MutationNotification.tsx | 2 +- .../LinodesDetail/LinodesDetailNavigation.tsx | 2 +- .../MutateDrawer/MutateDrawer.tsx | 2 +- .../linodes/LinodesLanding/AppsSection.tsx | 18 +- .../linodes/LinodesLanding/CardView.tsx | 127 --- .../linodes/LinodesLanding/DeleteDialog.tsx | 74 -- .../LinodesLanding/DeleteLinodeDialog.tsx | 57 ++ .../LinodesLanding/DisplayGroupedLinodes.tsx | 349 ------- .../linodes/LinodesLanding/DisplayLinodes.tsx | 226 ----- .../linodes/LinodesLanding/LinksSection.tsx | 30 - .../LinodesLanding/LinksSubSection.tsx | 81 -- .../LinodesLanding/LinodeActionMenu.test.tsx | 8 +- .../LinodesLanding/LinodeActionMenu.tsx | 84 +- .../LinodeRow/LinodeRow.style.ts | 162 ++-- .../LinodesLanding/LinodeRow/LinodeRow.tsx | 198 ++-- .../LinodeRow/LinodeRowBackupCell.tsx | 46 - .../LinodeRow/LinodeRowHeadCell.tsx | 154 --- .../LinodeRow/LinodeRowLoading.tsx | 84 -- .../linodes/LinodesLanding/LinodeRow/index.ts | 1 - .../LinodesLanding/LinodesLanding.styles.ts | 41 - .../LinodesLanding/LinodesLanding.test.tsx | 52 - .../linodes/LinodesLanding/LinodesLanding.tsx | 714 ++++---------- .../LinodesLandingCSVDownload.tsx | 75 ++ .../LinodesLandingEmptyState.tsx | 68 ++ .../LinodesLandingEmptyStateData.ts | 81 ++ .../LinodesLanding/ListLinodesEmptyState.tsx | 213 ----- .../linodes/LinodesLanding/ListView.tsx | 61 -- .../LinodesLanding/SortableTableHead.tsx | 205 ---- .../linodes/LinodesLanding/TableWrapper.tsx | 61 -- .../linodes/LinodesLanding/ToggleBox.tsx | 109 --- .../features/linodes/LinodesLanding/index.tsx | 12 - .../linodes/MigrateLinode/CautionNotice.tsx | 17 +- .../linodes/MigrateLinode/MigrateLinode.tsx | 14 +- .../MigrateLinode/MigrationImminentNotice.tsx | 2 +- .../linodes/PowerActionsDialogOrDrawer.tsx | 255 +++-- .../manager/src/features/linodes/index.tsx | 40 +- .../manager/src/hooks/useFormattedDate.ts | 9 + packages/manager/src/hooks/usePagination.ts | 2 +- packages/manager/src/index.css | 4 - packages/manager/src/mocks/serverHandlers.ts | 3 + packages/manager/src/queries/linodes.ts | 213 ----- .../manager/src/queries/linodes/backups.ts | 58 ++ packages/manager/src/queries/linodes/disks.ts | 17 + .../manager/src/queries/linodes/events.ts | 7 + .../manager/src/queries/linodes/firewalls.ts | 32 + .../manager/src/queries/linodes/linodes.ts | 163 ++++ packages/manager/src/queries/linodes/stats.ts | 76 ++ packages/manager/src/queries/objectStorage.ts | 2 +- packages/manager/src/queries/types.ts | 9 + packages/manager/src/queries/volumes.ts | 201 +--- .../src/store/linodes/linode.requests.ts | 10 +- packages/manager/src/themes.ts | 9 - packages/manager/src/utilities/ga.ts | 25 +- .../manager/src/utilities/testHelpers.tsx | 8 + packages/validation/src/account.schema.ts | 13 - yarn.lock | 290 +++--- 486 files changed, 7315 insertions(+), 8664 deletions(-) create mode 100644 packages/manager/.storybook/manager-head.html create mode 100644 packages/manager/cypress/e2e/kubernetes/lke-delete.spec.ts create mode 100644 packages/manager/cypress/e2e/kubernetes/lke-landing-page.spec.ts create mode 100644 packages/manager/cypress/e2e/kubernetes/lke-update.spec.ts create mode 100644 packages/manager/cypress/support/intercepts/domains.ts create mode 100644 packages/manager/cypress/support/intercepts/images.ts create mode 100644 packages/manager/cypress/support/util/downloads.ts delete mode 100644 packages/manager/src/components/BarPercent/BarPercent.stories.mdx create mode 100644 packages/manager/src/components/BarPercent/BarPercent.stories.tsx delete mode 100644 packages/manager/src/components/CopyableTextField/index.ts delete mode 100644 packages/manager/src/components/DialogTitle/index.tsx delete mode 100644 packages/manager/src/components/DownloadCSV/index.ts create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesLinkIcon.tsx create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesLinks.tsx create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSection.tsx create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSubSection.tsx create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesMoreLink.tsx create mode 100644 packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx delete mode 100644 packages/manager/src/components/EntityTable/APIPaginatedTable.tsx delete mode 100644 packages/manager/src/components/EntityTable/EntityTable.tsx delete mode 100644 packages/manager/src/components/EntityTable/EntityTableHeader.tsx delete mode 100644 packages/manager/src/components/EntityTable/GroupedEntitiesByTag.tsx delete mode 100644 packages/manager/src/components/EntityTable/ListEntities.tsx delete mode 100644 packages/manager/src/components/EntityTable/index.tsx delete mode 100644 packages/manager/src/components/EntityTable/types.ts create mode 100644 packages/manager/src/components/LineGraph/AccessibleGraphData.test.tsx create mode 100644 packages/manager/src/components/LineGraph/AccessibleGraphData.tsx delete mode 100644 packages/manager/src/components/Notice/index.tsx delete mode 100644 packages/manager/src/components/PaginationFooter/index.ts delete mode 100644 packages/manager/src/components/PrimaryNav/index.ts create mode 100644 packages/manager/src/components/TableBody.tsx create mode 100644 packages/manager/src/components/TableHead.tsx delete mode 100644 packages/manager/src/components/TableRow/TableRow.test.tsx delete mode 100644 packages/manager/src/components/core/Table.ts delete mode 100644 packages/manager/src/components/core/TableBody.ts delete mode 100644 packages/manager/src/components/core/TableCell.ts delete mode 100644 packages/manager/src/components/core/TableHead.ts delete mode 100644 packages/manager/src/components/core/TableRow.ts delete mode 100644 packages/manager/src/components/core/TableSortLabel.ts delete mode 100644 packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialog.tsx delete mode 100644 packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialogActionButtons.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts create mode 100644 packages/manager/src/features/Domains/DomainsEmptyLandingPage.tsx create mode 100644 packages/manager/src/features/Domains/DomainsEmptyResourcesData.ts create mode 100644 packages/manager/src/features/Domains/DownloadDNSZoneFileButton.test.tsx create mode 100644 packages/manager/src/features/Domains/DownloadDNSZoneFileButton.tsx delete mode 100644 packages/manager/src/features/Domains/SortableTableHead.tsx delete mode 100644 packages/manager/src/features/Firewalls/FirewallLanding/FirewallEmptyState.tsx create mode 100644 packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyResourcesData.ts create mode 100644 packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyState.tsx create mode 100644 packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyStateData.ts create mode 100644 packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx create mode 100644 packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyLandingPage.tsx create mode 100644 packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyResourcesData.ts create mode 100644 packages/manager/src/features/Volumes/VolumesLandingEmptyState.styles.ts create mode 100644 packages/manager/src/features/Volumes/VolumesLandingEmptyState.tsx create mode 100644 packages/manager/src/features/Volumes/VolumesLandingEmptyStateData.ts create mode 100644 packages/manager/src/features/linodes/LinodesCreate/PremiumPlansAvailabilityNotice.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshotConfirmationDialog.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/DestructiveSnapshotDialog.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackup.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/index.ts delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildDialog.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/index.ts delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.tsx rename packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/{RescueContainer.test.tsx => RescueDialog.test.tsx} (51%) create mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/index.ts delete mode 100644 packages/manager/src/features/linodes/LinodesDetail/LinodeResize/index.ts delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/CardView.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/DeleteDialog.tsx create mode 100644 packages/manager/src/features/linodes/LinodesLanding/DeleteLinodeDialog.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/DisplayGroupedLinodes.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/DisplayLinodes.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinksSection.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinksSubSection.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowBackupCell.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowHeadCell.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowLoading.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodeRow/index.ts delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.styles.ts delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.test.tsx create mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodesLandingCSVDownload.tsx create mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyState.tsx create mode 100644 packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyStateData.ts delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/ListLinodesEmptyState.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/ListView.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/SortableTableHead.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/TableWrapper.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/ToggleBox.tsx delete mode 100644 packages/manager/src/features/linodes/LinodesLanding/index.tsx create mode 100644 packages/manager/src/hooks/useFormattedDate.ts delete mode 100644 packages/manager/src/queries/linodes.ts create mode 100644 packages/manager/src/queries/linodes/backups.ts create mode 100644 packages/manager/src/queries/linodes/disks.ts create mode 100644 packages/manager/src/queries/linodes/events.ts create mode 100644 packages/manager/src/queries/linodes/firewalls.ts create mode 100644 packages/manager/src/queries/linodes/linodes.ts create mode 100644 packages/manager/src/queries/linodes/stats.ts diff --git a/.gitignore b/.gitignore index 6ac64cb05e2..d898806ca1f 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,7 @@ packages/manager/test-report.xml **/manager/config/development.json **/manager/config/staging.json **/manager/cypress/videos/ +**/manager/cypress/downloads/ # ignore all screenshots except records # we ignore the png files, not the whole folder recursively diff --git a/CHANGELOG.md b/CHANGELOG.md index 37aa4225237..d5769d76280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,50 @@ 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/). +## [2023-05-15] - v1.93.0 + +### Added: +- Resource links to empty state Volumes landing page #9065 +- Resource links to empty state Firewalls landing page #9078 +- Resource links to empty state StackScripts landing page #9091 +- Resource links to empty state Domains landing page #9092 +- Ability download DNS zone file #9075 +- New flag to deliver DC availability notice for premium plans #9066 +- Accessible graph data for LineGraphs #9045 + +### Changed: +- Banner text size and spacing to improve readability #9064 +- Updated ClusterControl description #9081 +- Highlighted Marketplace apps and button card height on empty state Linodes landing page #9083 + +### Fixed: +- Ability to search Linodes by IPv6 #9073 +- Surface general errors in the Object Storage Bucket Create Drawer #9067 +- Large file size for invoices due to uncompressed JPG logo #9069 +- Phone Verification error does not reset #9059 +- Show error for PayPal payments #9058 +- Send Adobe Analytics page views #9108 + +### Tech Stories: +- MUI v5 Migration - `Components > CheckoutSummary` #9100 +- MUI v5 Migration - `Components > CopyableTextField` #9018 +- MUI v5 Migration - `Components > DialogTitle` #9050 +- MUI v5 Migration - `Components > DownloadCSV` #9084 +- MUI v5 Migration - `Components > Notice` #9094 +- MUI v5 Migration - `Components > PrimaryNav` #9090 +- MUI v5 Migration - `Components > ShowMoreExpansion` #9096 +- MUI v5 Migration - `Components > Table` #9082 +- MUI v5 Migration - `Components > TableBody` #9082 +- MUI v5 Migration - `Components > TableCell` #9082 +- MUI v5 Migration - `Components > TableHead` #9082 +- MUI v5 Migration - `Components > TableRow` #9082 +- MUI v5 Migration - `Components > TableSortCell` #9082 +- React Query - Linodes - Prepare for React Query for Linodes #9049 +- React Query - Linodes - Landing #9062 +- React Query - Linodes - Detail - Backups #9079 +- Add Adobe Analytics custom event tracking #9004 + + ## [2023-05-01] - v1.92.0 ### Added: diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index ebe191d0ece..2f46c59e701 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,14 @@ + +## [2023-05-15] - v0.92.0 + +### Added: +- Ability download DNS zone file #9075 +- React Query - Linodes - Landing #9062 +- React Query - Linodes - Detail - Backups #9079 + +### Fixed: +- Show error for PayPal payments #9058 + ## [2023-05-01] - v0.91.0 ### Added: diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index f3692585d8d..f6e8ac62d7a 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.91.0", + "version": "0.92.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/payments.ts b/packages/api-v4/src/account/payments.ts index fdd2155f864..f808ba035ee 100644 --- a/packages/api-v4/src/account/payments.ts +++ b/packages/api-v4/src/account/payments.ts @@ -1,8 +1,6 @@ import { CreditCardSchema, - ExecutePaypalPaymentSchema, PaymentSchema, - StagePaypalPaymentSchema, PaymentMethodSchema, } from '@linode/validation/lib/account.schema'; import { API_ROOT } from '../constants'; @@ -16,12 +14,9 @@ import Request, { import { Filter, Params, ResourcePage } from '../types'; import { ClientToken, - ExecutePayload, Payment, PaymentMethod, PaymentResponse, - Paypal, - PaypalResponse, SaveCreditCardData, MakePaymentData, PaymentMethodPayload, @@ -86,50 +81,6 @@ export const makePayment = (data: MakePaymentData) => { ); }; -interface StagePaypalData { - checkout_token: string; - payment_id: string; -} - -/** - * stagePaypalPayment - * - * Begins the process of making a payment through Paypal. - * - * @param data { object } - * @param data.cancel_url The URL to have PayPal redirect to when Payment is canceled. - * @param data.redirect_url The URL to have PayPal redirect to when Payment is approved. - * @param data.usd { string } The dollar amount of the payment - * - * @returns a payment ID, used for submitting the payment to Paypal. - * - */ -export const stagePaypalPayment = (data: Paypal) => - Request( - setURL(`${API_ROOT}/account/payments/paypal`), - setMethod('POST'), - setData(data, StagePaypalPaymentSchema) - ); - -/** - * executePaypalPayment - * - * Executes a payment through Paypal that has been started with the - * stagePaypalPayment method above. Paypal will capture the designated - * funds and credit your Linode account. - * - * @param data { object } - * @param data.payment_id The ID returned by stagePaypalPayment - * @param data.payer_id The PayerID returned by PayPal during the transaction authorization process. - * - */ -export const executePaypalPayment = (data: ExecutePayload) => - Request( - setURL(`${API_ROOT}/account/payments/paypal/execute`), - setMethod('POST'), - setData(data, ExecutePaypalPaymentSchema) - ); - /** * saveCreditCard * diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index a302434f2ed..11f5df99cbe 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -128,10 +128,6 @@ export interface PaymentResponse extends Payment { warnings?: APIWarning[]; } -export interface PaypalResponse { - warnings?: APIWarning[]; -} - export type GrantLevel = null | 'read_only' | 'read_write'; export interface Grant { @@ -384,17 +380,6 @@ export interface OAuthClientRequest { public?: boolean; } -export interface Paypal { - cancel_url: string; - redirect_url: string; - usd: string; -} - -export interface ExecutePayload { - payer_id: string; - payment_id: string; -} - export interface SaveCreditCardData { card_number: string; expiry_year: number; diff --git a/packages/api-v4/src/domains/domains.ts b/packages/api-v4/src/domains/domains.ts index f55b3a92628..9191bc1dd98 100644 --- a/packages/api-v4/src/domains/domains.ts +++ b/packages/api-v4/src/domains/domains.ts @@ -18,6 +18,7 @@ import { Domain, ImportZonePayload, UpdateDomainPayload, + ZoneFile, } from './types'; /** @@ -99,3 +100,14 @@ export const importZone = (data: ImportZonePayload) => setURL(`${API_ROOT}/domains/import`), setMethod('POST') ); + +/** + * Download DNS Zone file. + * + ** @param domainId { number } The ID of the Domain to download DNS zone file. + */ +export const getDNSZoneFile = (domainId: number) => + Request( + setURL(`${API_ROOT}/domains/${domainId}/zone-file`), + setMethod('GET') + ); diff --git a/packages/api-v4/src/domains/types.ts b/packages/api-v4/src/domains/types.ts index 68bdd28f755..90a0b158d39 100644 --- a/packages/api-v4/src/domains/types.ts +++ b/packages/api-v4/src/domains/types.ts @@ -25,6 +25,10 @@ export interface ImportZonePayload { remote_nameserver: string; } +export type ZoneFile = { + zone_file: string[]; +}; + export type DomainStatus = 'active' | 'disabled' | 'edit_mode' | 'has_errors'; export type DomainType = 'master' | 'slave'; diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 77e1aba696e..636a19dbeec 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -22,7 +22,7 @@ export interface Linode { ipv4: string[]; ipv6: string | null; label: string; - type: null | string; + type: string | null; status: LinodeStatus; updated: string; hypervisor: Hypervisor; @@ -101,6 +101,7 @@ export interface LinodeBackup { finished: string; configs: string[]; disks: LinodeBackupDisk[]; + available: boolean; } export type LinodeBackupType = 'auto' | 'snapshot'; diff --git a/packages/manager/.storybook/main.ts b/packages/manager/.storybook/main.ts index 660fb7b5bd3..91d73fd95c6 100644 --- a/packages/manager/.storybook/main.ts +++ b/packages/manager/.storybook/main.ts @@ -10,7 +10,7 @@ const config: StorybookConfig = { '@storybook/addon-docs', '@storybook/addon-controls', '@storybook/addon-viewport', - 'storybook-dark-mode-v7', + 'storybook-dark-mode', ], staticDirs: ['../public'], framework: { @@ -18,8 +18,13 @@ const config: StorybookConfig = { options: {}, }, features: { storyStoreV7: true }, + docs: { + autodocs: true, + defaultName: 'Documentation', + }, async viteFinal(config) { return mergeConfig(config, { + base: './', resolve: { preserveSymlinks: true, }, diff --git a/packages/manager/.storybook/manager-head.html b/packages/manager/.storybook/manager-head.html new file mode 100644 index 00000000000..13d93017d02 --- /dev/null +++ b/packages/manager/.storybook/manager-head.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/manager/.storybook/preview-head.html b/packages/manager/.storybook/preview-head.html index 05da1e9dfbf..f7ff12e16ee 100644 --- a/packages/manager/.storybook/preview-head.html +++ b/packages/manager/.storybook/preview-head.html @@ -1,3 +1,5 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/packages/manager/.storybook/preview.tsx b/packages/manager/.storybook/preview.tsx index 3e18a073d9e..ab52c91b92d 100644 --- a/packages/manager/.storybook/preview.tsx +++ b/packages/manager/.storybook/preview.tsx @@ -1,13 +1,27 @@ import React from 'react'; +import { Preview } from '@storybook/react'; import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; +import { + Title, + Subtitle, + Description, + Primary, + Controls, + Stories, +} from '@storybook/blocks'; import { wrapWithTheme } from '../src/utilities/testHelpers'; -import { useDarkMode } from 'storybook-dark-mode-v7'; +import { useDarkMode } from 'storybook-dark-mode'; import { DocsContainer as BaseContainer } from '@storybook/addon-docs'; import { themes } from '@storybook/theming'; -import '../public/fonts/fonts.css'; -import '../src/index.css'; import { worker } from '../src/mocks/testBrowser'; +import '../src/index.css'; + +MINIMAL_VIEWPORTS.mobile1.styles = { + height: '667px', + width: '375px', +}; + export const DocsContainer = ({ children, context }) => { const isDark = useDarkMode(); @@ -28,35 +42,54 @@ export const DocsContainer = ({ children, context }) => { ); }; -export const decorators = [ - (Story) => { - const isDark = useDarkMode(); - return wrapWithTheme(, { theme: isDark ? 'dark' : 'light' }); - }, -]; - -MINIMAL_VIEWPORTS.mobile1.styles = { - height: '667px', - width: '375px', -}; - -export const parameters = { - controls: { expanded: true }, - darkMode: { - dark: { ...themes.dark }, - light: { ...themes.normal }, - }, - options: { - storySort: { - method: 'alphabetical', - order: ['Intro', 'Features', 'Components', 'Elements', 'Core Styles'], +const preview: Preview = { + decorators: [ + (Story) => { + const isDark = useDarkMode(); + return wrapWithTheme(, { theme: isDark ? 'dark' : 'light' }); + }, + ], + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + backgrounds: { + grid: { + disable: true, + }, + }, + options: { + storySort: { + method: 'alphabetical', + order: ['Intro', 'Features', 'Components', 'Elements', 'Core Styles'], + }, + }, + viewport: { + viewports: MINIMAL_VIEWPORTS, + }, + darkMode: { + dark: { ...themes.dark }, + light: { ...themes.normal }, + }, + controls: { + expanded: true, + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + docs: { + container: DocsContainer, + page: () => ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <Controls /> + <Stories /> + </> + ), }, - }, - viewMode: 'docs', - viewport: { - viewports: MINIMAL_VIEWPORTS, - }, - docs: { - container: DocsContainer, }, }; + +export default preview; diff --git a/packages/manager/Dockerfile b/packages/manager/Dockerfile index 2b2658928ab..d8d7f6b8823 100644 --- a/packages/manager/Dockerfile +++ b/packages/manager/Dockerfile @@ -18,7 +18,7 @@ CMD yarn start:manager:ci # `e2e` # # Runs Cloud Manager Cypress tests. -FROM cypress/included:12.2.0 as e2e +FROM cypress/included:12.11.0 as e2e WORKDIR /home/node/app VOLUME /home/node/app USER node diff --git a/packages/manager/cypress.config.ts b/packages/manager/cypress.config.ts index 061b52129c2..bfcde095bc6 100644 --- a/packages/manager/cypress.config.ts +++ b/packages/manager/cypress.config.ts @@ -73,6 +73,7 @@ export default defineConfig({ requestTimeout: 30000, responseTimeout: 80000, retries: 2, + experimentalMemoryManagement: true, e2e: { experimentalRunAllSpecs: true, baseUrl: 'http://localhost:3000', diff --git a/packages/manager/cypress/e2e/domains/smoke-create-domain-records.spec.ts b/packages/manager/cypress/e2e/domains/smoke-create-domain-records.spec.ts index aa9ecbfc5af..d158cfccdf5 100644 --- a/packages/manager/cypress/e2e/domains/smoke-create-domain-records.spec.ts +++ b/packages/manager/cypress/e2e/domains/smoke-create-domain-records.spec.ts @@ -2,8 +2,8 @@ import { createDomain, deleteAllTestDomains } from 'support/api/domains'; import { randomIp, randomLabel, randomDomainName } from 'support/util/random'; import { fbtClick, getClick } from 'support/helpers'; -import { apiMatcher } from 'support/util/intercepts'; import { authenticate } from 'support/api/authentication'; +import { interceptCreateDomainRecord } from 'support/intercepts/domains'; const createRecords = () => [ { @@ -98,9 +98,7 @@ describe('Creates Domains record with Form', () => { return it(rec.name, () => { createDomain().then((domain) => { // intercept create api record request - cy.intercept('POST', apiMatcher('domains/*/record*')).as( - 'apiCreateRecord' - ); + interceptCreateDomainRecord().as('apiCreateRecord'); const url = `/domains/${domain.id}`; cy.visitWithLogin(url); cy.url().should('contain', url); diff --git a/packages/manager/cypress/e2e/domains/smoke-create-domain.spec.ts b/packages/manager/cypress/e2e/domains/smoke-create-domain.spec.ts index 1a954f7390a..ec1080b6433 100644 --- a/packages/manager/cypress/e2e/domains/smoke-create-domain.spec.ts +++ b/packages/manager/cypress/e2e/domains/smoke-create-domain.spec.ts @@ -1,22 +1,25 @@ -import { fbtClick, fbtVisible, getClick, getVisible } from 'support/helpers'; -import { apiMatcher } from 'support/util/intercepts'; +import { Domain } from '@linode/api-v4/types'; +import { domainFactory } from '@src/factories'; +import { fbtClick, getClick, getVisible } from 'support/helpers'; +import { + interceptCreateDomain, + mockGetDomains, +} from 'support/intercepts/domains'; import { randomDomainName } from 'support/util/random'; describe('Create a Domain', () => { it('Creates first Domain', () => { - // modify incoming response - cy.intercept('GET', apiMatcher('domains*'), (req) => { - req.reply((res) => { - res.send({ - results: 0, - page: 1, - pages: 1, - data: [], + // Mock Domains to modify incoming response. + const mockDomains = new Array(2).fill(null).map( + (item: null, index: number): Domain => { + return domainFactory.build({ + label: `Domain ${index}`, }); - }); - }).as('getDomains'); + } + ); + mockGetDomains(mockDomains).as('getDomains'); // intercept create Domain request - cy.intercept('POST', apiMatcher('domains')).as('createDomain'); + interceptCreateDomain().as('createDomain'); cy.visitWithLogin('/domains'); cy.wait('@getDomains'); fbtClick('Create Domain'); diff --git a/packages/manager/cypress/e2e/images/create-linode-from-image.spec.ts b/packages/manager/cypress/e2e/images/create-linode-from-image.spec.ts index 3274fa98e78..8bd907e9307 100644 --- a/packages/manager/cypress/e2e/images/create-linode-from-image.spec.ts +++ b/packages/manager/cypress/e2e/images/create-linode-from-image.spec.ts @@ -3,6 +3,7 @@ import { createMockLinodeList } from 'support/api/linodes'; import { containsClick, fbtClick, fbtVisible, getClick } from 'support/helpers'; import { apiMatcher } from 'support/util/intercepts'; import { randomString } from 'support/util/random'; +import { mockGetImages } from 'support/intercepts/images'; const mockImage = createMockImage().data[0]; const imageLabel = mockImage.label; @@ -22,9 +23,7 @@ const mockLinodeList = createMockLinodeList({ const mockLinode = mockLinodeList.data[0]; const createLinodeWithImageMock = (preselectedImage: boolean) => { - cy.intercept(apiMatcher('images*'), (req) => { - req.reply(createMockImage()); - }).as('mockImage'); + mockGetImages(createMockImage().data).as('mockImage'); cy.intercept('POST', apiMatcher('linode/instances'), (req) => { req.reply({ diff --git a/packages/manager/cypress/e2e/images/machine-image-upload.spec.ts b/packages/manager/cypress/e2e/images/machine-image-upload.spec.ts index a305ddb4192..ce222763a9f 100644 --- a/packages/manager/cypress/e2e/images/machine-image-upload.spec.ts +++ b/packages/manager/cypress/e2e/images/machine-image-upload.spec.ts @@ -10,6 +10,7 @@ import { ui } from 'support/ui'; import { interceptOnce } from 'support/ui/common'; import { apiMatcher } from 'support/util/intercepts'; import { randomItem, randomLabel, randomPhrase } from 'support/util/random'; +import { mockGetImage } from 'support/intercepts/images'; /* * Amount of time to wait for toast notification after uploading image, in ms. @@ -74,27 +75,6 @@ const eventIntercept = ( ).as('getEvent'); }; -/** - * Intercepts the response for an image GET request. - * - * Responds with an image with the given label, ID, and status. - * - * @param label - Response image label. - * @param id - Response image ID. Expected to be prefixed with a string (e.g. 'private/12345'). - * @param status - Image status. - */ -const imageIntercept = (label: string, id: string, status: ImageStatus) => { - cy.intercept('GET', apiMatcher(`images/${id}*`), (req) => { - req.reply( - imageFactory.build({ - label, - id, - status, - }) - ); - }).as('getImage'); -}; - /** * Asserts that provisioning an image fails. * @@ -256,7 +236,7 @@ describe('machine image', () => { cy.wait('@imageUpload').then((xhr) => { const imageId = xhr.response?.body.image.id; assertProcessing(label, imageId); - imageIntercept(label, imageId, 'available'); + mockGetImage(label, imageId, 'available').as('getImage'); eventIntercept(label, imageId, status); ui.toast.assertMessage(uploadMessage); cy.wait('@getImage'); diff --git a/packages/manager/cypress/e2e/images/smoke-create-image.spec.ts b/packages/manager/cypress/e2e/images/smoke-create-image.spec.ts index f33744e884d..50b07dcc74f 100644 --- a/packages/manager/cypress/e2e/images/smoke-create-image.spec.ts +++ b/packages/manager/cypress/e2e/images/smoke-create-image.spec.ts @@ -1,9 +1,33 @@ -import type { Linode } from '@linode/api-v4/types'; +import type { Image, Linode, Disk } from '@linode/api-v4/types'; import { imageFactory } from 'src/factories/images'; import { createLinode, deleteLinodeById } from 'support/api/linodes'; -import { apiMatcher } from 'support/util/intercepts'; +import { interceptCreateImage, mockGetImages } from 'support/intercepts/images'; +import { mockGetLinodeDisks } from 'support/intercepts/linodes'; import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; +const diskLabel = 'Debian 10 Disk'; + +const mockDisks: Disk[] = [ + { + id: 44311273, + status: 'ready', + label: diskLabel, + created: '2020-08-21T17:26:14', + updated: '2020-08-21T17:26:30', + filesystem: 'ext4', + size: 81408, + }, + { + id: 44311274, + status: 'ready', + label: '512 MB Swap Image', + created: '2020-08-21T17:26:14', + updated: '2020-08-21T17:26:31', + filesystem: 'swap', + size: 512, + }, +]; + describe('create image', () => { it('captures image from Linode and mocks create image', () => { const imageLabel = randomLabel(); @@ -22,40 +46,12 @@ describe('create image', () => { }); // stub incoming response - cy.intercept(apiMatcher('images?*'), { - results: 0, - data: [], - page: 1, - pages: 1, - }).as('getImages'); - cy.intercept('POST', apiMatcher('images'), mockNewImage).as('createImage'); + const mockImages = imageFactory.buildList(2); + mockGetImages(mockImages).as('getImages'); + interceptCreateImage(mockNewImage).as('createImage'); createLinode().then((linode: Linode) => { // stub incoming disks response - cy.intercept('GET', apiMatcher(`linode/instances/${linode.id}/disks*`), { - results: 2, - data: [ - { - id: 44311273, - status: 'ready', - label: diskLabel, - created: '2020-08-21T17:26:14', - updated: '2020-08-21T17:26:30', - filesystem: 'ext4', - size: 81408, - }, - { - id: 44311274, - status: 'ready', - label: '512 MB Swap Image', - created: '2020-08-21T17:26:14', - updated: '2020-08-21T17:26:31', - filesystem: 'swap', - size: 512, - }, - ], - page: 1, - pages: 1, - }).as('getDisks'); + mockGetLinodeDisks(linode.id, mockDisks).as('getDisks'); cy.visitWithLogin('/images'); cy.get('[data-qa-header]') .should('be.visible') diff --git a/packages/manager/cypress/e2e/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/kubernetes/lke-create.spec.ts index d017d63e704..53da7081344 100644 --- a/packages/manager/cypress/e2e/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/kubernetes/lke-create.spec.ts @@ -2,7 +2,7 @@ * @file LKE creation end-to-end tests. */ -import { KubernetesCluster } from '@linode/api-v4/types'; +import { KubernetesCluster } from '@linode/api-v4'; import { LkePlanDescription } from 'support/api/lke'; import { lkeClusterPlans } from 'support/constants/lke'; import { regionsFriendly } from 'support/constants/regions'; diff --git a/packages/manager/cypress/e2e/kubernetes/lke-delete.spec.ts b/packages/manager/cypress/e2e/kubernetes/lke-delete.spec.ts new file mode 100644 index 00000000000..9aaa064782d --- /dev/null +++ b/packages/manager/cypress/e2e/kubernetes/lke-delete.spec.ts @@ -0,0 +1,106 @@ +import { kubernetesClusterFactory } from 'src/factories'; +import { + mockGetCluster, + mockGetClusters, + mockDeleteCluster, +} from 'support/intercepts/lke'; +import { ui } from 'support/ui'; +import { randomLabel } from 'support/util/random'; + +/* + * Fills out and submits Type to Confirm deletion dialog for cluster with the given label. + */ +const completeTypeToConfirmDialog = (clusterLabel: string) => { + const deletionWarning = `Deleting a cluster is permanent and can't be undone.`; + + ui.dialog + .findByTitle(`Delete Cluster ${clusterLabel}`) + .should('be.visible') + .within(() => { + cy.findByText(deletionWarning, { exact: false }).should('be.visible'); + cy.findByLabelText('Cluster Name') + .should('be.visible') + .click() + .type(clusterLabel); + + ui.buttonGroup + .findButtonByTitle('Delete Cluster') + .should('be.visible') + .should('be.enabled') + .click(); + }); +}; + +describe('LKE cluster deletion', () => { + /* + * - Confirms LKE cluster deletion flow via landing page. + * - Confirms that landing page updates to reflect deleted cluster. + */ + it('can delete an LKE cluster from summary page', () => { + const mockCluster = kubernetesClusterFactory.build({ + label: randomLabel(), + }); + + mockGetClusters([mockCluster]).as('getClusters'); + mockDeleteCluster(mockCluster.id).as('deleteCluster'); + cy.visitWithLogin('/kubernetes/clusters'); + cy.wait('@getClusters'); + + // Find mock cluster in table, click its "Delete" button. + cy.findByText(mockCluster.label) + .should('be.visible') + .closest('tr') + .within(() => { + ui.button + .findByTitle('Delete') + .should('be.enabled') + .should('be.visible') + .click(); + }); + + // Fill out and submit type-to-confirm. + mockGetClusters([]).as('getClusters'); + completeTypeToConfirmDialog(mockCluster.label); + + // Confirm that cluster is no longer listed on landing page. + cy.wait(['@deleteCluster', '@getClusters']); + cy.findByText(mockCluster.label).should('not.exist'); + + // Confirm that Kubernetes welcome page is shown when there are no clusters. + cy.findByText('Fully managed Kubernetes infrastructure').should( + 'be.visible' + ); + + ui.button + .findByTitle('Create Cluster') + .should('be.visible') + .should('be.enabled'); + }); + + /* + * - Confirms LKE cluster deletion flow via details page. + * - Confirms that user is redirected to landing page upon cluster deletion. + */ + it('can delete an LKE cluster from landing page', () => { + const mockCluster = kubernetesClusterFactory.build({ + label: randomLabel(), + }); + + // Navigate to details page for mocked LKE cluster. + mockGetCluster(mockCluster).as('getCluster'); + mockDeleteCluster(mockCluster.id).as('deleteCluster'); + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}/summary`); + cy.wait('@getCluster'); + + // Press "Delete Cluster" button, complete type-to-confirm, and confirm redirect. + ui.button + .findByTitle('Delete Cluster') + .should('be.visible') + .should('be.enabled') + .click(); + + completeTypeToConfirmDialog(mockCluster.label); + cy.wait('@deleteCluster'); + cy.url().should('endWith', 'kubernetes/clusters'); + }); +}); diff --git a/packages/manager/cypress/e2e/kubernetes/lke-landing-page.spec.ts b/packages/manager/cypress/e2e/kubernetes/lke-landing-page.spec.ts new file mode 100644 index 00000000000..9674f5a862c --- /dev/null +++ b/packages/manager/cypress/e2e/kubernetes/lke-landing-page.spec.ts @@ -0,0 +1,108 @@ +import type { KubernetesCluster } from '@linode/api-v4'; +import { + mockGetClusters, + mockGetClusterPools, + mockGetKubeconfig, +} from 'support/intercepts/lke'; +import { kubernetesClusterFactory, nodePoolFactory } from 'src/factories'; +import { regionsMap } from 'support/constants/regions'; +import { readDownload } from 'support/util/downloads'; +import { ui } from 'support/ui'; + +describe('LKE landing page', () => { + /* + * - Confirms that LKE clusters are listed on landing page. + */ + it('lists LKE clusters', () => { + const mockClusters = kubernetesClusterFactory.buildList(10); + mockGetClusters(mockClusters).as('getClusters'); + + mockClusters.forEach((cluster: KubernetesCluster) => { + mockGetClusterPools(cluster.id, nodePoolFactory.buildList(3)); + }); + + cy.visitWithLogin('/kubernetes/clusters'); + cy.wait('@getClusters'); + + mockClusters.forEach((cluster: KubernetesCluster) => { + cy.findByText(cluster.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText(regionsMap[cluster.region]).should('be.visible'); + cy.findByText(cluster.k8s_version).should('be.visible'); + + ui.button + .findByTitle('Download kubeconfig') + .should('be.visible') + .should('be.enabled'); + + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled'); + }); + }); + }); + + /* + * - Confirms that welcome page is shown when no LKE clusters exist. + * - Confirms that core page elements (create button, guides, playlist, etc.) are present. + */ + it('shows welcome page when there are no LKE clusters', () => { + mockGetClusters([]).as('getClusters'); + cy.visitWithLogin('/kubernetes/clusters'); + cy.wait('@getClusters'); + + cy.findByText('Fully managed Kubernetes infrastructure').should( + 'be.visible' + ); + + ui.button + .findByTitle('Create Cluster') + .should('be.visible') + .should('be.enabled'); + + cy.findByText('Getting Started Guides').should('be.visible'); + cy.findByText('Video Playlist').should('be.visible'); + }); + + /* + * - Confirms UI flow for Kubeconfig file downloading using mocked data. + * - Confirms that downloaded Kubeconfig contains expected content. + */ + it('can download kubeconfig', () => { + const mockCluster = kubernetesClusterFactory.build(); + const mockClusterNodePools = nodePoolFactory.buildList(2); + const mockKubeconfigFilename = `${mockCluster.label}-kubeconfig.yaml`; + const mockKubeconfigContents = '---'; // Valid YAML. + const mockKubeconfigResponse = { + kubeconfig: btoa(mockKubeconfigContents), + }; + + mockGetClusters([mockCluster]).as('getClusters'); + mockGetClusterPools(mockCluster.id, mockClusterNodePools).as( + 'getNodePools' + ); + mockGetKubeconfig(mockCluster.id, mockKubeconfigResponse).as( + 'getKubeconfig' + ); + + cy.visitWithLogin('/kubernetes/clusters'); + cy.wait(['@getClusters', '@getNodePools']); + + cy.findByText(mockCluster.label) + .should('be.visible') + .closest('tr') + .within(() => { + ui.button + .findByTitle('Download kubeconfig') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@getKubeconfig'); + readDownload(mockKubeconfigFilename).should('eq', mockKubeconfigContents); + }); +}); diff --git a/packages/manager/cypress/e2e/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/kubernetes/lke-update.spec.ts new file mode 100644 index 00000000000..54fce85e327 --- /dev/null +++ b/packages/manager/cypress/e2e/kubernetes/lke-update.spec.ts @@ -0,0 +1,731 @@ +import { + kubernetesClusterFactory, + nodePoolFactory, + kubeLinodeFactory, + linodeFactory, +} from 'src/factories'; +import { latestKubernetesVersion } from 'support/constants/lke'; +import { + mockGetCluster, + mockGetKubernetesVersions, + mockGetClusterPools, + mockResetKubeconfig, + mockUpdateCluster, + mockAddNodePool, + mockUpdateNodePool, + mockDeleteNodePool, + mockRecycleNode, + mockRecycleNodePool, + mockRecycleAllNodes, + mockGetDashboardUrl, + mockGetApiEndpoints, +} from 'support/intercepts/lke'; +import { mockGetLinodes } from 'support/intercepts/linodes'; +import type { PoolNodeResponse, Linode } from '@linode/api-v4'; +import { ui } from 'support/ui'; +import { randomIp, randomLabel } from 'support/util/random'; + +const mockNodePools = nodePoolFactory.buildList(2); + +describe('LKE cluster updates', () => { + /* + * - Confirms UI flow of upgrading a cluster to high availability control plane using mocked data. + * - Confirms that user is shown a warning and agrees to billing changes before upgrading. + * - Confirms that details page updates accordingly after upgrading to high availability. + */ + it('can upgrade to high availability', () => { + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + control_plane: { + high_availability: false, + }, + }); + + const mockClusterWithHA = { + ...mockCluster, + control_plane: { + high_availability: true, + }, + }; + + const haUpgradeWarnings = [ + 'All nodes will be deleted and new nodes will be created to replace them.', + 'Any local storage (such as ’hostPath’ volumes) will be erased.', + 'This may take several minutes, as nodes will be replaced on a rolling basis.', + ]; + + const haUpgradeAgreement = + 'I agree to the additional fee on my monthly bill and understand HA upgrade can only be reversed by deleting my cluster'; + + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); + mockGetKubernetesVersions().as('getVersions'); + mockUpdateCluster(mockCluster.id, mockClusterWithHA).as('updateCluster'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getVersions']); + + // Initiate high availability upgrade and agree to changes. + ui.button + .findByTitle('Upgrade to HA') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Upgrade to High Availability') + .should('be.visible') + .within(() => { + haUpgradeWarnings.forEach((warning: string) => { + cy.findByText(warning).should('be.visible'); + }); + + cy.findByText(haUpgradeAgreement, { exact: false }) + .should('be.visible') + .closest('label') + .click(); + + ui.button + .findByTitle('Upgrade to HA') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm toast message appears and HA Cluster chip is shown. + cy.wait('@updateCluster'); + ui.toast.assertMessage('Enabled HA Control Plane'); + cy.findByText('HA CLUSTER').should('be.visible'); + cy.findByText('Upgrade to HA').should('not.exist'); + }); + + /* + * - Confirms UI flow of upgrading Kubernetes 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 Kubernetes engine version', () => { + const oldVersion = '1.25'; + const newVersion = '1.26'; + + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: oldVersion, + }); + + const mockClusterUpdated = { + ...mockCluster, + k8s_version: newVersion, + }; + + const upgradePrompt = 'A new version of Kubernetes is available (1.26).'; + + 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, + ]; + + mockGetCluster(mockCluster).as('getCluster'); + mockGetKubernetesVersions([newVersion, oldVersion]).as('getVersions'); + mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); + mockUpdateCluster(mockCluster.id, mockClusterUpdated).as('updateCluster'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getVersions']); + + // Confirm that upgrade prompt is shown. + cy.findByText(upgradePrompt).should('be.visible'); + ui.button + .findByTitle('Upgrade Version') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle( + `Step 1: Upgrade ${mockCluster.label} to Kubernetes ${newVersion}` + ) + .should('be.visible') + .within(() => { + upgradeNotes.forEach((note: string) => { + cy.findAllByText(note, { exact: false }).should('be.visible'); + }); + + ui.button + .findByTitle('Upgrade Version') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Wait for API response and assert toast message is shown. + cy.wait('@updateCluster'); + cy.findByText(upgradePrompt).should('not.exist'); + }); + + /* + * - Confirms node, node pool, and cluster recycling UI flow using mocked API data. + * - Confirms that user is warned that recycling recreates nodes and may take a while. + */ + it('can recycle nodes', () => { + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + }); + + const mockKubeLinode = kubeLinodeFactory.build(); + + const mockNodePool = nodePoolFactory.build({ + count: 1, + type: 'g6-standard-1', + nodes: [mockKubeLinode], + }); + + const mockLinode = linodeFactory.build({ + label: randomLabel(), + id: mockKubeLinode.instance_id, + }); + + const recycleWarningSubstrings = [ + 'will be deleted', + 'will be created', + 'local storage (such as ’hostPath’ volumes) will be erased', + 'may take several minutes', + ]; + + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, [mockNodePool]).as('getNodePools'); + mockGetLinodes([mockLinode]).as('getLinodes'); + mockGetKubernetesVersions().as('getVersions'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getLinodes', '@getVersions']); + + // Recycle individual node. + ui.button + .findByTitle('Recycle') + .should('be.visible') + .should('be.enabled') + .click(); + + mockRecycleNode(mockCluster.id, mockKubeLinode.id).as('recycleNode'); + ui.dialog + .findByTitle(`Recycle ${mockKubeLinode.id}?`) + .should('be.visible') + .within(() => { + recycleWarningSubstrings.forEach((warning: string) => { + cy.findByText(warning, { exact: false }).should('be.visible'); + }); + + ui.button + .findByTitle('Recycle') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@recycleNode'); + ui.toast.assertMessage('Node queued for recycling.'); + + ui.button + .findByTitle('Recycle Pool Nodes') + .should('be.visible') + .should('be.enabled') + .click(); + + mockRecycleNodePool(mockCluster.id, mockNodePool.id).as('recycleNodePool'); + ui.dialog + .findByTitle('Recycle node pool?') + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Recycle Pool Nodes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@recycleNodePool'); + ui.toast.assertMessage( + `Recycled all nodes in node pool ${mockNodePool.id}` + ); + + ui.button + .findByTitle('Recycle All Nodes') + .should('be.visible') + .should('be.enabled') + .click(); + + mockRecycleAllNodes(mockCluster.id).as('recycleAllNodes'); + ui.dialog + .findByTitle('Recycle all nodes in cluster?') + .should('be.visible') + .within(() => { + recycleWarningSubstrings.forEach((warning: string) => { + cy.findByText(warning, { exact: false }).should('be.visible'); + }); + + ui.button + .findByTitle('Recycle All Cluster Nodes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@recycleAllNodes'); + ui.toast.assertMessage('All cluster nodes queued for recycling'); + }); + + /* + * - Confirms UI flow when enabling and disabling node pool autoscaling using mocked API responses. + * - Confirms that errors are shown when attempting to autoscale using invalid values. + * - Confirms that UI updates to reflect node pool autoscale state. + */ + it('can toggle autoscaling', () => { + const autoscaleMin = 3; + const autoscaleMax = 10; + + const minWarning = + 'Minimum must be between 1 and 99 nodes and cannot be greater than Maximum.'; + const maxWarning = 'Maximum must be between 1 and 100 nodes.'; + + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + }); + + const mockNodePool = nodePoolFactory.build({ + count: 1, + type: 'g6-standard-1', + nodes: kubeLinodeFactory.buildList(1), + }); + + const mockNodePoolAutoscale = { + ...mockNodePool, + autoscaler: { + enabled: true, + min: autoscaleMin, + max: autoscaleMax, + }, + }; + + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, [mockNodePool]).as('getNodePools'); + mockGetKubernetesVersions().as('getVersions'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getVersions']); + + // Click "Autoscale Pool", enable autoscaling, and set min and max values. + mockUpdateNodePool(mockCluster.id, mockNodePoolAutoscale).as( + 'toggleAutoscale' + ); + mockGetClusterPools(mockCluster.id, [mockNodePoolAutoscale]).as( + 'getNodePools' + ); + ui.button + .findByTitle('Autoscale Pool') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Autoscale Pool') + .should('be.visible') + .within(() => { + cy.findByText('Autoscaler').should('be.visible').click(); + + cy.findByLabelText('Min') + .should('be.visible') + .click() + .clear() + .type(`${autoscaleMin}`); + + cy.findByText(minWarning).should('be.visible'); + + cy.findByLabelText('Max') + .should('be.visible') + .click() + .clear() + .type('101'); + + cy.findByText(minWarning).should('not.exist'); + cy.findByText(maxWarning).should('be.visible'); + + cy.findByLabelText('Max') + .should('be.visible') + .click() + .clear() + .type(`${autoscaleMax}`); + + cy.findByText(minWarning).should('not.exist'); + cy.findByText(maxWarning).should('not.exist'); + + ui.button.findByTitle('Save Changes').should('be.visible').click(); + }); + + // Wait for API response and confirm that UI updates to reflect autoscale. + cy.wait(['@toggleAutoscale', '@getNodePools']); + ui.toast.assertMessage( + `Autoscaling updated for Node Pool ${mockNodePool.id}.` + ); + cy.findByText(`(Min ${autoscaleMin} / Max ${autoscaleMax})`).should( + 'be.visible' + ); + + // Click "Autoscale Pool" again and disable autoscaling. + mockUpdateNodePool(mockCluster.id, mockNodePool).as('toggleAutoscale'); + mockGetClusterPools(mockCluster.id, [mockNodePool]).as('getNodePools'); + ui.button + .findByTitle('Autoscale Pool') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Autoscale Pool') + .should('be.visible') + .within(() => { + cy.findByText('Autoscaler').should('be.visible').click(); + + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Wait for API response and confirm that UI updates to reflect no autoscale. + cy.wait(['@toggleAutoscale', '@getNodePools']); + ui.toast.assertMessage( + `Autoscaling updated for Node Pool ${mockNodePool.id}.` + ); + cy.findByText(`(Min ${autoscaleMin} / Max ${autoscaleMax})`).should( + 'not.exist' + ); + }); + + /* + * - Confirms node pool resize UI flow using mocked API responses. + * - Confirms that pool size can be increased and decreased. + * - Confirms that user is warned when decreasing node pool size. + * - Confirms that UI updates to reflect new node pool size. + */ + it('can resize pools', () => { + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + }); + + const mockNodePoolResized = nodePoolFactory.build({ + count: 3, + type: 'g6-standard-1', + nodes: kubeLinodeFactory.buildList(3), + }); + + const mockNodePoolInitial = { + ...mockNodePoolResized, + count: 1, + nodes: [mockNodePoolResized.nodes[0]], + }; + + const mockLinodes: Linode[] = mockNodePoolResized.nodes.map( + (node: PoolNodeResponse): Linode => { + return linodeFactory.build({ + id: node.instance_id, + ipv4: [randomIp()], + }); + } + ); + + const mockNodePoolDrawerTitle = 'Resize Pool: Linode 2 GB Plan'; + + const decreaseSizeWarning = + 'Resizing to fewer nodes will delete random nodes from the pool.'; + const nodeSizeRecommendation = + 'We recommend a minimum of 3 nodes in each Node Pool to avoid downtime during upgrades and maintenance.'; + + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, [mockNodePoolInitial]).as( + 'getNodePools' + ); + mockGetLinodes(mockLinodes).as('getLinodes'); + mockGetKubernetesVersions().as('getVersions'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getLinodes', '@getVersions']); + + // Confirm that nodes are listed with correct details. + mockNodePoolInitial.nodes.forEach((node: PoolNodeResponse) => { + cy.get(`tr[data-qa-node-row="${node.id}"]`) + .should('be.visible') + .within(() => { + const nodeLinode = mockLinodes.find( + (linode: Linode) => linode.id === node.instance_id + ); + if (nodeLinode) { + cy.findByText(nodeLinode.label).should('be.visible'); + cy.findByText(nodeLinode.ipv4[0]).should('be.visible'); + ui.button + .findByTitle('Recycle') + .should('be.visible') + .should('be.enabled'); + } + }); + }); + + // Click "Resize Pool" and increase size to 3 nodes. + ui.button + .findByTitle('Resize Pool') + .should('be.visible') + .should('be.enabled') + .click(); + + mockUpdateNodePool(mockCluster.id, mockNodePoolResized).as( + 'resizeNodePool' + ); + mockGetClusterPools(mockCluster.id, [mockNodePoolResized]).as( + 'getNodePools' + ); + ui.drawer + .findByTitle(mockNodePoolDrawerTitle) + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.disabled'); + + cy.findByText('Resized pool: $12/month (1 node at $12/month)').should( + 'be.visible' + ); + + cy.findByLabelText('Add 1') + .should('be.visible') + .should('be.enabled') + .click() + .click(); + + cy.findByLabelText('Edit Quantity').should('have.value', '3'); + cy.findByText('Resized pool: $36/month (3 nodes at $12/month)').should( + 'be.visible' + ); + + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@resizeNodePool', '@getNodePools']); + + // Confirm that new nodes are listed with correct info. + mockLinodes.forEach((mockLinode: Linode) => { + cy.findByText(mockLinode.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText(mockLinode.ipv4[0]).should('be.visible'); + }); + }); + + // Click "Resize Pool" and decrease size back to 1 node. + ui.button + .findByTitle('Resize Pool') + .should('be.visible') + .should('be.enabled') + .click(); + + mockUpdateNodePool(mockCluster.id, mockNodePoolInitial).as( + 'resizeNodePool' + ); + mockGetClusterPools(mockCluster.id, [mockNodePoolInitial]).as( + 'getNodePools' + ); + ui.drawer + .findByTitle(mockNodePoolDrawerTitle) + .should('be.visible') + .within(() => { + cy.findByLabelText('Subtract 1') + .should('be.visible') + .should('be.enabled') + .click() + .click(); + + cy.findByText(decreaseSizeWarning).should('be.visible'); + cy.findByText(nodeSizeRecommendation).should('be.visible'); + + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@resizeNodePool', '@getNodePools']); + cy.get('[data-qa-node-row]').should('have.length', 1); + }); + + /* + * - Confirms kubeconfig reset UI flow using mocked API responses. + * - Confirms that user is warned of repercussions before resetting config. + * - Confirms that toast appears confirming kubeconfig has reset. + */ + it('can reset kubeconfig', () => { + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + }); + + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); + mockGetKubernetesVersions().as('getVersions'); + mockResetKubeconfig(mockCluster.id).as('resetKubeconfig'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + const resetWarnings = [ + 'This will delete and regenerate the cluster’s Kubeconfig file', + 'You will no longer be able to access this cluster via your previous Kubeconfig file', + 'This action cannot be undone', + ]; + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getVersions']); + + // Click "Reset" button, proceed through confirmation dialog. + cy.findByText('Reset').should('be.visible').click(); + ui.dialog + .findByTitle('Reset Cluster Kubeconfig?') + .should('be.visible') + .within(() => { + resetWarnings.forEach((warning: string) => { + cy.findByText(warning, { exact: false }).should('be.visible'); + }); + + ui.button + .findByTitle('Reset Kubeconfig') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Wait for API response and assert toast message appears. + cy.wait('@resetKubeconfig'); + ui.toast.assertMessage('Successfully reset Kubeconfig'); + }); + + /* + * - Confirms UI flow when adding and deleting node pools. + * - Confirms that user cannot delete a node pool when there is only 1 pool. + * - Confirms that details page updates to reflect change when pools are added or deleted. + */ + it('can add and delete node pools', () => { + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + }); + + const mockNodePool = nodePoolFactory.build({ + type: 'g6-dedicated-4', + }); + + const mockNewNodePool = nodePoolFactory.build({ + type: 'g6-dedicated-2', + }); + + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, [mockNodePool]).as('getNodePools'); + mockGetKubernetesVersions().as('getVersions'); + mockAddNodePool(mockCluster.id, mockNewNodePool).as('addNodePool'); + mockDeleteNodePool(mockCluster.id, mockNewNodePool.id).as('deleteNodePool'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait(['@getCluster', '@getNodePools', '@getVersions']); + + // Assert that initial node pool is shown on the page. + cy.findByText('Dedicated 8 GB', { selector: 'h2' }).should('be.visible'); + + // "Delete Pool" button should be disabled when only 1 node pool exists. + ui.button + .findByTitle('Delete Pool') + .should('be.visible') + .should('be.disabled'); + + // Add a new node pool, select plan, submit form in drawer. + ui.button + .findByTitle('Add a Node Pool') + .should('be.visible') + .should('be.enabled') + .click(); + + mockGetClusterPools(mockCluster.id, [mockNodePool, mockNewNodePool]).as( + 'getNodePools' + ); + ui.drawer + .findByTitle(`Add a Node Pool: ${mockCluster.label}`) + .should('be.visible') + .within(() => { + cy.findByText('Dedicated 4 GB') + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByLabelText('Add 1').should('be.visible').click(); + }); + + ui.button + .findByTitle('Add pool') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Wait for API responses and confirm that both node pools are shown. + cy.wait(['@addNodePool', '@getNodePools']); + cy.findByText('Dedicated 8 GB', { selector: 'h2' }).should('be.visible'); + cy.findByText('Dedicated 4 GB', { selector: 'h2' }).should('be.visible'); + + // Delete the newly added node pool. + cy.get(`[data-qa-node-pool-id="${mockNewNodePool.id}"]`) + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Delete Pool') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + mockGetClusterPools(mockCluster.id, [mockNodePool]).as('getNodePools'); + ui.dialog + .findByTitle('Delete Node Pool?') + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm node pool is deleted, original node pool still exists, and + // delete pool button is once again disabled. + cy.wait(['@deleteNodePool', '@getNodePools']); + cy.findByText('Dedicated 8 GB', { selector: 'h2' }).should('be.visible'); + cy.findByText('Dedicated 4 GB', { selector: 'h2' }).should('not.exist'); + + ui.button + .findByTitle('Delete Pool') + .should('be.visible') + .should('be.disabled'); + }); +}); diff --git a/packages/manager/cypress/e2e/linodes/backup-linode.spec.ts b/packages/manager/cypress/e2e/linodes/backup-linode.spec.ts index 3d45d195da7..096f7013108 100644 --- a/packages/manager/cypress/e2e/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/linodes/backup-linode.spec.ts @@ -70,7 +70,7 @@ describe('linode backups', () => { getClick('[data-qa-confirm="true"]'); } cy.wait('@enableBackups').its('response.statusCode').should('eq', 200); - ui.toast.assertMessage('A snapshot is being taken'); + ui.toast.assertMessage('Starting to capture snapshot'); deleteLinodeById(linode.id); }); }); diff --git a/packages/manager/cypress/e2e/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/linodes/smoke-linode-landing-table.spec.ts index 6bebc81d615..2484e3d754f 100644 --- a/packages/manager/cypress/e2e/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/linodes/smoke-linode-landing-table.spec.ts @@ -146,7 +146,8 @@ describe('linode landing checks', () => { fbtVisible('Create Linode'); }); - it('checks label and region sorting behavior for linode table', () => { + // Skipping because the sorting is now done by the API + it.skip('checks label and region sorting behavior for linode table', () => { const linodesByLabel = [...mockLinodes.sort(sortByLabel)]; const linodesByRegion = [...mockLinodes.sort(sortByRegion)]; const linodesLastIndex = mockLinodes.length - 1; @@ -218,22 +219,21 @@ describe('linode landing checks', () => { it('checks the table and action menu buttons/labels', () => { const label = linodeLabel(1); const ip = mockLinodes[0].ipv4[0]; + getVisible('[aria-label="Sort by label"]').within(() => { fbtVisible('Label'); }); - getVisible('[aria-label="Sort by _statusPriority"]').within(() => { - fbtVisible('Status'); - }); - getVisible('[aria-label="Sort by type"]').within(() => { - fbtVisible('Plan'); - }); - getVisible('[aria-label="Sort by ipv4[0]"]').within(() => { - fbtVisible('IP Address'); - }); - - cy.findByLabelText('Toggle display').should('be.visible'); - cy.findByLabelText('Toggle group by tag').should('be.visible'); + // We can't sort by these with the API: + // getVisible('[aria-label="Sort by status"]').within(() => { + // fbtVisible('Status'); + // }); + // getVisible('[aria-label="Sort by type"]').within(() => { + // fbtVisible('Plan'); + // }); + // getVisible('[aria-label="Sort by ipv4[0]"]').within(() => { + // fbtVisible('IP Address'); + // }); getVisible(`tr[data-qa-linode="${label}"]`).within(() => { ui.button @@ -280,9 +280,10 @@ describe('linode landing actions', () => { cy.visitWithLogin('/linodes', { preferenceOverrides }); cy.wait('@getAccountSettings'); getVisible('[data-qa-header="Linodes"]'); - if (!cy.get('[data-qa-sort-label="asc"]')) { - getClick('[aria-label="Sort by label"]'); - } + // I think we can assume the linodes are on the page and we don't need to sort + // if (!cy.get('[data-qa-sort-label="asc"]')) { + // getClick('[aria-label="Sort by label"]'); + // } deleteLinodeFromActionMenu(linodeA.label); deleteLinodeFromActionMenu(linodeB.label); cy.findByText('Oh Snap!', { timeout: 1000 }).should('not.exist'); diff --git a/packages/manager/cypress/support/intercepts/domains.ts b/packages/manager/cypress/support/intercepts/domains.ts new file mode 100644 index 00000000000..c009928e4a1 --- /dev/null +++ b/packages/manager/cypress/support/intercepts/domains.ts @@ -0,0 +1,36 @@ +/** + * @file Cypress intercepts and mocks for Domain API requests. + */ + +import type { Domain } from '@linode/api-v4/types'; +import { apiMatcher } from 'support/util/intercepts'; +import { paginateResponse } from 'support/util/paginate'; + +/** + * Intercepts POST request to create a Domain. + * + * @returns Cypress chainable. + */ +export const interceptCreateDomain = (): Cypress.Chainable<null> => { + return cy.intercept('POST', apiMatcher('domains')); +}; + +/** + * Intercepts GET request to mock domain data. + * + * @param domains - an array of mock domain objects + * + * @returns Cypress chainable. + */ +export const mockGetDomains = (domains: Domain[]): Cypress.Chainable<null> => { + return cy.intercept('GET', apiMatcher('domains*'), paginateResponse(domains)); +}; + +/** + * Intercepts POST request to create a Domain record. + * + * @returns Cypress chainable. + */ +export const interceptCreateDomainRecord = (): Cypress.Chainable<null> => { + return cy.intercept('POST', apiMatcher('domains/*/record*')); +}; diff --git a/packages/manager/cypress/support/intercepts/images.ts b/packages/manager/cypress/support/intercepts/images.ts new file mode 100644 index 00000000000..734977df59e --- /dev/null +++ b/packages/manager/cypress/support/intercepts/images.ts @@ -0,0 +1,57 @@ +/** + * @file Cypress intercepts and mocks for Image API requests. + */ + +import type { Image, ImageStatus } from '@linode/api-v4/types'; +import { apiMatcher } from 'support/util/intercepts'; +import { paginateResponse } from 'support/util/paginate'; +import { imageFactory } from '@src/factories'; + +/** + * Intercepts POST request to create a Image. + * + * @param image - an image objects + * + * @returns Cypress chainable. + */ +export const interceptCreateImage = (image: Image): Cypress.Chainable<null> => { + return cy.intercept('POST', apiMatcher('images'), image); +}; + +/** + * Intercepts GET request to mock image data. + * + * @param images - an array of mock image objects + * + * @returns Cypress chainable. + */ +export const mockGetImages = (images: Image[]): Cypress.Chainable<null> => { + return cy.intercept('GET', apiMatcher('images*'), paginateResponse(images)); +}; + +/** + * Intercepts the response for an image GET request. + * + * Responds with an image with the given label, ID, and status. + * + * @param label - Response image label. + * @param id - Response image ID. Expected to be prefixed with a string (e.g. 'private/12345'). + * @param status - Image status. + * + * @returns Cypress chainable. + */ +export const mockGetImage = ( + label: string, + id: string, + status: ImageStatus +): Cypress.Chainable<null> => { + return cy.intercept('GET', apiMatcher(`images/${id}*`), (req) => { + return req.reply( + imageFactory.build({ + label, + id, + status, + }) + ); + }); +}; diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index c222753c965..d01e38c9a33 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -2,7 +2,7 @@ * @file Cypress intercepts and mocks for Cloud Manager Linode operations. */ -import type { Linode, Volume } from '@linode/api-v4/types'; +import type { Disk, Linode, Volume } from '@linode/api-v4/types'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; @@ -96,3 +96,22 @@ export const interceptRebootLinodeIntoRescueMode = ( apiMatcher(`linode/instances/${linodeId}/rescue`) ); }; + +/** + * Intercepts GET request to retrieve a Linode's Disks and mocks response. + * + * @param linodeId - ID of Linode for intercepted request. + * @param disks - Array of Disks with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetLinodeDisks = ( + linodeId: number, + disks: Disk[] +): Cypress.Chainable<null> => { + return cy.intercept( + 'GET', + apiMatcher(`linode/instances/${linodeId}/disks*`), + paginateResponse(disks) + ); +}; diff --git a/packages/manager/cypress/support/intercepts/lke.ts b/packages/manager/cypress/support/intercepts/lke.ts index a46cdad12d2..20853175f3b 100644 --- a/packages/manager/cypress/support/intercepts/lke.ts +++ b/packages/manager/cypress/support/intercepts/lke.ts @@ -2,9 +2,133 @@ * @file Cypress intercepts and mocks for Cloud Manager LKE operations. */ -import type { KubernetesCluster } from '@linode/api-v4/types'; +import type { + KubernetesCluster, + KubeNodePoolResponse, + KubeConfigResponse, + KubernetesVersion, +} from '@linode/api-v4/types'; +import { kubernetesVersions } from 'support/constants/lke'; import { apiMatcher } from 'support/util/intercepts'; +import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; +import { randomDomainName } from 'support/util/random'; +import { + kubernetesDashboardUrlFactory, + kubeEndpointFactory, +} from '@src/factories'; + +/** + * Intercepts GET request to retrieve Kubernetes versions and mocks response. + * + * @param versions - Optional array of strings containing mocked versions. + * + * @returns Cypress chainable. + */ +export const mockGetKubernetesVersions = (versions?: string[] | undefined) => { + const versionObjects = (versions ? versions : kubernetesVersions).map( + (kubernetesVersionString: string): KubernetesVersion => { + return { id: kubernetesVersionString }; + } + ); + + return cy.intercept( + 'GET', + apiMatcher('lke/versions*'), + paginateResponse(versionObjects) + ); +}; + +/** + * Intercepts GET request to retrieve LKE clusters and mocks response. + * + * @param clusters - LKE clusters with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetClusters = ( + clusters: KubernetesCluster[] +): Cypress.Chainable<null> => { + return cy.intercept( + 'GET', + apiMatcher('lke/clusters*'), + paginateResponse(clusters) + ); +}; + +/** + * Intercepts GET request to retrieve an LKE cluster and mocks the response. + * + * @param cluster - LKE cluster with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetCluster = ( + cluster: KubernetesCluster +): Cypress.Chainable<null> => { + return cy.intercept( + 'GET', + apiMatcher(`lke/clusters/${cluster.id}`), + makeResponse(cluster) + ); +}; + +/** + * Intercepts PUT request to update an LKE cluster and mocks response. + * + * @param clusterId - ID of cluster for which to intercept PUT request. + * @param cluster - Updated Kubernetes cluster with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockUpdateCluster = ( + clusterId: number, + cluster: KubernetesCluster +): Cypress.Chainable<null> => { + return cy.intercept( + 'PUT', + apiMatcher(`lke/clusters/${clusterId}`), + makeResponse(cluster) + ); +}; + +/** + * Intercepts GET request to retrieve an LKE cluster's node pools and mocks response. + * + * @param clusterId - ID of cluster for which to intercept GET request. + * @param pools - Array of LKE node pools with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetClusterPools = ( + clusterId: number, + pools: KubeNodePoolResponse[] +): Cypress.Chainable<null> => { + return cy.intercept( + 'GET', + apiMatcher(`lke/clusters/${clusterId}/pools*`), + paginateResponse(pools) + ); +}; + +/** + * Intercepts GET request to retrieve an LKE cluster's kubeconfig and mocks response. + * + * @param clusterId - ID of cluster for which to mock response. + * @param kubeconfig - Kubeconfig object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetKubeconfig = ( + clusterId: number, + kubeconfig: KubeConfigResponse +): Cypress.Chainable<null> => { + return cy.intercept( + 'GET', + apiMatcher(`lke/clusters/${clusterId}/kubeconfig`), + makeResponse(kubeconfig) + ); +}; /** * Intercepts POST request to create an LKE cluster. @@ -31,3 +155,201 @@ export const mockCreateCluster = ( makeResponse(cluster) ); }; + +/** + * Intercepts DELETE request to delete an LKE cluster and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockDeleteCluster = ( + clusterId: number +): Cypress.Chainable<null> => { + return cy.intercept( + 'DELETE', + apiMatcher(`lke/clusters/${clusterId}`), + makeResponse() + ); +}; + +/** + * Intercepts POST request to add a node pool and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param nodePool - Node pool response object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockAddNodePool = ( + clusterId: number, + nodePool: KubeNodePoolResponse +): Cypress.Chainable<null> => { + return cy.intercept( + 'POST', + apiMatcher(`lke/clusters/${clusterId}/pools`), + makeResponse(nodePool) + ); +}; + +/** + * Intercepts PUT request to update a node pool and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param nodePoolId - Numeric ID of node pool for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockUpdateNodePool = ( + clusterId: number, + nodePool: KubeNodePoolResponse +): Cypress.Chainable<null> => { + return cy.intercept( + 'PUT', + apiMatcher(`lke/clusters/${clusterId}/pools/${nodePool.id}`), + makeResponse(nodePool) + ); +}; + +/** + * Intercepts DELETE request to delete a node pool and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param nodePoolId - Numeric ID of node pool for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockDeleteNodePool = ( + clusterId: number, + nodePoolId: number +): Cypress.Chainable<null> => { + return cy.intercept( + 'DELETE', + apiMatcher(`lke/clusters/${clusterId}/pools/${nodePoolId}`), + makeResponse({}) + ); +}; + +/** + * Intercepts POST request to recycle a node and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param nodeId - ID of node for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockRecycleNode = ( + clusterId: number, + nodeId: string +): Cypress.Chainable<null> => { + return cy.intercept( + 'POST', + apiMatcher(`lke/clusters/${clusterId}/nodes/${nodeId}/recycle`), + makeResponse({}) + ); +}; + +/** + * Intercepts POST request to recycle a node pool and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param nodePoolId - Numeric ID of node pool for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockRecycleNodePool = ( + clusterId: number, + poolId: number +): Cypress.Chainable<null> => { + return cy.intercept( + 'POST', + apiMatcher(`lke/clusters/${clusterId}/pools/${poolId}/recycle`), + makeResponse({}) + ); +}; + +/** + * Intercepts POST request to recycle all of a cluster's nodes and mocks the response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockRecycleAllNodes = ( + clusterId: number +): Cypress.Chainable<null> => { + return cy.intercept( + 'POST', + apiMatcher(`lke/clusters/${clusterId}/recycle`), + makeResponse({}) + ); +}; + +/** + * Intercepts GET request to retrieve Kubernetes cluster dashboard URL and mocks response. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param url - Optional URL to include in mocked response. + * + * @returns Cypress chainable. + */ +export const mockGetDashboardUrl = (clusterId: number, url?: string) => { + const dashboardUrl = url ?? `https://${randomDomainName()}`; + const dashboardResponse = kubernetesDashboardUrlFactory.build({ + url: dashboardUrl, + }); + + return cy.intercept( + 'GET', + apiMatcher(`lke/clusters/${clusterId}/dashboard`), + makeResponse(dashboardResponse) + ); +}; + +/** + * Intercepts GET request to retrieve cluster API endpoints and mocks response. + * + * By default, a single endpoint 'https://cy-test.linodelke.net:443' is returned. + * Cloud Manager will only display endpoints that end with 'linodelke.net:443'. + * + * @param clusterId - Numeric ID of LKE cluster for which to mock response. + * @param endpoints - Optional array of API endpoints to include in mocked response. + * + * @returns Cypress chainable. + */ +export const mockGetApiEndpoints = ( + clusterId: number, + endpoints?: string[] +): Cypress.Chainable => { + // Endpoint has to end with 'linodelke.net:443' to be displayed in Cloud. + const kubeEndpoints = endpoints + ? endpoints.map((endpoint: string) => + kubeEndpointFactory.build({ endpoint }) + ) + : kubeEndpointFactory.build({ + endpoint: `https://cy-test.linodelke.net:443`, + }); + + return cy.intercept( + 'GET', + apiMatcher(`lke/clusters/${clusterId}/api-endpoints*`), + paginateResponse(kubeEndpoints) + ); +}; + +/** + * Intercepts DELETE request to reset Kubeconfig and mocks the response. + * + * @param clusterId - Numberic ID of LKE cluster for which to mock response. + * + * @returns Cypress chainable. + */ +export const mockResetKubeconfig = ( + clusterId: number +): Cypress.Chainable<null> => { + return cy.intercept( + 'DELETE', + apiMatcher(`lke/clusters/${clusterId}/kubeconfig`), + makeResponse({}) + ); +}; diff --git a/packages/manager/cypress/support/util/downloads.ts b/packages/manager/cypress/support/util/downloads.ts new file mode 100644 index 00000000000..7c53660fd6e --- /dev/null +++ b/packages/manager/cypress/support/util/downloads.ts @@ -0,0 +1,24 @@ +// Path to Cypress downloads folder. +const downloadsPath = Cypress.config('downloadsFolder'); + +/** + * Returns the path to the downloaded file with the given filename. + * + * @param filename - Filename of downloaded file for which to get path. + * + * @returns Path to download with the given filename. + */ +export const getDownloadFilepath = (filename: string) => { + return `${downloadsPath}/${filename}`; +}; + +/** + * Reads a downloaded file with the given filename. + * + * @param filename - Filename of downloaded file to read. + * + * @returns Cypress chainable. + */ +export const readDownload = (filename: string) => { + return cy.readFile(getDownloadFilepath(filename)); +}; diff --git a/packages/manager/package.json b/packages/manager/package.json index 991f45ade62..52ad113a0d7 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.92.0", + "version": "1.93.0", "private": true, "bugs": { "url": "https://github.com/Linode/manager/issues" @@ -173,7 +173,7 @@ "browserslist": "^4.16.5", "chai-string": "^1.5.0", "core-js": "^2.6.5", - "cypress": "^12.2.0", + "cypress": "^12.11.0", "cypress-axe": "^1.0.0", "cypress-file-upload": "^5.0.7", "cypress-real-events": "^1.7.0", @@ -208,7 +208,7 @@ "reselect-tools": "^0.0.7", "serve": "^14.0.1", "storybook": "~7.0.6", - "storybook-dark-mode-v7": "3.0.0-alpha.0", + "storybook-dark-mode": "^3.0.0", "vite": "^4.3.0", "vite-plugin-svgr": "^2.4.0" }, diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index ef2f9ade54b..255d24226c2 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -42,6 +42,7 @@ import { firewallEventsHandler } from './queries/firewalls'; import { nodebalanacerEventHandler } from './queries/nodebalancers'; import { oauthClientsEventHandler } from './queries/accountOAuth'; import { ADOBE_ANALYTICS_URL } from './constants'; +import { linodeEventsHandler } from './queries/linodes/events'; interface Props { location: RouteComponentProps['location']; @@ -90,12 +91,20 @@ export class App extends React.Component<CombinedProps, State> { } /** - * Send pageviews unless blocklisted. + * Send pageviews */ this.props.history.listen(({ pathname }) => { + // Send Google Analytics page view events if ((window as any).ga) { (window as any).ga('send', 'pageview', pathname); } + + // Send Adobe Analytics page view events + if ((window as any)._satellite) { + (window as any)._satellite.track('page view', { + url: pathname, + }); + } }); /** @@ -168,6 +177,15 @@ export class App extends React.Component<CombinedProps, State> { ) .subscribe(oauthClientsEventHandler); + events$ + .filter( + ({ event }) => + (event.action.startsWith('linode') || + event.action.startsWith('backups')) && + !event._initial + ) + .subscribe(linodeEventsHandler); + /* * We want to listen for migration events side-wide * It's unpredictable when a migration is going to happen. It could take diff --git a/packages/manager/src/cachedData/kernels.json b/packages/manager/src/cachedData/kernels.json index 933318d3674..f46b9f5e0cd 100644 --- a/packages/manager/src/cachedData/kernels.json +++ b/packages/manager/src/cachedData/kernels.json @@ -1 +1 @@ -{"data":[{"id":"linode/latest-2.6-32bit","label":"Latest 2.6 (2.6.39.1-linode34)","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/latest-2.6","label":"Latest 2.6 Stable (2.6.23.17-linode44)","version":"2.6.24","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/latest-32bit","label":"Latest 32 bit (6.0.10-x86-linode178)","version":"6.0.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-12-01T18:09:33"},{"id":"linode/2.6.18.8-linode22","label":"Latest Legacy (2.6.18.8-linode22)","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2006-06-25T04:00:00"},{"id":"linode/6.1.10-x86_64-linode159","label":"6.1.10-x86_64-linode159","version":"6.1.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-02-08T19:14:45"},{"id":"linode/6.1.10-x86-linode179","label":"6.1.10-x86-linode179","version":"6.1.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-02-08T19:07:56"},{"id":"linode/6.0.10-x86_64-linode158","label":"6.0.10-x86_64-linode158","version":"6.0.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-12-01T18:16:43"},{"id":"linode/6.0.10-x86-linode178","label":"6.0.10-x86-linode178","version":"6.0.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-12-01T18:09:33"},{"id":"linode/6.0.2-x86_64-linode157","label":"6.0.2-x86_64-linode157","version":"6.0.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-10-17T17:01:41"},{"id":"linode/6.0.2-x86-linode177","label":"6.0.2-x86-linode177","version":"6.0.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-10-17T16:54:28"},{"id":"linode/5.19.2-x86_64-linode156","label":"5.19.2-x86_64-linode156","version":"5.19.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-08-18T19:51:13"},{"id":"linode/5.19.2-x86-linode176","label":"5.19.2-x86-linode176","version":"5.19.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-08-18T19:44:26"},{"id":"linode/5.18.2-x86_64-linode155","label":"5.18.2-x86_64-linode155","version":"5.18.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-06-07T14:46:11"},{"id":"linode/5.18.2-x86-linode175","label":"5.18.2-x86-linode175","version":"5.18.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-06-07T14:39:32"},{"id":"linode/5.17.5-x86_64-linode154","label":"5.17.5-x86_64-linode154","version":"5.17.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-05-02T19:07:22"},{"id":"linode/5.17.5-x86-linode174","label":"5.17.5-x86-linode174","version":"5.17.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-05-02T19:00:48"},{"id":"linode/5.16.13-x86-linode173","label":"5.16.13-x86-linode173","version":"5.16.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-03-08T19:09:29"},{"id":"linode/5.16.13-x86_64-linode153","label":"5.16.13-x86_64-linode153","version":"5.16.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-03-08T19:16:05"},{"id":"linode/5.16.3-x86_64-linode152","label":"5.16.3-x86_64-linode152","version":"5.16.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-01-27T19:46:44"},{"id":"linode/5.16.3-x86-linode172","label":"5.16.3-x86-linode172","version":"5.16.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-01-27T19:40:10"},{"id":"linode/5.15.10-x86_64-linode151","label":"5.15.10-x86_64-linode151","version":"5.15.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2021-12-21T18:44:00"},{"id":"linode/5.15.10-x86-linode171","label":"5.15.10-x86-linode171","version":"5.15.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2021-12-21T18:37:00"},{"id":"linode/5.14.17-x86_64-linode150","label":"5.14.17-x86_64-linode150","version":"5.14.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-11-11T18:23:00"},{"id":"linode/5.14.17-x86-linode170","label":"5.14.17-x86-linode170","version":"5.14.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-11-11T18:17:00"},{"id":"linode/5.14.15-x86_64-linode149","label":"5.14.15-x86_64-linode149","version":"5.14.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-11-01T18:02:00"},{"id":"linode/5.14.15-x86-linode169","label":"5.14.15-x86-linode169","version":"5.14.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-11-01T17:55:00"},{"id":"linode/5.14.14-x86_64-linode148","label":"5.14.14-x86_64-linode148","version":"5.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-10-20T18:22:00"},{"id":"linode/5.14.14-x86-linode168","label":"5.14.14-x86-linode168","version":"5.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-10-20T18:15:00"},{"id":"linode/5.14.2-x86_64-linode147","label":"5.14.2-x86_64-linode147","version":"5.14.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-09-08T19:06:00"},{"id":"linode/5.14.2-x86-linode167","label":"5.14.2-x86-linode167","version":"5.14.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-09-08T19:00:00"},{"id":"linode/5.13.4-x86_64-linode146","label":"5.13.4-x86_64-linode146","version":"5.13.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-07-21T18:51:00"},{"id":"linode/5.13.4-x86-linode166","label":"5.13.4-x86-linode166","version":"5.13.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-07-21T18:45:00"},{"id":"linode/5.12.13-x86_64-linode145","label":"5.12.13-x86_64-linode145","version":"5.12.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-06-24T20:24:00"},{"id":"linode/5.12.13-x86-linode165","label":"5.12.13-x86-linode165","version":"5.12.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-06-24T20:19:00"},{"id":"linode/5.12.2-x86_64-linode144","label":"5.12.2-x86_64-linode144","version":"5.12.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-05-10T17:16:00"},{"id":"linode/5.12.2-x86-linode164","label":"5.12.2-x86-linode164","version":"5.12.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-05-10T17:10:00"},{"id":"linode/5.11.13-x86_64-linode143","label":"5.11.13-x86_64-linode143","version":"5.11.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-04-13T15:46:00"},{"id":"linode/5.11.13-x86-linode163","label":"5.11.13-x86-linode163","version":"5.11.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-04-13T15:40:00"},{"id":"linode/5.11.9-x86_64-linode142","label":"5.11.9-x86_64-linode142","version":"5.11.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-03-24T15:33:00"},{"id":"linode/5.11.9-x86-linode162","label":"5.11.9-x86-linode162","version":"5.11.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-03-24T15:28:00"},{"id":"linode/5.10.13-x86_64-linode141","label":"5.10.13-x86_64-linode141","version":"5.10.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2021-02-04T19:02:00"},{"id":"linode/5.10.13-x86-linode161","label":"5.10.13-x86-linode161","version":"5.10.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2021-02-04T18:56:00"},{"id":"linode/5.10.2-x86_64-linode140","label":"5.10.2-x86_64-linode140","version":"5.10.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-12-22T20:43:00"},{"id":"linode/5.10.2-x86-linode160","label":"5.10.2-x86-linode160","version":"5.10.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-12-22T20:38:00"},{"id":"linode/5.9.6-x86_64-linode139","label":"5.9.6-x86_64-linode139","version":"5.9.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-11-05T19:51:00"},{"id":"linode/5.9.6-x86-linode159","label":"5.9.6-x86-linode159","version":"5.9.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-11-05T19:45:00"},{"id":"linode/5.8.10-x86-linode158","label":"5.8.10-x86-linode158","version":"5.8.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-09-17T19:58:00"},{"id":"linode/5.8.10-x86_64-linode138","label":"5.8.10-x86_64-linode138","version":"5.8.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-09-17T20:03:00"},{"id":"linode/5.8.3-x86_64-linode137","label":"5.8.3-x86_64-linode137","version":"5.8.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-08-24T18:55:00"},{"id":"linode/5.8.3-x86-linode157","label":"5.8.3-x86-linode157","version":"5.8.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-08-24T18:50:00"},{"id":"linode/5.7.6-x86-linode156","label":"5.7.6-x86-linode156","version":"5.7.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-06-25T16:41:08"},{"id":"linode/5.6.14-x86-linode155","label":"5.6.14-x86-linode155","version":"5.6.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-06-01T14:05:47"},{"id":"linode/5.6.1-x86-linode154","label":"5.6.1-x86-linode154","version":"5.6.1","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-04-07T21:29:12"},{"id":"linode/5.4.10-x86-linode152","label":"5.4.10-x86-linode152","version":"5.4.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2020-01-10T21:02:10"},{"id":"linode/5.3.11-x86-linode151","label":"5.3.11-x86-linode151","version":"5.3.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-11-14T20:38:53"},{"id":"linode/5.3.7-x86-linode150","label":"5.3.7-x86-linode150","version":"5.3.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-10-30T21:10:08"},{"id":"linode/5.2.9-x86-linode149","label":"5.2.9-x86-linode149","version":"5.2.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-08-21T18:48:52"},{"id":"linode/5.1.17-x86-linode148","label":"5.1.17-x86-linode148","version":"5.1.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-07-16T15:25:35"},{"id":"linode/5.1.11-x86-linode147","label":"5.1.11-x86-linode147","version":"5.1.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-06-17T22:46:37"},{"id":"linode/5.1.5-x86-linode146","label":"5.1.5-x86-linode146","version":"5.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-30T20:30:37"},{"id":"linode/4.14.120-x86-linode145","label":"4.14.120-x86-linode145","version":"4.14.120","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-21T10:57:13"},{"id":"linode/5.1.2-x86-linode144","label":"5.1.2-x86-linode144","version":"5.1.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-15T16:13:49"},{"id":"linode/5.0.8-x86-linode143","label":"5.0.8-x86-linode143","version":"5.0.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-04-17T18:39:29"},{"id":"linode/4.20.4-x86-linode141","label":"4.20.4-x86-linode141","version":"4.20.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-01-25T19:13:27"},{"id":"linode/4.19.8-x86-linode140","label":"4.19.8-x86-linode140","version":"4.19.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-12-12T21:44:08"},{"id":"linode/4.19.5-x86-linode139","label":"4.19.5-x86-linode139","version":"4.19.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-11-29T16:36:09"},{"id":"linode/4.18.16-x86-linode138","label":"4.18.16-x86-linode138","version":"4.18.16","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-10-29T20:24:50"},{"id":"linode/4.18.8-x86-linode137","label":"4.18.8-x86-linode137","version":"4.18.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-10-04T18:06:19"},{"id":"linode/4.18.8-x86-linode136","label":"4.18.8-x86-linode136","version":"4.18.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-09-19T12:54:57"},{"id":"linode/4.17.17-x86-linode135","label":"4.17.17-x86-linode135","version":"4.17.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-20T17:24:09"},{"id":"linode/4.17.15-x86-linode134","label":"4.17.15-x86-linode134","version":"4.17.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-16T17:13:56"},{"id":"linode/4.17.14-x86-linode133","label":"4.17.14-x86-linode133","version":"4.17.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-13T16:40:31"},{"id":"linode/4.17.14-x86-linode132","label":"4.17.14-x86-linode132","version":"4.17.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-10T21:08:51"},{"id":"linode/4.17.12-x86-linode131","label":"4.17.12-x86-linode131","version":"4.17.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-07T13:01:28"},{"id":"linode/4.17.11-x86-linode130","label":"4.17.11-x86-linode130","version":"4.17.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-07-31T19:51:00"},{"id":"linode/4.17.8-x86-linode129","label":"4.17.8-x86-linode129","version":"4.17.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-07-18T17:17:29"},{"id":"linode/4.17.2-x86-linode128","label":"4.17.2-x86-linode128","version":"4.17.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-06-26T19:42:55"},{"id":"linode/4.16.11-x86-linode127","label":"4.16.11-x86-linode127","version":"4.16.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-05-24T21:33:29"},{"id":"linode/4.15.18-x86-linode126","label":"4.15.18-x86-linode126","version":"4.15.18","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-05-02T20:21:02"},{"id":"linode/4.15.13-x86-linode125","label":"4.15.13-x86-linode125","version":"4.15.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-27T17:17:56"},{"id":"linode/4.15.12-x86-linode124","label":"4.15.12-x86-linode124","version":"4.15.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-22T20:09:16"},{"id":"linode/4.15.10-x86-linode123","label":"4.15.10-x86-linode123","version":"4.15.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-16T21:18:40"},{"id":"linode/4.15.8-x86-linode122","label":"4.15.8-x86-linode122","version":"4.15.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-10T21:25:42"},{"id":"linode/4.15.7-x86-linode121","label":"4.15.7-x86-linode121","version":"4.15.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-01T17:20:09"},{"id":"linode/4.14.19-x86-linode119","label":"4.14.19-x86-linode119","version":"4.14.19","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-13T19:05:44"},{"id":"linode/4.14.17-x86-linode118","label":"4.14.17-x86-linode118","version":"4.14.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:34:40"},{"id":"linode/4.9.80-x86-linode117","label":"4.9.80-x86-linode117","version":"4.9.80","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:33:46"},{"id":"linode/4.4.115-x86-linode116","label":"4.4.115-x86-linode116","version":"4.4.115","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:32:57"},{"id":"linode/4.4.113-x86-linode115","label":"4.4.113-x86-linode115","version":"4.4.113","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-25T22:47:11"},{"id":"linode/4.9.78-x86-linode114","label":"4.9.78-x86-linode114","version":"4.9.78","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-23T23:23:58"},{"id":"linode/4.14.14-x86-linode113","label":"4.14.14-x86-linode113","version":"4.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-23T02:00:02"},{"id":"linode/4.14.14-x86-linode112","label":"4.14.14-x86-linode112","version":"4.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-18T20:09:53"},{"id":"linode/4.9.64-x86-linode107","label":"4.9.64-x86-linode107","version":"4.9.64","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-12-01T23:10:11"},{"id":"linode/4.9.68-x86-linode108","label":"4.9.68-x86-linode108","version":"4.9.68","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-12-11T19:00:15"},{"id":"linode/4.14.12-x86-linode111","label":"4.14.12-x86-linode111","version":"4.14.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-05T16:31:55"},{"id":"linode/4.14.11-x86-linode110","label":"4.14.11-x86-linode110","version":"4.14.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-04T18:56:25"},{"id":"linode/4.9.56-x86-linode106","label":"4.9.56-x86-linode106","version":"4.9.56","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-10-13T21:10:23"},{"id":"linode/4.9.50-x86-linode105","label":"4.9.50-x86-linode105","version":"4.9.50","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-09-14T21:46:56"},{"id":"linode/4.9.36-x86-linode104","label":"4.9.36-x86-linode104","version":"4.9.36","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-07-10T13:16:53"},{"id":"linode/4.9.33-x86-linode102","label":"4.9.33-x86-linode102","version":"4.9.33","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-06-23T22:06:05"},{"id":"linode/4.9.15-x86-linode100","label":"4.9.15-x86-linode100","version":"4.9.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-03-22T13:48:13"},{"id":"linode/4.9.7-x86-linode99","label":"4.9.7-x86-linode99","version":"4.9.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-02-03T22:54:57"},{"id":"linode/4.9.0-x86-linode98","label":"4.9.0-x86-linode98","version":"4.9.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-12-13T20:10:20"},{"id":"linode/4.8.6-x86-linode97","label":"4.8.6-x86-linode97","version":"4.8.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-11-02T15:23:43"},{"id":"linode/4.8.4-x86-linode96","label":"4.8.4-x86-linode96","version":"4.8.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-27T18:51:41"},{"id":"linode/4.8.3-x86-linode95","label":"4.8.3-x86-linode95","version":"4.8.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-20T23:10:27"},{"id":"linode/4.8.1-x86-linode94","label":"4.8.1-x86-linode94","version":"4.8.1","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-07T22:21:55"},{"id":"linode/4.7.3-x86-linode92","label":"4.7.3-x86-linode92","version":"4.7.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-09-15T13:13:40"},{"id":"linode/4.7.0-x86-linode90","label":"4.7.0-x86-linode90","version":"4.7.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-08-05T14:35:48"},{"id":"linode/4.6.5-x86-linode89","label":"4.6.5-x86-linode89","version":"4.6.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-08-04T15:28:59"},{"id":"linode/4.5.5-x86-linode88","label":"4.5.5-x86-linode88","version":"4.5.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-05-24T15:29:02"},{"id":"linode/4.5.3-x86-linode86","label":"4.5.3-x86-linode86","version":"4.5.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-05-10T19:39:51"},{"id":"linode/4.5.0-x86-linode84","label":"4.5.0-x86-linode84","version":"4.5.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-03-16T18:53:02"},{"id":"linode/4.4.4-x86-linode83","label":"4.4.4-x86-linode83","version":"4.4.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-03-10T22:20:19"},{"id":"linode/4.4.0-x86-linode82","label":"4.4.0-x86-linode82","version":"4.4.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-01-20T14:41:05"},{"id":"linode/4.1.5-x86-linode80","label":"4.1.5-x86-linode80","version":"4.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-08-24T19:00:43"},{"id":"linode/4.1.5-x86-linode79","label":"4.1.5-x86-linode79","version":"4.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-08-13T13:00:00"},{"id":"linode/4.1.0-x86-linode78","label":"4.1.0-x86-linode78","version":"4.1.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-22T15:19:32"},{"id":"linode/4.0.5-x86-linode77","label":"4.0.5-x86-linode77","version":"4.0.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-11T13:58:18"},{"id":"linode/4.0.5-x86-linode76","label":"4.0.5-x86-linode76","version":"4.0.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-10T15:31:52"},{"id":"linode/4.0.4-x86-linode75","label":"4.0.4-x86-linode75","version":"4.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-21T15:15:47"},{"id":"linode/4.0.2-x86-linode74","label":"4.0.2-x86-linode74","version":"4.0.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-11T20:56:58"},{"id":"linode/4.0-x86-linode73","label":"4.0.1-x86-linode73","version":"4.0.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-04T13:43:23"},{"id":"linode/4.0-x86-linode72","label":"4.0-x86-linode72","version":"4.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-04-21T13:02:24"},{"id":"linode/3.19.1-x86-linode71","label":"3.19.1-x86-linode71","version":"3.19.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-03-11T18:00:36"},{"id":"linode/3.18.5-x86-linode70","label":"3.18.5-x86-linode70","version":"3.18.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-02-05T05:00:00"},{"id":"linode/3.18.3-x86-linode69","label":"3.18.3-x86-linode69","version":"3.18.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-23T15:12:45"},{"id":"linode/3.18.1-x86-linode68","label":"3.18.1-x86-linode68","version":"3.18.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-01-06T17:32:39"},{"id":"linode/3.16.7-x86-linode67","label":"3.16.7-x86-linode67","version":"3.16.7","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-11-14T22:31:46"},{"id":"linode/3.16.5-x86-linode65","label":"3.16.5-x86-linode65","version":"3.16.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-10-13T13:40:00"},{"id":"linode/3.15.4-x86-linode64","label":"3.15.4-x86-linode64","version":"3.15.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-07-07T13:50:35"},{"id":"linode/3.15.3-x86-linode63","label":"3.15.3-x86-linode63","version":"3.15.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-07-02T12:12:37"},{"id":"linode/3.15.2-x86-linode62","label":"3.15.2-x86-linode62","version":"3.15.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-30T18:46:50"},{"id":"linode/3.14.5-x86-linode61","label":"3.14.5-x86-linode61","version":"3.14.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-05T20:05:44"},{"id":"linode/3.14.5-x86-linode60","label":"3.14.5-x86-linode60","version":"3.14.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-03T13:09:58"},{"id":"linode/3.14.4-x86-linode59","label":"3.14.4-x86-linode59","version":"3.14.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-05-13T17:42:22"},{"id":"linode/3.14.1-x86-linode58","label":"3.14.1-x86-linode58","version":"3.14.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-04-25T17:49:15"},{"id":"linode/3.13.7-x86-linode57","label":"3.13.7-x86-linode57","version":"3.13.7","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-03-25T18:21:50"},{"id":"linode/3.12.9-x86-linode56","label":"3.12.9-x86-linode56","version":"3.12.9","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-02-03T19:42:13"},{"id":"linode/3.11.6-x86-linode54","label":"3.11.6-x86-linode54","version":"3.11.6","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-10-23T16:06:29"},{"id":"linode/3.12.6-x86-linode55","label":"3.12.6-x86-linode55","version":"3.12.6","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-12-23T16:25:39"},{"id":"linode/3.10.3-x86-linode53","label":"3.10.3-x86-linode53","version":"3.10.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-07-26T16:35:12"},{"id":"linode/3.9.3-x86-linode52","label":"3.9.3-x86-linode52","version":"3.9.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-05-20T14:27:27"},{"id":"linode/3.9.2-x86-linode51","label":"3.9.2-x86-linode51","version":"3.9.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-05-14T16:13:27"},{"id":"linode/3.8.4-linode50","label":"3.8.4-linode50","version":"3.8.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-03-25T20:42:49"},{"id":"linode/3.7.10-linode49","label":"3.7.10-linode49","version":"3.7.10","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-02-27T19:49:45"},{"id":"linode/3.7.5-linode48","label":"3.7.5-linode48","version":"3.7.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-01-31T19:52:25"},{"id":"linode/3.6.5-linode47","label":"3.6.5-linode47","version":"3.6.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-11-04T17:42:14"},{"id":"linode/3.5.3-linode46","label":"3.5.3-linode46","version":"3.5.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-09-05T20:45:36"},{"id":"linode/3.5.2-linode45","label":"3.5.2-linode45","version":"3.5.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-08-15T18:16:29"},{"id":"linode/3.4.2-linode44","label":"3.4.2-linode44","version":"3.4.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-06-11T19:03:10"},{"id":"linode/3.0.18-linode43","label":"3.0.18-linode43","version":"3.0.18","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-30T17:42:21"},{"id":"linode/3.1.10-linode42","label":"3.1.10-linode42","version":"3.1.10","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-25T21:24:07"},{"id":"linode/3.0.17-linode41","label":"3.0.17-linode41","version":"3.0.17","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-25T21:24:05"},{"id":"linode/3.2.1-linode40","label":"3.2.1-linode40","version":"3.2.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-23T16:04:48"},{"id":"linode/3.1.0-linode39","label":"3.1.0-linode39","version":"3.1.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-10-25T17:57:05"},{"id":"linode/3.0.4-linode38","label":"3.0.4-linode38","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-22T18:57:59"},{"id":"linode/3.0.4-linode37","label":"3.0.4-linode37","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-12T16:03:31"},{"id":"linode/3.0.4-linode36","label":"3.0.4-linode36","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-02T01:08:55"},{"id":"linode/3.0-linode35","label":"3.0.0-linode35","version":"3.0.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-08-02T15:43:52"},{"id":"linode/2.6.39.1-linode34","label":"2.6.39.1-linode34","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-06-21T14:42:50"},{"id":"linode/2.6.39-linode33","label":"2.6.39-linode33","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-05-25T19:05:05"},{"id":"linode/2.6.38.3-linode32","label":"2.6.38.3-linode32","version":"2.6.38","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-04-21T20:21:48"},{"id":"linode/2.6.38-linode31","label":"2.6.38-linode31","version":"2.6.38","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-03-21T21:44:09"},{"id":"linode/2.6.37-linode30","label":"2.6.37-linode30","version":"2.6.37","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-01-27T05:00:00"},{"id":"linode/2.6.35.7-linode29","label":"2.6.35.7-linode29","version":"2.6.35","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-10-13T04:00:00"},{"id":"linode/2.6.32.16-linode28","label":"2.6.32.16-linode28","version":"2.6.32","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-07-25T21:34:00"},{"id":"linode/2.6.34-linode27","label":"2.6.34-linode27","version":"2.6.34","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-07-16T04:00:00"},{"id":"linode/2.6.32.12-linode25","label":"2.6.32.12-linode25","version":"2.6.33","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-04-28T04:00:00"},{"id":"linode/2.6.33-linode24","label":"2.6.33-linode24","version":"2.6.33","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-02-24T22:05:00"},{"id":"linode/2.6.32-linode23","label":"2.6.32-linode23","version":"2.6.32","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-12-05T16:14:00"},{"id":"linode/2.6.18.8-linode22","label":"2.6.18.8-linode22","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-11-10T05:00:00"},{"id":"linode/2.6.31.5-linode21","label":"2.6.31.5-linode21","version":"2.6.31","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/2.6.30.5-linode20","label":"2.6.30.5-linode20","version":"2.6.30","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.23.17-linode44","label":"2.6.23.17-linode44","version":"2.6.23","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.18.8-linode19","label":"2.6.18.8-linode19","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-14T04:00:00"},{"id":"linode/2.6.29-linode18","label":"2.6.29-linode18","version":"2.6.29","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-04-01T04:00:00"},{"id":"linode/2.6.28.3-linode17","label":"2.6.28.3-linode17","version":"2.6.28","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-02-04T05:00:00"},{"id":"linode/2.6.18.8-linode16","label":"2.6.18.8-linode16","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-01-12T14:47:00"},{"id":"linode/2.6.28-linode15","label":"2.6.28-linode15","version":"2.6.28","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-12-29T05:00:00"},{"id":"linode/2.6.27.4-linode14","label":"2.6.27.4-linode14","version":"2.6.27","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-11-05T05:00:00"},{"id":"linode/2.6.26-linode13","label":"2.6.26-linode13","version":"2.6.26","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-07-13T23:15:00"},{"id":"linode/2.6.25.10-linode12","label":"2.6.25.10-linode12","version":"2.6.25","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-07-03T04:00:00"},{"id":"linode/2.6.18.8-linode10","label":"2.6.18.8-linode10","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2008-06-23T04:00:00"},{"id":"linode/2.6.25-linode9","label":"2.6.25-linode9","version":"2.6.25","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-04-10T04:00:00"},{"id":"linode/2.6.24.4-linode8","label":"2.6.24.4-linode8","version":"2.6.24","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-03-31T04:00:00"},{"id":"linode/2.6.18.8-domU-linode7","label":"2.6.18.8-domU-linode7","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":null},{"id":"linode/latest-2.6-64bit","label":"Latest 2.6 (2.6.39.1-x86_64-linode19)","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/latest-64bit","label":"Latest 64 bit (6.0.10-x86_64-linode158)","version":"6.0.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-12-01T18:16:43"},{"id":"linode/2.6.18.8-x86_64-linode10","label":"Latest Legacy (2.6.18.8-x86_64-linode10)","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/5.7.6-x86_64-linode136","label":"5.7.6-x86_64-linode136","version":"5.7.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-06-25T16:41:35"},{"id":"linode/5.6.14-x86_64-linode135","label":"5.6.14-x86_64-linode135","version":"5.6.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-06-01T14:06:45"},{"id":"linode/5.6.1-x86_64-linode134","label":"5.6.1-x86_64-linode134","version":"5.6.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-04-07T21:29:51"},{"id":"linode/5.4.10-x86_64-linode132","label":"5.4.10-x86_64-linode132","version":"5.4.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2020-01-10T21:03:16"},{"id":"linode/5.3.11-x86_64-linode131","label":"5.3.11-x86_64-linode131","version":"5.3.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-11-14T20:39:27"},{"id":"linode/5.3.7-x86_64-linode130","label":"5.3.7-x86_64-linode130","version":"5.3.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-10-30T21:10:29"},{"id":"linode/5.2.9-x86_64-linode129","label":"5.2.9-x86_64-linode129","version":"5.2.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-08-21T18:49:31"},{"id":"linode/5.1.17-x86_64-linode128","label":"5.1.17-x86_64-linode128","version":"5.1.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-07-16T15:26:33"},{"id":"linode/5.1.11-x86_64-linode127","label":"5.1.11-x86_64-linode127","version":"5.1.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-06-17T22:47:20"},{"id":"linode/5.1.5-x86_64-linode126","label":"5.1.5-x86_64-linode126","version":"5.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-30T20:30:39"},{"id":"linode/4.14.120-x86_64-linode125","label":"4.14.120-x86_64-linode125","version":"4.14.120","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-21T10:57:46"},{"id":"linode/5.1.2-x86_64-linode124","label":"5.1.2-x86_64-linode124","version":"5.1.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-15T16:14:35"},{"id":"linode/5.0.8-x86_64-linode123","label":"5.0.8-x86_64-linode123","version":"5.0.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-04-17T18:39:56"},{"id":"linode/5.0.1-x86_64-linode122","label":"5.0.1-x86_64-linode122","version":"5.0.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-03-13T16:51:01"},{"id":"linode/4.20.4-x86_64-linode121","label":"4.20.4-x86_64-linode121","version":"4.20.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-01-25T19:13:29"},{"id":"linode/4.19.8-x86_64-linode120","label":"4.19.8-x86_64-linode120","version":"4.19.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-12-12T21:44:08"},{"id":"linode/4.19.5-x86_64-linode119","label":"4.19.5-x86_64-linode119","version":"4.19.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-11-29T16:36:53"},{"id":"linode/4.18.16-x86_64-linode118","label":"4.18.16-x86_64-linode118","version":"4.18.16","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-10-29T20:24:13"},{"id":"linode/4.18.8-x86_64-linode117","label":"4.18.8-x86_64-linode117","version":"4.18.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-09-19T12:55:56"},{"id":"linode/4.17.17-x86_64-linode116","label":"4.17.17-x86_64-linode116","version":"4.17.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-20T17:23:32"},{"id":"linode/4.17.15-x86_64-linode115","label":"4.17.15-x86_64-linode115","version":"4.17.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-16T17:13:28"},{"id":"linode/4.17.14-x86_64-linode114","label":"4.17.14-x86_64-linode114","version":"4.17.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-13T16:41:06"},{"id":"linode/4.17.14-x86_64-linode113","label":"4.17.14-x86_64-linode113","version":"4.17.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-10T21:07:56"},{"id":"linode/4.17.12-x86_64-linode112","label":"4.17.12-x86_64-linode112","version":"4.17.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-07T13:02:24"},{"id":"linode/4.17.11-x86_64-linode111","label":"4.17.11-x86_64-linode111","version":"4.17.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-07-31T19:51:53"},{"id":"linode/4.17.8-x86_64-linode110","label":"4.17.8-x86_64-linode110","version":"4.17.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-07-18T17:18:30"},{"id":"linode/4.17.2-x86_64-linode109","label":"4.17.2-x86_64-linode109","version":"4.17.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-06-26T19:42:57"},{"id":"linode/4.16.11-x86_64-linode108","label":"4.16.11-x86_64-linode108","version":"4.16.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-05-24T21:33:31"},{"id":"linode/4.15.18-x86_64-linode107","label":"4.15.18-x86_64-linode107","version":"4.15.18","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-05-02T20:21:04"},{"id":"linode/4.15.13-x86_64-linode106","label":"4.15.13-x86_64-linode106","version":"4.15.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-27T17:17:56"},{"id":"linode/4.15.12-x86_64-linode105","label":"4.15.12-x86_64-linode105","version":"4.15.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-22T20:08:43"},{"id":"linode/4.15.10-x86_64-linode104","label":"4.15.10-x86_64-linode104","version":"4.15.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-16T21:18:35"},{"id":"linode/4.15.8-x86_64-linode103","label":"4.15.8-x86_64-linode103","version":"4.15.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-10T21:25:43"},{"id":"linode/4.15.7-x86_64-linode102","label":"4.15.7-x86_64-linode102","version":"4.15.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-01T17:20:54"},{"id":"linode/4.14.19-x86_64-linode100","label":"4.14.19-x86_64-linode100","version":"4.14.19","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-13T19:07:46"},{"id":"linode/4.14.17-x86_64-linode99","label":"4.14.17-x86_64-linode99","version":"4.14.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:35:09"},{"id":"linode/4.9.80-x86_64-linode98","label":"4.9.80-x86_64-linode98","version":"4.9.80","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:34:16"},{"id":"linode/4.4.115-x86_64-linode97","label":"4.4.115-x86_64-linode97","version":"4.4.115","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:33:23"},{"id":"linode/4.4.113-x86_64-linode96","label":"4.4.113-x86_64-linode96","version":"4.4.113","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-25T22:47:11"},{"id":"linode/4.9.78-x86_64-linode95","label":"4.9.78-x86_64-linode95","version":"4.9.78","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-23T23:23:58"},{"id":"linode/4.14.14-x86_64-linode94","label":"4.14.14-x86_64-linode94","version":"4.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-23T02:10:08"},{"id":"linode/4.14.14-x86_64-linode93","label":"4.14.14-x86_64-linode93","version":"4.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-18T20:08:56"},{"id":"linode/4.9.64-x86_64-linode88","label":"4.9.64-x86_64-linode88","version":"4.9.64","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-12-01T23:10:11"},{"id":"linode/4.9.68-x86_64-linode89","label":"4.9.68-x86_64-linode89","version":"4.9.68","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-12-11T19:00:48"},{"id":"linode/4.14.12-x86_64-linode92","label":"4.14.12-x86_64-linode92","version":"4.14.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-05T16:31:28"},{"id":"linode/4.14.11-x86_64-linode91","label":"4.14.11-x86_64-linode91","version":"4.14.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-04T18:56:22"},{"id":"linode/4.9.56-x86_64-linode87","label":"4.9.56-x86_64-linode87","version":"4.9.56","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-10-13T21:09:35"},{"id":"linode/4.9.50-x86_64-linode86","label":"4.9.50-x86_64-linode86","version":"4.9.50","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-09-14T21:46:25"},{"id":"linode/4.9.36-x86_64-linode85","label":"4.9.36-x86_64-linode85","version":"4.9.36","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-07-10T13:16:08"},{"id":"linode/4.9.33-x86_64-linode83","label":"4.9.33-x86_64-linode83","version":"4.9.33","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-06-23T21:04:33"},{"id":"linode/4.9.15-x86_64-linode81","label":"4.9.15-x86_64-linode81","version":"4.9.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-03-22T13:49:33"},{"id":"linode/4.9.7-x86_64-linode80","label":"4.9.7-x86_64-linode80","version":"4.9.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-02-03T22:55:37"},{"id":"linode/4.9.0-x86_64-linode79","label":"4.9.0-x86_64-linode79","version":"4.9.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-12-13T20:11:03"},{"id":"linode/4.8.6-x86_64-linode78","label":"4.8.6-x86_64-linode78","version":"4.8.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-11-02T15:24:17"},{"id":"linode/4.8.4-x86_64-linode77","label":"4.8.4-x86_64-linode77","version":"4.8.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-27T18:53:07"},{"id":"linode/4.8.3-x86_64-linode76","label":"4.8.3-x86_64-linode76","version":"4.8.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-20T23:10:27"},{"id":"linode/4.8.1-x86_64-linode75","label":"4.8.1-x86_64-linode75","version":"4.8.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-07T22:22:13"},{"id":"linode/4.7.3-x86_64-linode73","label":"4.7.3-x86_64-linode73","version":"4.7.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-09-15T13:13:01"},{"id":"linode/4.7.0-x86_64-linode72","label":"4.7.0-x86_64-linode72","version":"4.7.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-08-05T14:34:25"},{"id":"linode/4.6.5-x86_64-linode71","label":"4.6.5-x86_64-linode71","version":"4.6.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-08-04T15:28:01"},{"id":"linode/4.6.3-x86_64-linode70","label":"4.6.3-x86_64-linode70","version":"4.6.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-07-07T22:08:28"},{"id":"linode/4.5.5-x86_64-linode69","label":"4.5.5-x86_64-linode69","version":"4.5.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-05-24T15:30:08"},{"id":"linode/4.5.3-x86_64-linode67","label":"4.5.3-x86_64-linode67","version":"4.5.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-05-10T19:42:43"},{"id":"linode/4.5.0-x86_64-linode65","label":"4.5.0-x86_64-linode65","version":"4.5.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-03-16T18:53:02"},{"id":"linode/4.4.4-x86_64-linode64","label":"4.4.4-x86_64-linode64","version":"4.4.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-03-10T22:24:51"},{"id":"linode/4.4.0-x86_64-linode63","label":"4.4.0-x86_64-linode63","version":"4.4.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-01-20T14:41:05"},{"id":"linode/4.1.5-x86_64-linode61","label":"4.1.5-x86_64-linode61","version":"4.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-08-24T19:00:43"},{"id":"linode/4.1.5-x86_64-linode60","label":"4.1.5-x86_64-linode60 ","version":"4.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-08-13T13:00:00"},{"id":"linode/4.1.0-x86_64-linode59","label":"4.1.0-x86_64-linode59 ","version":"4.1.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-06-22T15:19:32"},{"id":"linode/4.0.5-x86_64-linode58","label":"4.0.5-x86_64-linode58","version":"4.0.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-06-10T15:31:52"},{"id":"linode/4.0.4-x86_64-linode57","label":"4.0.4-x86_64-linode57","version":"4.0.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-21T15:15:47"},{"id":"linode/4.0.2-x86_64-linode56","label":"4.0.2-x86_64-linode56","version":"4.0.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-11T20:56:58"},{"id":"linode/4.0.1-x86_64-linode55","label":"4.0.1-x86_64-linode55","version":"4.0.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-04T13:43:23"},{"id":"linode/4.0-x86_64-linode54","label":"4.0-x86_64-linode54","version":"4.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-04-21T13:02:24"},{"id":"linode/3.19.1-x86_64-linode53","label":"3.19.1-x86_64-linode53","version":"3.19.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-03-11T18:00:36"},{"id":"linode/3.18.5-x86_64-linode52","label":"3.18.5-x86_64-linode52","version":"3.18.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-02-05T05:00:00"},{"id":"linode/3.18.3-x86_64-linode51","label":"3.18.3-x86_64-linode51","version":"3.18.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-23T15:12:45"},{"id":"linode/3.18.1-x86_64-linode50","label":"3.18.1-x86_64-linode50","version":"3.18.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-06T17:32:39"},{"id":"linode/3.16.7-x86_64-linode49","label":"3.16.7-x86_64-linode49","version":"3.16.7","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-11-14T22:31:46"},{"id":"linode/3.16.5-x86_64-linode46","label":"3.16.5-x86_64-linode46","version":"3.16.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-10-13T13:42:00"},{"id":"linode/3.15.4-x86_64-linode45","label":"3.15.4-x86_64-linode45","version":"3.15.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-07-07T13:50:35"},{"id":"linode/3.15.3-x86_64-linode44","label":"3.15.3-x86_64-linode44","version":"3.15.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-07-02T12:12:37"},{"id":"linode/3.15.2-x86_64-linode43","label":"3.15.2-x86_64-linode43","version":"3.15.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-30T18:46:50"},{"id":"linode/3.14.5-x86_64-linode42","label":"3.14.5-x86_64-linode42","version":"3.14.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-05T20:05:44"},{"id":"linode/3.14.5-x86_64-linode41","label":"3.14.5-x86_64-linode41","version":"3.14.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-03T13:09:58"},{"id":"linode/3.14.4-x86_64-linode40","label":"3.14.4-x86_64-linode40","version":"3.14.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-05-13T17:42:22"},{"id":"linode/3.14.1-x86_64-linode39","label":"3.14.1-x86_64-linode39","version":"3.14.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-04-25T17:42:13"},{"id":"linode/3.13.7-x86_64-linode38","label":"3.13.7-x86_64-linode38","version":"3.13.7","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-03-25T18:21:50"},{"id":"linode/3.12.9-x86_64-linode37","label":"3.12.9-x86_64-linode37","version":"3.12.9","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-02-03T19:42:13"},{"id":"linode/3.12.6-x86_64-linode36","label":"3.12.6-x86_64-linode36","version":"3.12.6","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-12-23T16:24:18"},{"id":"linode/3.11.6-x86_64-linode35","label":"3.11.6-x86_64-linode35","version":"3.11.6","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-10-23T16:06:29"},{"id":"linode/3.10.3-x86_64-linode34","label":"3.10.3-x86_64-linode34","version":"3.10.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-07-26T16:35:12"},{"id":"linode/3.9.3-x86_64-linode33","label":"3.9.3-x86_64-linode33","version":"3.9.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-05-20T14:27:27"},{"id":"linode/3.9.2-x86_64-linode32","label":"3.9.2-x86_64-linode32","version":"3.9.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-05-14T15:53:02"},{"id":"linode/3.8.4-x86_64-linode31","label":"3.8.4-x86_64-linode31","version":"3.8.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-03-25T20:42:49"},{"id":"linode/3.7.10-x86_64-linode30","label":"3.7.10-x86_64-linode30","version":"3.7.10","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-02-27T19:49:45"},{"id":"linode/3.7.5-x86_64-linode29","label":"3.7.5-x86_64-linode29","version":"3.7.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-01-31T19:52:25"},{"id":"linode/3.6.5-x86_64-linode28","label":"3.6.5-x86_64-linode28","version":"3.6.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-11-04T17:42:14"},{"id":"linode/3.5.3-x86_64-linode27","label":"3.5.3-x86_64-linode27","version":"3.5.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-09-05T20:32:28"},{"id":"linode/3.4.2-x86_64-linode25","label":"3.4.2-x86_64-linode25","version":"3.2.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-06-11T18:40:20"},{"id":"linode/3.0.18-x86_64-linode24","label":"3.0.18-x86_64-linode24 ","version":"3.0.18","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-01-30T17:42:21"},{"id":"linode/3.2.1-x86_64-linode23","label":"3.2.1-x86_64-linode23","version":"3.2.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-01-23T16:04:48"},{"id":"linode/3.1.0-x86_64-linode22","label":"3.1.0-x86_64-linode22","version":"3.1.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-10-25T18:24:49"},{"id":"linode/3.0.4-x86_64-linode21","label":"3.0.4-x86_64-linode21","version":"3.0.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-09-02T01:08:55"},{"id":"linode/3.0.0-x86_64-linode20","label":"3.0.0-x86_64-linode20","version":"3.0.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-08-02T16:59:12"},{"id":"linode/2.6.39.1-x86_64-linode19","label":"2.6.39.1-x86_64-linode19","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-06-21T14:06:03"},{"id":"linode/2.6.39-x86_64-linode18","label":"2.6.39-x86_64-linode18","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-05-25T19:05:05"},{"id":"linode/2.6.38-x86_64-linode17","label":"2.6.38-x86_64-linode17","version":"2.6.38","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-03-21T21:44:09"},{"id":"linode/2.6.35.4-x86_64-linode16","label":"2.6.35.4-x86_64-linode16","version":"2.6.35","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-09-20T04:00:00"},{"id":"linode/2.6.32.12-x86_64-linode15","label":"2.6.32.12-x86_64-linode15","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-07-25T21:34:00"},{"id":"linode/2.6.34-x86_64-linode13","label":"2.6.34-x86_64-linode13","version":"2.6.34","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-06-17T04:00:00"},{"id":"linode/2.6.34-x86_64-linode14","label":"2.6.34-x86_64-linode14","version":"2.6.34","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-07-14T04:00:00"},{"id":"linode/2.6.32.12-x86_64-linode12","label":"2.6.32.12-x86_64-linode12","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-04-28T04:00:00"},{"id":"linode/2.6.32-x86_64-linode11","label":"2.6.32-x86_64-linode11","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-12-05T17:01:00"},{"id":"linode/2.6.18.8-x86_64-linode10","label":"2.6.18.8-x86_64-linode10","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-11-10T16:53:00"},{"id":"linode/2.6.31.5-x86_64-linode9","label":"2.6.31.5-x86_64-linode9","version":"2.6.31","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/2.6.30.5-x86_64-linode8","label":"2.6.30.5-x86_64-linode8","version":"2.6.30","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.18.8-x86_64-linode7","label":"2.6.18.8-x86_64-linode7","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-08-14T04:00:00"},{"id":"linode/2.6.29-x86_64-linode6","label":"2.6.29-x86_64-linode6","version":"2.6.29","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-04-02T04:00:00"},{"id":"linode/2.6.28.3-x86_64-linode5","label":"2.6.28.3-x86_64-linode5","version":"2.6.28","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-02-04T05:00:00"},{"id":"linode/2.6.28-x86_64-linode4","label":"2.6.28-x86_64-linode4","version":"2.6.28","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2008-12-29T05:00:00"},{"id":"linode/2.6.27.4-x86_64-linode3","label":"2.6.27.4-x86_64-linode3","version":"2.6.27","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2008-11-05T05:00:00"},{"id":"linode/2.6.16.38-x86_64-linode2","label":"2.6.16.38-x86_64-linode2","version":"2.6.16","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2008-03-23T04:00:00"},{"id":"linode/2.6.18.8-x86_64-linode1","label":"2.6.18.8-x86_64-linode1","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2008-03-23T04:00:00"},{"id":"linode/3.5.2-x86_64-linode26","label":"3.5.2-x86_64-linode26","version":"3.5.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-08-15T18:38:16"},{"id":"linode/grub2","label":"GRUB 2","version":"2.06","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2022-08-29T14:28:00"},{"id":"linode/direct-disk","label":"Direct Disk","version":"","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2015-05-05T01:51:43"},{"id":"linode/grub-legacy","label":"GRUB (Legacy)","version":"2.0.0","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2015-04-29T15:32:30"},{"id":"linode/pv-grub_x86_32","label":"pv-grub-x86_32","version":"2.6.26","kvm":false,"architecture":"i386","pvops":false,"deprecated":false,"built":"2008-09-15T04:00:00"},{"id":"linode/pv-grub_x86_64","label":"pv-grub-x86_64","version":"2.6.26","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2008-11-14T05:00:00"}],"page":1,"pages":1,"results":320} \ No newline at end of file +{"data":[{"id":"linode/latest-2.6-32bit","label":"Latest 2.6 (2.6.39.1-linode34)","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/latest-2.6","label":"Latest 2.6 Stable (2.6.23.17-linode44)","version":"2.6.24","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/latest-32bit","label":"Latest 32 bit (6.1.10-x86-linode179)","version":"6.1.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-02-08T19:07:56"},{"id":"linode/2.6.18.8-linode22","label":"Latest Legacy (2.6.18.8-linode22)","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2006-06-25T04:00:00"},{"id":"linode/6.2.9-x86_64-linode160","label":"6.2.9-x86_64-linode160","version":"6.2.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-04-05T19:30:32"},{"id":"linode/6.2.9-x86-linode180","label":"6.2.9-x86-linode180","version":"6.2.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-04-05T19:23:04"},{"id":"linode/6.1.10-x86_64-linode159","label":"6.1.10-x86_64-linode159","version":"6.1.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-02-08T19:14:45"},{"id":"linode/6.1.10-x86-linode179","label":"6.1.10-x86-linode179","version":"6.1.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-02-08T19:07:56"},{"id":"linode/6.0.10-x86_64-linode158","label":"6.0.10-x86_64-linode158","version":"6.0.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-12-01T18:16:43"},{"id":"linode/6.0.10-x86-linode178","label":"6.0.10-x86-linode178","version":"6.0.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-12-01T18:09:33"},{"id":"linode/6.0.2-x86_64-linode157","label":"6.0.2-x86_64-linode157","version":"6.0.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-10-17T17:01:41"},{"id":"linode/6.0.2-x86-linode177","label":"6.0.2-x86-linode177","version":"6.0.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-10-17T16:54:28"},{"id":"linode/5.19.2-x86_64-linode156","label":"5.19.2-x86_64-linode156","version":"5.19.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-08-18T19:51:13"},{"id":"linode/5.19.2-x86-linode176","label":"5.19.2-x86-linode176","version":"5.19.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-08-18T19:44:26"},{"id":"linode/5.18.2-x86_64-linode155","label":"5.18.2-x86_64-linode155","version":"5.18.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-06-07T14:46:11"},{"id":"linode/5.18.2-x86-linode175","label":"5.18.2-x86-linode175","version":"5.18.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-06-07T14:39:32"},{"id":"linode/5.17.5-x86_64-linode154","label":"5.17.5-x86_64-linode154","version":"5.17.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-05-02T19:07:22"},{"id":"linode/5.17.5-x86-linode174","label":"5.17.5-x86-linode174","version":"5.17.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-05-02T19:00:48"},{"id":"linode/5.16.13-x86-linode173","label":"5.16.13-x86-linode173","version":"5.16.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-03-08T19:09:29"},{"id":"linode/5.16.13-x86_64-linode153","label":"5.16.13-x86_64-linode153","version":"5.16.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-03-08T19:16:05"},{"id":"linode/5.16.3-x86_64-linode152","label":"5.16.3-x86_64-linode152","version":"5.16.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-01-27T19:46:44"},{"id":"linode/5.16.3-x86-linode172","label":"5.16.3-x86-linode172","version":"5.16.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-01-27T19:40:10"},{"id":"linode/5.15.10-x86_64-linode151","label":"5.15.10-x86_64-linode151","version":"5.15.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2021-12-21T18:44:00"},{"id":"linode/5.15.10-x86-linode171","label":"5.15.10-x86-linode171","version":"5.15.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2021-12-21T18:37:00"},{"id":"linode/5.14.17-x86_64-linode150","label":"5.14.17-x86_64-linode150","version":"5.14.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-11-11T18:23:00"},{"id":"linode/5.14.17-x86-linode170","label":"5.14.17-x86-linode170","version":"5.14.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-11-11T18:17:00"},{"id":"linode/5.14.15-x86_64-linode149","label":"5.14.15-x86_64-linode149","version":"5.14.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-11-01T18:02:00"},{"id":"linode/5.14.15-x86-linode169","label":"5.14.15-x86-linode169","version":"5.14.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-11-01T17:55:00"},{"id":"linode/5.14.14-x86_64-linode148","label":"5.14.14-x86_64-linode148","version":"5.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-10-20T18:22:00"},{"id":"linode/5.14.14-x86-linode168","label":"5.14.14-x86-linode168","version":"5.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-10-20T18:15:00"},{"id":"linode/5.14.2-x86_64-linode147","label":"5.14.2-x86_64-linode147","version":"5.14.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-09-08T19:06:00"},{"id":"linode/5.14.2-x86-linode167","label":"5.14.2-x86-linode167","version":"5.14.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-09-08T19:00:00"},{"id":"linode/5.13.4-x86_64-linode146","label":"5.13.4-x86_64-linode146","version":"5.13.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-07-21T18:51:00"},{"id":"linode/5.13.4-x86-linode166","label":"5.13.4-x86-linode166","version":"5.13.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-07-21T18:45:00"},{"id":"linode/5.12.13-x86_64-linode145","label":"5.12.13-x86_64-linode145","version":"5.12.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-06-24T20:24:00"},{"id":"linode/5.12.13-x86-linode165","label":"5.12.13-x86-linode165","version":"5.12.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-06-24T20:19:00"},{"id":"linode/5.12.2-x86_64-linode144","label":"5.12.2-x86_64-linode144","version":"5.12.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-05-10T17:16:00"},{"id":"linode/5.12.2-x86-linode164","label":"5.12.2-x86-linode164","version":"5.12.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-05-10T17:10:00"},{"id":"linode/5.11.13-x86_64-linode143","label":"5.11.13-x86_64-linode143","version":"5.11.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-04-13T15:46:00"},{"id":"linode/5.11.13-x86-linode163","label":"5.11.13-x86-linode163","version":"5.11.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-04-13T15:40:00"},{"id":"linode/5.11.9-x86_64-linode142","label":"5.11.9-x86_64-linode142","version":"5.11.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-03-24T15:33:00"},{"id":"linode/5.11.9-x86-linode162","label":"5.11.9-x86-linode162","version":"5.11.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-03-24T15:28:00"},{"id":"linode/5.10.13-x86_64-linode141","label":"5.10.13-x86_64-linode141","version":"5.10.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2021-02-04T19:02:00"},{"id":"linode/5.10.13-x86-linode161","label":"5.10.13-x86-linode161","version":"5.10.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2021-02-04T18:56:00"},{"id":"linode/5.10.2-x86_64-linode140","label":"5.10.2-x86_64-linode140","version":"5.10.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-12-22T20:43:00"},{"id":"linode/5.10.2-x86-linode160","label":"5.10.2-x86-linode160","version":"5.10.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-12-22T20:38:00"},{"id":"linode/5.9.6-x86_64-linode139","label":"5.9.6-x86_64-linode139","version":"5.9.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-11-05T19:51:00"},{"id":"linode/5.9.6-x86-linode159","label":"5.9.6-x86-linode159","version":"5.9.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-11-05T19:45:00"},{"id":"linode/5.8.10-x86-linode158","label":"5.8.10-x86-linode158","version":"5.8.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-09-17T19:58:00"},{"id":"linode/5.8.10-x86_64-linode138","label":"5.8.10-x86_64-linode138","version":"5.8.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-09-17T20:03:00"},{"id":"linode/5.8.3-x86_64-linode137","label":"5.8.3-x86_64-linode137","version":"5.8.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-08-24T18:55:00"},{"id":"linode/5.8.3-x86-linode157","label":"5.8.3-x86-linode157","version":"5.8.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-08-24T18:50:00"},{"id":"linode/5.7.6-x86-linode156","label":"5.7.6-x86-linode156","version":"5.7.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-06-25T16:41:08"},{"id":"linode/5.6.14-x86-linode155","label":"5.6.14-x86-linode155","version":"5.6.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-06-01T14:05:47"},{"id":"linode/5.6.1-x86-linode154","label":"5.6.1-x86-linode154","version":"5.6.1","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-04-07T21:29:12"},{"id":"linode/5.4.10-x86-linode152","label":"5.4.10-x86-linode152","version":"5.4.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2020-01-10T21:02:10"},{"id":"linode/5.3.11-x86-linode151","label":"5.3.11-x86-linode151","version":"5.3.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-11-14T20:38:53"},{"id":"linode/5.3.7-x86-linode150","label":"5.3.7-x86-linode150","version":"5.3.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-10-30T21:10:08"},{"id":"linode/5.2.9-x86-linode149","label":"5.2.9-x86-linode149","version":"5.2.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-08-21T18:48:52"},{"id":"linode/5.1.17-x86-linode148","label":"5.1.17-x86-linode148","version":"5.1.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-07-16T15:25:35"},{"id":"linode/5.1.11-x86-linode147","label":"5.1.11-x86-linode147","version":"5.1.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-06-17T22:46:37"},{"id":"linode/5.1.5-x86-linode146","label":"5.1.5-x86-linode146","version":"5.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-30T20:30:37"},{"id":"linode/4.14.120-x86-linode145","label":"4.14.120-x86-linode145","version":"4.14.120","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-21T10:57:13"},{"id":"linode/5.1.2-x86-linode144","label":"5.1.2-x86-linode144","version":"5.1.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-15T16:13:49"},{"id":"linode/5.0.8-x86-linode143","label":"5.0.8-x86-linode143","version":"5.0.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-04-17T18:39:29"},{"id":"linode/4.20.4-x86-linode141","label":"4.20.4-x86-linode141","version":"4.20.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-01-25T19:13:27"},{"id":"linode/4.19.8-x86-linode140","label":"4.19.8-x86-linode140","version":"4.19.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-12-12T21:44:08"},{"id":"linode/4.19.5-x86-linode139","label":"4.19.5-x86-linode139","version":"4.19.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-11-29T16:36:09"},{"id":"linode/4.18.16-x86-linode138","label":"4.18.16-x86-linode138","version":"4.18.16","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-10-29T20:24:50"},{"id":"linode/4.18.8-x86-linode137","label":"4.18.8-x86-linode137","version":"4.18.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-10-04T18:06:19"},{"id":"linode/4.18.8-x86-linode136","label":"4.18.8-x86-linode136","version":"4.18.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-09-19T12:54:57"},{"id":"linode/4.17.17-x86-linode135","label":"4.17.17-x86-linode135","version":"4.17.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-20T17:24:09"},{"id":"linode/4.17.15-x86-linode134","label":"4.17.15-x86-linode134","version":"4.17.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-16T17:13:56"},{"id":"linode/4.17.14-x86-linode133","label":"4.17.14-x86-linode133","version":"4.17.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-13T16:40:31"},{"id":"linode/4.17.14-x86-linode132","label":"4.17.14-x86-linode132","version":"4.17.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-10T21:08:51"},{"id":"linode/4.17.12-x86-linode131","label":"4.17.12-x86-linode131","version":"4.17.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-07T13:01:28"},{"id":"linode/4.17.11-x86-linode130","label":"4.17.11-x86-linode130","version":"4.17.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-07-31T19:51:00"},{"id":"linode/4.17.8-x86-linode129","label":"4.17.8-x86-linode129","version":"4.17.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-07-18T17:17:29"},{"id":"linode/4.17.2-x86-linode128","label":"4.17.2-x86-linode128","version":"4.17.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-06-26T19:42:55"},{"id":"linode/4.16.11-x86-linode127","label":"4.16.11-x86-linode127","version":"4.16.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-05-24T21:33:29"},{"id":"linode/4.15.18-x86-linode126","label":"4.15.18-x86-linode126","version":"4.15.18","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-05-02T20:21:02"},{"id":"linode/4.15.13-x86-linode125","label":"4.15.13-x86-linode125","version":"4.15.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-27T17:17:56"},{"id":"linode/4.15.12-x86-linode124","label":"4.15.12-x86-linode124","version":"4.15.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-22T20:09:16"},{"id":"linode/4.15.10-x86-linode123","label":"4.15.10-x86-linode123","version":"4.15.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-16T21:18:40"},{"id":"linode/4.15.8-x86-linode122","label":"4.15.8-x86-linode122","version":"4.15.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-10T21:25:42"},{"id":"linode/4.15.7-x86-linode121","label":"4.15.7-x86-linode121","version":"4.15.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-01T17:20:09"},{"id":"linode/4.14.19-x86-linode119","label":"4.14.19-x86-linode119","version":"4.14.19","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-13T19:05:44"},{"id":"linode/4.14.17-x86-linode118","label":"4.14.17-x86-linode118","version":"4.14.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:34:40"},{"id":"linode/4.9.80-x86-linode117","label":"4.9.80-x86-linode117","version":"4.9.80","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:33:46"},{"id":"linode/4.4.115-x86-linode116","label":"4.4.115-x86-linode116","version":"4.4.115","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:32:57"},{"id":"linode/4.4.113-x86-linode115","label":"4.4.113-x86-linode115","version":"4.4.113","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-25T22:47:11"},{"id":"linode/4.9.78-x86-linode114","label":"4.9.78-x86-linode114","version":"4.9.78","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-23T23:23:58"},{"id":"linode/4.14.14-x86-linode113","label":"4.14.14-x86-linode113","version":"4.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-23T02:00:02"},{"id":"linode/4.14.14-x86-linode112","label":"4.14.14-x86-linode112","version":"4.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-18T20:09:53"},{"id":"linode/4.9.64-x86-linode107","label":"4.9.64-x86-linode107","version":"4.9.64","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-12-01T23:10:11"},{"id":"linode/4.9.68-x86-linode108","label":"4.9.68-x86-linode108","version":"4.9.68","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-12-11T19:00:15"},{"id":"linode/4.14.12-x86-linode111","label":"4.14.12-x86-linode111","version":"4.14.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-05T16:31:55"},{"id":"linode/4.14.11-x86-linode110","label":"4.14.11-x86-linode110","version":"4.14.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-04T18:56:25"},{"id":"linode/4.9.56-x86-linode106","label":"4.9.56-x86-linode106","version":"4.9.56","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-10-13T21:10:23"},{"id":"linode/4.9.50-x86-linode105","label":"4.9.50-x86-linode105","version":"4.9.50","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-09-14T21:46:56"},{"id":"linode/4.9.36-x86-linode104","label":"4.9.36-x86-linode104","version":"4.9.36","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-07-10T13:16:53"},{"id":"linode/4.9.33-x86-linode102","label":"4.9.33-x86-linode102","version":"4.9.33","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-06-23T22:06:05"},{"id":"linode/4.9.15-x86-linode100","label":"4.9.15-x86-linode100","version":"4.9.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-03-22T13:48:13"},{"id":"linode/4.9.7-x86-linode99","label":"4.9.7-x86-linode99","version":"4.9.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-02-03T22:54:57"},{"id":"linode/4.9.0-x86-linode98","label":"4.9.0-x86-linode98","version":"4.9.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-12-13T20:10:20"},{"id":"linode/4.8.6-x86-linode97","label":"4.8.6-x86-linode97","version":"4.8.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-11-02T15:23:43"},{"id":"linode/4.8.4-x86-linode96","label":"4.8.4-x86-linode96","version":"4.8.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-27T18:51:41"},{"id":"linode/4.8.3-x86-linode95","label":"4.8.3-x86-linode95","version":"4.8.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-20T23:10:27"},{"id":"linode/4.8.1-x86-linode94","label":"4.8.1-x86-linode94","version":"4.8.1","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-07T22:21:55"},{"id":"linode/4.7.3-x86-linode92","label":"4.7.3-x86-linode92","version":"4.7.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-09-15T13:13:40"},{"id":"linode/4.7.0-x86-linode90","label":"4.7.0-x86-linode90","version":"4.7.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-08-05T14:35:48"},{"id":"linode/4.6.5-x86-linode89","label":"4.6.5-x86-linode89","version":"4.6.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-08-04T15:28:59"},{"id":"linode/4.5.5-x86-linode88","label":"4.5.5-x86-linode88","version":"4.5.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-05-24T15:29:02"},{"id":"linode/4.5.3-x86-linode86","label":"4.5.3-x86-linode86","version":"4.5.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-05-10T19:39:51"},{"id":"linode/4.5.0-x86-linode84","label":"4.5.0-x86-linode84","version":"4.5.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-03-16T18:53:02"},{"id":"linode/4.4.4-x86-linode83","label":"4.4.4-x86-linode83","version":"4.4.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-03-10T22:20:19"},{"id":"linode/4.4.0-x86-linode82","label":"4.4.0-x86-linode82","version":"4.4.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-01-20T14:41:05"},{"id":"linode/4.1.5-x86-linode80","label":"4.1.5-x86-linode80","version":"4.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-08-24T19:00:43"},{"id":"linode/4.1.5-x86-linode79","label":"4.1.5-x86-linode79","version":"4.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-08-13T13:00:00"},{"id":"linode/4.1.0-x86-linode78","label":"4.1.0-x86-linode78","version":"4.1.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-22T15:19:32"},{"id":"linode/4.0.5-x86-linode77","label":"4.0.5-x86-linode77","version":"4.0.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-11T13:58:18"},{"id":"linode/4.0.5-x86-linode76","label":"4.0.5-x86-linode76","version":"4.0.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-10T15:31:52"},{"id":"linode/4.0.4-x86-linode75","label":"4.0.4-x86-linode75","version":"4.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-21T15:15:47"},{"id":"linode/4.0.2-x86-linode74","label":"4.0.2-x86-linode74","version":"4.0.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-11T20:56:58"},{"id":"linode/4.0-x86-linode73","label":"4.0.1-x86-linode73","version":"4.0.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-04T13:43:23"},{"id":"linode/4.0-x86-linode72","label":"4.0-x86-linode72","version":"4.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-04-21T13:02:24"},{"id":"linode/3.19.1-x86-linode71","label":"3.19.1-x86-linode71","version":"3.19.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-03-11T18:00:36"},{"id":"linode/3.18.5-x86-linode70","label":"3.18.5-x86-linode70","version":"3.18.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-02-05T05:00:00"},{"id":"linode/3.18.3-x86-linode69","label":"3.18.3-x86-linode69","version":"3.18.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-23T15:12:45"},{"id":"linode/3.18.1-x86-linode68","label":"3.18.1-x86-linode68","version":"3.18.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-01-06T17:32:39"},{"id":"linode/3.16.7-x86-linode67","label":"3.16.7-x86-linode67","version":"3.16.7","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-11-14T22:31:46"},{"id":"linode/3.16.5-x86-linode65","label":"3.16.5-x86-linode65","version":"3.16.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-10-13T13:40:00"},{"id":"linode/3.15.4-x86-linode64","label":"3.15.4-x86-linode64","version":"3.15.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-07-07T13:50:35"},{"id":"linode/3.15.3-x86-linode63","label":"3.15.3-x86-linode63","version":"3.15.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-07-02T12:12:37"},{"id":"linode/3.15.2-x86-linode62","label":"3.15.2-x86-linode62","version":"3.15.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-30T18:46:50"},{"id":"linode/3.14.5-x86-linode61","label":"3.14.5-x86-linode61","version":"3.14.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-05T20:05:44"},{"id":"linode/3.14.5-x86-linode60","label":"3.14.5-x86-linode60","version":"3.14.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-03T13:09:58"},{"id":"linode/3.14.4-x86-linode59","label":"3.14.4-x86-linode59","version":"3.14.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-05-13T17:42:22"},{"id":"linode/3.14.1-x86-linode58","label":"3.14.1-x86-linode58","version":"3.14.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-04-25T17:49:15"},{"id":"linode/3.13.7-x86-linode57","label":"3.13.7-x86-linode57","version":"3.13.7","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-03-25T18:21:50"},{"id":"linode/3.12.9-x86-linode56","label":"3.12.9-x86-linode56","version":"3.12.9","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-02-03T19:42:13"},{"id":"linode/3.11.6-x86-linode54","label":"3.11.6-x86-linode54","version":"3.11.6","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-10-23T16:06:29"},{"id":"linode/3.12.6-x86-linode55","label":"3.12.6-x86-linode55","version":"3.12.6","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-12-23T16:25:39"},{"id":"linode/3.10.3-x86-linode53","label":"3.10.3-x86-linode53","version":"3.10.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-07-26T16:35:12"},{"id":"linode/3.9.3-x86-linode52","label":"3.9.3-x86-linode52","version":"3.9.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-05-20T14:27:27"},{"id":"linode/3.9.2-x86-linode51","label":"3.9.2-x86-linode51","version":"3.9.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-05-14T16:13:27"},{"id":"linode/3.8.4-linode50","label":"3.8.4-linode50","version":"3.8.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-03-25T20:42:49"},{"id":"linode/3.7.10-linode49","label":"3.7.10-linode49","version":"3.7.10","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-02-27T19:49:45"},{"id":"linode/3.7.5-linode48","label":"3.7.5-linode48","version":"3.7.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-01-31T19:52:25"},{"id":"linode/3.6.5-linode47","label":"3.6.5-linode47","version":"3.6.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-11-04T17:42:14"},{"id":"linode/3.5.3-linode46","label":"3.5.3-linode46","version":"3.5.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-09-05T20:45:36"},{"id":"linode/3.5.2-linode45","label":"3.5.2-linode45","version":"3.5.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-08-15T18:16:29"},{"id":"linode/3.4.2-linode44","label":"3.4.2-linode44","version":"3.4.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-06-11T19:03:10"},{"id":"linode/3.0.18-linode43","label":"3.0.18-linode43","version":"3.0.18","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-30T17:42:21"},{"id":"linode/3.1.10-linode42","label":"3.1.10-linode42","version":"3.1.10","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-25T21:24:07"},{"id":"linode/3.0.17-linode41","label":"3.0.17-linode41","version":"3.0.17","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-25T21:24:05"},{"id":"linode/3.2.1-linode40","label":"3.2.1-linode40","version":"3.2.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-23T16:04:48"},{"id":"linode/3.1.0-linode39","label":"3.1.0-linode39","version":"3.1.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-10-25T17:57:05"},{"id":"linode/3.0.4-linode38","label":"3.0.4-linode38","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-22T18:57:59"},{"id":"linode/3.0.4-linode37","label":"3.0.4-linode37","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-12T16:03:31"},{"id":"linode/3.0.4-linode36","label":"3.0.4-linode36","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-02T01:08:55"},{"id":"linode/3.0-linode35","label":"3.0.0-linode35","version":"3.0.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-08-02T15:43:52"},{"id":"linode/2.6.39.1-linode34","label":"2.6.39.1-linode34","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-06-21T14:42:50"},{"id":"linode/2.6.39-linode33","label":"2.6.39-linode33","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-05-25T19:05:05"},{"id":"linode/2.6.38.3-linode32","label":"2.6.38.3-linode32","version":"2.6.38","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-04-21T20:21:48"},{"id":"linode/2.6.38-linode31","label":"2.6.38-linode31","version":"2.6.38","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-03-21T21:44:09"},{"id":"linode/2.6.37-linode30","label":"2.6.37-linode30","version":"2.6.37","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-01-27T05:00:00"},{"id":"linode/2.6.35.7-linode29","label":"2.6.35.7-linode29","version":"2.6.35","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-10-13T04:00:00"},{"id":"linode/2.6.32.16-linode28","label":"2.6.32.16-linode28","version":"2.6.32","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-07-25T21:34:00"},{"id":"linode/2.6.34-linode27","label":"2.6.34-linode27","version":"2.6.34","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-07-16T04:00:00"},{"id":"linode/2.6.32.12-linode25","label":"2.6.32.12-linode25","version":"2.6.33","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-04-28T04:00:00"},{"id":"linode/2.6.33-linode24","label":"2.6.33-linode24","version":"2.6.33","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-02-24T22:05:00"},{"id":"linode/2.6.32-linode23","label":"2.6.32-linode23","version":"2.6.32","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-12-05T16:14:00"},{"id":"linode/2.6.18.8-linode22","label":"2.6.18.8-linode22","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-11-10T05:00:00"},{"id":"linode/2.6.31.5-linode21","label":"2.6.31.5-linode21","version":"2.6.31","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/2.6.30.5-linode20","label":"2.6.30.5-linode20","version":"2.6.30","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.23.17-linode44","label":"2.6.23.17-linode44","version":"2.6.23","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.18.8-linode19","label":"2.6.18.8-linode19","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-14T04:00:00"},{"id":"linode/2.6.29-linode18","label":"2.6.29-linode18","version":"2.6.29","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-04-01T04:00:00"},{"id":"linode/2.6.28.3-linode17","label":"2.6.28.3-linode17","version":"2.6.28","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-02-04T05:00:00"},{"id":"linode/2.6.18.8-linode16","label":"2.6.18.8-linode16","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-01-12T14:47:00"},{"id":"linode/2.6.28-linode15","label":"2.6.28-linode15","version":"2.6.28","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-12-29T05:00:00"},{"id":"linode/2.6.27.4-linode14","label":"2.6.27.4-linode14","version":"2.6.27","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-11-05T05:00:00"},{"id":"linode/2.6.26-linode13","label":"2.6.26-linode13","version":"2.6.26","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-07-13T23:15:00"},{"id":"linode/2.6.25.10-linode12","label":"2.6.25.10-linode12","version":"2.6.25","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-07-03T04:00:00"},{"id":"linode/2.6.18.8-linode10","label":"2.6.18.8-linode10","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2008-06-23T04:00:00"},{"id":"linode/2.6.25-linode9","label":"2.6.25-linode9","version":"2.6.25","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-04-10T04:00:00"},{"id":"linode/2.6.24.4-linode8","label":"2.6.24.4-linode8","version":"2.6.24","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-03-31T04:00:00"},{"id":"linode/2.6.18.8-domU-linode7","label":"2.6.18.8-domU-linode7","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":null},{"id":"linode/latest-2.6-64bit","label":"Latest 2.6 (2.6.39.1-x86_64-linode19)","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/latest-64bit","label":"Latest 64 bit (6.1.10-x86_64-linode159)","version":"6.1.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-02-08T19:14:45"},{"id":"linode/2.6.18.8-x86_64-linode10","label":"Latest Legacy (2.6.18.8-x86_64-linode10)","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/5.7.6-x86_64-linode136","label":"5.7.6-x86_64-linode136","version":"5.7.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-06-25T16:41:35"},{"id":"linode/5.6.14-x86_64-linode135","label":"5.6.14-x86_64-linode135","version":"5.6.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-06-01T14:06:45"},{"id":"linode/5.6.1-x86_64-linode134","label":"5.6.1-x86_64-linode134","version":"5.6.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-04-07T21:29:51"},{"id":"linode/5.4.10-x86_64-linode132","label":"5.4.10-x86_64-linode132","version":"5.4.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2020-01-10T21:03:16"},{"id":"linode/5.3.11-x86_64-linode131","label":"5.3.11-x86_64-linode131","version":"5.3.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-11-14T20:39:27"},{"id":"linode/5.3.7-x86_64-linode130","label":"5.3.7-x86_64-linode130","version":"5.3.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-10-30T21:10:29"},{"id":"linode/5.2.9-x86_64-linode129","label":"5.2.9-x86_64-linode129","version":"5.2.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-08-21T18:49:31"},{"id":"linode/5.1.17-x86_64-linode128","label":"5.1.17-x86_64-linode128","version":"5.1.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-07-16T15:26:33"},{"id":"linode/5.1.11-x86_64-linode127","label":"5.1.11-x86_64-linode127","version":"5.1.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-06-17T22:47:20"},{"id":"linode/5.1.5-x86_64-linode126","label":"5.1.5-x86_64-linode126","version":"5.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-30T20:30:39"},{"id":"linode/4.14.120-x86_64-linode125","label":"4.14.120-x86_64-linode125","version":"4.14.120","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-21T10:57:46"},{"id":"linode/5.1.2-x86_64-linode124","label":"5.1.2-x86_64-linode124","version":"5.1.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-15T16:14:35"},{"id":"linode/5.0.8-x86_64-linode123","label":"5.0.8-x86_64-linode123","version":"5.0.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-04-17T18:39:56"},{"id":"linode/5.0.1-x86_64-linode122","label":"5.0.1-x86_64-linode122","version":"5.0.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-03-13T16:51:01"},{"id":"linode/4.20.4-x86_64-linode121","label":"4.20.4-x86_64-linode121","version":"4.20.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-01-25T19:13:29"},{"id":"linode/4.19.8-x86_64-linode120","label":"4.19.8-x86_64-linode120","version":"4.19.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-12-12T21:44:08"},{"id":"linode/4.19.5-x86_64-linode119","label":"4.19.5-x86_64-linode119","version":"4.19.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-11-29T16:36:53"},{"id":"linode/4.18.16-x86_64-linode118","label":"4.18.16-x86_64-linode118","version":"4.18.16","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-10-29T20:24:13"},{"id":"linode/4.18.8-x86_64-linode117","label":"4.18.8-x86_64-linode117","version":"4.18.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-09-19T12:55:56"},{"id":"linode/4.17.17-x86_64-linode116","label":"4.17.17-x86_64-linode116","version":"4.17.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-20T17:23:32"},{"id":"linode/4.17.15-x86_64-linode115","label":"4.17.15-x86_64-linode115","version":"4.17.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-16T17:13:28"},{"id":"linode/4.17.14-x86_64-linode114","label":"4.17.14-x86_64-linode114","version":"4.17.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-13T16:41:06"},{"id":"linode/4.17.14-x86_64-linode113","label":"4.17.14-x86_64-linode113","version":"4.17.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-10T21:07:56"},{"id":"linode/4.17.12-x86_64-linode112","label":"4.17.12-x86_64-linode112","version":"4.17.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-07T13:02:24"},{"id":"linode/4.17.11-x86_64-linode111","label":"4.17.11-x86_64-linode111","version":"4.17.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-07-31T19:51:53"},{"id":"linode/4.17.8-x86_64-linode110","label":"4.17.8-x86_64-linode110","version":"4.17.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-07-18T17:18:30"},{"id":"linode/4.17.2-x86_64-linode109","label":"4.17.2-x86_64-linode109","version":"4.17.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-06-26T19:42:57"},{"id":"linode/4.16.11-x86_64-linode108","label":"4.16.11-x86_64-linode108","version":"4.16.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-05-24T21:33:31"},{"id":"linode/4.15.18-x86_64-linode107","label":"4.15.18-x86_64-linode107","version":"4.15.18","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-05-02T20:21:04"},{"id":"linode/4.15.13-x86_64-linode106","label":"4.15.13-x86_64-linode106","version":"4.15.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-27T17:17:56"},{"id":"linode/4.15.12-x86_64-linode105","label":"4.15.12-x86_64-linode105","version":"4.15.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-22T20:08:43"},{"id":"linode/4.15.10-x86_64-linode104","label":"4.15.10-x86_64-linode104","version":"4.15.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-16T21:18:35"},{"id":"linode/4.15.8-x86_64-linode103","label":"4.15.8-x86_64-linode103","version":"4.15.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-10T21:25:43"},{"id":"linode/4.15.7-x86_64-linode102","label":"4.15.7-x86_64-linode102","version":"4.15.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-01T17:20:54"},{"id":"linode/4.14.19-x86_64-linode100","label":"4.14.19-x86_64-linode100","version":"4.14.19","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-13T19:07:46"},{"id":"linode/4.14.17-x86_64-linode99","label":"4.14.17-x86_64-linode99","version":"4.14.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:35:09"},{"id":"linode/4.9.80-x86_64-linode98","label":"4.9.80-x86_64-linode98","version":"4.9.80","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:34:16"},{"id":"linode/4.4.115-x86_64-linode97","label":"4.4.115-x86_64-linode97","version":"4.4.115","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:33:23"},{"id":"linode/4.4.113-x86_64-linode96","label":"4.4.113-x86_64-linode96","version":"4.4.113","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-25T22:47:11"},{"id":"linode/4.9.78-x86_64-linode95","label":"4.9.78-x86_64-linode95","version":"4.9.78","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-23T23:23:58"},{"id":"linode/4.14.14-x86_64-linode94","label":"4.14.14-x86_64-linode94","version":"4.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-23T02:10:08"},{"id":"linode/4.14.14-x86_64-linode93","label":"4.14.14-x86_64-linode93","version":"4.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-18T20:08:56"},{"id":"linode/4.9.64-x86_64-linode88","label":"4.9.64-x86_64-linode88","version":"4.9.64","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-12-01T23:10:11"},{"id":"linode/4.9.68-x86_64-linode89","label":"4.9.68-x86_64-linode89","version":"4.9.68","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-12-11T19:00:48"},{"id":"linode/4.14.12-x86_64-linode92","label":"4.14.12-x86_64-linode92","version":"4.14.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-05T16:31:28"},{"id":"linode/4.14.11-x86_64-linode91","label":"4.14.11-x86_64-linode91","version":"4.14.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-04T18:56:22"},{"id":"linode/4.9.56-x86_64-linode87","label":"4.9.56-x86_64-linode87","version":"4.9.56","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-10-13T21:09:35"},{"id":"linode/4.9.50-x86_64-linode86","label":"4.9.50-x86_64-linode86","version":"4.9.50","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-09-14T21:46:25"},{"id":"linode/4.9.36-x86_64-linode85","label":"4.9.36-x86_64-linode85","version":"4.9.36","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-07-10T13:16:08"},{"id":"linode/4.9.33-x86_64-linode83","label":"4.9.33-x86_64-linode83","version":"4.9.33","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-06-23T21:04:33"},{"id":"linode/4.9.15-x86_64-linode81","label":"4.9.15-x86_64-linode81","version":"4.9.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-03-22T13:49:33"},{"id":"linode/4.9.7-x86_64-linode80","label":"4.9.7-x86_64-linode80","version":"4.9.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-02-03T22:55:37"},{"id":"linode/4.9.0-x86_64-linode79","label":"4.9.0-x86_64-linode79","version":"4.9.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-12-13T20:11:03"},{"id":"linode/4.8.6-x86_64-linode78","label":"4.8.6-x86_64-linode78","version":"4.8.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-11-02T15:24:17"},{"id":"linode/4.8.4-x86_64-linode77","label":"4.8.4-x86_64-linode77","version":"4.8.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-27T18:53:07"},{"id":"linode/4.8.3-x86_64-linode76","label":"4.8.3-x86_64-linode76","version":"4.8.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-20T23:10:27"},{"id":"linode/4.8.1-x86_64-linode75","label":"4.8.1-x86_64-linode75","version":"4.8.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-07T22:22:13"},{"id":"linode/4.7.3-x86_64-linode73","label":"4.7.3-x86_64-linode73","version":"4.7.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-09-15T13:13:01"},{"id":"linode/4.7.0-x86_64-linode72","label":"4.7.0-x86_64-linode72","version":"4.7.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-08-05T14:34:25"},{"id":"linode/4.6.5-x86_64-linode71","label":"4.6.5-x86_64-linode71","version":"4.6.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-08-04T15:28:01"},{"id":"linode/4.6.3-x86_64-linode70","label":"4.6.3-x86_64-linode70","version":"4.6.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-07-07T22:08:28"},{"id":"linode/4.5.5-x86_64-linode69","label":"4.5.5-x86_64-linode69","version":"4.5.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-05-24T15:30:08"},{"id":"linode/4.5.3-x86_64-linode67","label":"4.5.3-x86_64-linode67","version":"4.5.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-05-10T19:42:43"},{"id":"linode/4.5.0-x86_64-linode65","label":"4.5.0-x86_64-linode65","version":"4.5.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-03-16T18:53:02"},{"id":"linode/4.4.4-x86_64-linode64","label":"4.4.4-x86_64-linode64","version":"4.4.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-03-10T22:24:51"},{"id":"linode/4.4.0-x86_64-linode63","label":"4.4.0-x86_64-linode63","version":"4.4.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-01-20T14:41:05"},{"id":"linode/4.1.5-x86_64-linode61","label":"4.1.5-x86_64-linode61","version":"4.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-08-24T19:00:43"},{"id":"linode/4.1.5-x86_64-linode60","label":"4.1.5-x86_64-linode60 ","version":"4.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-08-13T13:00:00"},{"id":"linode/4.1.0-x86_64-linode59","label":"4.1.0-x86_64-linode59 ","version":"4.1.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-06-22T15:19:32"},{"id":"linode/4.0.5-x86_64-linode58","label":"4.0.5-x86_64-linode58","version":"4.0.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-06-10T15:31:52"},{"id":"linode/4.0.4-x86_64-linode57","label":"4.0.4-x86_64-linode57","version":"4.0.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-21T15:15:47"},{"id":"linode/4.0.2-x86_64-linode56","label":"4.0.2-x86_64-linode56","version":"4.0.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-11T20:56:58"},{"id":"linode/4.0.1-x86_64-linode55","label":"4.0.1-x86_64-linode55","version":"4.0.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-04T13:43:23"},{"id":"linode/4.0-x86_64-linode54","label":"4.0-x86_64-linode54","version":"4.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-04-21T13:02:24"},{"id":"linode/3.19.1-x86_64-linode53","label":"3.19.1-x86_64-linode53","version":"3.19.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-03-11T18:00:36"},{"id":"linode/3.18.5-x86_64-linode52","label":"3.18.5-x86_64-linode52","version":"3.18.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-02-05T05:00:00"},{"id":"linode/3.18.3-x86_64-linode51","label":"3.18.3-x86_64-linode51","version":"3.18.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-23T15:12:45"},{"id":"linode/3.18.1-x86_64-linode50","label":"3.18.1-x86_64-linode50","version":"3.18.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-06T17:32:39"},{"id":"linode/3.16.7-x86_64-linode49","label":"3.16.7-x86_64-linode49","version":"3.16.7","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-11-14T22:31:46"},{"id":"linode/3.16.5-x86_64-linode46","label":"3.16.5-x86_64-linode46","version":"3.16.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-10-13T13:42:00"},{"id":"linode/3.15.4-x86_64-linode45","label":"3.15.4-x86_64-linode45","version":"3.15.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-07-07T13:50:35"},{"id":"linode/3.15.3-x86_64-linode44","label":"3.15.3-x86_64-linode44","version":"3.15.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-07-02T12:12:37"},{"id":"linode/3.15.2-x86_64-linode43","label":"3.15.2-x86_64-linode43","version":"3.15.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-30T18:46:50"},{"id":"linode/3.14.5-x86_64-linode42","label":"3.14.5-x86_64-linode42","version":"3.14.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-05T20:05:44"},{"id":"linode/3.14.5-x86_64-linode41","label":"3.14.5-x86_64-linode41","version":"3.14.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-03T13:09:58"},{"id":"linode/3.14.4-x86_64-linode40","label":"3.14.4-x86_64-linode40","version":"3.14.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-05-13T17:42:22"},{"id":"linode/3.14.1-x86_64-linode39","label":"3.14.1-x86_64-linode39","version":"3.14.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-04-25T17:42:13"},{"id":"linode/3.13.7-x86_64-linode38","label":"3.13.7-x86_64-linode38","version":"3.13.7","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-03-25T18:21:50"},{"id":"linode/3.12.9-x86_64-linode37","label":"3.12.9-x86_64-linode37","version":"3.12.9","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-02-03T19:42:13"},{"id":"linode/3.12.6-x86_64-linode36","label":"3.12.6-x86_64-linode36","version":"3.12.6","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-12-23T16:24:18"},{"id":"linode/3.11.6-x86_64-linode35","label":"3.11.6-x86_64-linode35","version":"3.11.6","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-10-23T16:06:29"},{"id":"linode/3.10.3-x86_64-linode34","label":"3.10.3-x86_64-linode34","version":"3.10.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-07-26T16:35:12"},{"id":"linode/3.9.3-x86_64-linode33","label":"3.9.3-x86_64-linode33","version":"3.9.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-05-20T14:27:27"},{"id":"linode/3.9.2-x86_64-linode32","label":"3.9.2-x86_64-linode32","version":"3.9.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-05-14T15:53:02"},{"id":"linode/3.8.4-x86_64-linode31","label":"3.8.4-x86_64-linode31","version":"3.8.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-03-25T20:42:49"},{"id":"linode/3.7.10-x86_64-linode30","label":"3.7.10-x86_64-linode30","version":"3.7.10","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-02-27T19:49:45"},{"id":"linode/3.7.5-x86_64-linode29","label":"3.7.5-x86_64-linode29","version":"3.7.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-01-31T19:52:25"},{"id":"linode/3.6.5-x86_64-linode28","label":"3.6.5-x86_64-linode28","version":"3.6.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-11-04T17:42:14"},{"id":"linode/3.5.3-x86_64-linode27","label":"3.5.3-x86_64-linode27","version":"3.5.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-09-05T20:32:28"},{"id":"linode/3.4.2-x86_64-linode25","label":"3.4.2-x86_64-linode25","version":"3.2.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-06-11T18:40:20"},{"id":"linode/3.0.18-x86_64-linode24","label":"3.0.18-x86_64-linode24 ","version":"3.0.18","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-01-30T17:42:21"},{"id":"linode/3.2.1-x86_64-linode23","label":"3.2.1-x86_64-linode23","version":"3.2.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-01-23T16:04:48"},{"id":"linode/3.1.0-x86_64-linode22","label":"3.1.0-x86_64-linode22","version":"3.1.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-10-25T18:24:49"},{"id":"linode/3.0.4-x86_64-linode21","label":"3.0.4-x86_64-linode21","version":"3.0.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-09-02T01:08:55"},{"id":"linode/3.0.0-x86_64-linode20","label":"3.0.0-x86_64-linode20","version":"3.0.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-08-02T16:59:12"},{"id":"linode/2.6.39.1-x86_64-linode19","label":"2.6.39.1-x86_64-linode19","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-06-21T14:06:03"},{"id":"linode/2.6.39-x86_64-linode18","label":"2.6.39-x86_64-linode18","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-05-25T19:05:05"},{"id":"linode/2.6.38-x86_64-linode17","label":"2.6.38-x86_64-linode17","version":"2.6.38","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-03-21T21:44:09"},{"id":"linode/2.6.35.4-x86_64-linode16","label":"2.6.35.4-x86_64-linode16","version":"2.6.35","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-09-20T04:00:00"},{"id":"linode/2.6.32.12-x86_64-linode15","label":"2.6.32.12-x86_64-linode15","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-07-25T21:34:00"},{"id":"linode/2.6.34-x86_64-linode13","label":"2.6.34-x86_64-linode13","version":"2.6.34","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-06-17T04:00:00"},{"id":"linode/2.6.34-x86_64-linode14","label":"2.6.34-x86_64-linode14","version":"2.6.34","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-07-14T04:00:00"},{"id":"linode/2.6.32.12-x86_64-linode12","label":"2.6.32.12-x86_64-linode12","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-04-28T04:00:00"},{"id":"linode/2.6.32-x86_64-linode11","label":"2.6.32-x86_64-linode11","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-12-05T17:01:00"},{"id":"linode/2.6.18.8-x86_64-linode10","label":"2.6.18.8-x86_64-linode10","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-11-10T16:53:00"},{"id":"linode/2.6.31.5-x86_64-linode9","label":"2.6.31.5-x86_64-linode9","version":"2.6.31","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/2.6.30.5-x86_64-linode8","label":"2.6.30.5-x86_64-linode8","version":"2.6.30","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.18.8-x86_64-linode7","label":"2.6.18.8-x86_64-linode7","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-08-14T04:00:00"},{"id":"linode/2.6.29-x86_64-linode6","label":"2.6.29-x86_64-linode6","version":"2.6.29","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-04-02T04:00:00"},{"id":"linode/2.6.28.3-x86_64-linode5","label":"2.6.28.3-x86_64-linode5","version":"2.6.28","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-02-04T05:00:00"},{"id":"linode/2.6.28-x86_64-linode4","label":"2.6.28-x86_64-linode4","version":"2.6.28","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2008-12-29T05:00:00"},{"id":"linode/2.6.27.4-x86_64-linode3","label":"2.6.27.4-x86_64-linode3","version":"2.6.27","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2008-11-05T05:00:00"},{"id":"linode/2.6.16.38-x86_64-linode2","label":"2.6.16.38-x86_64-linode2","version":"2.6.16","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2008-03-23T04:00:00"},{"id":"linode/2.6.18.8-x86_64-linode1","label":"2.6.18.8-x86_64-linode1","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2008-03-23T04:00:00"},{"id":"linode/3.5.2-x86_64-linode26","label":"3.5.2-x86_64-linode26","version":"3.5.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-08-15T18:38:16"},{"id":"linode/grub2","label":"GRUB 2","version":"2.06","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2022-08-29T14:28:00"},{"id":"linode/direct-disk","label":"Direct Disk","version":"","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2015-05-05T01:51:43"},{"id":"linode/grub-legacy","label":"GRUB (Legacy)","version":"2.0.0","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2015-04-29T15:32:30"},{"id":"linode/pv-grub_x86_32","label":"pv-grub-x86_32","version":"2.6.26","kvm":false,"architecture":"i386","pvops":false,"deprecated":false,"built":"2008-09-15T04:00:00"},{"id":"linode/pv-grub_x86_64","label":"pv-grub-x86_64","version":"2.6.26","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2008-11-14T05:00:00"}],"page":1,"pages":1,"results":322} \ No newline at end of file diff --git a/packages/manager/src/cachedData/regions.json b/packages/manager/src/cachedData/regions.json index 061d255c8a1..fe51e16de98 100644 --- a/packages/manager/src/cachedData/regions.json +++ b/packages/manager/src/cachedData/regions.json @@ -1 +1 @@ -{"data":[{"id":"ap-west","label":"Mumbai, IN","country":"in","capabilities":["Linodes","NodeBalancers","Block Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"172.105.34.5,172.105.35.5,172.105.36.5,172.105.37.5,172.105.38.5,172.105.39.5,172.105.40.5,172.105.41.5,172.105.42.5,172.105.43.5","ipv6":"2400:8904::f03c:91ff:fea5:659,2400:8904::f03c:91ff:fea5:9282,2400:8904::f03c:91ff:fea5:b9b3,2400:8904::f03c:91ff:fea5:925a,2400:8904::f03c:91ff:fea5:22cb,2400:8904::f03c:91ff:fea5:227a,2400:8904::f03c:91ff:fea5:924c,2400:8904::f03c:91ff:fea5:f7e2,2400:8904::f03c:91ff:fea5:2205,2400:8904::f03c:91ff:fea5:9207"}},{"id":"ca-central","label":"Toronto, CA","country":"ca","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"172.105.0.5,172.105.3.5,172.105.4.5,172.105.5.5,172.105.6.5,172.105.7.5,172.105.8.5,172.105.9.5,172.105.10.5,172.105.11.5","ipv6":"2600:3c04::f03c:91ff:fea9:f63,2600:3c04::f03c:91ff:fea9:f6d,2600:3c04::f03c:91ff:fea9:f80,2600:3c04::f03c:91ff:fea9:f0f,2600:3c04::f03c:91ff:fea9:f99,2600:3c04::f03c:91ff:fea9:fbd,2600:3c04::f03c:91ff:fea9:fdd,2600:3c04::f03c:91ff:fea9:fe2,2600:3c04::f03c:91ff:fea9:f68,2600:3c04::f03c:91ff:fea9:f4a"}},{"id":"ap-southeast","label":"Sydney, AU","country":"au","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"172.105.166.5,172.105.169.5,172.105.168.5,172.105.172.5,172.105.162.5,172.105.170.5,172.105.167.5,172.105.171.5,172.105.181.5,172.105.161.5","ipv6":"2400:8907::f03c:92ff:fe6e:ec8,2400:8907::f03c:92ff:fe6e:98e4,2400:8907::f03c:92ff:fe6e:1c58,2400:8907::f03c:92ff:fe6e:c299,2400:8907::f03c:92ff:fe6e:c210,2400:8907::f03c:92ff:fe6e:c219,2400:8907::f03c:92ff:fe6e:1c5c,2400:8907::f03c:92ff:fe6e:c24e,2400:8907::f03c:92ff:fe6e:e6b,2400:8907::f03c:92ff:fe6e:e3d"}},{"id":"us-central","label":"Dallas, TX","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"72.14.179.5,72.14.188.5,173.255.199.5,66.228.53.5,96.126.122.5,96.126.124.5,96.126.127.5,198.58.107.5,198.58.111.5,23.239.24.5","ipv6":"2600:3c00::2,2600:3c00::9,2600:3c00::7,2600:3c00::5,2600:3c00::3,2600:3c00::8,2600:3c00::6,2600:3c00::4,2600:3c00::c,2600:3c00::b"}},{"id":"us-west","label":"Fremont, CA","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"173.230.145.5,173.230.147.5,173.230.155.5,173.255.212.5,173.255.219.5,173.255.241.5,173.255.243.5,173.255.244.5,74.207.241.5,74.207.242.5","ipv6":"2600:3c01::2,2600:3c01::9,2600:3c01::5,2600:3c01::7,2600:3c01::3,2600:3c01::8,2600:3c01::4,2600:3c01::b,2600:3c01::c,2600:3c01::6"}},{"id":"us-southeast","label":"Atlanta, GA","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"74.207.231.5,173.230.128.5,173.230.129.5,173.230.136.5,173.230.140.5,66.228.59.5,66.228.62.5,50.116.35.5,50.116.41.5,23.239.18.5","ipv6":"2600:3c02::3,2600:3c02::5,2600:3c02::4,2600:3c02::6,2600:3c02::c,2600:3c02::7,2600:3c02::2,2600:3c02::9,2600:3c02::8,2600:3c02::b"}},{"id":"us-east","label":"Newark, NJ","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Bare Metal","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"66.228.42.5,96.126.106.5,50.116.53.5,50.116.58.5,50.116.61.5,50.116.62.5,66.175.211.5,97.107.133.4,207.192.69.4,207.192.69.5","ipv6":"2600:3c03::7,2600:3c03::4,2600:3c03::9,2600:3c03::6,2600:3c03::3,2600:3c03::c,2600:3c03::5,2600:3c03::b,2600:3c03::2,2600:3c03::8"}},{"id":"eu-west","label":"London, UK","country":"uk","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases", "Metadata"],"status":"ok","resolvers":{"ipv4":"178.79.182.5,176.58.107.5,176.58.116.5,176.58.121.5,151.236.220.5,212.71.252.5,212.71.253.5,109.74.192.20,109.74.193.20,109.74.194.20","ipv6":"2a01:7e00::9,2a01:7e00::3,2a01:7e00::c,2a01:7e00::5,2a01:7e00::6,2a01:7e00::8,2a01:7e00::b,2a01:7e00::4,2a01:7e00::7,2a01:7e00::2"}},{"id":"ap-south","label":"Singapore, SG","country":"sg","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"139.162.11.5,139.162.13.5,139.162.14.5,139.162.15.5,139.162.16.5,139.162.21.5,139.162.27.5,103.3.60.18,103.3.60.19,103.3.60.20","ipv6":"2400:8901::5,2400:8901::4,2400:8901::b,2400:8901::3,2400:8901::9,2400:8901::2,2400:8901::8,2400:8901::7,2400:8901::c,2400:8901::6"}},{"id":"eu-central","label":"Frankfurt, DE","country":"de","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"139.162.130.5,139.162.131.5,139.162.132.5,139.162.133.5,139.162.134.5,139.162.135.5,139.162.136.5,139.162.137.5,139.162.138.5,139.162.139.5","ipv6":"2a01:7e01::5,2a01:7e01::9,2a01:7e01::7,2a01:7e01::c,2a01:7e01::2,2a01:7e01::4,2a01:7e01::3,2a01:7e01::6,2a01:7e01::b,2a01:7e01::8"}},{"id":"ap-northeast","label":"Tokyo, JP","country":"jp","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"139.162.66.5,139.162.67.5,139.162.68.5,139.162.69.5,139.162.70.5,139.162.71.5,139.162.72.5,139.162.73.5,139.162.74.5,139.162.75.5","ipv6":"2400:8902::3,2400:8902::6,2400:8902::c,2400:8902::4,2400:8902::2,2400:8902::8,2400:8902::7,2400:8902::5,2400:8902::b,2400:8902::9"}}],"page":1,"pages":1,"results":11} \ No newline at end of file +{"data":[{"id":"ap-west","label":"Mumbai, IN","country":"in","capabilities":["Linodes","NodeBalancers","Block Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"172.105.34.5,172.105.35.5,172.105.36.5,172.105.37.5,172.105.38.5,172.105.39.5,172.105.40.5,172.105.41.5,172.105.42.5,172.105.43.5","ipv6":"2400:8904::f03c:91ff:fea5:659,2400:8904::f03c:91ff:fea5:9282,2400:8904::f03c:91ff:fea5:b9b3,2400:8904::f03c:91ff:fea5:925a,2400:8904::f03c:91ff:fea5:22cb,2400:8904::f03c:91ff:fea5:227a,2400:8904::f03c:91ff:fea5:924c,2400:8904::f03c:91ff:fea5:f7e2,2400:8904::f03c:91ff:fea5:2205,2400:8904::f03c:91ff:fea5:9207"}},{"id":"ca-central","label":"Toronto, CA","country":"ca","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"172.105.0.5,172.105.3.5,172.105.4.5,172.105.5.5,172.105.6.5,172.105.7.5,172.105.8.5,172.105.9.5,172.105.10.5,172.105.11.5","ipv6":"2600:3c04::f03c:91ff:fea9:f63,2600:3c04::f03c:91ff:fea9:f6d,2600:3c04::f03c:91ff:fea9:f80,2600:3c04::f03c:91ff:fea9:f0f,2600:3c04::f03c:91ff:fea9:f99,2600:3c04::f03c:91ff:fea9:fbd,2600:3c04::f03c:91ff:fea9:fdd,2600:3c04::f03c:91ff:fea9:fe2,2600:3c04::f03c:91ff:fea9:f68,2600:3c04::f03c:91ff:fea9:f4a"}},{"id":"ap-southeast","label":"Sydney, AU","country":"au","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"172.105.166.5,172.105.169.5,172.105.168.5,172.105.172.5,172.105.162.5,172.105.170.5,172.105.167.5,172.105.171.5,172.105.181.5,172.105.161.5","ipv6":"2400:8907::f03c:92ff:fe6e:ec8,2400:8907::f03c:92ff:fe6e:98e4,2400:8907::f03c:92ff:fe6e:1c58,2400:8907::f03c:92ff:fe6e:c299,2400:8907::f03c:92ff:fe6e:c210,2400:8907::f03c:92ff:fe6e:c219,2400:8907::f03c:92ff:fe6e:1c5c,2400:8907::f03c:92ff:fe6e:c24e,2400:8907::f03c:92ff:fe6e:e6b,2400:8907::f03c:92ff:fe6e:e3d"}},{"id":"us-central","label":"Dallas, TX","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"72.14.179.5,72.14.188.5,173.255.199.5,66.228.53.5,96.126.122.5,96.126.124.5,96.126.127.5,198.58.107.5,198.58.111.5,23.239.24.5","ipv6":"2600:3c00::2,2600:3c00::9,2600:3c00::7,2600:3c00::5,2600:3c00::3,2600:3c00::8,2600:3c00::6,2600:3c00::4,2600:3c00::c,2600:3c00::b"}},{"id":"us-west","label":"Fremont, CA","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"173.230.145.5,173.230.147.5,173.230.155.5,173.255.212.5,173.255.219.5,173.255.241.5,173.255.243.5,173.255.244.5,74.207.241.5,74.207.242.5","ipv6":"2600:3c01::2,2600:3c01::9,2600:3c01::5,2600:3c01::7,2600:3c01::3,2600:3c01::8,2600:3c01::4,2600:3c01::b,2600:3c01::c,2600:3c01::6"}},{"id":"us-southeast","label":"Atlanta, GA","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"74.207.231.5,173.230.128.5,173.230.129.5,173.230.136.5,173.230.140.5,66.228.59.5,66.228.62.5,50.116.35.5,50.116.41.5,23.239.18.5","ipv6":"2600:3c02::3,2600:3c02::5,2600:3c02::4,2600:3c02::6,2600:3c02::c,2600:3c02::7,2600:3c02::2,2600:3c02::9,2600:3c02::8,2600:3c02::b"}},{"id":"us-east","label":"Newark, NJ","country":"us","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Bare Metal","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"66.228.42.5,96.126.106.5,50.116.53.5,50.116.58.5,50.116.61.5,50.116.62.5,66.175.211.5,97.107.133.4,207.192.69.4,207.192.69.5","ipv6":"2600:3c03::7,2600:3c03::4,2600:3c03::9,2600:3c03::6,2600:3c03::3,2600:3c03::c,2600:3c03::5,2600:3c03::b,2600:3c03::2,2600:3c03::8"}},{"id":"eu-west","label":"London, UK","country":"uk","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"178.79.182.5,176.58.107.5,176.58.116.5,176.58.121.5,151.236.220.5,212.71.252.5,212.71.253.5,109.74.192.20,109.74.193.20,109.74.194.20","ipv6":"2a01:7e00::9,2a01:7e00::3,2a01:7e00::c,2a01:7e00::5,2a01:7e00::6,2a01:7e00::8,2a01:7e00::b,2a01:7e00::4,2a01:7e00::7,2a01:7e00::2"}},{"id":"ap-south","label":"Singapore, SG","country":"sg","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"139.162.11.5,139.162.13.5,139.162.14.5,139.162.15.5,139.162.16.5,139.162.21.5,139.162.27.5,103.3.60.18,103.3.60.19,103.3.60.20","ipv6":"2400:8901::5,2400:8901::4,2400:8901::b,2400:8901::3,2400:8901::9,2400:8901::2,2400:8901::8,2400:8901::7,2400:8901::c,2400:8901::6"}},{"id":"eu-central","label":"Frankfurt, DE","country":"de","capabilities":["Linodes","NodeBalancers","Block Storage","Object Storage","GPU Linodes","Kubernetes","Cloud Firewall","Vlans","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"139.162.130.5,139.162.131.5,139.162.132.5,139.162.133.5,139.162.134.5,139.162.135.5,139.162.136.5,139.162.137.5,139.162.138.5,139.162.139.5","ipv6":"2a01:7e01::5,2a01:7e01::9,2a01:7e01::7,2a01:7e01::c,2a01:7e01::2,2a01:7e01::4,2a01:7e01::3,2a01:7e01::6,2a01:7e01::b,2a01:7e01::8"}},{"id":"ap-northeast","label":"Tokyo, JP","country":"jp","capabilities":["Linodes","NodeBalancers","Block Storage","Kubernetes","Cloud Firewall","Block Storage Migrations","Managed Databases"],"status":"ok","resolvers":{"ipv4":"139.162.66.5,139.162.67.5,139.162.68.5,139.162.69.5,139.162.70.5,139.162.71.5,139.162.72.5,139.162.73.5,139.162.74.5,139.162.75.5","ipv6":"2400:8902::3,2400:8902::6,2400:8902::c,2400:8902::4,2400:8902::2,2400:8902::8,2400:8902::7,2400:8902::5,2400:8902::b,2400:8902::9"}}],"page":1,"pages":1,"results":11} \ No newline at end of file diff --git a/packages/manager/src/cachedData/typesLegacy.json b/packages/manager/src/cachedData/typesLegacy.json index 6aa8578da4a..5b57f35e381 100644 --- a/packages/manager/src/cachedData/typesLegacy.json +++ b/packages/manager/src/cachedData/typesLegacy.json @@ -1 +1 @@ -{"data":[{"id":"standard-1","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.03,"monthly":20},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":512,"disk":24576,"transfer":2000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-2"},{"id":"standard-2","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.045,"monthly":30},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":768,"disk":36864,"transfer":3000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-3-s"},{"id":"standard-3","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.06,"monthly":40},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":1024,"disk":49152,"transfer":4000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4"},{"id":"standard-4","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":1536,"disk":73728,"transfer":6000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4-s"},{"id":"standard-5","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.12,"monthly":80},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":2048,"disk":98304,"transfer":8000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-6"},{"id":"standard-6","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.24,"monthly":160},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":4096,"disk":196608,"transfer":16000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-8"},{"id":"standard-7","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.48,"monthly":320},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":8192,"disk":393216,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-16"},{"id":"standard-8","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":12288,"disk":589824,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20"},{"id":"standard-9","label":"Linode 128GB (pending upgrade)","price":{"hourly":0.96,"monthly":640},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":16384,"disk":786432,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-24"},{"id":"standard-10","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.2,"monthly":800},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":20480,"disk":983040,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20-s"},{"id":"standard-46","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.03,"monthly":20},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":1024,"disk":24576,"transfer":2000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-2"},{"id":"standard-47","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.05,"monthly":30},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":1536,"disk":36864,"transfer":3000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-3-s"},{"id":"standard-48","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.06,"monthly":40},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":2048,"disk":49152,"transfer":4000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4"},{"id":"standard-49","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":3072,"disk":73728,"transfer":6000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4-s"},{"id":"standard-50","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.12,"monthly":80},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":4096,"disk":98304,"transfer":8000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-6"},{"id":"standard-51","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.24,"monthly":160},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":8192,"disk":196608,"transfer":16000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-8"},{"id":"standard-52","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.5,"monthly":320},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":16384,"disk":393216,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-16"},{"id":"standard-53","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":24576,"disk":589824,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20"},{"id":"standard-54","label":"Linode 128GB (pending upgrade)","price":{"hourly":0.96,"monthly":640},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":32768,"disk":786432,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-24"},{"id":"standard-55","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.2,"monthly":800},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":40960,"disk":983040,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20-s"},{"id":"standard-92","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.03,"monthly":20},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":1024,"disk":49152,"transfer":2000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-2"},{"id":"standard-93","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.05,"monthly":30},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":1536,"disk":73728,"transfer":3000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-3-s"},{"id":"standard-94","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.06,"monthly":40},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":2048,"disk":98304,"transfer":4000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4"},{"id":"standard-95","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":3072,"disk":147456,"transfer":6000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4-s"},{"id":"standard-96","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.12,"monthly":80},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":4096,"disk":196608,"transfer":8000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-6"},{"id":"standard-97","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.24,"monthly":160},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":8192,"disk":393216,"transfer":16000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-8"},{"id":"standard-98","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.48,"monthly":320},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":16384,"disk":786432,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-16"},{"id":"standard-99","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":24576,"disk":1179648,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20"},{"id":"standard-100","label":"Linode 128GB (pending upgrade)","price":{"hourly":0.96,"monthly":640},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":32768,"disk":1572864,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-24"},{"id":"standard-101","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.2,"monthly":800},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":40960,"disk":1966080,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20-s"},{"id":"g4-standard-1","label":"Linode 2GB (pending upgrade)","price":{"hourly":0.015,"monthly":10},"addons":{"backups":{"price":{"hourly":0.004,"monthly":2.5}}},"memory":1024,"disk":24576,"transfer":2000,"vcpus":1,"gpus":0,"network_out":125,"class":"standard","successor":"g6-standard-1"},{"id":"g4-standard-2","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.03,"monthly":20},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":2048,"disk":49152,"transfer":3000,"vcpus":2,"gpus":0,"network_out":250,"class":"standard","successor":"g6-standard-2"},{"id":"g4-standard-3-2","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.05,"monthly":30},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":3072,"disk":73728,"transfer":3000,"vcpus":3,"gpus":0,"network_out":375,"class":"standard","successor":"g6-standard-3-s"},{"id":"g4-standard-4","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.06,"monthly":40},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":4096,"disk":98304,"transfer":4000,"vcpus":4,"gpus":0,"network_out":500,"class":"standard","successor":"g6-standard-4"},{"id":"g4-standard-4-s","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":6144,"disk":147456,"transfer":6000,"vcpus":4,"gpus":0,"network_out":750,"class":"standard","successor":"g6-standard-4-s"},{"id":"g4-standard-6","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.12,"monthly":80},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":8192,"disk":196608,"transfer":8000,"vcpus":6,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-6"},{"id":"g4-standard-8","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.24,"monthly":160},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":16384,"disk":393216,"transfer":16000,"vcpus":8,"gpus":0,"network_out":2000,"class":"standard","successor":"g6-standard-8"},{"id":"g4-standard-12","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.48,"monthly":320},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":32768,"disk":786432,"transfer":20000,"vcpus":12,"gpus":0,"network_out":4000,"class":"standard","successor":"g6-standard-16"},{"id":"g4-standard-16","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":49152,"disk":1179648,"transfer":20000,"vcpus":16,"gpus":0,"network_out":6000,"class":"standard","successor":"g6-standard-20"},{"id":"g4-standard-20","label":"Linode 128GB (pending upgrade)","price":{"hourly":0.96,"monthly":640},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":65536,"disk":1572864,"transfer":20000,"vcpus":20,"gpus":0,"network_out":8000,"class":"standard","successor":"g6-standard-24"},{"id":"g4-standard-20-s1","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.2,"monthly":800},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":81920,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-20-s"},{"id":"g4-standard-20-s2","label":"Linode 192GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":98304,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-32"},{"id":"g5-nanode-1","label":"Nanode 1GB (pending upgrade)","price":{"hourly":0.0075,"monthly":5},"addons":{"backups":{"price":{"hourly":0.003,"monthly":2}}},"memory":1024,"disk":20480,"transfer":1000,"vcpus":1,"gpus":0,"network_out":1000,"class":"nanode","successor":"g6-nanode-1"},{"id":"g5-standard-1","label":"Linode 2GB (pending upgrade)","price":{"hourly":0.015,"monthly":10},"addons":{"backups":{"price":{"hourly":0.004,"monthly":2.5}}},"memory":2048,"disk":30720,"transfer":2000,"vcpus":1,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-1"},{"id":"g5-standard-2","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.03,"monthly":20},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":4096,"disk":49152,"transfer":3000,"vcpus":2,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-2"},{"id":"g5-standard-3-s","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.05,"monthly":30},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":6144,"disk":73728,"transfer":3000,"vcpus":3,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-3-s"},{"id":"g5-standard-4","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.06,"monthly":40},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":8192,"disk":98304,"transfer":4000,"vcpus":4,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-4"},{"id":"g5-standard-4-s","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":10240,"disk":147456,"transfer":6000,"vcpus":4,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-4-s"},{"id":"g5-standard-6","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.12,"monthly":80},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":12288,"disk":196608,"transfer":8000,"vcpus":6,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-6"},{"id":"g5-standard-8","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.24,"monthly":160},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":24576,"disk":393216,"transfer":16000,"vcpus":8,"gpus":0,"network_out":2000,"class":"standard","successor":"g6-standard-8"},{"id":"g5-standard-12","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.48,"monthly":320},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":49152,"disk":786432,"transfer":20000,"vcpus":12,"gpus":0,"network_out":4000,"class":"standard","successor":"g6-standard-16"},{"id":"g5-standard-16","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":65536,"disk":1179648,"transfer":20000,"vcpus":16,"gpus":0,"network_out":6000,"class":"standard","successor":"g6-standard-20"},{"id":"g5-standard-20","label":"Linode 128GB (pending upgrade)","price":{"hourly":0.96,"monthly":640},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":81920,"disk":1572864,"transfer":20000,"vcpus":20,"gpus":0,"network_out":8000,"class":"standard","successor":"g6-standard-24"},{"id":"g5-standard-20-s1","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.2,"monthly":800},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":102400,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-20-s"},{"id":"g5-standard-20-s2","label":"Linode 192GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":122880,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-32"},{"id":"g5-highmem-1","label":"Linode 24GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":16384,"disk":20480,"transfer":5000,"vcpus":1,"gpus":0,"network_out":1000,"class":"highmem","successor":"g6-highmem-1"},{"id":"g5-highmem-2","label":"Linode 48GB (pending upgrade)","price":{"hourly":0.18,"monthly":120},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":32768,"disk":40960,"transfer":6000,"vcpus":2,"gpus":0,"network_out":1500,"class":"highmem","successor":"g6-highmem-2"},{"id":"g5-highmem-4","label":"Linode 90GB (pending upgrade)","price":{"hourly":0.36,"monthly":240},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":61440,"disk":92160,"transfer":7000,"vcpus":4,"gpus":0,"network_out":3000,"class":"highmem","successor":"g6-highmem-4"},{"id":"g5-highmem-8","label":"Linode 150GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":102400,"disk":204800,"transfer":8000,"vcpus":8,"gpus":0,"network_out":6000,"class":"highmem","successor":"g6-highmem-8"},{"id":"g5-highmem-16","label":"Linode 300GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.09,"monthly":60}}},"memory":204800,"disk":348160,"transfer":9000,"vcpus":16,"gpus":0,"network_out":10000,"class":"highmem","successor":"g6-highmem-16"},{"id":"g6-highmem-1","label":"Linode 24GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.0075,"monthly":5}}},"memory":24576,"disk":20480,"transfer":5000,"vcpus":1,"gpus":0,"network_out":5000,"class":"highmem","successor":"g7-highmem-1"},{"id":"g6-highmem-2","label":"Linode 48GB (pending upgrade)","price":{"hourly":0.18,"monthly":120},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":49152,"disk":40960,"transfer":6000,"vcpus":2,"gpus":0,"network_out":6000,"class":"highmem","successor":"g7-highmem-2"},{"id":"g6-highmem-4","label":"Linode 90GB (pending upgrade)","price":{"hourly":0.36,"monthly":240},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":92160,"disk":92160,"transfer":7000,"vcpus":4,"gpus":0,"network_out":7000,"class":"highmem","successor":"g7-highmem-4"},{"id":"g6-highmem-8","label":"Linode 150GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":153600,"disk":204800,"transfer":8000,"vcpus":8,"gpus":0,"network_out":8000,"class":"highmem","successor":"g7-highmem-8"},{"id":"g6-highmem-16","label":"Linode 300GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":307200,"disk":348160,"transfer":9000,"vcpus":16,"gpus":0,"network_out":9000,"class":"highmem","successor":"g7-highmem-16"}],"page":1,"pages":1,"results":65} \ No newline at end of file +{"data":[{"id":"standard-1","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.036,"monthly":24},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":512,"disk":24576,"transfer":2000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-2"},{"id":"standard-2","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.054,"monthly":36},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":768,"disk":36864,"transfer":3000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-3-s"},{"id":"standard-3","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.072,"monthly":48},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":1024,"disk":49152,"transfer":4000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4"},{"id":"standard-4","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.108,"monthly":72},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":1536,"disk":73728,"transfer":6000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4-s"},{"id":"standard-5","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.144,"monthly":96},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":2048,"disk":98304,"transfer":8000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-6"},{"id":"standard-6","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.288,"monthly":192},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":4096,"disk":196608,"transfer":16000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-8"},{"id":"standard-7","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.576,"monthly":384},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":8192,"disk":393216,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-16"},{"id":"standard-8","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.864,"monthly":576},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":12288,"disk":589824,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20"},{"id":"standard-9","label":"Linode 128GB (pending upgrade)","price":{"hourly":1.152,"monthly":768},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":16384,"disk":786432,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-24"},{"id":"standard-10","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":20480,"disk":983040,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20-s"},{"id":"standard-46","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.036,"monthly":24},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":1024,"disk":24576,"transfer":2000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-2"},{"id":"standard-47","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.06,"monthly":36},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":1536,"disk":36864,"transfer":3000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-3-s"},{"id":"standard-48","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.072,"monthly":48},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":2048,"disk":49152,"transfer":4000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4"},{"id":"standard-49","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.108,"monthly":72},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":3072,"disk":73728,"transfer":6000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4-s"},{"id":"standard-50","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.144,"monthly":96},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":4096,"disk":98304,"transfer":8000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-6"},{"id":"standard-51","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.288,"monthly":192},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":8192,"disk":196608,"transfer":16000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-8"},{"id":"standard-52","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.6,"monthly":384},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":16384,"disk":393216,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-16"},{"id":"standard-53","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.864,"monthly":576},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":24576,"disk":589824,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20"},{"id":"standard-54","label":"Linode 128GB (pending upgrade)","price":{"hourly":1.152,"monthly":768},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":32768,"disk":786432,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-24"},{"id":"standard-55","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":40960,"disk":983040,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20-s"},{"id":"standard-92","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.036,"monthly":24},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":1024,"disk":49152,"transfer":2000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-2"},{"id":"standard-93","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.06,"monthly":36},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":1536,"disk":73728,"transfer":3000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-3-s"},{"id":"standard-94","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.072,"monthly":48},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":2048,"disk":98304,"transfer":4000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4"},{"id":"standard-95","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.108,"monthly":72},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":3072,"disk":147456,"transfer":6000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-4-s"},{"id":"standard-96","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.144,"monthly":96},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":4096,"disk":196608,"transfer":8000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-6"},{"id":"standard-97","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.288,"monthly":192},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":8192,"disk":393216,"transfer":16000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-8"},{"id":"standard-98","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.576,"monthly":384},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":16384,"disk":786432,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-16"},{"id":"standard-99","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.864,"monthly":576},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":24576,"disk":1179648,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20"},{"id":"standard-100","label":"Linode 128GB (pending upgrade)","price":{"hourly":1.152,"monthly":768},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":32768,"disk":1572864,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-24"},{"id":"standard-101","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":40960,"disk":1966080,"transfer":20000,"vcpus":8,"gpus":0,"network_out":250,"class":null,"successor":"g6-standard-20-s"},{"id":"g4-standard-1","label":"Linode 2GB (pending upgrade)","price":{"hourly":0.018,"monthly":12},"addons":{"backups":{"price":{"hourly":0.004,"monthly":2.5}}},"memory":1024,"disk":24576,"transfer":2000,"vcpus":1,"gpus":0,"network_out":125,"class":"standard","successor":"g6-standard-1"},{"id":"g4-standard-2","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.036,"monthly":24},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":2048,"disk":49152,"transfer":3000,"vcpus":2,"gpus":0,"network_out":250,"class":"standard","successor":"g6-standard-2"},{"id":"g4-standard-3-2","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.06,"monthly":36},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":3072,"disk":73728,"transfer":3000,"vcpus":3,"gpus":0,"network_out":375,"class":"standard","successor":"g6-standard-3-s"},{"id":"g4-standard-4","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.072,"monthly":48},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":4096,"disk":98304,"transfer":4000,"vcpus":4,"gpus":0,"network_out":500,"class":"standard","successor":"g6-standard-4"},{"id":"g4-standard-4-s","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.108,"monthly":72},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":6144,"disk":147456,"transfer":6000,"vcpus":4,"gpus":0,"network_out":750,"class":"standard","successor":"g6-standard-4-s"},{"id":"g4-standard-6","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.144,"monthly":96},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":8192,"disk":196608,"transfer":8000,"vcpus":6,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-6"},{"id":"g4-standard-8","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.288,"monthly":192},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":16384,"disk":393216,"transfer":16000,"vcpus":8,"gpus":0,"network_out":2000,"class":"standard","successor":"g6-standard-8"},{"id":"g4-standard-12","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.576,"monthly":384},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":32768,"disk":786432,"transfer":20000,"vcpus":12,"gpus":0,"network_out":4000,"class":"standard","successor":"g6-standard-16"},{"id":"g4-standard-16","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.864,"monthly":576},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":49152,"disk":1179648,"transfer":20000,"vcpus":16,"gpus":0,"network_out":6000,"class":"standard","successor":"g6-standard-20"},{"id":"g4-standard-20","label":"Linode 128GB (pending upgrade)","price":{"hourly":1.152,"monthly":768},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":65536,"disk":1572864,"transfer":20000,"vcpus":20,"gpus":0,"network_out":8000,"class":"standard","successor":"g6-standard-24"},{"id":"g4-standard-20-s1","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":81920,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-20-s"},{"id":"g4-standard-20-s2","label":"Linode 192GB (pending upgrade)","price":{"hourly":1.728,"monthly":1152},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":98304,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-32"},{"id":"g5-nanode-1","label":"Nanode 1GB (pending upgrade)","price":{"hourly":0.0075,"monthly":5},"addons":{"backups":{"price":{"hourly":0.003,"monthly":2}}},"memory":1024,"disk":20480,"transfer":1000,"vcpus":1,"gpus":0,"network_out":1000,"class":"nanode","successor":"g6-nanode-1"},{"id":"g5-standard-1","label":"Linode 2GB (pending upgrade)","price":{"hourly":0.018,"monthly":12},"addons":{"backups":{"price":{"hourly":0.004,"monthly":2.5}}},"memory":2048,"disk":30720,"transfer":2000,"vcpus":1,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-1"},{"id":"g5-standard-2","label":"Linode 4GB (pending upgrade)","price":{"hourly":0.036,"monthly":24},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":4096,"disk":49152,"transfer":3000,"vcpus":2,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-2"},{"id":"g5-standard-3-s","label":"Linode 6GB (pending upgrade)","price":{"hourly":0.06,"monthly":36},"addons":{"backups":{"price":{"hourly":0.012,"monthly":7.5}}},"memory":6144,"disk":73728,"transfer":3000,"vcpus":3,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-3-s"},{"id":"g5-standard-4","label":"Linode 8GB (pending upgrade)","price":{"hourly":0.072,"monthly":48},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":8192,"disk":98304,"transfer":4000,"vcpus":4,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-4"},{"id":"g5-standard-4-s","label":"Linode 10GB (pending upgrade)","price":{"hourly":0.108,"monthly":72},"addons":{"backups":{"price":{"hourly":0.024,"monthly":15}}},"memory":10240,"disk":147456,"transfer":6000,"vcpus":4,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-4-s"},{"id":"g5-standard-6","label":"Linode 16GB (pending upgrade)","price":{"hourly":0.144,"monthly":96},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":12288,"disk":196608,"transfer":8000,"vcpus":6,"gpus":0,"network_out":1000,"class":"standard","successor":"g6-standard-6"},{"id":"g5-standard-8","label":"Linode 32GB (pending upgrade)","price":{"hourly":0.288,"monthly":192},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":24576,"disk":393216,"transfer":16000,"vcpus":8,"gpus":0,"network_out":2000,"class":"standard","successor":"g6-standard-8"},{"id":"g5-standard-12","label":"Linode 64GB (pending upgrade)","price":{"hourly":0.576,"monthly":384},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":49152,"disk":786432,"transfer":20000,"vcpus":12,"gpus":0,"network_out":4000,"class":"standard","successor":"g6-standard-16"},{"id":"g5-standard-16","label":"Linode 96GB (pending upgrade)","price":{"hourly":0.864,"monthly":576},"addons":{"backups":{"price":{"hourly":0.18,"monthly":120}}},"memory":65536,"disk":1179648,"transfer":20000,"vcpus":16,"gpus":0,"network_out":6000,"class":"standard","successor":"g6-standard-20"},{"id":"g5-standard-20","label":"Linode 128GB (pending upgrade)","price":{"hourly":1.152,"monthly":768},"addons":{"backups":{"price":{"hourly":0.24,"monthly":160}}},"memory":81920,"disk":1572864,"transfer":20000,"vcpus":20,"gpus":0,"network_out":8000,"class":"standard","successor":"g6-standard-24"},{"id":"g5-standard-20-s1","label":"Linode 160GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":102400,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-20-s"},{"id":"g5-standard-20-s2","label":"Linode 192GB (pending upgrade)","price":{"hourly":1.728,"monthly":1152},"addons":{"backups":{"price":{"hourly":0.3,"monthly":200}}},"memory":122880,"disk":1966080,"transfer":20000,"vcpus":20,"gpus":0,"network_out":10000,"class":"standard","successor":"g6-standard-32"},{"id":"g5-highmem-1","label":"Linode 24GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.008,"monthly":5}}},"memory":16384,"disk":20480,"transfer":5000,"vcpus":1,"gpus":0,"network_out":1000,"class":"highmem","successor":"g6-highmem-1"},{"id":"g5-highmem-2","label":"Linode 48GB (pending upgrade)","price":{"hourly":0.18,"monthly":120},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":32768,"disk":40960,"transfer":6000,"vcpus":2,"gpus":0,"network_out":1500,"class":"highmem","successor":"g6-highmem-2"},{"id":"g5-highmem-4","label":"Linode 90GB (pending upgrade)","price":{"hourly":0.36,"monthly":240},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":61440,"disk":92160,"transfer":7000,"vcpus":4,"gpus":0,"network_out":3000,"class":"highmem","successor":"g6-highmem-4"},{"id":"g5-highmem-8","label":"Linode 150GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":102400,"disk":204800,"transfer":8000,"vcpus":8,"gpus":0,"network_out":6000,"class":"highmem","successor":"g6-highmem-8"},{"id":"g5-highmem-16","label":"Linode 300GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.09,"monthly":60}}},"memory":204800,"disk":348160,"transfer":9000,"vcpus":16,"gpus":0,"network_out":10000,"class":"highmem","successor":"g6-highmem-16"},{"id":"g6-highmem-1","label":"Linode 24GB (pending upgrade)","price":{"hourly":0.09,"monthly":60},"addons":{"backups":{"price":{"hourly":0.0075,"monthly":5}}},"memory":24576,"disk":20480,"transfer":5000,"vcpus":1,"gpus":0,"network_out":5000,"class":"highmem","successor":"g7-highmem-1"},{"id":"g6-highmem-2","label":"Linode 48GB (pending upgrade)","price":{"hourly":0.18,"monthly":120},"addons":{"backups":{"price":{"hourly":0.015,"monthly":10}}},"memory":49152,"disk":40960,"transfer":6000,"vcpus":2,"gpus":0,"network_out":6000,"class":"highmem","successor":"g7-highmem-2"},{"id":"g6-highmem-4","label":"Linode 90GB (pending upgrade)","price":{"hourly":0.36,"monthly":240},"addons":{"backups":{"price":{"hourly":0.03,"monthly":20}}},"memory":92160,"disk":92160,"transfer":7000,"vcpus":4,"gpus":0,"network_out":7000,"class":"highmem","successor":"g7-highmem-4"},{"id":"g6-highmem-8","label":"Linode 150GB (pending upgrade)","price":{"hourly":0.72,"monthly":480},"addons":{"backups":{"price":{"hourly":0.06,"monthly":40}}},"memory":153600,"disk":204800,"transfer":8000,"vcpus":8,"gpus":0,"network_out":8000,"class":"highmem","successor":"g7-highmem-8"},{"id":"g6-highmem-16","label":"Linode 300GB (pending upgrade)","price":{"hourly":1.44,"monthly":960},"addons":{"backups":{"price":{"hourly":0.12,"monthly":80}}},"memory":307200,"disk":348160,"transfer":9000,"vcpus":16,"gpus":0,"network_out":9000,"class":"highmem","successor":"g7-highmem-16"}],"page":1,"pages":1,"results":65} \ No newline at end of file diff --git a/packages/manager/src/components/AccessPanel/AccessPanel.tsx b/packages/manager/src/components/AccessPanel/AccessPanel.tsx index f66d2674edb..a391ce0da23 100644 --- a/packages/manager/src/components/AccessPanel/AccessPanel.tsx +++ b/packages/manager/src/components/AccessPanel/AccessPanel.tsx @@ -1,7 +1,7 @@ import { Theme } from '@mui/material/styles'; import * as React from 'react'; import Paper from 'src/components/core/Paper'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SuspenseLoader from 'src/components/SuspenseLoader'; import { makeStyles } from 'tss-react/mui'; import Divider from '../core/Divider'; diff --git a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx index bf55338b6d0..a8b355a8f08 100644 --- a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx +++ b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx @@ -2,12 +2,12 @@ import { Theme } from '@mui/material/styles'; import * as React from 'react'; import Button from 'src/components/Button'; import CheckBox from 'src/components/CheckBox'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import SSHKeyCreationDrawer from 'src/features/Profile/SSHKeys/CreateSSHKeyDrawer'; @@ -17,7 +17,7 @@ import { useProfile, useSSHKeysQuery } from 'src/queries/profile'; import { truncateAndJoinList } from 'src/utilities/stringUtils'; import { makeStyles } from 'tss-react/mui'; import { GravatarByEmail } from '../GravatarByEmail'; -import PaginationFooter from '../PaginationFooter/PaginationFooter'; +import { PaginationFooter } from '../PaginationFooter/PaginationFooter'; import { TableRowLoading } from '../TableRowLoading/TableRowLoading'; export const MAX_SSH_KEYS_DISPLAY = 25; diff --git a/packages/manager/src/components/Accordion/Accordion.tsx b/packages/manager/src/components/Accordion/Accordion.tsx index ed1507e6dda..ea919897fda 100644 --- a/packages/manager/src/components/Accordion/Accordion.tsx +++ b/packages/manager/src/components/Accordion/Accordion.tsx @@ -11,7 +11,7 @@ import Typography, { TypographyProps } from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import RenderGuard from 'src/components/RenderGuard'; import { makeStyles } from 'tss-react/mui'; -import Notice from '../Notice'; +import { Notice } from 'src/components/Notice/Notice'; const useStyles = makeStyles()(() => ({ itemCount: { diff --git a/packages/manager/src/components/BarPercent/BarPercent.stories.mdx b/packages/manager/src/components/BarPercent/BarPercent.stories.mdx deleted file mode 100644 index 28e67c0aca4..00000000000 --- a/packages/manager/src/components/BarPercent/BarPercent.stories.mdx +++ /dev/null @@ -1,47 +0,0 @@ -import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; -import BarPercent from './BarPercent'; - -<Meta title="Components/Loading States/Bar Percent" component={BarPercent} /> - -# Bar Percent - -Determinate indicator that displays how long a process will take. - -export const Template = (args) => <BarPercent {...args} />; - -<Canvas> - <Story - name="Bar Percent" - args={{ max: 100, value: 60 }} - argTypes={{ - max: { - description: - 'The maximum allowed value and should not be equal to min.', - }, - value: { - description: - 'The value of the progress indicator for the determinate and buffer variants.', - }, - className: { - table: { disable: true }, - }, - isFetchingValue: { - description: - 'Applies styles to show that the value is being retrieved.', - }, - narrow: { - description: 'Decreases the height of the bar.', - }, - rounded: { - description: 'Applies a `border-radius` to the bar.', - }, - valueBuffer: { - description: 'The value for the buffer variant.', - }, - }} - > - {Template.bind()} - </Story> -</Canvas> - -<ArgsTable story="Bar Percent" sort="requiredFirst" /> diff --git a/packages/manager/src/components/BarPercent/BarPercent.stories.tsx b/packages/manager/src/components/BarPercent/BarPercent.stories.tsx new file mode 100644 index 00000000000..c0745b97259 --- /dev/null +++ b/packages/manager/src/components/BarPercent/BarPercent.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { BarPercent } from './BarPercent'; +import type { BarPercentProps } from './BarPercent'; +import type { Meta, StoryObj } from '@storybook/react'; + +/** Default BarPercent */ +export const Default: StoryObj<BarPercentProps> = { + render: (args) => <BarPercent {...args} />, +}; + +/** Narrow BarPercent */ +export const Narrow: StoryObj<BarPercentProps> = { + render: (args) => <BarPercent {...args} narrow value={20} />, +}; + +const meta: Meta<BarPercentProps> = { + title: 'Components/Loading States/Bar Percent', + component: BarPercent, + args: { max: 100, value: 60 }, +}; +export default meta; diff --git a/packages/manager/src/components/BarPercent/BarPercent.tsx b/packages/manager/src/components/BarPercent/BarPercent.tsx index e8b5c8737b9..cce8736053c 100644 --- a/packages/manager/src/components/BarPercent/BarPercent.tsx +++ b/packages/manager/src/components/BarPercent/BarPercent.tsx @@ -3,18 +3,28 @@ import { SxProps } from '@mui/system'; import * as React from 'react'; import LinearProgress from 'src/components/core/LinearProgress'; -interface Props { - max: number; - value: number; +export interface BarPercentProps { + /** Additional css class to pass to the component */ className?: string; - valueBuffer?: number; + /** Applies styles to show that the value is being retrieved. */ isFetchingValue?: boolean; - rounded?: boolean; + /** The maximum allowed value and should not be equal to min. */ + max: number; + /** Decreases the height of the bar. */ narrow?: boolean; + /** Applies a `border-radius` to the bar. */ + rounded?: boolean; sx?: SxProps; + /** The value of the progress indicator for the determinate and buffer variants. */ + value: number; + /** The value for the buffer variant. */ + valueBuffer?: number; } -export const BarPercent = (props: Props) => { +/** + * Determinate indicator that displays how long a process will take. + */ +export const BarPercent = (props: BarPercentProps) => { const { max, value, @@ -59,7 +69,7 @@ const StyledDiv = styled('div')({ const StyledLinearProgress = styled(LinearProgress, { label: 'StyledLinearProgress', -})<Partial<Props>>(({ theme, ...props }) => ({ +})<Partial<BarPercentProps>>(({ theme, ...props }) => ({ backgroundColor: theme.color.grey2, padding: props.narrow ? 8 : 12, width: '100%', diff --git a/packages/manager/src/components/Button/Button.tsx b/packages/manager/src/components/Button/Button.tsx index 5b2fef6441b..33ea18e71d9 100644 --- a/packages/manager/src/components/Button/Button.tsx +++ b/packages/manager/src/components/Button/Button.tsx @@ -7,8 +7,10 @@ import { SxProps } from '@mui/system'; import { isPropValid } from '../../utilities/isPropValid'; import { rotate360 } from '../../styles/keyframes'; +export type ButtonType = 'primary' | 'secondary' | 'outlined'; + export interface Props extends ButtonProps { - buttonType?: 'primary' | 'secondary' | 'outlined'; + buttonType?: ButtonType; className?: string; sx?: SxProps; compactX?: boolean; diff --git a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx index 548efdd74c6..e272d8ea209 100644 --- a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx +++ b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx @@ -1,8 +1,8 @@ +import { styled, Theme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { useTheme } from '@mui/styles'; import * as React from 'react'; import Paper from '../core/Paper'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { makeStyles, useTheme } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; import Typography from '../core/Typography'; import Grid from '../Grid'; @@ -22,59 +22,53 @@ export interface SummaryItem { hourly?: number; } -const useStyles = makeStyles((theme: Theme) => ({ - paper: { - marginTop: theme.spacing(3), - marginBottom: theme.spacing(3), - }, - heading: { - marginBottom: theme.spacing(3), - }, - summary: { - [theme.breakpoints.up('md')]: { - '& > div': { - borderRight: 'solid 1px #9DA4A6', - '&:last-child': { - borderRight: 'none', - }, - }, - }, - }, -})); - export const CheckoutSummary = (props: Props) => { - const classes = useStyles(); const theme = useTheme<Theme>(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); const { heading, agreement, displaySections, children } = props; return ( - <Paper data-qa-summary className={classes.paper}> - <Typography - variant="h2" - data-qa-order-summary - className={classes.heading} - > + <StyledPaper data-qa-summary> + <StyledHeading variant="h2" data-qa-order-summary> {heading} - </Typography> + </StyledHeading> {displaySections.length === 0 ? ( - <Typography variant="body1" className={classes.heading}> + <StyledHeading variant="body1"> Please configure your Linode. - </Typography> + </StyledHeading> ) : null} - <Grid + <StyledSummary container spacing={3} direction={matchesSmDown ? 'column' : 'row'} - className={classes.summary} > {displaySections.map((item) => ( <SummaryItem key={`${item.title}-${item.details}`} {...item} /> ))} - </Grid> + </StyledSummary> {children} {agreement ? agreement : null} - </Paper> + </StyledPaper> ); }; + +const StyledPaper = styled(Paper)(({ theme }) => ({ + marginTop: theme.spacing(3), + marginBottom: theme.spacing(3), +})); + +const StyledHeading = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(3), +})); + +const StyledSummary = styled(Grid)(({ theme }) => ({ + [theme.breakpoints.up('md')]: { + '& > div': { + borderRight: 'solid 1px #9DA4A6', + '&:last-child': { + borderRight: 'none', + }, + }, + }, +})); diff --git a/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx b/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx index 674929b45b2..5afaa8be308 100644 --- a/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx +++ b/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx @@ -1,27 +1,15 @@ +import { styled } from '@mui/material/styles'; import React from 'react'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; import Typography from '../core/Typography'; import Grid from '../Grid'; import { SummaryItem as Props } from './CheckoutSummary'; -const useStyles = makeStyles((theme: Theme) => ({ - item: { - paddingTop: '0 !important', - paddingBottom: '0 !important', - marginTop: theme.spacing(), - marginBottom: theme.spacing(), - }, -})); - export const SummaryItem = ({ title, details }: Props) => { - const classes = useStyles(); - return ( - <Grid item className={classes.item}> + <StyledGrid item> {title ? ( <> - <Typography style={{ fontWeight: 'bold' }} component="span"> + <Typography sx={{ fontWeight: 'bold' }} component="span"> {title} </Typography>{' '} </> @@ -29,6 +17,13 @@ export const SummaryItem = ({ title, details }: Props) => { <Typography component="span" data-qa-details={details}> {details} </Typography> - </Grid> + </StyledGrid> ); }; + +const StyledGrid = styled(Grid)(({ theme }) => ({ + paddingTop: '0 !important', + paddingBottom: '0 !important', + marginTop: `${theme.spacing()} !important`, + marginBottom: `${theme.spacing()} !important`, +})); diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx index 09c0c264a40..8b367d1460c 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -5,7 +5,7 @@ import DialogContent from 'src/components/core/DialogContent'; import DialogContentText from 'src/components/core/DialogContentText'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import DialogTitle from 'src/components/DialogTitle'; +import { DialogTitle } from 'src/components/DialogTitle/DialogTitle'; const useStyles = makeStyles()((theme: Theme) => ({ root: { @@ -38,14 +38,14 @@ export interface ConfirmationDialogProps extends DialogProps { export const ConfirmationDialog = (props: ConfirmationDialogProps) => { const { classes } = useStyles(); - const { title, children, actions, error, ...dialogProps } = props; + const { title, children, actions, error, onClose, ...dialogProps } = props; return ( <Dialog {...dialogProps} onClose={(_, reason) => { if (reason !== 'backdropClick') { - dialogProps.onClose(); + onClose(); } }} className={classes.root} @@ -55,7 +55,7 @@ export const ConfirmationDialog = (props: ConfirmationDialogProps) => { data-qa-dialog data-testid="drawer" > - <DialogTitle className="dialog-title" title={title} /> + <DialogTitle title={title} onClose={onClose} /> <DialogContent data-qa-dialog-content className="dialog-content"> {children} {error && ( diff --git a/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx b/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx index ad278edba64..72d07c99093 100644 --- a/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx +++ b/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx @@ -1,54 +1,25 @@ import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; import TextField, { Props as TextFieldProps } from 'src/components/TextField'; -const useStyles = makeStyles((theme: Theme) => ({ - removeDisabledStyles: { - '& .MuiInput-input': { - borderColor: theme.name === 'light' ? '#ccc' : '#222', - color: - theme.name === 'light' - ? `${theme.palette.text.primary} !important` - : '#fff !important', - opacity: 1, - '-webkit-text-fill-color': 'unset !important', - }, - '& .MuiInput-root': { - borderColor: theme.name === 'light' ? '#ccc' : '#222', - opacity: 1, - }, - }, - copyIcon: { - marginRight: theme.spacing(0.5), - '& svg': { - height: 14, - top: 1, - }, - }, -})); - -type Props = TextFieldProps & { +type CopyableTextFieldProps = TextFieldProps & { className?: string; hideIcon?: boolean; }; -type CombinedProps = Props; - -export const CopyableTextField: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); +export const CopyableTextField = (props: CopyableTextFieldProps) => { const { value, className, hideIcon, ...restProps } = props; return ( - <TextField + <StyledTextField value={value} {...restProps} - className={`${className} ${'copy'} ${classes.removeDisabledStyles}`} + className={`${className} copy removeDisabledStyles`} disabled InputProps={{ endAdornment: hideIcon ? undefined : ( - <CopyTooltip text={`${value}`} className={classes.copyIcon} /> + <CopyTooltip text={`${value}`} className="copyIcon" /> ), }} data-qa-copy-tooltip @@ -56,4 +27,32 @@ export const CopyableTextField: React.FC<CombinedProps> = (props) => { ); }; -export default CopyableTextField; +const StyledTextField = styled(TextField)(({ theme }) => ({ + '.removeDisabledStyles': { + '& .MuiInput-input': { + borderColor: theme.name === 'light' ? '#ccc' : '#222', + color: + theme.name === 'light' + ? `${theme.palette.text.primary} !important` + : '#fff !important', + opacity: theme.name === 'dark' ? 0.5 : 0.8, + '-webkit-text-fill-color': 'unset !important', + }, + '&& .MuiInput-root': { + borderColor: theme.name === 'light' ? '#ccc' : '#222', + opacity: 1, + }, + }, + '.copyIcon': { + marginRight: theme.spacing(0.5), + '& svg': { + height: 14, + top: 1, + color: '#3683dc', + }, + }, + '&.copy > div': { + backgroundColor: theme.name === 'dark' ? '#2f3236' : '#f4f4f4', + opacity: 1, + }, +})); diff --git a/packages/manager/src/components/CopyableTextField/index.ts b/packages/manager/src/components/CopyableTextField/index.ts deleted file mode 100644 index 196c3d20eb0..00000000000 --- a/packages/manager/src/components/CopyableTextField/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CopyableTextField'; diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx index f4053fd1222..7b143fb6874 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx @@ -5,7 +5,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { useTheme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import TypeToConfirm from 'src/components/TypeToConfirm'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { titlecase } from 'src/features/linodes/presentation'; import { capitalize } from 'src/utilities/capitalize'; import { DialogProps } from '../Dialog/Dialog'; diff --git a/packages/manager/src/components/Dialog/Dialog.tsx b/packages/manager/src/components/Dialog/Dialog.tsx index 7ffe94821ec..9af5ddbd179 100644 --- a/packages/manager/src/components/Dialog/Dialog.tsx +++ b/packages/manager/src/components/Dialog/Dialog.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import _Dialog, { DialogProps as _DialogProps } from '@mui/material/Dialog'; import Box from '@mui/material/Box'; -import Button from 'src/components/Button'; -import Close from '@mui/icons-material/Close'; -import Notice from 'src/components/Notice'; -import Typography from 'src/components/core/Typography'; +import { Notice } from 'src/components/Notice/Notice'; +import DialogContent from 'src/components/core/DialogContent'; +import { DialogTitle } from 'src/components/DialogTitle/DialogTitle'; import { isPropValid } from 'src/utilities/isPropValid'; -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; import { convertForAria } from 'src/components/TabLink/TabLink'; export interface DialogProps extends _DialogProps { @@ -18,6 +17,7 @@ export interface DialogProps extends _DialogProps { } const Dialog = (props: DialogProps) => { + const theme = useTheme(); const { children, className, @@ -52,30 +52,21 @@ const Dialog = (props: DialogProps) => { alignItems: 'center', }} > - <StyledDialogHeader> - <Typography - variant="h2" - id={titleID} - data-qa-drawer-title={title} - data-qa-dialog-title={title} - > - {title} - </Typography> - - <StyledButton - buttonType="secondary" - onClick={onClose as (e: any) => void} - data-qa-close-drawer - aria-label="Close" - > - <Close /> - </StyledButton> - </StyledDialogHeader> + <DialogTitle + title={title} + onClose={() => onClose && onClose({}, 'backdropClick')} + id={titleID} + /> {titleBottomBorder && <StyledHr />} - <Box className={className}> + <DialogContent + className={className} + sx={{ + paddingBottom: theme.spacing(3), + }} + > {error && <Notice text={error} error />} {children} - </Box> + </DialogContent> </Box> </StyledDialog> ); @@ -87,7 +78,7 @@ const StyledDialog = styled(_Dialog, { '& .MuiDialog-paper': { height: props.fullHeight ? '100vh' : undefined, maxHeight: '100%', - padding: `${theme.spacing(4)}`, + padding: 0, }, '& .MuiDialogActions-root': { display: 'flex', @@ -107,24 +98,4 @@ const StyledHr = styled('hr')({ width: '100%', }); -const StyledButton = styled(Button)(() => ({ - minHeight: 'auto', - minWidth: 'auto', - position: 'absolute', - right: '-16px', -})); - -const StyledDialogHeader = styled(Box)(({ theme }) => ({ - alignItems: 'center', - backgroundColor: theme.bg.bgPaper, - display: 'flex', - justifyContent: 'space-between', - paddingBottom: theme.spacing(2), - marginRight: theme.spacing(7), - position: 'sticky', - top: 0, - width: '100%', - zIndex: 2, -})); - export { Dialog }; diff --git a/packages/manager/src/components/DialogTitle/DialogTitle.tsx b/packages/manager/src/components/DialogTitle/DialogTitle.tsx index 323e6103ffe..9cbc3bbe7cb 100644 --- a/packages/manager/src/components/DialogTitle/DialogTitle.tsx +++ b/packages/manager/src/components/DialogTitle/DialogTitle.tsx @@ -1,65 +1,68 @@ -import Close from '@mui/icons-material/Close'; import * as React from 'react'; -import DialogTitle from 'src/components/core/DialogTitle'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; +import _DialogTitle from '@mui/material/DialogTitle'; +import Box from '@mui/material/Box'; +import Close from '@mui/icons-material/Close'; +import IconButton from 'src/components/IconButton/IconButton'; +import { SxProps } from '@mui/system'; -const useStyles = makeStyles((theme: Theme) => ({ - root: { - width: '100%', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - }, - button: { - border: 'none', - backgroundColor: 'inherit', - paddingRight: 0, - paddingLeft: 0, - cursor: 'pointer', - '&:hover': { - color: theme.palette.primary.main, - }, - }, -})); -interface Props { - title: string; +interface DialogTitleProps { className?: string; + id?: string; onClose?: () => void; + sx?: SxProps; + title: string; } -// Accessibility Feature: -// Focus on modal title on component mount - -const _DialogTitle: React.FC<Props> = (props) => { - const dialogTitle = React.useRef<HTMLDivElement>(null); - const { className, onClose, title } = props; - const classes = useStyles(); +const DialogTitle = (props: DialogTitleProps) => { + const ref = React.useRef<HTMLDivElement>(null); + const { className, onClose, title, id, sx } = props; React.useEffect(() => { - if (dialogTitle.current !== null) { - dialogTitle.current.focus(); + if (ref.current === null) { + return; } + + ref.current.focus(); }, []); return ( - <DialogTitle + <_DialogTitle + className={className} data-qa-dialog-title={title} + id={id} + ref={ref} + sx={sx} title={title} - tabIndex={0} - className={className} - ref={dialogTitle} > - <div className={classes.root}> + <Box + data-qa-drawer-title={title} + data-qa-dialog-title={title} + sx={{ + alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', + position: 'relative', + width: '100%', + }} + > {title} - {onClose ? ( - <button className={classes.button} onClick={onClose}> + {onClose != null && ( + <IconButton + aria-label="Close" + data-qa-close-drawer + onClick={onClose} + size="large" + sx={{ + position: 'absolute', + right: '-12px', + }} + > <Close /> - </button> - ) : null} - </div> - </DialogTitle> + </IconButton> + )} + </Box> + </_DialogTitle> ); }; -export default _DialogTitle; +export { DialogTitle }; diff --git a/packages/manager/src/components/DialogTitle/index.tsx b/packages/manager/src/components/DialogTitle/index.tsx deleted file mode 100644 index 79acdf3ad15..00000000000 --- a/packages/manager/src/components/DialogTitle/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import _DialogTitle from './DialogTitle'; -export default _DialogTitle; diff --git a/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx b/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx index af737aac10f..d7f34403931 100644 --- a/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx +++ b/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx @@ -3,7 +3,8 @@ import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import { SxProps } from '@mui/system'; import * as React from 'react'; -import Notice, { NoticeProps } from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; +import type { NoticeProps } from 'src/components/Notice/Notice'; import useDismissibleNotifications, { DismissibleNotificationOptions, } from 'src/hooks/useDismissibleNotifications'; @@ -84,6 +85,11 @@ const StyledNotice = styled(Notice)(({ theme }) => ({ marginBottom: theme.spacing(), padding: theme.spacing(2), background: theme.bg.bgPaper, + '&&': { + p: { + lineHeight: '1.25rem', + }, + }, })); const StyledButton = styled('button')(({ theme }) => ({ diff --git a/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx b/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx index 1136c300b63..dcb094523ff 100644 --- a/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx +++ b/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx @@ -1,44 +1,60 @@ import * as React from 'react'; +import Button from 'src/components/Button'; import { CSVLink } from 'react-csv'; -import { compose } from 'recompose'; +import { SxProps } from '@mui/system'; +import type { ButtonType } from 'src/components/Button/Button'; -/** - * these aren't all the props provided by react-csv - * check out the docs for all props: https://github.com/react-csv/react-csv - */ -interface Props { - data: any[]; - headers: { label: string; key: string }[]; - filename: string; +interface DownloadCSVProps { + buttonType?: ButtonType; + children?: React.ReactNode; className?: string; + csvRef?: React.RefObject<any>; + data: unknown[]; + filename: string; + headers: { label: string; key: string }[]; + onClick: () => void; + sx?: SxProps; + text?: string; } -type CombinedProps = Props; - -const DownloadCSV: React.FC<CombinedProps> = (props) => { - const { className, headers, filename, data, children } = props; +/** + * Hidden CSVLink component controlled by a ref. This is done + * so we can use Button styles, and in other areas like + * "MaintainanceTable" to lazy load potentially large sets + * of events on mount. + * + * These aren't all the props provided by react-csv. + * @see https://github.com/react-csv/react-csv + */ +export const DownloadCSV = ({ + buttonType = 'secondary', + className, + csvRef, + data, + filename, + headers, + onClick, + sx, + text = 'Download CSV', +}: DownloadCSVProps) => { return ( - <CSVLink - className={className} - headers={headers} - filename={filename} - data={cleanCSVData(data)} - > - {children} - </CSVLink> + <> + <CSVLink + aria-hidden="true" + className={className} + data={cleanCSVData(data)} + filename={filename} + headers={headers} + ref={csvRef} + tabIndex={-1} + /> + <Button buttonType={buttonType} onClick={onClick} sx={sx}> + {text} + </Button> + </> ); }; -/** - * prevents CSV injections. Without this logic, a user - * could, for example, add a tag of =cmd|' /C calc'!A0 - * then open the CSV up in Microsoft Excel and it would - * automatically open the calculator. - * - * For now, we're just going to strip "=", "+", and "-" - * signs, at the recommendation of hackerone discussions. - * See M3-3022 for more info. - */ export const cleanCSVData = (data: any): any => { /** safety check because typeof null === 'object' */ if (data === null) { @@ -76,5 +92,3 @@ export const cleanCSVData = (data: any): any => { return data; }; - -export default compose<CombinedProps, Props>(React.memo)(DownloadCSV); diff --git a/packages/manager/src/components/DownloadCSV/index.ts b/packages/manager/src/components/DownloadCSV/index.ts deleted file mode 100644 index 08afe5b4f4a..00000000000 --- a/packages/manager/src/components/DownloadCSV/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './DownloadCSV'; diff --git a/packages/manager/src/components/DrawerContent/DrawerContent.tsx b/packages/manager/src/components/DrawerContent/DrawerContent.tsx index 761a346b924..daa7f793d86 100644 --- a/packages/manager/src/components/DrawerContent/DrawerContent.tsx +++ b/packages/manager/src/components/DrawerContent/DrawerContent.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; export interface Props { title: string; diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinkIcon.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinkIcon.tsx new file mode 100644 index 00000000000..bfa447030a1 --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinkIcon.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { isPropValid } from 'src/utilities/isPropValid'; +import { styled } from '@mui/material/styles'; + +interface ResourcesLinkIconProps { + /** + * The icon to display as a rendered SVG (JSX) + */ + icon: JSX.Element; + iconType: 'pointer' | 'external'; +} + +const StyledResourcesLinkIcon = styled('span', { + label: 'StyledResourcesLinkIcon', + shouldForwardProp: (prop) => isPropValid(['icon', 'iconType'], prop), +})<ResourcesLinkIconProps>(({ theme, ...props }) => ({ + color: theme.textColors.linkActiveLight, + display: 'inline-block', + position: 'relative', + // nifty trick to avoid the icon from wrapping by itself after the last word + marginLeft: -10, + transform: 'translateX(18px)', + '& svg': { + height: props.iconType === 'external' ? 12 : 16, + width: props.iconType === 'external' ? 12 : 16, + }, + top: props.iconType === 'pointer' ? 3 : 0, +})); + +export const ResourcesLinkIcon = (props: ResourcesLinkIconProps) => { + const { icon } = props; + + return <StyledResourcesLinkIcon {...props}>{icon}</StyledResourcesLinkIcon>; +}; diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinks.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinks.tsx new file mode 100644 index 00000000000..d7eeea16f88 --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinks.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; +import Link from 'src/components/Link'; +import List from 'src/components/core/List'; +import ListItem from 'src/components/core/ListItem'; +import { getLinkOnClick } from 'src/utilities/emptyStateLandingUtils'; +import { ResourcesLinkIcon } from 'src/components/EmptyLandingPageResources/ResourcesLinkIcon'; +import type { ResourcesLinks } from './ResourcesLinksTypes'; + +export const ResourceLinks = (props: ResourcesLinks) => { + const { linkGAEvent, links } = props; + + return ( + <List> + {links.map((linkData) => ( + <ListItem key={linkData.to}> + <Link + to={linkData.to} + onClick={getLinkOnClick(linkGAEvent, linkData.text)} + > + {linkData.text} + {linkData.external && ( + <ResourcesLinkIcon + icon={<ExternalLinkIcon />} + iconType="external" + /> + )} + </Link> + </ListItem> + ))} + </List> + ); +}; diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSection.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSection.tsx new file mode 100644 index 00000000000..921289a3492 --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSection.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +interface ResourcesLinksSectionProps { + children: JSX.Element[] | JSX.Element; + /** + * If true, the section will be 100% width (more than 2 columns) + * + * @example true on linodes empty state landing + * @default true + * */ + wide?: boolean; +} + +const StyledResourcesLinksSection = styled('div', { + label: 'StyledResourcesLinksSection', +})<ResourcesLinksSectionProps>(({ theme, ...props }) => ({ + columnGap: theme.spacing(5), + display: 'grid', + gridAutoColumns: '1fr', + gridAutoFlow: 'column', + justifyItems: 'center', + maxWidth: props.wide === false ? 762 : '100%', + [theme.breakpoints.down('md')]: { + gridAutoFlow: 'row', + rowGap: theme.spacing(8), + justifyItems: 'start', + }, +})); + +export const ResourcesLinksSection = ({ + children, + wide = true, +}: ResourcesLinksSectionProps) => { + return ( + <StyledResourcesLinksSection wide={wide}> + {children} + </StyledResourcesLinksSection> + ); +}; diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSubSection.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSubSection.tsx new file mode 100644 index 00000000000..3a376661605 --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksSubSection.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import Typography from 'src/components/core/Typography'; +import { styled } from '@mui/material/styles'; + +interface ResourcesLinksSubSectionProps { + children?: JSX.Element[] | JSX.Element; + external?: boolean; + icon: JSX.Element; + MoreLink: (props: { className?: any }) => JSX.Element; + title: string; +} + +const StyledResourcesLinksSubSection = styled('div', { + label: 'StyledResourcesLinksSubSection', +})(({ theme }) => ({ + display: 'grid', + gridTemplateRows: `22px minmax(${theme.spacing(3)}, 100%) 1.125rem`, + rowGap: theme.spacing(2), + width: '100%', + [theme.breakpoints.between('md', 'lg')]: { + gridTemplateRows: `50px minmax(${theme.spacing(3)}, 100%) 1.125rem`, + }, + '& > h2': { + color: theme.palette.text.primary, + }, + '& > h2 > svg': { + color: theme.palette.primary.main, + height: '1.125rem', + marginRight: theme.spacing(), + width: '1.125rem', + }, + '& > a': { + color: theme.textColors.linkActiveLight, + display: 'inline-block', + fontSize: '0.875rem', + fontWeight: 700, + }, + '& li': { + paddingLeft: 0, + paddingRight: 0, + '& > a': { + fontSize: '0.875rem', + color: theme.textColors.linkActiveLight, + }, + }, +})); + +export const ResourcesLinksSubSection = ( + props: ResourcesLinksSubSectionProps +) => { + const { children, icon, MoreLink, title } = props; + + return ( + <StyledResourcesLinksSubSection> + <Typography variant="h2"> + {icon} {title} + </Typography> + {children} + <MoreLink /> + </StyledResourcesLinksSubSection> + ); +}; diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts new file mode 100644 index 00000000000..981376056ca --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts @@ -0,0 +1,27 @@ +interface ResourcesLink { + to: string; + text: string; + external?: boolean; +} + +export interface LinkGAEvent { + category: string; + action: string; +} + +export interface ResourcesHeaders { + title: string; + subtitle: string; + description: string; +} + +export interface ResourcesLinks { + links: ResourcesLink[]; + linkGAEvent: LinkGAEvent; +} + +export interface ResourcesLinkSection { + title: string; + links: ResourcesLinks['links']; + moreInfo: ResourcesLink; +} diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesMoreLink.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesMoreLink.tsx new file mode 100644 index 00000000000..87a3abec13b --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesMoreLink.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import Link from 'src/components/Link'; +import { styled } from '@mui/material/styles'; +import { LinkProps } from 'react-router-dom'; + +type ResourcesMoreLinkProps = LinkProps & { + external?: boolean; +}; + +const StyledMoreLink = styled(Link)<ResourcesMoreLinkProps>(({ ...props }) => ({ + alignItems: props.external ? 'baseline' : 'center', +})); + +export const ResourcesMoreLink = (props: ResourcesMoreLinkProps) => { + return <StyledMoreLink {...props} />; +}; diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx new file mode 100644 index 00000000000..0eed6702328 --- /dev/null +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx @@ -0,0 +1,158 @@ +import * as React from 'react'; +import DocsIcon from 'src/assets/icons/docs.svg'; +import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; +import Placeholder from 'src/components/Placeholder'; +import PointerIcon from 'src/assets/icons/pointer.svg'; +import Typography from 'src/components/core/Typography'; +import YoutubeIcon from 'src/assets/icons/youtube.svg'; +import { ResourceLinks } from 'src/components/EmptyLandingPageResources/ResourcesLinks'; +import { ResourcesLinkIcon } from 'src/components/EmptyLandingPageResources/ResourcesLinkIcon'; +import { ResourcesLinksSection } from 'src/components/EmptyLandingPageResources/ResourcesLinksSection'; +import { ResourcesLinksSubSection } from 'src/components/EmptyLandingPageResources/ResourcesLinksSubSection'; +import { ResourcesMoreLink } from 'src/components/EmptyLandingPageResources/ResourcesMoreLink'; +import { + getLinkOnClick, + youtubeChannelLink, + youtubeMoreLinkLabel, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + LinkGAEvent, + ResourcesLinkSection, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +interface ButtonProps { + onClick: () => void; + children: string; +} + +interface ResourcesSectionProps { + /** + * The button's handlers and text + */ + buttonProps: ButtonProps[]; + /** + * The custom resource to be rendered between docs and youtube links + * @example <AppsSection /> on linodes empty state landing + */ + CustomResource?: () => JSX.Element; + /** + * Allow to set a custom max width for the description (better word wrapping) + * */ + descriptionMaxWidth?: number; + /** + * The data for the docs links section + */ + gettingStartedGuidesData: ResourcesLinkSection; + /** + * The headers for the section (title, subtitle, description) + */ + headers: ResourcesHeaders; + /** + * The icon for the section + */ + icon: React.ComponentType<any>; + /** + * The event data to be sent when the call to action is clicked + */ + linkGAEvent: LinkGAEvent; + /** + * If true, the transfer display will be shown at the bottom + * */ + showTransferDisplay?: boolean; + /** + * The data for the youtube links section + */ + youtubeLinkData: ResourcesLinkSection; + /** + * If true, the section will be 100% width (more than 2 columns) + * + * @example true on linodes empty state landing + * @default true + * */ + wide?: boolean; +} + +const GuideLinks = (guides: ResourcesLinkSection, linkGAEvent: LinkGAEvent) => ( + <ResourceLinks links={guides.links} linkGAEvent={linkGAEvent} /> +); + +const YoutubeLinks = ( + youtube: ResourcesLinkSection, + linkGAEvent: LinkGAEvent +) => <ResourceLinks links={youtube.links} linkGAEvent={linkGAEvent} />; + +export const ResourcesSection = (props: ResourcesSectionProps) => { + const { + buttonProps, + CustomResource = () => null, + descriptionMaxWidth, + gettingStartedGuidesData, + headers, + icon, + linkGAEvent, + showTransferDisplay, + youtubeLinkData, + wide = false, + } = props; + const { title, subtitle, description } = headers; + + return ( + <Placeholder + buttonProps={buttonProps} + descriptionMaxWidth={descriptionMaxWidth} + icon={icon} + isEntity + linksSection={ + <ResourcesLinksSection wide={wide}> + <ResourcesLinksSubSection + icon={<DocsIcon />} + MoreLink={(props) => ( + <ResourcesMoreLink + onClick={getLinkOnClick( + linkGAEvent, + gettingStartedGuidesData.moreInfo.text + )} + to={gettingStartedGuidesData.moreInfo.to} + {...props} + > + {gettingStartedGuidesData.moreInfo.text} + <ResourcesLinkIcon icon={<PointerIcon />} iconType="pointer" /> + </ResourcesMoreLink> + )} + title={gettingStartedGuidesData.title} + > + {GuideLinks(gettingStartedGuidesData, linkGAEvent)} + </ResourcesLinksSubSection> + {CustomResource && <CustomResource />} + <ResourcesLinksSubSection + icon={<YoutubeIcon />} + MoreLink={(props) => ( + <ResourcesMoreLink + external + onClick={getLinkOnClick(linkGAEvent, youtubeMoreLinkLabel)} + to={youtubeChannelLink} + {...props} + > + {youtubeMoreLinkText} + <ResourcesLinkIcon + icon={<ExternalLinkIcon />} + iconType="external" + /> + </ResourcesMoreLink> + )} + title={youtubeLinkData.title} + > + {YoutubeLinks(youtubeLinkData, linkGAEvent)} + </ResourcesLinksSubSection> + </ResourcesLinksSection> + } + showTransferDisplay={showTransferDisplay} + subtitle={subtitle} + title={title} + > + <Typography variant="subtitle1">{description}</Typography> + </Placeholder> + ); +}; diff --git a/packages/manager/src/components/EntityDetail/.EntityDetail.stories.mdx b/packages/manager/src/components/EntityDetail/.EntityDetail.stories.mdx index ebe37fbfcac..1132fa0bc65 100644 --- a/packages/manager/src/components/EntityDetail/.EntityDetail.stories.mdx +++ b/packages/manager/src/components/EntityDetail/.EntityDetail.stories.mdx @@ -5,7 +5,6 @@ import { linodeConfigFactory } from 'src/factories/linodeConfigs'; import { linodeBackupsFactory, linodeFactory } from 'src/factories/linodes'; import LinodeEntityDetail from 'src/features/linodes/LinodeEntityDetail'; import KubeSummaryPanel from 'src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel'; -import store from 'src/store'; <Meta title="Features/Entity Detail" component={LinodeEntityDetail} /> @@ -13,23 +12,21 @@ import store from 'src/store'; <Canvas> <Story name="Linode"> - <Provider store={store}> - <div> - <h2 style={{ margin: 0 }}>Linode Details: </h2> - <LinodeEntityDetail - variant="details" - numVolumes={2} - id={0} - linode={linodeFactory.build()} - username="linode-user" - openTagDrawer={() => null} - openDialog={() => null} - openPowerActionDialog={() => null} - backups={linodeBackupsFactory.build()} - linodeConfigs={linodeConfigFactory.buildList(2)} - /> - </div> - </Provider> + <div> + <h2 style={{ margin: 0 }}>Linode Details: </h2> + <LinodeEntityDetail + variant="details" + numVolumes={2} + id={0} + linode={linodeFactory.build()} + username="linode-user" + openTagDrawer={() => null} + openDialog={() => null} + openPowerActionDialog={() => null} + backups={linodeBackupsFactory.build()} + linodeConfigs={linodeConfigFactory.buildList(2)} + /> + </div> </Story> </Canvas> diff --git a/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx b/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx index 1f0d877cb4e..077b74d6145 100644 --- a/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx +++ b/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx @@ -79,7 +79,6 @@ export const Default: Story = { linodeRegion="us-east" linodeStatus="running" linodeType={{ - subHeadings: ['2GB RAM', '1 vCPU', '80GB Storage', 'Linode 4GB'], addons: { backups: { price: { @@ -97,17 +96,18 @@ export const Default: Story = { }, class: 'standard', successor: 'g6-standard-1', - isDeprecated: false, - heading: 'Linode 2GB', disk: 81920, id: 'g6-standard-2', - formattedLabel: 'Linode 2 GB', label: 'Linode 2GB', memory: 2048, vcpus: 1, }} - openDialog={action('openDialog')} - openPowerActionDialog={action('openPowerActionDialog')} + onOpenPowerDialog={action('onOpenPowerDialog')} + onOpenDeleteDialog={action('onOpenDeleteDialog')} + onOpenResizeDialog={action('onOpenResizeDialog')} + onOpenRebuildDialog={action('onOpenRebuildDialog')} + onOpenRescueDialog={action('onOpenRescueDialog')} + onOpenMigrateDialog={action('onOpenMigrateDialog')} /> </Box> </EntityHeader> diff --git a/packages/manager/src/components/EntityTable/APIPaginatedTable.tsx b/packages/manager/src/components/EntityTable/APIPaginatedTable.tsx deleted file mode 100644 index 70e4c2aca87..00000000000 --- a/packages/manager/src/components/EntityTable/APIPaginatedTable.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import * as React from 'react'; -import { compose } from 'recompose'; -import Paper from 'src/components/core/Paper'; -import TableBody from 'src/components/core/TableBody'; -import Pagey, { PaginationProps } from 'src/components/Pagey'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableContentWrapper from 'src/components/TableContentWrapper'; -import { HiddenProps } from '../core/Hidden'; -import EntityTableHeader from './EntityTableHeader'; -import { Entity, ListProps, PageyIntegrationProps } from './types'; - -interface Props { - isLargeAccount?: boolean; -} - -export type CombinedProps = ListProps & - PaginationProps<Entity> & - PageyIntegrationProps & - Props; - -export const APIPaginatedTable: React.FC<CombinedProps> = (props) => { - const { - count, - data, - error, - loading, - page, - pageSize, - request, - handleOrderChange, - handlePageChange, - handlePageSizeChange, - entity, - handlers, - headers, - initialOrder, - RowComponent, - emptyMessage, - toggleGroupByTag, - isGroupedByTag, - isLargeAccount, - } = props; - - const _data = data ?? []; - - const normalizedData = props.normalizeData - ? props.normalizeData(_data) - : _data; - - React.useEffect(() => { - if (initialOrder) { - handleOrderChange(initialOrder.orderBy, initialOrder.order); - } else { - request(); - } - }, [request, handleOrderChange, initialOrder]); - - const responsiveHeaders = (): Record<number, HiddenProps> => { - const responsive = {}; - for (let i = 0; i < headers.length; i++) { - if (headers[i].hideOnMobile) { - responsive[i] = { xsDown: true }; - } - if (headers[i].hideOnTablet) { - responsive[i] = { smDown: true }; - } - } - return responsive; - }; - - return ( - <> - <Paper style={{ padding: 0 }}> - <Table aria-label={`List of ${entity}`}> - <EntityTableHeader - headers={headers} - order={props.order} - orderBy={props.orderBy ?? 'asc'} - handleOrderChange={props.handleOrderChange} - toggleGroupByTag={toggleGroupByTag} - isGroupedByTag={isGroupedByTag} - isLargeAccount={isLargeAccount} - /> - <TableBody> - <TableContentWrapper - loadingProps={{ - // Add 1 because the headers array does not inclue the action menu - columns: headers.length + 1, - responsive: responsiveHeaders(), - }} - length={count} - loading={loading} - error={error} - lastUpdated={100} - emptyMessage={emptyMessage} - > - {normalizedData.map((thisEntity) => ( - <RowComponent - key={thisEntity.id} - {...thisEntity} - {...handlers} - /> - ))} - </TableContentWrapper> - </TableBody> - </Table> - </Paper> - <PaginationFooter - count={count} - page={page} - pageSize={pageSize} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - eventCategory={`${entity} landing table`} - /> - </> - ); -}; - -const enhanced = compose<CombinedProps, any>( - Pagey((ownProps, params, filters) => ownProps.request(params, filters), { - cb: (ownProps, response) => { - if (ownProps.persistData) { - ownProps.persistData(response.data); - } - }, - }) -); - -export default enhanced(APIPaginatedTable); diff --git a/packages/manager/src/components/EntityTable/EntityTable.tsx b/packages/manager/src/components/EntityTable/EntityTable.tsx deleted file mode 100644 index e85a349fb24..00000000000 --- a/packages/manager/src/components/EntityTable/EntityTable.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from 'react'; -import { makeStyles } from '@mui/styles'; -import { OrderByProps } from 'src/components/OrderBy'; -import APIPaginatedTable from './APIPaginatedTable'; -import GroupedEntitiesByTag from './GroupedEntitiesByTag'; -import ListEntities from './ListEntities'; -import type { BaseProps, Entity, PageyIntegrationProps } from './types'; - -const useStyles = makeStyles(() => ({ - root: { - '& td': { - borderTop: 0, - paddingLeft: '15px', - paddingRight: '15px', - }, - }, -})); - -export interface EntityTableRow<T> extends BaseProps { - Component: React.ComponentType<any>; - data: T[]; - request?: () => Promise<any>; -} - -interface Props { - entity: string; - headers: HeaderCell[]; - row: EntityTableRow<any>; - emptyMessage?: string; - initialOrder?: { - order: OrderByProps<Entity>['order']; - orderBy: OrderByProps<Entity>['orderBy']; - }; - toggleGroupByTag?: () => boolean; - isGroupedByTag?: boolean; - isLargeAccount?: boolean; -} - -export type CombinedProps = Props & PageyIntegrationProps; - -export const LandingTable: React.FC<CombinedProps> = (props) => { - const { - entity, - headers, - row, - emptyMessage, - initialOrder, - toggleGroupByTag, - isGroupedByTag, - isLargeAccount, - } = props; - const classes = useStyles(); - const tableProps = { - data: row.data, - request: row.request, - error: row.error, - loading: row.loading, - lastUpdated: row.lastUpdated, - entity, - headers, - RowComponent: row.Component, - handlers: row.handlers, - emptyMessage, - initialOrder, - toggleGroupByTag, - isGroupedByTag, - isLargeAccount, - }; - - if (row.request) { - return ( - <APIPaginatedTable - {...tableProps} - persistData={props.persistData} - normalizeData={props.normalizeData} - data={undefined} - /> - ); - } - - if (isGroupedByTag) { - return ( - <div className={classes.root}> - <GroupedEntitiesByTag {...tableProps} /> - </div> - ); - } - return ( - <div className={classes.root}> - <ListEntities {...tableProps} /> - </div> - ); -}; - -export interface HeaderCell { - sortable: boolean; - label: string; - dataColumn: string; - widthPercent: number; - visuallyHidden?: boolean; - hideOnMobile?: boolean; - hideOnTablet?: boolean; -} - -export default LandingTable; diff --git a/packages/manager/src/components/EntityTable/EntityTableHeader.tsx b/packages/manager/src/components/EntityTable/EntityTableHeader.tsx deleted file mode 100644 index 44b2673f4ee..00000000000 --- a/packages/manager/src/components/EntityTable/EntityTableHeader.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import * as React from 'react'; -import GroupByTag from 'src/assets/icons/group-by-tag.svg'; -import Hidden from 'src/components/core/Hidden'; -import IconButton from 'src/components/core/IconButton'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import TableHead from 'src/components/core/TableHead'; -import Tooltip from 'src/components/core/Tooltip'; -import { OrderByProps } from 'src/components/OrderBy'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; -import { Entity, HeaderCell } from './types'; - -const useStyles = makeStyles((theme: Theme) => ({ - hiddenHeaderCell: theme.visually.hidden, - groupByTagCell: { - backgroundColor: theme.bg.tableHeader, - paddingRight: `0px !important`, - textAlign: 'right', - }, -})); - -interface Props extends Omit<OrderByProps<Entity>, 'data'> { - headers: HeaderCell[]; - toggleGroupByTag?: () => boolean; - isGroupedByTag?: boolean; - isLargeAccount?: boolean; -} - -interface SortCellProps extends Omit<Props, 'headers'> { - thisCell: HeaderCell; -} - -interface NormalCellProps { - thisCell: HeaderCell; -} - -export const EntityTableHeader: React.FC<Props> = (props) => { - const { - headers, - handleOrderChange, - order, - orderBy, - toggleGroupByTag, - isGroupedByTag, - isLargeAccount, - } = props; - const classes = useStyles(); - - const SortCell: React.FC<SortCellProps> = (props) => { - const { orderBy, order, thisCell, handleOrderChange } = props; - return ( - <TableSortCell - active={orderBy === thisCell.dataColumn} - label={thisCell.dataColumn} - direction={order} - handleClick={handleOrderChange} - style={{ width: `${thisCell.widthPercent}%` }} - data-testid={`${thisCell.label}-header-cell`} - > - {thisCell.label} - </TableSortCell> - ); - }; - - const _NormalCell: React.FC<NormalCellProps> = (props) => { - const { thisCell } = props; - return ( - <TableCell - data-testid={`${thisCell.label}-header-cell`} - style={{ width: `${thisCell.widthPercent}%` }} - > - <span - className={ - thisCell.visuallyHidden ? classes.hiddenHeaderCell : undefined - } - > - {thisCell.label} - </span> - </TableCell> - ); - }; - - return ( - <TableHead> - <TableRow> - {headers.map((thisCell) => - thisCell.sortable ? ( - thisCell.hideOnTablet ? ( - <Hidden mdDown key={thisCell.dataColumn}> - <SortCell - thisCell={thisCell} - order={order} - orderBy={orderBy} - handleOrderChange={handleOrderChange} - /> - </Hidden> - ) : thisCell.hideOnMobile ? ( - <Hidden smDown key={thisCell.dataColumn}> - <SortCell - thisCell={thisCell} - order={order} - orderBy={orderBy} - handleOrderChange={handleOrderChange} - /> - </Hidden> - ) : ( - <SortCell - thisCell={thisCell} - key={thisCell.dataColumn} - order={order} - orderBy={orderBy} - handleOrderChange={handleOrderChange} - /> - ) - ) : ( - [ - thisCell.hideOnTablet ? ( - <Hidden mdDown key={thisCell.dataColumn}> - <_NormalCell thisCell={thisCell} /> - </Hidden> - ) : thisCell.hideOnMobile ? ( - <Hidden smDown key={thisCell.dataColumn}> - <_NormalCell thisCell={thisCell} /> - </Hidden> - ) : ( - <_NormalCell thisCell={thisCell} key={thisCell.dataColumn} /> - ), - ] - ) - )} - {toggleGroupByTag && typeof isGroupedByTag !== 'undefined' ? ( - <TableCell className={classes.groupByTagCell}> - <GroupByTagToggle - isLargeAccount={isLargeAccount} - toggleGroupByTag={toggleGroupByTag} - isGroupedByTag={isGroupedByTag} - /> - </TableCell> - ) : null} - </TableRow> - </TableHead> - ); -}; - -export default React.memo(EntityTableHeader); - -// ============================================================================= -// <GroupByTagToggle /> -// ============================================================================= -interface GroupByTagToggleProps { - toggleGroupByTag: () => boolean; - isGroupedByTag: boolean; - isLargeAccount?: boolean; -} - -const useGroupByTagToggleStyles = makeStyles(() => ({ - toggleButton: { - color: '#d2d3d4', - padding: '0 10px', - '&:focus': { - outline: '1px dotted #999', - }, - '&.Mui-disabled': { - display: 'none', - }, - }, -})); - -export const GroupByTagToggle: React.FC<GroupByTagToggleProps> = React.memo( - (props) => { - const classes = useGroupByTagToggleStyles(); - - const { toggleGroupByTag, isGroupedByTag, isLargeAccount } = props; - - return ( - <> - <div id="groupByDescription" className="visually-hidden"> - {isGroupedByTag - ? 'group by tag is currently enabled' - : 'group by tag is currently disabled'} - </div> - <Tooltip - placement="top-end" - title={`${isGroupedByTag ? 'Ungroup' : 'Group'} by tag`} - > - <IconButton - aria-label={`Toggle group by tag`} - aria-describedby={'groupByDescription'} - onClick={toggleGroupByTag} - disableRipple - className={classes.toggleButton} - // Group by Tag is not available if you have a large account. - // See https://github.com/linode/manager/pull/6653 for more details - disabled={isLargeAccount} - size="large" - > - <GroupByTag /> - </IconButton> - </Tooltip> - </> - ); - } -); diff --git a/packages/manager/src/components/EntityTable/GroupedEntitiesByTag.tsx b/packages/manager/src/components/EntityTable/GroupedEntitiesByTag.tsx deleted file mode 100644 index 7a43ffe5421..00000000000 --- a/packages/manager/src/components/EntityTable/GroupedEntitiesByTag.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { compose } from 'ramda'; -import * as React from 'react'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import Typography from 'src/components/core/Typography'; -import OrderBy from 'src/components/OrderBy'; -import Paginate from 'src/components/Paginate'; -import PaginationFooter, { - MIN_PAGE_SIZE, -} from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; -import { groupByTags, sortGroups } from 'src/utilities/groupByTags'; -import EntityTableHeader from './EntityTableHeader'; -import { ListProps } from './types'; - -const useStyles = makeStyles((theme: Theme) => ({ - tagHeaderRow: { - backgroundColor: theme.bg.main, - height: 'auto', - '& td': { - // This is maintaining the spacing between groups because of how tables handle margin/padding. Adjust with care! - padding: `${theme.spacing(2.5)} 0 ${theme.spacing(1.25)}`, - borderBottom: 'none', - borderTop: 'none', - }, - }, - groupContainer: { - [theme.breakpoints.up('md')]: { - '& $tagHeaderRow > td': { - borderTop: 'none', - padding: `${theme.spacing(1.25)} 0`, - }, - }, - }, - tagHeader: { - marginBottom: 2, - }, - paginationCell: { - paddingTop: 2, - '& div:first-of-type': { - marginTop: 0, - }, - }, -})); - -export type CombinedProps = ListProps; - -export const GroupedEntitiesByTag: React.FC<CombinedProps> = (props) => { - const { - data, - entity, - handlers, - headers, - initialOrder, - RowComponent, - toggleGroupByTag, - isGroupedByTag, - } = props; - const classes = useStyles(); - const { infinitePageSize, setInfinitePageSize } = useInfinitePageSize(); - - return ( - <OrderBy - data={data} - orderBy={initialOrder?.orderBy} - order={initialOrder?.order} - > - {({ data: orderedData, handleOrderChange, order, orderBy }) => { - const groupedEntities = compose(sortGroups, groupByTags)(orderedData); - return ( - <Table aria-label={`List of ${entity}`}> - <EntityTableHeader - headers={headers} - handleOrderChange={handleOrderChange} - order={order} - orderBy={orderBy} - toggleGroupByTag={toggleGroupByTag} - isGroupedByTag={isGroupedByTag} - /> - {groupedEntities.map(([tag, domains]: [string, any[]]) => { - return ( - <React.Fragment key={tag}> - <Paginate - data={domains} - pageSize={infinitePageSize} - pageSizeSetter={setInfinitePageSize} - > - {({ - data: paginatedData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - count, - }) => { - return ( - <TableBody - className={classes.groupContainer} - data-qa-tag-header={tag} - > - <TableRow className={classes.tagHeaderRow}> - <TableCell colSpan={7}> - <Typography - variant="h2" - component="h3" - className={classes.tagHeader} - > - {tag} - </Typography> - </TableCell> - </TableRow> - {paginatedData.map((thisEntity) => ( - <RowComponent - key={thisEntity.id} - {...thisEntity} - {...handlers} - /> - ))} - {count > MIN_PAGE_SIZE && ( - <TableRow> - <TableCell - colSpan={7} - className={classes.paginationCell} - > - <PaginationFooter - count={count} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - pageSize={pageSize} - page={page} - eventCategory={'Entity table'} - // Disabling showAll as it is impacting page performance. - showAll={false} - /> - </TableCell> - </TableRow> - )} - </TableBody> - ); - }} - </Paginate> - </React.Fragment> - ); - })} - </Table> - ); - }} - </OrderBy> - ); -}; - -export default GroupedEntitiesByTag; diff --git a/packages/manager/src/components/EntityTable/ListEntities.tsx b/packages/manager/src/components/EntityTable/ListEntities.tsx deleted file mode 100644 index 2734efa9f63..00000000000 --- a/packages/manager/src/components/EntityTable/ListEntities.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from 'react'; -import TableBody from 'src/components/core/TableBody'; -import OrderBy from 'src/components/OrderBy'; -import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableContentWrapper from 'src/components/TableContentWrapper'; -import EntityTableHeader from './EntityTableHeader'; -import { ListProps } from './types'; - -export type CombinedProps = ListProps; - -export const ListEntities: React.FC<CombinedProps> = (props) => { - const { - data, - entity, - error, - handlers, - headers, - initialOrder, - loading, - lastUpdated, - RowComponent, - toggleGroupByTag, - isGroupedByTag, - emptyMessage, - } = props; - - return ( - <OrderBy - data={data} - orderBy={initialOrder?.orderBy} - order={initialOrder?.order} - preferenceKey={entity} - > - {({ data: orderedData, handleOrderChange, order, orderBy }) => ( - <Paginate data={orderedData}> - {({ - data: paginatedAndOrderedData, - count, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => ( - <> - <Table aria-label={`List of ${entity}`}> - <EntityTableHeader - headers={headers} - handleOrderChange={handleOrderChange} - order={order} - orderBy={orderBy} - toggleGroupByTag={toggleGroupByTag} - isGroupedByTag={isGroupedByTag} - /> - - <TableBody> - <TableContentWrapper - loadingProps={{ - // @TODO this is not done - columns: headers.length, - }} - emptyMessage={emptyMessage} - length={paginatedAndOrderedData.length} - loading={loading} - error={error} - lastUpdated={lastUpdated} - > - {paginatedAndOrderedData.map((thisEntity) => ( - <RowComponent - key={thisEntity.id} - {...thisEntity} - {...handlers} - /> - ))} - </TableContentWrapper> - </TableBody> - </Table> - <PaginationFooter - count={count} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - page={page} - pageSize={pageSize} - eventCategory={`${entity} table view`} - /> - </> - )} - </Paginate> - )} - </OrderBy> - ); -}; - -export default ListEntities; diff --git a/packages/manager/src/components/EntityTable/index.tsx b/packages/manager/src/components/EntityTable/index.tsx deleted file mode 100644 index 9e4b7c26e3a..00000000000 --- a/packages/manager/src/components/EntityTable/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import EntityTable, { - EntityTableRow as _EntityTableRow, - HeaderCell as _HeaderCell, -} from './EntityTable'; -export default EntityTable; -export type EntityTableRow<T> = _EntityTableRow<T>; -export type HeaderCell = _HeaderCell; diff --git a/packages/manager/src/components/EntityTable/types.ts b/packages/manager/src/components/EntityTable/types.ts deleted file mode 100644 index cf8e9d4e42a..00000000000 --- a/packages/manager/src/components/EntityTable/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Domain } from '@linode/api-v4/lib/domains/types'; -import { Image } from '@linode/api-v4/lib/images/types'; -import { Firewall } from '@linode/api-v4/lib/firewalls/types'; -import { Linode } from '@linode/api-v4/lib/linodes/types'; -import { Volume } from '@linode/api-v4/lib/volumes/types'; -import { APIError } from '@linode/api-v4/lib/types'; -import { OrderByProps } from 'src/components/OrderBy'; -// eslint-disable-next-line -export type Handlers = any; -export type Entity = Linode | Domain | Firewall | Image | Volume; // @todo add more here - -export interface HeaderCell { - sortable: boolean; - label: string; - dataColumn: string; - widthPercent: number; - visuallyHidden?: boolean; - hideOnMobile?: boolean; - hideOnTablet?: boolean; -} -export interface BaseProps { - error?: APIError[]; - loading: boolean; - lastUpdated: number; - handlers?: Handlers; -} -export interface ListProps extends BaseProps { - entity: string; - data: Entity[]; - RowComponent: React.ComponentType; - headers: HeaderCell[]; - initialOrder?: { - order: OrderByProps<Entity>['order']; - orderBy: OrderByProps<Entity>['orderBy']; - }; - toggleGroupByTag?: () => boolean; - isGroupedByTag?: boolean; - emptyMessage?: string; -} - -export interface EntityTableRow<T> extends BaseProps { - Component: React.ComponentType<any>; - data: T[]; - request?: () => Promise<T[]>; -} - -export interface PageyIntegrationProps { - persistData?: (data: Entity[]) => void; - normalizeData?: (data: Entity[]) => Entity[]; -} diff --git a/packages/manager/src/components/IconButton/IconButton.tsx b/packages/manager/src/components/IconButton/IconButton.tsx index bf69f3fa4af..1035d7ac38d 100644 --- a/packages/manager/src/components/IconButton/IconButton.tsx +++ b/packages/manager/src/components/IconButton/IconButton.tsx @@ -17,6 +17,10 @@ interface Props extends IconButtonProps { const styles = (theme: Theme) => createStyles({ root: { + color: theme.palette.primary.light, + '&:hover': { + color: theme.palette.primary.light, + }, transition: theme.transitions.create(['opacity']), }, destructive: { diff --git a/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx b/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx index edcc64176cd..b63d9a363cc 100644 --- a/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx +++ b/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx @@ -3,7 +3,7 @@ import { compose } from 'recompose'; import Paper from 'src/components/core/Paper'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import { TagsInput, TagsInputProps } from 'src/components/TagsInput/TagsInput'; import TextField, { Props as TextFieldProps } from 'src/components/TextField'; diff --git a/packages/manager/src/components/LineGraph/AccessibleGraphData.test.tsx b/packages/manager/src/components/LineGraph/AccessibleGraphData.test.tsx new file mode 100644 index 00000000000..dafc1d13215 --- /dev/null +++ b/packages/manager/src/components/LineGraph/AccessibleGraphData.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import AccessibleGraphData from './AccessibleGraphData'; +import type { GraphTabledDataProps } from './AccessibleGraphData'; + +const chartInstance = { + config: { + data: { + datasets: [ + { + label: 'Dataset 1', + data: [ + { t: 1631026800000, y: 10 }, + { t: 1631030400000, y: 20 }, + { t: 1631034000000, y: 30 }, + ], + }, + { + label: 'Dataset 2', + data: [ + { t: 1631026800000, y: 5 }, + { t: 1631030400000, y: 15 }, + { t: 1631034000000, y: 25 }, + ], + }, + ], + }, + }, +}; + +describe('AccessibleGraphData', () => { + it('renders a table with correct data', () => { + const { getAllByRole } = render( + <AccessibleGraphData + ariaLabel="data filter" + chartInstance={chartInstance as GraphTabledDataProps['chartInstance']} + hiddenDatasets={[]} + accessibleUnit="%" + /> + ); + + // Check that the component renders + const table = getAllByRole('table')[0]; + expect(table).toBeInTheDocument(); + + // Check that the correct number of tables are rendered + const tables = getAllByRole('table'); + expect(tables.length).toEqual(2); + + // Check that the table has the correct summary attribute + expect(table).toHaveAttribute( + 'summary', + 'This table contains the data for the data filter (Dataset 1)' + ); + + // Check that the table header is correct + const tableHeader = table.querySelector('thead > tr'); + expect(tableHeader).toHaveTextContent('Time'); + expect(tableHeader).toHaveTextContent('Dataset 1'); + + // Check that the table data is correct in the body + const tableBodyRows = table.querySelectorAll('tbody > tr'); + expect(tableBodyRows.length).toEqual(3); + + tableBodyRows.forEach((row, idx) => { + const value: any = chartInstance.config.data.datasets[0].data[ + idx + ].y.toFixed(2); + + expect(row.querySelector('td:nth-child(2)')).toHaveTextContent( + value + '%' + ); + }); + }); + + it('hides the correct datasets', () => { + const { getByRole, queryByText } = render( + <AccessibleGraphData + chartInstance={chartInstance as GraphTabledDataProps['chartInstance']} + hiddenDatasets={[0]} + accessibleUnit="%" + /> + ); + + // Check that the first table is hidden + expect(getByRole('table', { hidden: true })).toBeInTheDocument(); + expect(getByRole('table', { hidden: false })).toBeInTheDocument(); + + // Check that the hidden table is not visible + expect(queryByText('Dataset 1')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager/src/components/LineGraph/AccessibleGraphData.tsx b/packages/manager/src/components/LineGraph/AccessibleGraphData.tsx new file mode 100644 index 00000000000..aab5957d6c3 --- /dev/null +++ b/packages/manager/src/components/LineGraph/AccessibleGraphData.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import type { ChartData, ChartPoint } from 'chart.js'; +import { DateTime } from 'luxon'; +import { visuallyHidden } from '@mui/utils'; +import Box from 'src/components/core/Box'; + +export interface GraphTabledDataProps { + ariaLabel?: string; + accessibleUnit: string; + chartInstance: React.MutableRefObject<Chart | null>['current']; + hiddenDatasets: number[]; +} + +/** + * This component is used to provide an accessible representation of the data + * It does not care about styles, it only cares about presenting the data in bare HTML tables, + * visually hidden from the user, yet available to screen readers. + */ +const AccessibleGraphData = (props: GraphTabledDataProps) => { + const { accessibleUnit, ariaLabel, chartInstance, hiddenDatasets } = props; + + // This is necessary because the chartInstance is not immediately available + if (!chartInstance?.config?.data?.datasets) { + return null; + } + + const { datasets }: ChartData = chartInstance.config.data; + + const tables = datasets.map((dataset, tableID) => { + const { label, data } = dataset; + const hidden = hiddenDatasets.includes(tableID); + + const TableHeader = ( + <tr> + <th>Time</th> + <th>{label}</th> + </tr> + ); + + const TableBody = + data && + data.map((entry, idx) => { + const { t: timestamp, y: value } = entry as ChartPoint; + + return ( + <tr key={`accessible-graph-data-body-row-${idx}`}> + <td> + {timestamp + ? DateTime.fromMillis(Number(timestamp)).toLocaleString( + DateTime.DATETIME_SHORT + ) + : 'timestamp unavailable'} + </td> + <td> + {value !== undefined + ? Number(value).toFixed(2) + : 'value unavailable'} + {value !== undefined && accessibleUnit} + </td> + </tr> + ); + }); + + return ( + !hidden && ( + <table + key={`accessible-graph-data-table-${tableID}`} + summary={`This table contains the data for the ${ + ariaLabel && label ? ariaLabel + ` (${label})` : 'graph below' + }`} + > + <thead>{TableHeader}</thead> + <tbody>{TableBody}</tbody> + </table> + ) + ); + }); + + return <Box sx={visuallyHidden}>{tables}</Box>; +}; + +export default AccessibleGraphData; diff --git a/packages/manager/src/components/LineGraph/LineGraph.stories.mdx b/packages/manager/src/components/LineGraph/LineGraph.stories.mdx index e60c68c55be..4ecfae89b66 100644 --- a/packages/manager/src/components/LineGraph/LineGraph.stories.mdx +++ b/packages/manager/src/components/LineGraph/LineGraph.stories.mdx @@ -66,6 +66,10 @@ export const Template = (args) => <LineGraph {...args} />; nativeLegend: false, timezone: 'America/New_York', unit: '%', + accessibleDataTable: { + display: true, + unit: "%" + }, chartHeight: undefined, suggestedMax: undefined, rowHeaders: undefined, diff --git a/packages/manager/src/components/LineGraph/LineGraph.tsx b/packages/manager/src/components/LineGraph/LineGraph.tsx index d7e99ad8e12..7469683f392 100644 --- a/packages/manager/src/components/LineGraph/LineGraph.tsx +++ b/packages/manager/src/components/LineGraph/LineGraph.tsx @@ -13,12 +13,13 @@ import Button from 'src/components/Button'; import { makeStyles, useTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import AccessibleGraphData from './AccessibleGraphData'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { setUpCharts } from 'src/utilities/charts'; import roundTo from 'src/utilities/roundTo'; import { Metrics } from 'src/utilities/statMetrics'; @@ -46,6 +47,12 @@ export interface Props { rowHeaders?: Array<string>; legendRows?: Array<any>; unit?: string; + /** + * `accessibleDataTable` is responsible to both rendering the accessible graph data table and an associated unit. + */ + accessibleDataTable?: { + unit: string; + }; nativeLegend?: boolean; // Display chart.js native legend formatData?: (value: number) => number | null; formatTooltip?: (value: number) => string; @@ -105,6 +112,7 @@ const LineGraph: React.FC<CombinedProps> = (props: CombinedProps) => { nativeLegend, tabIndex, unit, + accessibleDataTable, } = props; const finalRowHeaders = rowHeaders ? rowHeaders : ['Max', 'Avg', 'Last']; @@ -284,15 +292,12 @@ const LineGraph: React.FC<CombinedProps> = (props: CombinedProps) => { return ( // Allow `tabIndex` on `<div>` because it represents an interactive element. // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex - <div - className={classes.wrapper} - tabIndex={tabIndex ?? 0} - role="graphics-document" - aria-label={ariaLabel || 'Stats and metrics'} - > - <div className={classes.canvasContainer}> - <canvas height={chartHeight || 300} ref={inputEl} /> - </div> + + // Note on markup and styling: the legend is rendered first for accessibility reasons. + // Screen readers read from top to bottom, so the legend should be read before the data tables, esp considering their size + // and the fact that the legend can filter them. + // Meanwhile the CSS uses column-reverse to visually retain the original order + <div className={classes.wrapper} tabIndex={tabIndex ?? 0}> {legendRendered && legendRows && ( <div className={classes.container}> <Table @@ -390,6 +395,22 @@ const LineGraph: React.FC<CombinedProps> = (props: CombinedProps) => { </Table> </div> )} + <div className={classes.canvasContainer}> + <canvas + height={chartHeight || 300} + ref={inputEl} + role="img" + aria-label={ariaLabel || 'Stats and metrics'} + /> + </div> + {accessibleDataTable?.unit && ( + <AccessibleGraphData + chartInstance={chartInstance.current} + ariaLabel={ariaLabel} + hiddenDatasets={hiddenDatasets} + accessibleUnit={accessibleDataTable.unit} + /> + )} </div> ); }; diff --git a/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx b/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx index e14d908e953..ce4a0b554d0 100644 --- a/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx +++ b/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx @@ -44,7 +44,7 @@ describe('CPUMetrics', () => { ); it('renders a table', () => { - expect(wrapper.find('WrappedTable')).toHaveLength(1); + expect(wrapper.find('Table')).toHaveLength(1); }); it('renders Max, Avg, and Last table headers', () => { diff --git a/packages/manager/src/components/LineGraph/MetricsDisplay.tsx b/packages/manager/src/components/LineGraph/MetricsDisplay.tsx index 20a0334f04b..6198332e6f3 100644 --- a/packages/manager/src/components/LineGraph/MetricsDisplay.tsx +++ b/packages/manager/src/components/LineGraph/MetricsDisplay.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { Metrics } from 'src/utilities/statMetrics'; import styled, { StyleProps } from './MetricDisplay.styles'; diff --git a/packages/manager/src/components/LineGraph/NewMetricDisplay.styles.ts b/packages/manager/src/components/LineGraph/NewMetricDisplay.styles.ts index be4edb6d4c3..6aa0ead6e64 100644 --- a/packages/manager/src/components/LineGraph/NewMetricDisplay.styles.ts +++ b/packages/manager/src/components/LineGraph/NewMetricDisplay.styles.ts @@ -24,10 +24,11 @@ const newMetricDisplayStyles = (theme: Theme) => createStyles({ wrapper: { display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', + flexDirection: 'column-reverse', + flex: 1, + width: '100%', '& > div': { - flexBasis: '100%', + width: '100%', }, }, container: { diff --git a/packages/manager/src/components/LinodeMultiSelect/LinodeMultiSelect.tsx b/packages/manager/src/components/LinodeMultiSelect/LinodeMultiSelect.tsx index c265b5b1a7d..d3d0858065b 100644 --- a/packages/manager/src/components/LinodeMultiSelect/LinodeMultiSelect.tsx +++ b/packages/manager/src/components/LinodeMultiSelect/LinodeMultiSelect.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useInfiniteLinodesQuery } from 'src/queries/linodes'; +import { useInfiniteLinodesQuery } from 'src/queries/linodes/linodes'; import Autocomplete from '@mui/material/Autocomplete'; import Popper from '@mui/material/Popper'; import TextField from 'src/components/TextField'; diff --git a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx index a34eda9b8af..57b027ad17f 100644 --- a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx +++ b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx @@ -5,10 +5,11 @@ import { compose } from 'recompose'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useProfile } from 'src/queries/profile'; import { formatDate } from 'src/utilities/formatDate'; import isPast from 'src/utilities/isPast'; +import { useAllAccountMaintenanceQuery } from 'src/queries/accountMaintenance'; type ClassNames = 'root' | 'dateTime'; @@ -38,6 +39,11 @@ interface Props { type CombinedProps = Props & WithStyles<ClassNames>; const MaintenanceBanner: React.FC<CombinedProps> = (props) => { + const { data: accountMaintenanceData } = useAllAccountMaintenanceQuery( + {}, + { status: { '+or': ['pending, started'] } } + ); + const { type, maintenanceEnd, maintenanceStart } = props; const { data: profile, @@ -78,6 +84,10 @@ const MaintenanceBanner: React.FC<CombinedProps> = (props) => { return null; } + if (!accountMaintenanceData || accountMaintenanceData?.length === 0) { + return null; + } + return ( <Notice warning important className={props.classes.root}> <Typography> diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 8aeed386876..dbc66c5f7e7 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -10,7 +10,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { ExtendedIP } from 'src/utilities/ipUtils'; diff --git a/packages/manager/src/components/Notice/Notice.stories.mdx b/packages/manager/src/components/Notice/Notice.stories.mdx index cd0d622fd86..c9f7bda1356 100644 --- a/packages/manager/src/components/Notice/Notice.stories.mdx +++ b/packages/manager/src/components/Notice/Notice.stories.mdx @@ -1,6 +1,5 @@ import { Canvas, Meta, Story } from '@storybook/addon-docs'; import { Provider } from 'react-redux'; -import store from 'src/store'; import Notice from './Notice'; <Meta title="Components/Notifications/Notices" component={Notice} /> diff --git a/packages/manager/src/components/Notice/Notice.tsx b/packages/manager/src/components/Notice/Notice.tsx index cb1da90c12b..2d29ec0d307 100644 --- a/packages/manager/src/components/Notice/Notice.tsx +++ b/packages/manager/src/components/Notice/Notice.tsx @@ -4,7 +4,6 @@ import Check from 'src/assets/icons/check.svg'; import Flag from 'src/assets/icons/flag.svg'; import Warning from 'src/assets/icons/warning.svg'; import { makeStyles } from 'tss-react/mui'; -import { withTheme, WithTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography, { TypographyProps } from 'src/components/core/Typography'; import Grid, { Grid2Props } from '@mui/material/Unstable_Grid2'; @@ -57,9 +56,6 @@ export const useStyles = makeStyles< }, inner: { width: '100%', - '& p': { - fontSize: '1rem', - }, }, breakWords: { [`& .${classes.noticeText}`]: { @@ -112,7 +108,7 @@ export const useStyles = makeStyles< }, })); -export interface Props extends Grid2Props { +export interface NoticeProps extends Grid2Props { text?: string; error?: boolean; errorGroup?: string; @@ -133,9 +129,7 @@ export interface Props extends Grid2Props { dismissibleButton?: JSX.Element; } -type CombinedProps = Props & WithTheme; - -const Notice: React.FC<CombinedProps> = (props) => { +export const Notice = (props: NoticeProps) => { const { className, important, @@ -243,5 +237,3 @@ const Notice: React.FC<CombinedProps> = (props) => { </Grid> ); }; - -export default withTheme(Notice); diff --git a/packages/manager/src/components/Notice/index.tsx b/packages/manager/src/components/Notice/index.tsx deleted file mode 100644 index 0db2a3d734d..00000000000 --- a/packages/manager/src/components/Notice/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import Notice, { Props as _NoticeProps } from './Notice'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface NoticeProps extends _NoticeProps {} -export default Notice; diff --git a/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx b/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx index 6b71b95b986..3b5b332f183 100644 --- a/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx +++ b/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx @@ -59,7 +59,7 @@ const baseOptions = [ { label: 'Show 100', value: PAGE_SIZES[3] }, ]; -const PaginationFooter = (props: Props) => { +export const PaginationFooter = (props: Props) => { const classes = useStyles(); const { count, @@ -131,8 +131,6 @@ const PaginationFooter = (props: Props) => { ); }; -export default PaginationFooter; - /** * Return the minimum page size needed to display a given number of items (`value`). * Example: getMinimumPageSizeForNumberOfItems(30, [25, 50, 75]) === 50 diff --git a/packages/manager/src/components/PaginationFooter/index.ts b/packages/manager/src/components/PaginationFooter/index.ts deleted file mode 100644 index 95cf7cc6fde..00000000000 --- a/packages/manager/src/components/PaginationFooter/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import PaginationFooter, { - PaginationProps as _PaginationProps, -} from './PaginationFooter'; -/* tslint:disable */ -export interface PaginationProps extends _PaginationProps {} -export default PaginationFooter; -export { MIN_PAGE_SIZE } from './PaginationFooter'; diff --git a/packages/manager/src/components/Placeholder/Placeholder.tsx b/packages/manager/src/components/Placeholder/Placeholder.tsx index 4e3d8ceddf4..391dbfb64b1 100644 --- a/packages/manager/src/components/Placeholder/Placeholder.tsx +++ b/packages/manager/src/components/Placeholder/Placeholder.tsx @@ -66,6 +66,12 @@ const useStyles = makeStyles((theme: Theme) => ({ padding: `${theme.spacing(10)} 0`, }, }, + rootWithShowTransferDisplay: { + padding: `${theme.spacing(4)} 0`, + [theme.breakpoints.up('md')]: { + padding: `${theme.spacing(10)} 0 ${theme.spacing(4)}`, + }, + }, copy: { textAlign: 'center', gridArea: 'copy', @@ -155,6 +161,7 @@ export interface Props { title: string; buttonProps?: ExtendedButtonProps[]; className?: string; + descriptionMaxWidth?: number; isEntity?: boolean; renderAsSecondary?: boolean; subtitle?: string; @@ -168,6 +175,7 @@ const Placeholder: React.FC<Props> = (props) => { title, icon: Icon, buttonProps, + descriptionMaxWidth, renderAsSecondary, subtitle, linksSection, @@ -186,6 +194,7 @@ const Placeholder: React.FC<Props> = (props) => { [classes.root]: true, [classes.containerAdjustment]: showTransferDisplay && linksSection === undefined, + [classes.rootWithShowTransferDisplay]: showTransferDisplay, })} > <div @@ -206,7 +215,14 @@ const Placeholder: React.FC<Props> = (props) => { </Typography> ) : null} - <div className={classes.copy}> + <div + className={classes.copy} + style={{ + maxWidth: descriptionMaxWidth + ? descriptionMaxWidth + : classes.copy['maxWidth'], + }} + > {typeof props.children === 'string' ? ( <Typography variant="subtitle1">{props.children}</Typography> ) : ( diff --git a/packages/manager/src/components/PrimaryNav/AdditionalMenuItems.tsx b/packages/manager/src/components/PrimaryNav/AdditionalMenuItems.tsx index 3c2801aba9b..420203052d2 100644 --- a/packages/manager/src/components/PrimaryNav/AdditionalMenuItems.tsx +++ b/packages/manager/src/components/PrimaryNav/AdditionalMenuItems.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { compose } from 'recompose'; -import NavItem, { PrimaryLink } from './NavItem'; +import { NavItem, PrimaryLink } from './NavItem'; import Help from 'src/assets/icons/help.svg'; @@ -12,9 +11,7 @@ interface Props { isCollapsed?: boolean; } -type CombinedProps = Props; - -const AdditionalMenuItems: React.FC<CombinedProps> = (props) => { +export const AdditionalMenuItems = React.memo((props: Props) => { const { isCollapsed } = props; const links: PrimaryLink[] = [ { @@ -39,6 +36,4 @@ const AdditionalMenuItems: React.FC<CombinedProps> = (props) => { })} </React.Fragment> ); -}; - -export default compose<CombinedProps, Props>(React.memo)(AdditionalMenuItems); +}); diff --git a/packages/manager/src/components/PrimaryNav/NavItem.tsx b/packages/manager/src/components/PrimaryNav/NavItem.tsx index 21594053728..1dc8e0ca648 100644 --- a/packages/manager/src/components/PrimaryNav/NavItem.tsx +++ b/packages/manager/src/components/PrimaryNav/NavItem.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import * as React from 'react'; import { Link } from 'react-router-dom'; -import { compose } from 'recompose'; import Divider from 'src/components/core/Divider'; import ListItem from 'src/components/core/ListItem'; import ListItemText from 'src/components/core/ListItemText'; @@ -25,9 +24,7 @@ export interface PrimaryLink { isDisabled?: () => string; } -type CombinedProps = Props; - -const NavItem: React.SFC<CombinedProps> = (props) => { +export const NavItem = React.memo((props: Props) => { const { href, onClick, @@ -96,6 +93,4 @@ const NavItem: React.SFC<CombinedProps> = (props) => { <Divider className={props.dividerClasses} /> </React.Fragment> ); -}; - -export default compose<CombinedProps, Props>(React.memo)(NavItem); +}); diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts b/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts index 13c7ff6c34e..aa80e03f818 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts @@ -1,164 +1,170 @@ -import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; +import { keyframes } from 'tss-react'; +import { makeStyles } from 'tss-react/mui'; const SLIDE_IN_TRANSFORM = 'matrix(0.01471, 0, 0, 1, 123.982745, 0.000015)'; const SLIDE_OUT_TRANSFORM = 'translate(0)'; -const useStyles = makeStyles((theme: Theme) => ({ - '@keyframes slideIn': { - from: { - transform: SLIDE_IN_TRANSFORM, - }, - to: { - transform: SLIDE_OUT_TRANSFORM, - }, +const slideIn = keyframes` + from { + transform: ${SLIDE_IN_TRANSFORM}; + } + to { + transform: ${SLIDE_OUT_TRANSFORM}; + } +`; + +const slideOut = keyframes` + from { + transform: ${SLIDE_OUT_TRANSFORM}; }, - '@keyframes slideOut': { - from: { - transform: SLIDE_OUT_TRANSFORM, - }, - to: { - transform: SLIDE_IN_TRANSFORM, - }, + to { + transform: ${SLIDE_IN_TRANSFORM}; }, - menuGrid: { - minHeight: 64, - height: '100%', - width: '100%', - margin: 0, - padding: 0, - [theme.breakpoints.up('sm')]: { - minHeight: 72, +`; + +const useStyles = makeStyles<void, 'linkItem'>()( + (theme: Theme, _params, classes) => ({ + menuGrid: { + minHeight: 64, + height: '100%', + width: '100%', + margin: 0, + padding: 0, + [theme.breakpoints.up('sm')]: { + minHeight: 72, + }, + [theme.breakpoints.up('md')]: { + minHeight: 80, + }, + '&:hover': { + '& path.akamai-clip-path': { + animation: `${slideIn} .33s ease-in-out`, + }, + }, }, - [theme.breakpoints.up('md')]: { - minHeight: 80, + fadeContainer: { + display: 'flex', + flexDirection: 'column', + height: 'calc(100% - 90px)', + width: '100%', }, - '&:hover': { + logoItemAkamai: { + paddingTop: 12, + paddingLeft: 12, + transition: 'padding-left .03s linear', '& path.akamai-clip-path': { - animation: '$slideIn .33s ease-in-out', + animation: `${slideIn} .33s ease-in-out`, }, }, - }, - fadeContainer: { - display: 'flex', - flexDirection: 'column', - height: 'calc(100% - 90px)', - width: '100%', - }, - logoItemAkamai: { - paddingTop: 12, - paddingLeft: 12, - transition: 'padding-left .03s linear', - '& path.akamai-clip-path': { - animation: '$slideIn .33s ease-in-out', + logoItemAkamaiCollapsed: { + paddingLeft: 8, }, - }, - logoItemAkamaiCollapsed: { - paddingLeft: 8, - }, - logoAkamaiCollapsed: { - background: theme.bg.primaryNavPaper, - width: 96, - '& path.akamai-clip-path': { - animation: '$slideOut 0s ease-in-out 0s forwards', + logoAkamaiCollapsed: { + background: theme.bg.primaryNavPaper, + width: 96, + '& path.akamai-clip-path': { + animation: `${slideOut} 0s ease-in-out 0s forwards`, + }, }, - }, - listItem: { - display: 'flex', - alignItems: 'center', - cursor: 'pointer', - height: 36, - lineHeight: 0, - padding: '12px 16px', - position: 'relative', - transition: theme.transitions.create(['background-color']), - '& p': { - marginTop: 0, - marginBottom: 0, + listItem: { + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + height: 36, + lineHeight: 0, + padding: '12px 16px', + position: 'relative', + transition: theme.transitions.create(['background-color']), + '& p': { + marginTop: 0, + marginBottom: 0, + }, + '&:focus': { + textDecoration: 'none', + }, + '&:hover': { + border: 'red', + backgroundImage: 'linear-gradient(98deg, #38584B 1%, #3A5049 166%)', + textDecoration: 'none', + [`& .${classes.linkItem}`]: { + color: 'white', + }, + '& .icon': { + opacity: 1, + }, + '& svg': { + color: theme.color.teal, + fill: theme.color.teal, + }, + }, + '& .icon': { + color: '#CFD0D2', + marginRight: theme.spacing(2), + opacity: 0.5, + '& svg': { + display: 'flex', + alignItems: 'center', + height: 20, + width: 20, + '&:not(.wBorder) circle, & .circle': { + display: 'none', + }, + }, + }, }, - '&:focus': { - textDecoration: 'none', + linkItem: { + display: 'flex', + alignItems: 'center', + color: '#fff', + fontFamily: 'LatoWebBold', // we keep this bold at all times + opacity: 1, + transition: theme.transitions.create(['color']), + whiteSpace: 'nowrap', + '&.hiddenWhenCollapsed': { + opacity: 0, + }, }, - '&:hover': { - border: 'red', + active: { backgroundImage: 'linear-gradient(98deg, #38584B 1%, #3A5049 166%)', textDecoration: 'none', - '& $linkItem': { - color: 'white', - }, '& .icon': { opacity: 1, }, '& svg': { color: theme.color.teal, - fill: theme.color.teal, }, }, - '& .icon': { - color: '#CFD0D2', - marginRight: theme.spacing(2), - opacity: 0.5, - '& svg': { - display: 'flex', - alignItems: 'center', - height: 20, - width: 20, - '&:not(.wBorder) circle, & .circle': { - display: 'none', - }, - }, + divider: { + backgroundColor: 'rgba(0, 0, 0, 0.12)', + color: '#222', }, - }, - linkItem: { - display: 'flex', - alignItems: 'center', - color: '#fff', - fontFamily: 'LatoWebBold', // we keep this bold at all times - opacity: 1, - transition: theme.transitions.create(['color']), - whiteSpace: 'nowrap', - '&.hiddenWhenCollapsed': { - opacity: 0, + chip: { + marginTop: 2, }, - }, - active: { - backgroundImage: 'linear-gradient(98deg, #38584B 1%, #3A5049 166%)', - textDecoration: 'none', - '& .icon': { - opacity: 1, - }, - '& svg': { - color: theme.color.teal, - }, - }, - divider: { - backgroundColor: 'rgba(0, 0, 0, 0.12)', - color: '#222', - }, - chip: { - marginTop: 2, - }, - logoSvgCollapsed: { - // Hide 'Akamai Cloud Computing' when the navigation is collapsed and the nav is not hovered - '& > g ': { - display: 'none', + logoSvgCollapsed: { + // Hide 'Akamai' text when the navigation is collapsed and the nav is not hovered + '& > g ': { + display: 'none', + }, + 'nav:hover & > g ': { + display: 'unset', + }, + // Make the logo 115px so that the Akamai logo is centered when the navigation is collapsed + width: 115, }, - 'nav:hover & > g ': { - display: 'unset', + logoContainer: { + // when the nav is collapsed, but hovered by the user, make the logo full sized + 'nav:hover & > svg ': { + width: 128, + }, }, - // Make the logo 115px so that the Linode 'bug' is centered when the navigation is collapsed - width: 115, - }, - logoContainer: { - // when the nav is collapsed, but hovered by the user, make the logo full sized - 'nav:hover & > svg ': { - width: 128, + logo: { + // give the svg a transition so it smoothly resizes + transition: 'width .1s linear', }, - }, - logo: { - // give the svg a transition so it smoothly resizes - transition: 'width .1s linear', - }, -})); + }) +); +// TODO jss-to-tss-react codemod: usages of this hook outside of this file will not be converted. export default useStyles; diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 54078ca35e2..b2e02e1f2b9 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -1,4 +1,4 @@ -import classNames from 'classnames'; +import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { Link, LinkProps, useLocation } from 'react-router-dom'; import Account from 'src/assets/icons/account.svg'; @@ -19,7 +19,6 @@ import Longview from 'src/assets/icons/longview.svg'; import AkamaiLogo from 'src/assets/logo/akamai-logo.svg'; import { BetaChip } from 'src/components/BetaChip/BetaChip'; import Divider from 'src/components/core/Divider'; -import Grid from '@mui/material/Unstable_Grid2'; import useAccountManagement from 'src/hooks/useAccountManagement'; import useFlags from 'src/hooks/useFlags'; import usePrefetch from 'src/hooks/usePreFetch'; @@ -69,9 +68,9 @@ export interface Props { isCollapsed: boolean; } -export const PrimaryNav: React.FC<Props> = (props) => { +export const PrimaryNav = (props: Props) => { const { closeMenu, isCollapsed } = props; - const classes = useStyles(); + const { classes, cx } = useStyles(); const flags = useFlags(); const location = useLocation(); @@ -260,7 +259,7 @@ export const PrimaryNav: React.FC<Props> = (props) => { > <Grid> <div - className={classNames(classes.logoItemAkamai, { + className={cx(classes.logoItemAkamai, { [classes.logoItemAkamaiCollapsed]: isCollapsed, })} > @@ -269,13 +268,13 @@ export const PrimaryNav: React.FC<Props> = (props) => { onClick={closeMenu} aria-label="Akamai - Dashboard" title="Akamai - Dashboard" - className={classNames({ + className={cx({ [classes.logoContainer]: isCollapsed, })} > <AkamaiLogo width={128} - className={classNames( + className={cx( { [classes.logoAkamaiCollapsed]: isCollapsed, }, @@ -286,7 +285,7 @@ export const PrimaryNav: React.FC<Props> = (props) => { </div> </Grid> <div - className={classNames({ + className={cx({ ['fade-in-table']: true, [classes.fadeContainer]: true, })} @@ -351,8 +350,8 @@ interface PrimaryLinkProps extends PrimaryLink { }; } -const PrimaryLink: React.FC<PrimaryLinkProps> = React.memo((props) => { - const classes = useStyles(); +const PrimaryLink = React.memo((props: PrimaryLinkProps) => { + const { classes, cx } = useStyles(); const { isBeta, @@ -384,7 +383,7 @@ const PrimaryLink: React.FC<PrimaryLinkProps> = React.memo((props) => { }} {...prefetchProps} {...attr} - className={classNames({ + className={cx({ [classes.listItem]: true, [classes.active]: isActiveLink, })} @@ -397,7 +396,7 @@ const PrimaryLink: React.FC<PrimaryLinkProps> = React.memo((props) => { </div> )} <p - className={classNames({ + className={cx({ [classes.linkItem]: true, primaryNavLink: true, hiddenWhenCollapsed: isCollapsed, @@ -418,20 +417,20 @@ interface PrefetchPrimaryLinkProps { } // Wrapper around PrimaryLink that includes the usePrefetchHook. -export const PrefetchPrimaryLink: React.FC< - PrimaryLinkProps & PrefetchPrimaryLinkProps -> = React.memo((props) => { - const { makeRequest, cancelRequest } = usePrefetch( - props.prefetchRequestFn, - props.prefetchRequestCondition - ); +export const PrefetchPrimaryLink = React.memo( + (props: PrimaryLinkProps & PrefetchPrimaryLinkProps) => { + const { makeRequest, cancelRequest } = usePrefetch( + props.prefetchRequestFn, + props.prefetchRequestCondition + ); - const prefetchProps: PrimaryLinkProps['prefetchProps'] = { - onMouseEnter: makeRequest, - onFocus: makeRequest, - onMouseLeave: cancelRequest, - onBlur: cancelRequest, - }; + const prefetchProps: PrimaryLinkProps['prefetchProps'] = { + onMouseEnter: makeRequest, + onFocus: makeRequest, + onMouseLeave: cancelRequest, + onBlur: cancelRequest, + }; - return <PrimaryLink {...props} prefetchProps={prefetchProps} />; -}); + return <PrimaryLink {...props} prefetchProps={prefetchProps} />; + } +); diff --git a/packages/manager/src/components/PrimaryNav/index.ts b/packages/manager/src/components/PrimaryNav/index.ts deleted file mode 100644 index a68814e0d08..00000000000 --- a/packages/manager/src/components/PrimaryNav/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import PrimaryNav from './PrimaryNav'; -export default PrimaryNav; diff --git a/packages/manager/src/components/ProductInformationBanner/ProductInformationBanner.tsx b/packages/manager/src/components/ProductInformationBanner/ProductInformationBanner.tsx index 30fb3f65dbe..a3cbc973ec8 100644 --- a/packages/manager/src/components/ProductInformationBanner/ProductInformationBanner.tsx +++ b/packages/manager/src/components/ProductInformationBanner/ProductInformationBanner.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import HighlightedMarkdown from 'src/components/HighlightedMarkdown'; -import { NoticeProps } from 'src/components/Notice'; +import type { NoticeProps } from 'src/components/Notice/Notice'; import { reportException } from 'src/exceptionReporting'; import { ProductInformationBannerLocation } from 'src/featureFlags'; import useFlags from 'src/hooks/useFlags'; diff --git a/packages/manager/src/components/ProductNotification/ProductNotification.tsx b/packages/manager/src/components/ProductNotification/ProductNotification.tsx index d575f0a382d..1490bb23d07 100644 --- a/packages/manager/src/components/ProductNotification/ProductNotification.tsx +++ b/packages/manager/src/components/ProductNotification/ProductNotification.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface Props { text: string; diff --git a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx index 165aaadc0fd..772a0c0c02d 100644 --- a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx +++ b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx @@ -8,7 +8,7 @@ import Box from 'src/components/core/Box'; import Paper from 'src/components/core/Paper'; import Typography from 'src/components/core/Typography'; import { RegionSelect } from 'src/components/EnhancedSelect/variants/RegionSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import { CROSS_DATA_CENTER_CLONE_WARNING } from 'src/features/linodes/LinodesCreate/utilities'; import { sendLinodeCreateDocsEvent } from 'src/utilities/ga'; diff --git a/packages/manager/src/components/SelectableTableRow/SelectableTableRow.tsx b/packages/manager/src/components/SelectableTableRow/SelectableTableRow.tsx index 5638b572a39..42ee843fbe6 100644 --- a/packages/manager/src/components/SelectableTableRow/SelectableTableRow.tsx +++ b/packages/manager/src/components/SelectableTableRow/SelectableTableRow.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import CheckBox from 'src/components/CheckBox'; import { makeStyles } from '@mui/styles'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; const useStyles = makeStyles(() => ({ root: { diff --git a/packages/manager/src/components/ShowMoreExpansion/ShowMoreExpansion.tsx b/packages/manager/src/components/ShowMoreExpansion/ShowMoreExpansion.tsx index 6a8fa4c890f..eea3bddd241 100644 --- a/packages/manager/src/components/ShowMoreExpansion/ShowMoreExpansion.tsx +++ b/packages/manager/src/components/ShowMoreExpansion/ShowMoreExpansion.tsx @@ -1,49 +1,52 @@ import * as React from 'react'; -import { makeStyles } from '@mui/styles'; +import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; import Button from 'src/components/Button'; import Collapse from 'src/components/core/Collapse'; -const useStyles = makeStyles((theme: Theme) => ({ - root: { - paddingLeft: 0, - paddingRight: 0, - backgroundColor: 'transparent !important', - display: 'flex', - alignItems: 'center', - fontFamily: theme.font.bold, - width: 'auto', - color: theme.color.headline, - transition: theme.transitions.create('color'), - '&:hover': { - color: theme.palette.primary.main, - '& $caret': { - color: theme.palette.primary.light, +const useStyles = makeStyles<void, 'caret'>()( + (theme: Theme, _params, classes) => ({ + root: { + paddingLeft: 0, + paddingRight: 0, + backgroundColor: 'transparent !important', + display: 'flex', + alignItems: 'center', + fontFamily: theme.font.bold, + width: 'auto', + color: theme.color.headline, + transition: theme.transitions.create('color'), + '&:hover': { + color: theme.palette.primary.main, + [`& .${classes.caret}`]: { + color: theme.palette.primary.light, + }, }, }, - }, - caret: { - color: theme.palette.primary.main, - marginRight: theme.spacing(0.5), - fontSize: 28, - transition: 'transform .1s ease-in-out', - '&.rotate': { - transition: 'transform .3s ease-in-out', - transform: 'rotate(90deg)', + caret: { + color: theme.palette.primary.main, + marginRight: theme.spacing(0.5), + fontSize: 28, + transition: 'transform .1s ease-in-out', + '&.rotate': { + transition: 'transform .3s ease-in-out', + transform: 'rotate(90deg)', + }, }, - }, -})); + }) +); -interface Props { +interface ShowMoreExpansionProps { name: string; defaultExpanded?: boolean; + children?: JSX.Element; } -const ShowMoreExpansion: React.FC<Props> = (props) => { +const ShowMoreExpansion = (props: ShowMoreExpansionProps) => { const { name, defaultExpanded, children } = props; - const classes = useStyles(); + const { classes } = useStyles(); const [open, setOpen] = React.useState<boolean>(defaultExpanded || false); diff --git a/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx b/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx index 3f1527f5809..0894b34b72a 100644 --- a/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx +++ b/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx @@ -5,7 +5,7 @@ import Button from 'src/components/Button'; import Box from 'src/components/core/Box'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField, { Props as TextFieldProps } from 'src/components/TextField'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx b/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx index 2a052f82408..9d53bb0e10c 100644 --- a/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx +++ b/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx @@ -8,7 +8,7 @@ import Tabs from 'src/components/core/ReachTabs'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from '../Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Grid from '@mui/material/Unstable_Grid2'; const useStyles = makeStyles((theme: Theme) => ({ diff --git a/packages/manager/src/components/Table/Table.stories.mdx b/packages/manager/src/components/Table/Table.stories.mdx index d35a6fe9182..fef787518ee 100644 --- a/packages/manager/src/components/Table/Table.stories.mdx +++ b/packages/manager/src/components/Table/Table.stories.mdx @@ -1,18 +1,9 @@ import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; -import { Provider } from 'react-redux'; import TableBody from 'src/components/core/TableBody'; import TableHead from 'src/components/core/TableHead'; -import OrderBy from 'src/components/OrderBy'; -import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import { linodeFactory } from 'src/factories/linodes'; -import LinodeRow from 'src/features/linodes/LinodesLanding/LinodeRow'; -import SortableTableHead from 'src/features/linodes/LinodesLanding/SortableTableHead'; -import store from 'src/store'; -import capitalize from 'src/utilities/capitalize'; -import TableWrapper from './Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import Table from './Table'; export const linodes = linodeFactory.buildList(30); @@ -42,7 +33,7 @@ export const linodes = linodeFactory.buildList(30); <Canvas> <Story name="Table"> - <TableWrapper> + <Table> <TableHead> <TableRow> <TableCell>Label</TableCell> @@ -67,85 +58,6 @@ export const linodes = linodeFactory.buildList(30); <TableCell>Newark, NJ</TableCell> </TableRow> </TableBody> - </TableWrapper> + </Table> </Story> </Canvas> - -## Paginated - -<Canvas> - <Story name="Paginated"> - <PaginatedExample /> - </Story> -</Canvas> - -export const PaginatedExample = () => { - return ( - <> - <Provider store={store}> - <OrderBy data={linodes} orderBy="label" order="asc"> - {({ data: orderedData, handleOrderChange, order, orderBy }) => ( - <Paginate data={orderedData}> - {({ - data, - count, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => ( - <> - <TableWrapper> - <SortableTableHead - order={order} - orderBy={orderBy} - handleOrderChange={handleOrderChange} - toggleLinodeView={() => 'grid'} - linodesAreGrouped={false} - toggleGroupLinodes={() => true} - linodeViewPreference="list" - /> - <TableBody> - {data?.map((linode) => ( - <LinodeRow - key={linode.id} - id={linode.id} - image={linode.image} - ipv4={linode.ipv4} - ipv6={linode.ipv6} - label={linode.label} - backups={linode.backups} - displayStatus={capitalize(linode.status)} - region={linode.region} - status={linode.status} - disk={linode.specs.disk} - memory={linode.specs.memory} - mostRecentBackup={linode.backups.last_successful} - tags={linode.tags} - openTagDrawer={() => null} - type={{ label: 'Linode 2 GB' }} - vcpus={linode.specs.vcpus} - openDialog={() => null} - openPowerActionDialog={() => null} - openNotificationMenu={() => null} - ></LinodeRow> - ))} - </TableBody> - </TableWrapper> - <PaginationFooter - count={count} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - page={page} - pageSize={pageSize} - eventCategory="Paginated Table" - /> - </> - )} - </Paginate> - )} - </OrderBy> - </Provider> - </> - ); -}; diff --git a/packages/manager/src/components/Table/Table.tsx b/packages/manager/src/components/Table/Table.tsx index 2c05383c44e..ed7d11ccfb4 100644 --- a/packages/manager/src/components/Table/Table.tsx +++ b/packages/manager/src/components/Table/Table.tsx @@ -1,19 +1,15 @@ -import classNames from 'classnames'; import * as React from 'react'; -import { makeStyles } from '@mui/styles'; +import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import Table, { TableProps } from 'src/components/core/Table'; +import { + TableProps as _TableProps, + default as _Table, +} from '@mui/material/Table'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles()((theme: Theme) => ({ root: { overflowX: 'auto', overflowY: 'hidden', - '& tbody': { - transition: theme.transitions.create(['opacity']), - }, - '& tbody.sorting': { - opacity: 0.5, - }, '& thead': { '& th': { backgroundColor: theme.bg.tableHeader, @@ -41,7 +37,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -export interface Props extends TableProps { +export interface TableProps extends _TableProps { className?: string; noOverflow?: boolean; tableClass?: string; @@ -53,10 +49,8 @@ export interface Props extends TableProps { rowCount?: number; } -type CombinedProps = Props; - -export const WrappedTable: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); +export const Table = (props: TableProps) => { + const { classes, cx } = useStyles(); const { className, @@ -72,8 +66,7 @@ export const WrappedTable: React.FC<CombinedProps> = (props) => { return ( <div - className={classNames( - 'tableWrapper', + className={cx( { [classes.root]: !noOverflow, [classes.noBorder]: noBorder, @@ -85,7 +78,7 @@ export const WrappedTable: React.FC<CombinedProps> = (props) => { marginBottom: spacingBottom !== undefined ? spacingBottom : 0, }} > - <Table + <_Table className={tableClass} {...rest} aria-colcount={colCount} @@ -93,9 +86,7 @@ export const WrappedTable: React.FC<CombinedProps> = (props) => { role="table" > {props.children} - </Table> + </_Table> </div> ); }; - -export default WrappedTable; diff --git a/packages/manager/src/components/Table/index.ts b/packages/manager/src/components/Table/index.ts index 03abe7db68d..4d2ede9211a 100644 --- a/packages/manager/src/components/Table/index.ts +++ b/packages/manager/src/components/Table/index.ts @@ -1,4 +1,2 @@ -import Table, { Props as _TableProps } from './Table'; -/* tslint:disable-next-line */ -export interface TableProps extends _TableProps {} -export default Table; +export { Table } from './Table'; +export type { TableProps } from './Table'; diff --git a/packages/manager/src/components/TableBody.tsx b/packages/manager/src/components/TableBody.tsx new file mode 100644 index 00000000000..c71a71f5b92 --- /dev/null +++ b/packages/manager/src/components/TableBody.tsx @@ -0,0 +1,2 @@ +export { default as TableBody } from '@mui/material/TableBody'; +export type { TableBodyProps } from '@mui/material/TableBody'; diff --git a/packages/manager/src/components/TableCell/TableCell.tsx b/packages/manager/src/components/TableCell/TableCell.tsx index 2e254b7842b..ac1c570ff4b 100644 --- a/packages/manager/src/components/TableCell/TableCell.tsx +++ b/packages/manager/src/components/TableCell/TableCell.tsx @@ -1,10 +1,12 @@ -import classNames from 'classnames'; import * as React from 'react'; -import { makeStyles } from '@mui/styles'; +import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableCell, { TableCellProps } from 'src/components/core/TableCell'; +import { + default as _TableCell, + TableCellProps as _TableCellProps, +} from '@mui/material/TableCell'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles()((theme: Theme) => ({ root: { borderTop: 'none', borderBottom: `1px solid ${theme.borderColors.borderTable}`, @@ -65,7 +67,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -export interface Props extends TableCellProps { +export interface TableCellProps extends _TableCellProps { noWrap?: boolean; sortable?: boolean; className?: string; @@ -80,10 +82,8 @@ export interface Props extends TableCellProps { center?: boolean; } -type CombinedProps = Props; - -export const WrappedTableCell: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); +export const TableCell = (props: TableCellProps) => { + const { classes, cx } = useStyles(); const { className, @@ -98,17 +98,20 @@ export const WrappedTableCell: React.FC<CombinedProps> = (props) => { } = props; return ( - <TableCell - className={classNames(className, { - [classes.root]: true, - [classes.noWrap]: noWrap, - [classes.sortable]: sortable, - [classes.compact]: compact, - [classes.actionCell]: actionCell, - [classes.center]: center, - // hide the cell at small breakpoints if it's empty with no parent column - emptyCell: !parentColumn && !props.children, - })} + <_TableCell + className={cx( + { + [classes.root]: true, + [classes.noWrap]: noWrap, + [classes.sortable]: sortable, + [classes.compact]: compact, + [classes.actionCell]: actionCell, + [classes.center]: center, + // hide the cell at small breakpoints if it's empty with no parent column + emptyCell: !parentColumn && !props.children, + }, + className + )} {...rest} > {statusCell ? ( @@ -116,8 +119,6 @@ export const WrappedTableCell: React.FC<CombinedProps> = (props) => { ) : ( props.children )} - </TableCell> + </_TableCell> ); }; - -export default WrappedTableCell; diff --git a/packages/manager/src/components/TableCell/index.ts b/packages/manager/src/components/TableCell/index.ts index a721cfa7efd..de295e057e2 100644 --- a/packages/manager/src/components/TableCell/index.ts +++ b/packages/manager/src/components/TableCell/index.ts @@ -1,4 +1,2 @@ -import TableCell, { Props as _TableCellProps } from './TableCell'; -/* tslint:disable */ -export interface TableCellProps extends _TableCellProps {} -export default TableCell; +export { TableCell } from './TableCell'; +export type { TableCellProps } from './TableCell'; diff --git a/packages/manager/src/components/TableHead.tsx b/packages/manager/src/components/TableHead.tsx new file mode 100644 index 00000000000..2614409df79 --- /dev/null +++ b/packages/manager/src/components/TableHead.tsx @@ -0,0 +1,2 @@ +export { default as TableHead } from '@mui/material/TableHead'; +export type { TableHeadProps } from '@mui/material/TableHead'; diff --git a/packages/manager/src/components/TableRow/TableRow.test.tsx b/packages/manager/src/components/TableRow/TableRow.test.tsx deleted file mode 100644 index 9e9687ba0e7..00000000000 --- a/packages/manager/src/components/TableRow/TableRow.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { fireEvent, render } from '@testing-library/react'; -import * as React from 'react'; -import TableCell from 'src/components/TableCell'; -import { wrapWithTableBody } from 'src/utilities/testHelpers'; -import { reactRouterProps } from 'src/__data__/reactRouterProps'; -import { CombinedProps, TableRow } from './TableRow'; - -const mockHistoryPush = jest.fn(); - -const props: CombinedProps = { - classes: { - root: '', - selected: '', - }, - ...reactRouterProps, - history: { - ...reactRouterProps.history, - push: mockHistoryPush, - }, - staticContext: undefined, -}; - -describe('TableRow component', () => { - it.skip('calls history.push with the given rowLink', () => { - const { getByText } = render( - wrapWithTableBody( - <TableRow {...(props as any)} rowLink={'/test-url'}> - <TableCell>Test Text</TableCell> - </TableRow> - ) - ); - - fireEvent.click(getByText('Test Text')); - expect(mockHistoryPush).toHaveBeenCalledWith('/test-url'); - }); -}); diff --git a/packages/manager/src/components/TableRow/TableRow.tsx b/packages/manager/src/components/TableRow/TableRow.tsx index 8bd145cf7dd..139d125c98a 100644 --- a/packages/manager/src/components/TableRow/TableRow.tsx +++ b/packages/manager/src/components/TableRow/TableRow.tsx @@ -1,13 +1,13 @@ -import classNames from 'classnames'; import * as React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { compose } from 'recompose'; import Hidden from 'src/components/core/Hidden'; -import { makeStyles } from '@mui/styles'; +import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import _TableRow, { TableRowProps } from 'src/components/core/TableRow'; +import { + default as _TableRow, + TableRowProps as _TableRowProps, +} from '@mui/material/TableRow'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles()((theme: Theme) => ({ root: { borderLeft: `1px solid ${theme.borderColors.borderTable}`, borderRight: `1px solid ${theme.borderColors.borderTable}`, @@ -113,9 +113,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -type onClickFn = (e: React.ChangeEvent<HTMLTableRowElement>) => void; - -export interface Props { +export interface TableRowProps extends _TableRowProps { className?: string; ariaLabel?: string; disabled?: boolean; @@ -123,15 +121,12 @@ export interface Props { forceIndex?: boolean; highlight?: boolean; htmlFor?: string; - onClick?: onClickFn; onKeyUp?: any; selected?: boolean; } -export type CombinedProps = Props & TableRowProps & RouteComponentProps<{}>; - -export const TableRow: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); +export const TableRow = React.memo((props: TableRowProps) => { + const { classes, cx } = useStyles(); const { className, @@ -141,17 +136,13 @@ export const TableRow: React.FC<CombinedProps> = (props) => { forceIndex, highlight, selected, - // Defining `staticContext` here to prevent `...rest` from containing it - // since it leads to a console warning - // eslint-disable-next-line @typescript-eslint/no-unused-vars - staticContext, ...rest } = props; return ( <_TableRow aria-label={ariaLabel ?? `View Details`} - className={classNames(className, { + className={cx(className, { [classes.root]: true, [classes.selected]: selected, [classes.withForcedIndex]: forceIndex, @@ -172,11 +163,4 @@ export const TableRow: React.FC<CombinedProps> = (props) => { )} </_TableRow> ); -}; - -const enhanced = compose<CombinedProps, Props>( - withRouter, - React.memo -)(TableRow); - -export default enhanced; +}); diff --git a/packages/manager/src/components/TableRow/index.ts b/packages/manager/src/components/TableRow/index.ts index d4eb1fd87b7..e1b4b06a26b 100644 --- a/packages/manager/src/components/TableRow/index.ts +++ b/packages/manager/src/components/TableRow/index.ts @@ -1,4 +1,2 @@ -import TableRow, { Props as _TableRowProps } from './TableRow'; -/* eslint-disable */ -export interface TableRowProps extends _TableRowProps {} -export default TableRow; +export { TableRow } from './TableRow'; +export type { TableRowProps } from './TableRow'; diff --git a/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.stories.mdx b/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.stories.mdx index 40c787b8aae..7ae551e065a 100644 --- a/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.stories.mdx +++ b/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.stories.mdx @@ -1,9 +1,9 @@ import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; import TableBody from 'src/components/core/TableBody'; import TableHead from 'src/components/core/TableHead'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; <Meta title="Components/Table" component={TableRowEmptyState} /> diff --git a/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.tsx b/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.tsx index 5b98ce3db89..5bfb08ad794 100644 --- a/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.tsx +++ b/packages/manager/src/components/TableRowEmptyState/TableRowEmptyState.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; const useStyles = makeStyles((theme: Theme) => ({ root: { diff --git a/packages/manager/src/components/TableRowError/TableRowError.stories.mdx b/packages/manager/src/components/TableRowError/TableRowError.stories.mdx index 4e1ff41abed..7df72e580ab 100644 --- a/packages/manager/src/components/TableRowError/TableRowError.stories.mdx +++ b/packages/manager/src/components/TableRowError/TableRowError.stories.mdx @@ -1,9 +1,9 @@ import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; import TableBody from 'src/components/core/TableBody'; import TableHead from 'src/components/core/TableHead'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowError from 'src/components/TableRowError'; <Meta title="Components/Table" component={TableRowError} /> diff --git a/packages/manager/src/components/TableRowError/TableRowError.tsx b/packages/manager/src/components/TableRowError/TableRowError.tsx index 38a9bc8bcb8..2c611b4ffdf 100644 --- a/packages/manager/src/components/TableRowError/TableRowError.tsx +++ b/packages/manager/src/components/TableRowError/TableRowError.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ErrorState from 'src/components/ErrorState'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; export interface Props { colSpan: number; diff --git a/packages/manager/src/components/TableRowLoading/TableRowLoading.stories.mdx b/packages/manager/src/components/TableRowLoading/TableRowLoading.stories.mdx index a21aaa92d8e..37431b55b0c 100644 --- a/packages/manager/src/components/TableRowLoading/TableRowLoading.stories.mdx +++ b/packages/manager/src/components/TableRowLoading/TableRowLoading.stories.mdx @@ -1,9 +1,9 @@ import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; import TableBody from 'src/components/core/TableBody'; import TableHead from 'src/components/core/TableHead'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { TableRowLoading } from './TableRowLoading'; <Meta title="Components/Table" component={TableRowLoading} /> diff --git a/packages/manager/src/components/TableRowLoading/TableRowLoading.tsx b/packages/manager/src/components/TableRowLoading/TableRowLoading.tsx index ea72611a467..9ced217ef50 100644 --- a/packages/manager/src/components/TableRowLoading/TableRowLoading.tsx +++ b/packages/manager/src/components/TableRowLoading/TableRowLoading.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import Hidden, { HiddenProps } from '../core/Hidden'; import Skeleton from '../core/Skeleton'; import { makeStyles } from '@mui/styles'; -import TableCell from '../TableCell/TableCell'; -import TableRow from '../TableRow/TableRow'; +import { TableCell } from '../TableCell/TableCell'; +import { TableRow } from '../TableRow/TableRow'; const useStyles = makeStyles(() => ({ root: { diff --git a/packages/manager/src/components/TableSortCell/TableSortCell.tsx b/packages/manager/src/components/TableSortCell/TableSortCell.tsx index 87ab51a5811..ccca8da1270 100644 --- a/packages/manager/src/components/TableSortCell/TableSortCell.tsx +++ b/packages/manager/src/components/TableSortCell/TableSortCell.tsx @@ -1,14 +1,16 @@ -import classNames from 'classnames'; import * as React from 'react'; import SortUp from 'src/assets/icons/sort-up.svg'; import Sort from 'src/assets/icons/unsorted.svg'; +import TableSortLabel from '@mui/material/TableSortLabel'; import { CircleProgress } from 'src/components/CircleProgress'; -import { makeStyles } from '@mui/styles'; +import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableCell, { TableCellProps } from 'src/components/core/TableCell'; -import TableSortLabel from 'src/components/core/TableSortLabel'; +import { + default as TableCell, + TableCellProps as _TableCellProps, +} from '@mui/material/TableCell'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles()((theme: Theme) => ({ root: { '& svg': { marginLeft: 4, @@ -43,7 +45,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -export interface Props extends TableCellProps { +export interface TableSortCellProps extends _TableCellProps { active: boolean; isLoading?: boolean; label: string; @@ -52,10 +54,8 @@ export interface Props extends TableCellProps { noWrap?: boolean; } -type CombinedProps = Props; - -export const TableSortCell: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); +export const TableSortCell = (props: TableSortCellProps) => { + const { classes, cx } = useStyles(); const { children, @@ -77,7 +77,7 @@ export const TableSortCell: React.FC<CombinedProps> = (props) => { return ( <TableCell - className={classNames(props.className, { + className={cx(props.className, { [classes.root]: true, [classes.noWrap]: noWrap, })} @@ -101,5 +101,3 @@ export const TableSortCell: React.FC<CombinedProps> = (props) => { </TableCell> ); }; - -export default TableSortCell; diff --git a/packages/manager/src/components/TableSortCell/index.ts b/packages/manager/src/components/TableSortCell/index.ts index ecb43a2355b..f997d0ca5d3 100644 --- a/packages/manager/src/components/TableSortCell/index.ts +++ b/packages/manager/src/components/TableSortCell/index.ts @@ -1,4 +1,2 @@ -import TableSortCell, { Props } from './TableSortCell'; -/* tslint:disable-next-line */ -export interface TableSortCellProps extends Props {} -export default TableSortCell; +export { TableSortCell } from './TableSortCell'; +export type { TableSortCellProps } from './TableSortCell'; diff --git a/packages/manager/src/components/Tile/Tile.tsx b/packages/manager/src/components/Tile/Tile.tsx index 73a4ea3760e..ab9a504b5e2 100644 --- a/packages/manager/src/components/Tile/Tile.tsx +++ b/packages/manager/src/components/Tile/Tile.tsx @@ -4,7 +4,7 @@ import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; const useStyles = makeStyles<void, 'icon' | 'buttonTitle'>()( (theme: Theme, _params, classes) => ({ diff --git a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx index 56b396b06b8..35700e7376a 100644 --- a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx +++ b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx @@ -20,7 +20,7 @@ interface Props { children: React.ReactNode; loading: boolean; confirmationText?: string | JSX.Element; - errors?: APIError[] | undefined; + errors?: APIError[] | undefined | null; onClick: () => void; } diff --git a/packages/manager/src/components/core/Table.ts b/packages/manager/src/components/core/Table.ts deleted file mode 100644 index 2c20477e2d0..00000000000 --- a/packages/manager/src/components/core/Table.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Table, { TableProps as _TableProps } from '@mui/material/Table'; - -/* tslint:disable-next-line:no-empty-interface */ -export interface TableProps extends _TableProps {} - -export default Table; diff --git a/packages/manager/src/components/core/TableBody.ts b/packages/manager/src/components/core/TableBody.ts deleted file mode 100644 index 738a0817e0e..00000000000 --- a/packages/manager/src/components/core/TableBody.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TableBody, { - TableBodyProps as _TableBodyProps, -} from '@mui/material/TableBody'; - -/* tslint:disable-next-line:no-empty-interface */ -export interface TableBodyProps extends _TableBodyProps {} - -export default TableBody; diff --git a/packages/manager/src/components/core/TableCell.ts b/packages/manager/src/components/core/TableCell.ts deleted file mode 100644 index a3f22cc5a09..00000000000 --- a/packages/manager/src/components/core/TableCell.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TableCell, { - TableCellProps as _TableCellProps, -} from '@mui/material/TableCell'; - -/* tslint:disable-next-line:no-empty-interface */ -export interface TableCellProps extends _TableCellProps {} - -export default TableCell; diff --git a/packages/manager/src/components/core/TableHead.ts b/packages/manager/src/components/core/TableHead.ts deleted file mode 100644 index 955344878f0..00000000000 --- a/packages/manager/src/components/core/TableHead.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TableHead, { - TableHeadProps as _TableHeadProps, -} from '@mui/material/TableHead'; - -/* tslint:disable-next-line:no-empty-interface */ -export interface TableHeadProps extends _TableHeadProps {} - -export default TableHead; diff --git a/packages/manager/src/components/core/TableRow.ts b/packages/manager/src/components/core/TableRow.ts deleted file mode 100644 index f760822cb83..00000000000 --- a/packages/manager/src/components/core/TableRow.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TableRow, { - TableRowProps as _TableRowProps, -} from '@mui/material/TableRow'; - -/* tslint:disable-next-line:no-empty-interface */ -export interface TableRowProps extends _TableRowProps {} - -export default TableRow; diff --git a/packages/manager/src/components/core/TableSortLabel.ts b/packages/manager/src/components/core/TableSortLabel.ts deleted file mode 100644 index d70a4745704..00000000000 --- a/packages/manager/src/components/core/TableSortLabel.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TableSortLabel, { - TableSortLabelProps as _TableSortLabelProps, -} from '@mui/material/TableSortLabel'; - -/* tslint:disable-next-line:no-empty-interface */ -export interface TableSortLabelProps extends _TableSortLabelProps {} - -export default TableSortLabel; diff --git a/packages/manager/src/factories/billing.ts b/packages/manager/src/factories/billing.ts index 08254c71d48..29acc2df5fc 100644 --- a/packages/manager/src/factories/billing.ts +++ b/packages/manager/src/factories/billing.ts @@ -4,7 +4,6 @@ import { InvoiceItem, Payment, PaymentResponse, - PaypalResponse, Invoice, } from '@linode/api-v4/lib/account'; @@ -68,7 +67,3 @@ export const creditPaymentResponseFactory = Factory.Sync.makeFactory<PaymentResp warnings: warningFactory.buildList(1), } ); - -export const paypalResponseFactory = Factory.Sync.makeFactory<PaypalResponse>({ - warnings: warningFactory.buildList(1), -}); diff --git a/packages/manager/src/factories/kubernetesCluster.ts b/packages/manager/src/factories/kubernetesCluster.ts index aaf53d09bbd..789370c985b 100644 --- a/packages/manager/src/factories/kubernetesCluster.ts +++ b/packages/manager/src/factories/kubernetesCluster.ts @@ -5,6 +5,7 @@ import { KubeNodePoolResponse, PoolNodeResponse, KubernetesVersion, + KubernetesDashboardResponse, } from '@linode/api-v4/lib/kubernetes/types'; import { v4 } from 'uuid'; @@ -46,6 +47,12 @@ export const kubeEndpointFactory = Factory.Sync.makeFactory<KubernetesEndpointRe } ); +export const kubernetesDashboardUrlFactory = Factory.Sync.makeFactory<KubernetesDashboardResponse>( + { + url: `https://${v4()}`, + } +); + export const kubernetesAPIResponse = Factory.Sync.makeFactory<KubernetesCluster>( { id: Factory.each((id) => id), diff --git a/packages/manager/src/factories/linodes.ts b/packages/manager/src/factories/linodes.ts index d3f62ea1ad6..c4f7ae7c2ac 100644 --- a/packages/manager/src/factories/linodes.ts +++ b/packages/manager/src/factories/linodes.ts @@ -3,6 +3,7 @@ import { CreateLinodeRequest, Linode, LinodeAlerts, + LinodeBackup, LinodeBackups, LinodeIPsResponse, LinodeSpecs, @@ -216,3 +217,28 @@ export const createLinodeRequestFactory = Factory.Sync.makeFactory<CreateLinodeR booted: true, } ); + +export const backupFactory = Factory.Sync.makeFactory<LinodeBackup>({ + id: Factory.each((i) => i), + region: 'us-central', + type: 'auto', + status: 'successful', + available: true, + created: '2023-05-03T04:00:47', + updated: '2023-05-03T04:04:07', + finished: '2023-05-03T04:02:11', + label: null, + configs: ['Restore 319718 - My Alpine 3.17 Disk Profile'], + disks: [ + { + label: 'Restore 319718 - Alpine 3.17 Disk', + size: 25088, + filesystem: 'ext4', + }, + { + label: 'Restore 319718 - 512 MB Swap Image', + size: 512, + filesystem: 'swap', + }, + ], +}); diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 2803351f198..bdd098c5768 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -56,6 +56,7 @@ export interface Flags { taxCollectionBanner: TaxCollectionBanner; databaseBeta: boolean; metadata: boolean; + premiumPlansAvailabilityNotice: string; } type PromotionalOfferFeature = diff --git a/packages/manager/src/features/Account/AccountLogins.tsx b/packages/manager/src/features/Account/AccountLogins.tsx index 7fe39dc415b..24a5621422e 100644 --- a/packages/manager/src/features/Account/AccountLogins.tsx +++ b/packages/manager/src/features/Account/AccountLogins.tsx @@ -4,16 +4,16 @@ import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useAccountLoginsQuery } from 'src/queries/accountLogins'; diff --git a/packages/manager/src/features/Account/AccountLoginsTableRow.tsx b/packages/manager/src/features/Account/AccountLoginsTableRow.tsx index ff59ab5663e..992134a8e43 100644 --- a/packages/manager/src/features/Account/AccountLoginsTableRow.tsx +++ b/packages/manager/src/features/Account/AccountLoginsTableRow.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import Link from 'src/components/Link'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import formatDate from 'src/utilities/formatDate'; import { StatusIcon, Status } from 'src/components/StatusIcon/StatusIcon'; import { capitalize } from 'src/utilities/capitalize'; diff --git a/packages/manager/src/features/Account/AutoBackups.tsx b/packages/manager/src/features/Account/AutoBackups.tsx index 3ba4ffecac2..c30860fb52e 100644 --- a/packages/manager/src/features/Account/AutoBackups.tsx +++ b/packages/manager/src/features/Account/AutoBackups.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import Accordion from 'src/components/Accordion'; import FormControlLabel from 'src/components/core/FormControlLabel'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import OpenInNew from '@mui/icons-material/OpenInNew'; import { Toggle } from 'src/components/Toggle'; import Typography from 'src/components/core/Typography'; diff --git a/packages/manager/src/features/Account/CloseAccountDialog.tsx b/packages/manager/src/features/Account/CloseAccountDialog.tsx index d35fa2b9e5e..b787afad865 100644 --- a/packages/manager/src/features/Account/CloseAccountDialog.tsx +++ b/packages/manager/src/features/Account/CloseAccountDialog.tsx @@ -7,7 +7,7 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Typography from 'src/components/core/Typography'; import TypeToConfirm from 'src/components/TypeToConfirm'; import TextField from 'src/components/TextField'; diff --git a/packages/manager/src/features/Account/EnableObjectStorage.tsx b/packages/manager/src/features/Account/EnableObjectStorage.tsx index 9a5113c3eff..7b68a7a7fd8 100644 --- a/packages/manager/src/features/Account/EnableObjectStorage.tsx +++ b/packages/manager/src/features/Account/EnableObjectStorage.tsx @@ -7,7 +7,7 @@ import Accordion from 'src/components/Accordion'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TypeToConfirm from 'src/components/TypeToConfirm'; import Typography from 'src/components/core/Typography'; import ExternalLink from 'src/components/ExternalLink'; diff --git a/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx b/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx index dbc90e1a930..14b8189b888 100644 --- a/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx +++ b/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx @@ -2,25 +2,25 @@ import * as React from 'react'; import Box from 'src/components/core/Box'; import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import Table from 'src/components/Table/Table'; -import TableCell from 'src/components/TableCell/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; -import PaginationFooter from 'src/components/PaginationFooter'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import TableRowError from 'src/components/TableRowError'; -import TableSortCell from 'src/components/TableSortCell/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import Typography from 'src/components/core/Typography'; import { usePagination } from 'src/hooks/usePagination'; import { AccountMaintenance } from '@linode/api-v4/lib/account/types'; -import { CSVLink } from 'react-csv'; +import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import { cleanCSVData } from 'src/components/DownloadCSV/DownloadCSV'; import { useOrder } from 'src/hooks/useOrder'; import { MaintenanceTableRow } from './MaintenanceTableRow'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; +import { useFormattedDate } from 'src/hooks/useFormattedDate'; import { useAccountMaintenanceQuery, useAllAccountMaintenanceQuery, @@ -39,13 +39,6 @@ const headersForCSVDownload = [ ]; const useStyles = makeStyles()((theme: Theme) => ({ - csvLink: { - [theme.breakpoints.down('md')]: { - marginRight: theme.spacing(), - }, - color: theme.textColors.tableHeader, - fontSize: '.9rem', - }, cell: { width: '12%', }, @@ -67,6 +60,7 @@ const MaintenanceTable = ({ type }: Props) => { const csvRef = React.useRef<any>(); const { classes } = useStyles(); const pagination = usePagination(1, `${preferenceKey}-${type}`, type); + const formattedDate = useFormattedDate(); const { order, orderBy, handleOrderChange } = useOrder( { @@ -151,28 +145,14 @@ const MaintenanceTable = ({ type }: Props) => { <Typography variant="h3" style={{ textTransform: 'capitalize' }}> {type} </Typography> - {/* - We are using a hidden CSVLink and an <a> to allow us to lazy load the - entire maintenance list for the CSV download. The <a> is what shows up - to the user and the onClick fetches the full user data and then - uses a ref to 'click' the real CSVLink. - This adds some complexity but gives us the benefit of lazy loading a potentially - large set of maintenance events on mount for the CSV download. - */} <Box> - <CSVLink - ref={csvRef} + <DownloadCSV + csvRef={csvRef} + data={csv || []} + filename={`${type}-maintenance-${formattedDate}.csv`} headers={headersForCSVDownload} - filename={`${type}-maintenance-${Date.now()}.csv`} - data={cleanCSVData(csv || [])} - /> - <a - className={classes.csvLink} onClick={downloadCSV} - aria-hidden="true" - > - Download CSV - </a> + /> </Box> </Box> <Table> diff --git a/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx b/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx index 22bc3c37be6..b2cb8e5f487 100644 --- a/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx +++ b/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import Tooltip from 'src/components/core/Tooltip'; import Link from 'src/components/Link'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import HighlightedMarkdown from 'src/components/HighlightedMarkdown'; import { capitalize } from 'src/utilities/capitalize'; import { StatusIcon, Status } from 'src/components/StatusIcon/StatusIcon'; diff --git a/packages/manager/src/features/Backups/AutoEnroll.test.tsx b/packages/manager/src/features/Backups/AutoEnroll.test.tsx index fdb3833d0b2..8364c80e547 100644 --- a/packages/manager/src/features/Backups/AutoEnroll.test.tsx +++ b/packages/manager/src/features/Backups/AutoEnroll.test.tsx @@ -1,7 +1,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { AutoEnroll } from './AutoEnroll'; diff --git a/packages/manager/src/features/Backups/AutoEnroll.tsx b/packages/manager/src/features/Backups/AutoEnroll.tsx index 717321a71d2..f1699241893 100644 --- a/packages/manager/src/features/Backups/AutoEnroll.tsx +++ b/packages/manager/src/features/Backups/AutoEnroll.tsx @@ -5,7 +5,7 @@ import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import ExternalLink from 'src/components/ExternalLink'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { Toggle } from 'src/components/Toggle'; type ClassNames = 'root' | 'header' | 'toggleLabel' | 'toggleLabelText'; diff --git a/packages/manager/src/features/Backups/BackupDrawer.tsx b/packages/manager/src/features/Backups/BackupDrawer.tsx index 45ba6d49db0..7edbb9a368d 100644 --- a/packages/manager/src/features/Backups/BackupDrawer.tsx +++ b/packages/manager/src/features/Backups/BackupDrawer.tsx @@ -12,7 +12,7 @@ import { DisplayPrice } from 'src/components/DisplayPrice'; import Drawer from 'src/components/Drawer'; import Grid from '@mui/material/Unstable_Grid2'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { withAccountSettings, WithAccountSettingsProps, diff --git a/packages/manager/src/features/Backups/BackupLinodes.tsx b/packages/manager/src/features/Backups/BackupLinodes.tsx index afb6a76becb..a89ec1a194c 100644 --- a/packages/manager/src/features/Backups/BackupLinodes.tsx +++ b/packages/manager/src/features/Backups/BackupLinodes.tsx @@ -3,8 +3,8 @@ import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { displayPrice as _displayPrice } from 'src/components/DisplayPrice/DisplayPrice'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { ExtendedType } from 'src/utilities/extendType'; import { ExtendedLinode } from './types'; diff --git a/packages/manager/src/features/Backups/BackupsTable.tsx b/packages/manager/src/features/Backups/BackupsTable.tsx index df33c075ed1..6974549a4bc 100644 --- a/packages/manager/src/features/Backups/BackupsTable.tsx +++ b/packages/manager/src/features/Backups/BackupsTable.tsx @@ -1,11 +1,11 @@ import { isEmpty } from 'ramda'; import * as React from 'react'; import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import BackupLinodes from './BackupLinodes'; import { ExtendedLinode } from './types'; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx index 614c6cad35c..26cb2584c5d 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx @@ -9,8 +9,8 @@ import * as React from 'react'; import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { TextTooltip } from 'src/components/TextTooltip'; import { Currency } from 'src/components/Currency'; @@ -20,11 +20,11 @@ import InlineMenuAction from 'src/components/InlineMenuAction'; import Link from 'src/components/Link'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; import TableContentWrapper from 'src/components/TableContentWrapper'; -import TableRow from 'src/components/TableRow'; +import { TableRow } from 'src/components/TableRow'; import Grid from '@mui/material/Unstable_Grid2'; import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants'; import { getShouldUseAkamaiBilling } from 'src/features/Billing/billingUtils'; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx index b8d4d5f9de2..8f5df10d83b 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx @@ -209,6 +209,7 @@ export const PayPalButton = (props: Props) => { 'An error occurred when trying to make a one-time PayPal payment.', { error } ); + setError('Unable to open PayPal.'); }; if (clientTokenLoading || isPending || !options['data-client-token']) { diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx index 416f3804933..4f035ce280b 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx @@ -4,7 +4,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface Actions { executePayment: () => void; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialog.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialog.tsx deleted file mode 100644 index 462e6805cb9..00000000000 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialog.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react'; -import { compose } from 'recompose'; - -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import Typography from 'src/components/core/Typography'; -import DialogActions, { - Props as ActionsProps, -} from './PaypalDialogActionButtons'; - -interface Props extends ActionsProps { - open: boolean; - usd: string; -} - -type CombinedProps = Props; - -interface Content { - title: string; - message?: string; - error?: string; -} - -const PaypalDialog: React.SFC<CombinedProps> = (props) => { - const { - open, - closeDialog, - usd, - ...rest // ...rest being the other unused ActionsProps - } = props; - - const renderActions = () => ( - <DialogActions closeDialog={closeDialog} {...rest} /> - ); - - const handleCloseDialog = () => closeDialog(false); - - const dialogContent: Content = props.isStagingPaypalPayment - ? { - title: 'Preparing Payment', - } - : props.paypalPaymentFailed - ? { - title: 'Payment failed', - error: 'Could not complete PayPal payment', - } - : { - title: 'Confirm Payment', - message: `Confirm PayPal payment for $${usd} USD to Linode LLC?`, - }; - - return ( - <ConfirmationDialog - open={open} - error={dialogContent.error} - title={dialogContent.title} - onClose={handleCloseDialog} - actions={renderActions()} - > - <Typography>{dialogContent.message || ''}</Typography> - </ConfirmationDialog> - ); -}; - -export default compose<CombinedProps, Props>(React.memo)(PaypalDialog); diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialogActionButtons.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialogActionButtons.tsx deleted file mode 100644 index 56444473012..00000000000 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/PaypalDialogActionButtons.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { compose } from 'recompose'; -import ActionsPanel from 'src/components/ActionsPanel'; -import Button from 'src/components/Button'; - -export interface Props { - isStagingPaypalPayment: boolean; - paypalPaymentFailed: boolean; - isExecutingPayment: boolean; - initExecutePayment: () => void; - closeDialog: (wasCanceled: boolean) => void; -} - -type CombinedProps = Props; - -const PaypalDialogActionButtons: React.SFC<CombinedProps> = (props) => { - const { - isStagingPaypalPayment, - paypalPaymentFailed, - isExecutingPayment, - initExecutePayment, - closeDialog, - } = props; - - /** intentionally displays "payment canceled" message to the user */ - const handleCancelPayment = () => closeDialog(true); - - const handleCloseDialog = () => closeDialog(false); - - if (isStagingPaypalPayment) { - return ( - <ActionsPanel> - <Button loading={true}>Preparing Payment</Button> - </ActionsPanel> - ); - } else if (paypalPaymentFailed) { - return ( - <ActionsPanel> - <Button onClick={handleCloseDialog}>Got it</Button> - </ActionsPanel> - ); - } else { - return ( - <ActionsPanel> - <Button - buttonType="secondary" - onClick={handleCancelPayment} - data-qa-cancel - > - Cancel - </Button> - <Button - buttonType="primary" - onClick={initExecutePayment} - loading={isExecutingPayment} - data-qa-submit - > - Confirm Payment - </Button> - </ActionsPanel> - ); - } -}; - -export default compose<CombinedProps, Props>(React.memo)( - PaypalDialogActionButtons -); diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx index 9fa9ba15e22..6801072ed18 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx @@ -15,7 +15,7 @@ import ErrorState from 'src/components/ErrorState'; import Grid from '@mui/material/Unstable_Grid2'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; import LinearProgress from 'src/components/LinearProgress'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { SupportLink } from 'src/components/SupportLink'; import TextField from 'src/components/TextField'; import PayPalErrorBoundary from 'src/features/Billing/BillingPanels/PaymentInfoPanel/PayPalErrorBoundary'; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PromoDialog.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PromoDialog.tsx index 52880a8dda7..caabbf4cf18 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PromoDialog.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PromoDialog.tsx @@ -4,7 +4,7 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { makeStyles } from 'tss-react/mui'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { addPromotion } from '@linode/api-v4/lib'; diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx index b73552bb0fa..c98865c9471 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx @@ -6,7 +6,7 @@ import Button from 'src/components/Button'; import { makeStyles } from 'tss-react/mui'; import EnhancedSelect, { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { getErrorMap } from 'src/utilities/errorUtils'; import { Country } from './types'; diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx index 5e911cd9db1..dab66eeae6b 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx @@ -8,7 +8,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import { addPaymentMethod } from '@linode/api-v4/lib'; import { useSnackbar } from 'notistack'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { CreditCardSchema } from '@linode/validation'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import NumberFormat, { NumberFormatProps } from 'react-number-format'; diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx index a1e8995a637..01624a972bc 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx @@ -8,7 +8,7 @@ import Grid from '@mui/material/Unstable_Grid2'; import LinearProgress from 'src/components/LinearProgress'; import GooglePayChip from '../GooglePayChip'; import AddCreditCardForm from './AddCreditCardForm'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { MAXIMUM_PAYMENT_METHODS } from 'src/constants'; import { PayPalChip } from '../PayPalChip'; import PayPalErrorBoundary from '../PayPalErrorBoundary'; @@ -147,6 +147,7 @@ export const AddPaymentMethodDrawer = (props: Props) => { onClose={onClose} setProcessing={setIsProcessing} renderError={renderError} + setMessage={setMessage} disabled={disabled} /> </PayPalErrorBoundary> diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PayPalChip.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PayPalChip.tsx index 8e08f85e28d..f55919fca0a 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PayPalChip.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PayPalChip.tsx @@ -10,14 +10,15 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { reportException } from 'src/exceptionReporting'; import Grid from '@mui/material/Unstable_Grid2'; import { - OnApproveBraintreeData, BraintreePayPalButtons, CreateBillingAgreementActions, FUNDING, OnApproveBraintreeActions, + OnApproveBraintreeData, usePayPalScriptReducer, } from '@paypal/react-paypal-js'; import { QueryClient, useQueryClient } from 'react-query'; +import { PaymentMessage } from 'src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer'; const useStyles = makeStyles()(() => ({ disabled: { @@ -30,11 +31,12 @@ interface Props { setProcessing: (processing: boolean) => void; onClose: () => void; renderError: (errorMsg: string) => JSX.Element; + setMessage: (message: PaymentMessage) => void; disabled: boolean; } export const PayPalChip = (props: Props) => { - const { onClose, disabled, setProcessing, renderError } = props; + const { onClose, disabled, setProcessing, renderError, setMessage } = props; const { data, isLoading, error: clientTokenError } = useClientToken(); const [{ options, isPending }, dispatch] = usePayPalScriptReducer(); const { classes, cx } = useStyles(); @@ -145,6 +147,10 @@ export const PayPalChip = (props: Props) => { 'A PayPal error occurred preventing a user from adding PayPal as a payment method.', { error } ); + setMessage({ + text: 'Unable to open PayPal.', + variant: 'error', + }); }; if (clientTokenError) { diff --git a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx index d58f0e6cef7..b5f1a62e6ca 100644 --- a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx +++ b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx @@ -8,7 +8,7 @@ import { import { APIError } from '@linode/api-v4/lib/types'; import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; import * as React from 'react'; -import { CSVLink } from 'react-csv'; +import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV'; import { useParams } from 'react-router-dom'; import Button from 'src/components/Button'; import Paper from 'src/components/core/Paper'; @@ -18,7 +18,7 @@ import { Currency } from 'src/components/Currency'; import Grid from '@mui/material/Unstable_Grid2'; import IconButton from 'src/components/IconButton'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { printInvoice } from 'src/features/Billing/PdfGenerator/PdfGenerator'; import useFlags from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account'; @@ -140,21 +140,14 @@ export const InvoiceDetail = () => { > {account && invoice && items && ( <> - {/* Hidden CSVLink component controlled by a ref. - This is done so we can use Button styles. */} - <CSVLink - ref={csvRef} + <DownloadCSV + csvRef={csvRef} + data={items} filename={`invoice-${invoice.date}.csv`} headers={csvHeaders} - data={items} - /> - <Button - buttonType="secondary" onClick={() => csvRef.current.link.click()} sx={{ ...sxDownloadButton, marginRight: '8px' }} - > - Download CSV - </Button> + /> <Button buttonType="secondary" onClick={() => printInvoicePDF(account, invoice, items)} diff --git a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx index 9160aa79173..632d7c0ea1a 100644 --- a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx +++ b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx @@ -3,15 +3,15 @@ import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import { Currency } from 'src/components/Currency'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; diff --git a/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts b/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts index 9b1fae46149..2b1fee400d2 100644 --- a/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts +++ b/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts @@ -219,7 +219,7 @@ export const printInvoice = ( // Create a separate page for each set of invoice items itemsChunks.forEach((itemsChunk, index) => { - doc.addImage(AkamaiLogo, 'JPEG', 160, 10, 120, 40); + doc.addImage(AkamaiLogo, 'JPEG', 160, 10, 120, 40, undefined, "MEDIUM"); const leftHeaderYPosition = addLeftHeader( doc, @@ -277,7 +277,7 @@ export const printPayment = ( }); doc.setFontSize(10); - doc.addImage(AkamaiLogo, 'JPEG', 160, 10, 120, 40); + doc.addImage(AkamaiLogo, 'JPEG', 160, 10, 120, 40, undefined, "MEDIUM"); const leftHeaderYPosition = addLeftHeader( doc, diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 8ecf303cb22..300f2aafcbf 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -35,7 +35,7 @@ import Grid from '@mui/material/Unstable_Grid2'; import LandingHeader from 'src/components/LandingHeader'; import Link from 'src/components/Link'; import MultipleIPInput from 'src/components/MultipleIPInput'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import ProductInformationBanner from 'src/components/ProductInformationBanner'; import Radio from 'src/components/Radio'; import { regionHelperText } from 'src/components/SelectRegionPanel/SelectRegionPanel'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx b/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx index 86f505e4885..ead87ca6bda 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx @@ -6,13 +6,13 @@ import ActionsPanel from 'src/components/ActionsPanel'; import AddNewLink from 'src/components/AddNewLink'; import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import TableBody from 'src/components/core/TableBody'; +import { TableBody } from 'src/components/TableBody'; import Typography from 'src/components/core/Typography'; import InlineMenuAction from 'src/components/InlineMenuAction'; -import Notice from 'src/components/Notice'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Notice } from 'src/components/Notice/Notice'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { useDatabaseMutation } from 'src/queries/databases'; import { ExtendedIP, stringToExtendedIP } from 'src/utilities/ipUtils'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx index 435c09035b8..ddb74bac9cc 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx @@ -7,7 +7,7 @@ import Button from 'src/components/Button'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; import MultipleIPInput from 'src/components/MultipleIPInput/MultipleIPInput'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { enforceIPMasks } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackupTableRow.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackupTableRow.tsx index 4a622e7e79b..5175386095a 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackupTableRow.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackupTableRow.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { DatabaseBackup } from '@linode/api-v4/lib/databases'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import DatabaseBackupActionMenu from './DatabaseBackupActionMenu'; import formatDate from 'src/utilities/formatDate'; import { parseAPIDate } from 'src/utilities/date'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx index 7e43c5a955a..e446a982182 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import Paper from 'src/components/core/Paper'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import DatabaseBackupTableRow from './DatabaseBackupTableRow'; import TableRowError from 'src/components/TableRowError'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx index b230372a707..30a3e092825 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx @@ -8,7 +8,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import TypeToConfirm from 'src/components/TypeToConfirm'; import Typography from 'src/components/core/Typography'; import { DialogProps } from 'src/components/Dialog/Dialog'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import formatDate from 'src/utilities/formatDate'; import { usePreferences } from 'src/queries/preferences'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx index d4a03a324fe..60a3f04146e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx @@ -6,7 +6,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TypeToConfirm from 'src/components/TypeToConfirm'; import { useDeleteDatabaseMutation } from 'src/queries/databases'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsResetPasswordDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsResetPasswordDialog.tsx index d3cbc52c682..25f932627b2 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsResetPasswordDialog.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsResetPasswordDialog.tsx @@ -4,7 +4,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useDatabaseCredentialsMutation } from 'src/queries/databases'; interface Props { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx index fc1a171962f..c6c2ad770b1 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx @@ -13,7 +13,7 @@ import RadioGroup from 'src/components/core/RadioGroup'; import Typography from 'src/components/core/Typography'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import { useDatabaseMutation } from 'src/queries/databases'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx index a536295b13f..4b7d55c4111 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx @@ -1,185 +1,43 @@ import * as React from 'react'; import { useHistory } from 'react-router-dom'; -import DocsIcon from 'src/assets/icons/docs.svg'; import DatabaseIcon from 'src/assets/icons/entityIcons/database.svg'; -import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; -import PointerIcon from 'src/assets/icons/pointer.svg'; -import YoutubeIcon from 'src/assets/icons/youtube.svg'; -import List from 'src/components/core/List'; -import ListItem from 'src/components/core/ListItem'; -import Typography from 'src/components/core/Typography'; -import Link from 'src/components/Link'; -import Placeholder from 'src/components/Placeholder'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; import ProductInformationBanner from 'src/components/ProductInformationBanner'; -import LinksSection from 'src/features/linodes/LinodesLanding/LinksSection'; -import LinkSubSection from 'src/features/linodes/LinodesLanding/LinksSubSection'; -import { - docsLink, - getLinkOnClick, - guidesMoreLinkText, - youtubeChannelLink, - youtubeMoreLinkLabel, - youtubeMoreLinkText, -} from 'src/utilities/emptyStateLandingUtils'; import { sendEvent } from 'src/utilities/ga'; -import { makeStyles } from 'tss-react/mui'; - -const gaCategory = 'Managed Databases landing page empty'; -const linkGAEventTemplate = { - category: gaCategory, - action: 'Click:link', -}; - -const guidesLinkData = [ - { - to: 'https://www.linode.com/docs/products/databases/managed-databases/', - text: 'Overview of Managed Databases', - }, - { - to: - 'https://www.linode.com/docs/products/databases/managed-databases/get-started/', - text: 'Get Started with Managed Databases', - }, - { - to: - 'https://www.linode.com/docs/products/databases/managed-databases/guides/database-engines/', - text: 'Choosing a Database Engine', - }, -]; - -const youtubeLinkData = [ - { - to: 'https://www.youtube.com/watch?v=loEVtzUN2i8', - text: 'Linode Managed Databases Overview', - }, - { - to: 'https://www.youtube.com/watch?v=dnV-6TtfYfY', - text: 'How to Choose the Right Database for Your Application', - }, - { - to: - 'https://www.youtube.com/playlist?list=PLTnRtjQN5ieZl3kM_jqfnK98uqYeXbfmC', - text: 'MySQL Beginner Series', - }, -]; - -const guideLinks = ( - <List> - {guidesLinkData.map((linkData) => ( - <ListItem key={linkData.to}> - <Link - to={linkData.to} - onClick={getLinkOnClick(linkGAEventTemplate, linkData.text)} - > - {linkData.text} - </Link> - </ListItem> - ))} - </List> -); - -const youtubeLinks = ( - <List> - {youtubeLinkData.map((linkData) => ( - <ListItem key={linkData.to}> - <Link - to={linkData.to} - onClick={getLinkOnClick(linkGAEventTemplate, linkData.text)} - > - {linkData.text} - <ExternalLinkIcon /> - </Link> - </ListItem> - ))} - </List> -); - -const useStyles = makeStyles()(() => ({ - root: { - '& > svg': { - transform: 'scale(0.8)', - }, - }, -})); +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './DatabaseLandingEmptyStateData'; -const DatabaseEmptyState = () => { - const { classes } = useStyles(); - const history = useHistory(); +export const DatabaseEmptyState = () => { + const { push } = useHistory(); return ( <> <ProductInformationBanner bannerLocation="Databases" warning important /> - <Placeholder - title="Databases" - subtitle="Fully managed cloud database clusters" - className={classes.root} - icon={DatabaseIcon} - isEntity + + <ResourcesSection buttonProps={[ { onClick: () => { sendEvent({ - category: gaCategory, + category: linkGAEvent.category, action: 'Click:button', label: 'Create Database Cluster', }); - history.push('/databases/create'); + push('/databases/create'); }, children: 'Create Database Cluster', }, ]} - linksSection={ - <LinksSection> - <LinkSubSection - title="Getting Started Guides" - icon={<DocsIcon />} - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick( - linkGAEventTemplate, - guidesMoreLinkText - )} - to={docsLink} - {...props} - > - {guidesMoreLinkText} - <PointerIcon /> - </Link> - )} - > - {guideLinks} - </LinkSubSection> - <LinkSubSection - title="Video Playlist" - icon={<YoutubeIcon />} - external - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick( - linkGAEventTemplate, - youtubeMoreLinkLabel - )} - to={youtubeChannelLink} - {...props} - > - {youtubeMoreLinkText} - <ExternalLinkIcon style={{ marginLeft: 8 }} /> - </Link> - )} - > - {youtubeLinks} - </LinkSubSection> - </LinksSection> - } - > - <Typography variant="subtitle1"> - Deploy popular database engines such as MySQL and PostgreSQL using - Linode’s performant, reliable, and fully managed database - solution. - </Typography> - </Placeholder> + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={DatabaseIcon} + linkGAEvent={linkGAEvent} + youtubeLinkData={youtubeLinkData} + /> </> ); }; - -export default React.memo(DatabaseEmptyState); diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx index 25bcb55741c..40c04827639 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx @@ -103,7 +103,7 @@ describe('Database Table', () => { expect( getByText( - 'Deploy popular database engines such as MySQL and PostgreSQL using Linode’s performant, reliable, and fully managed database solution.' + "Deploy popular database engines such as MySQL and PostgreSQL using Linode's performant, reliable, and fully managed database solution." ) ).toBeInTheDocument(); }); diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx index f823668fb88..5f5b56764e2 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx @@ -1,24 +1,24 @@ -import { DatabaseInstance } from '@linode/api-v4/lib/databases'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; -import { CircleProgress } from 'src/components/CircleProgress'; -import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import TableRow from 'src/components/core/TableRow'; import ErrorState from 'src/components/ErrorState'; +import Hidden from 'src/components/core/Hidden'; import LandingHeader from 'src/components/LandingHeader'; -import PaginationFooter from 'src/components/PaginationFooter'; import ProductInformationBanner from 'src/components/ProductInformationBanner'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableSortCell from 'src/components/TableSortCell'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { DatabaseEmptyState } from './DatabaseEmptyState'; +import { DatabaseInstance } from '@linode/api-v4/lib/databases'; +import { DatabaseRow } from './DatabaseRow'; +import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; +import { useDatabasesQuery } from 'src/queries/databases'; +import { useHistory } from 'react-router-dom'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; -import { useDatabasesQuery } from 'src/queries/databases'; -import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import DatabaseEmptyState from './DatabaseEmptyState'; -import { DatabaseRow } from './DatabaseRow'; const preferenceKey = 'databases'; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts new file mode 100644 index 00000000000..8c888ecd006 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts @@ -0,0 +1,73 @@ +import { + docsLink, + guidesMoreLinkText, + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + "Deploy popular database engines such as MySQL and PostgreSQL using Linode's performant, reliable, and fully managed database solution.", + subtitle: 'Fully managed cloud database clusters', + title: 'Databases', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: 'https://www.linode.com/docs/products/databases/managed-databases/', + text: 'Overview of Managed Databases', + }, + { + to: + 'https://www.linode.com/docs/products/databases/managed-databases/get-started/', + text: 'Get Started with Managed Databases', + }, + { + to: + 'https://www.linode.com/docs/products/databases/managed-databases/guides/database-engines/', + text: 'Choosing a Database Engine', + }, + ], + moreInfo: { + to: docsLink, + text: guidesMoreLinkText, + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=loEVtzUN2i8', + text: 'Linode Managed Databases Overview', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=dnV-6TtfYfY', + text: 'How to Choose the Right Database for Your Application', + external: true, + }, + { + to: + 'https://www.youtube.com/playlist?list=PLTnRtjQN5ieZl3kM_jqfnK98uqYeXbfmC', + text: 'MySQL Beginner Series', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'Managed Databases landing page empty', +}; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx index 08a084d037b..c2786e73b55 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx @@ -9,8 +9,8 @@ import Chip from 'src/components/core/Chip'; import Hidden from 'src/components/core/Hidden'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { Status } from 'src/components/StatusIcon/StatusIcon'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { capitalize } from 'src/utilities/capitalize'; diff --git a/packages/manager/src/features/Domains/CloneDomainDrawer.tsx b/packages/manager/src/features/Domains/CloneDomainDrawer.tsx index 532c6e43fe4..fc377d4ec33 100644 --- a/packages/manager/src/features/Domains/CloneDomainDrawer.tsx +++ b/packages/manager/src/features/Domains/CloneDomainDrawer.tsx @@ -10,7 +10,7 @@ import { useCloneDomainMutation } from 'src/queries/domains'; import { useFormik } from 'formik'; import { Domain } from '@linode/api-v4'; import { useProfile, useGrants } from 'src/queries/profile'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useHistory } from 'react-router-dom'; interface Props { diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 8f45528690a..0b4f1a0e30b 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -23,7 +23,7 @@ import { Theme } from '@mui/material/styles'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import MultipleIPInput from 'src/components/MultipleIPInput'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import TextField from 'src/components/TextField'; import { reportException } from 'src/exceptionReporting'; diff --git a/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx b/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx index 424b59cbe8a..45a8a8ddabf 100644 --- a/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx +++ b/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx @@ -4,7 +4,7 @@ import { CircleProgress } from 'src/components/CircleProgress'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import summaryPanelStyles from 'src/containers/SummaryPanels.styles'; import LandingHeader from 'src/components/LandingHeader'; import Grid from '@mui/material/Unstable_Grid2'; @@ -13,6 +13,7 @@ import Typography from 'src/components/core/Typography'; import { TagsPanel } from 'src/components/TagsPanel/TagsPanel'; import DomainRecords from '../DomainRecords'; import DeleteDomain from '../DeleteDomain'; +import { DownloadDNSZoneFileButton } from '../DownloadDNSZoneFileButton'; import { useDomainQuery, useDomainRecordsQuery, @@ -135,6 +136,12 @@ export const DomainDetail = () => { errorText: updateError, }, }} + extraActions={ + <DownloadDNSZoneFileButton + domainId={domain.id} + domainLabel={domain.domain} + /> + } /> {location.state && location.state.recordError && ( <Notice diff --git a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx index 5b8b0dbf865..7bcbb68aace 100644 --- a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx +++ b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx @@ -25,7 +25,7 @@ import Button, { ButtonProps } from 'src/components/Button'; import Drawer from 'src/components/Drawer'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import MultipleIPInput from 'src/components/MultipleIPInput'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Domains/DomainRecords.tsx b/packages/manager/src/features/Domains/DomainRecords.tsx index bea5db311e9..8392046b2f6 100644 --- a/packages/manager/src/features/Domains/DomainRecords.tsx +++ b/packages/manager/src/features/Domains/DomainRecords.tsx @@ -27,17 +27,17 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import withFeatureFlags, { FeatureFlagConsumerProps, diff --git a/packages/manager/src/features/Domains/DomainTableRow.tsx b/packages/manager/src/features/Domains/DomainTableRow.tsx index 16fa2840718..d933cd5c539 100644 --- a/packages/manager/src/features/Domains/DomainTableRow.tsx +++ b/packages/manager/src/features/Domains/DomainTableRow.tsx @@ -6,8 +6,8 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import ActionMenu, { Handlers } from './DomainActionMenu'; import { getDomainDisplayType } from './domainUtils'; diff --git a/packages/manager/src/features/Domains/DomainZoneImportDrawer.tsx b/packages/manager/src/features/Domains/DomainZoneImportDrawer.tsx index 88146d9dc2c..6ac4f2fe73c 100644 --- a/packages/manager/src/features/Domains/DomainZoneImportDrawer.tsx +++ b/packages/manager/src/features/Domains/DomainZoneImportDrawer.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { ImportZonePayload } from '@linode/api-v4/lib/domains'; import { useFormik } from 'formik'; diff --git a/packages/manager/src/features/Domains/DomainsEmptyLandingPage.tsx b/packages/manager/src/features/Domains/DomainsEmptyLandingPage.tsx new file mode 100644 index 00000000000..7a380a6f093 --- /dev/null +++ b/packages/manager/src/features/Domains/DomainsEmptyLandingPage.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import DomainIcon from 'src/assets/icons/entityIcons/domain.svg'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; +import { sendEvent } from 'src/utilities/ga'; +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './DomainsEmptyResourcesData'; + +interface Props { + navigateToCreate: () => void; + openImportZoneDrawer: () => void; +} + +export const DomainsEmptyLandingState = (props: Props) => { + const { navigateToCreate, openImportZoneDrawer } = props; + + return ( + <ResourcesSection + buttonProps={[ + { + onClick: () => { + sendEvent({ + category: linkGAEvent.category, + action: 'Click:button', + label: 'Create Domain', + }); + navigateToCreate(); + }, + children: 'Create Domain', + }, + { + onClick: () => { + sendEvent({ + category: linkGAEvent.category, + action: 'Click:button', + label: 'Import a Zone', + }); + openImportZoneDrawer(); + }, + children: 'Import a Zone', + }, + ]} + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={DomainIcon} + linkGAEvent={linkGAEvent} + youtubeLinkData={youtubeLinkData} + /> + ); +}; diff --git a/packages/manager/src/features/Domains/DomainsEmptyResourcesData.ts b/packages/manager/src/features/Domains/DomainsEmptyResourcesData.ts new file mode 100644 index 00000000000..1628a4d8f60 --- /dev/null +++ b/packages/manager/src/features/Domains/DomainsEmptyResourcesData.ts @@ -0,0 +1,71 @@ +import { + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + 'A comprehensive, reliable, and fast DNS service that provides easy domain management for no additional cost.', + + subtitle: 'Easy domain management', + title: 'Domains', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: 'https://www.linode.com/docs/products/networking/dns-manager/', + text: 'Overview of DNS Manager', + }, + { + to: + 'https://www.linode.com/docs/products/networking/dns-manager/get-started/', + text: 'Getting Started with DNS Manager', + }, + { + to: + 'https://www.linode.com/docs/products/networking/dns-manager/guides/create-domain/', + text: 'Create a Domain Zone', + }, + ], + moreInfo: { + to: 'https://www.linode.com/docs/products/networking/dns-manager/guides/', + text: 'View additional DNS Manager guides', + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=ganwcCm53Qs', + text: 'Linode DNS Manager | Total Control Over Your DNS Records', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=Vb1JsfZlFLE', + text: 'Using Domains with Your Server | Common DNS Configurations', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=mKfx4ryuMtY', + text: 'Connect a Domain to a Linode Server', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'Domains landing page empty', +}; diff --git a/packages/manager/src/features/Domains/DomainsLanding.tsx b/packages/manager/src/features/Domains/DomainsLanding.tsx index a2441a4e1ce..c691702aac0 100644 --- a/packages/manager/src/features/Domains/DomainsLanding.tsx +++ b/packages/manager/src/features/Domains/DomainsLanding.tsx @@ -2,19 +2,15 @@ import * as React from 'react'; import { Domain } from '@linode/api-v4/lib/domains'; import { useSnackbar } from 'notistack'; import { useHistory, useLocation } from 'react-router-dom'; -import DomainIcon from 'src/assets/icons/entityIcons/domain.svg'; import Button from 'src/components/Button'; import { CircleProgress } from 'src/components/CircleProgress'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; import { DeletionDialog } from 'src/components/DeletionDialog/DeletionDialog'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ErrorState from 'src/components/ErrorState'; import LandingHeader from 'src/components/LandingHeader'; -import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; -import Placeholder from 'src/components/Placeholder'; +import { Notice } from 'src/components/Notice/Notice'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import DisableDomainDialog from './DisableDomainDialog'; import { Handlers as DomainHandlers } from './DomainActionMenu'; @@ -22,7 +18,7 @@ import DomainBanner from './DomainBanner'; import DomainRow from './DomainTableRow'; import DomainZoneImportDrawer from './DomainZoneImportDrawer'; import { useProfile } from 'src/queries/profile'; -import { useLinodesQuery } from 'src/queries/linodes'; +import { useLinodesQuery } from 'src/queries/linodes/linodes'; import { useDeleteDomainMutation, useDomainsQuery, @@ -30,16 +26,17 @@ import { } from 'src/queries/domains'; import { usePagination } from 'src/hooks/usePagination'; import { useOrder } from 'src/hooks/useOrder'; -import Table from 'src/components/Table/Table'; -import TableHead from 'src/components/core/TableHead'; -import TableRow from 'src/components/TableRow/TableRow'; -import TableBody from 'src/components/core/TableBody'; -import TableSortCell from 'src/components/TableSortCell/TableSortCell'; -import TableCell from 'src/components/core/TableCell'; -import PaginationFooter from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableSortCell } from 'src/components/TableSortCell'; +import { TableCell } from 'src/components/TableCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import Hidden from 'src/components/core/Hidden'; import { CloneDomainDrawer } from './CloneDomainDrawer'; import { EditDomainDrawer } from './EditDomainDrawer'; +import { DomainsEmptyLandingState } from './DomainsEmptyLandingPage'; const DOMAIN_CREATE_ROUTE = '/domains/create'; @@ -207,35 +204,10 @@ export const DomainsLanding: React.FC<Props> = (props) => { if (domains?.results === 0) { return ( <> - <DocumentTitleSegment segment="Domains" /> - <Placeholder - title="Domains" - isEntity - icon={DomainIcon} - buttonProps={[ - { - onClick: navigateToCreate, - children: 'Create Domain', - }, - { - onClick: openImportZoneDrawer, - children: 'Import a Zone', - }, - ]} - > - <Typography variant="subtitle1"> - Create a Domain, add Domain records, import zones and domains. - </Typography> - <Typography variant="subtitle1"> - <Link to="https://www.linode.com/docs/platform/manager/dns-manager-new-manager/"> - Get help managing your Domains - </Link> -  or  - <Link to="https://www.linode.com/docs/"> - visit our guides and tutorials. - </Link> - </Typography> - </Placeholder> + <DomainsEmptyLandingState + navigateToCreate={navigateToCreate} + openImportZoneDrawer={openImportZoneDrawer} + /> <DomainZoneImportDrawer open={importDrawerOpen} onClose={closeImportZoneDrawer} diff --git a/packages/manager/src/features/Domains/DownloadDNSZoneFileButton.test.tsx b/packages/manager/src/features/Domains/DownloadDNSZoneFileButton.test.tsx new file mode 100644 index 00000000000..61e80a485ee --- /dev/null +++ b/packages/manager/src/features/Domains/DownloadDNSZoneFileButton.test.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { DownloadDNSZoneFileButton } from './DownloadDNSZoneFileButton'; +import { downloadFile } from 'src/utilities/downloadFile'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +jest.mock('@linode/api-v4/lib/domains', () => ({ + getDNSZoneFile: jest.fn().mockResolvedValue({ + zone_file: [ + 'example.com. 86400 IN SOA ns1.linode.com. test.example.com. 2013072519 14400 14400 1209600 86400', + ], + }), +})); + +jest.mock('src/utilities/downloadFile', () => ({ + downloadFile: jest.fn(), +})); + +describe('DownloadDNSZoneFileButton', () => { + it('renders button text correctly', () => { + const { getByText } = renderWithTheme( + <DownloadDNSZoneFileButton domainLabel="test.com" domainId={1} /> + ); + expect(getByText('Download DNS Zone File')).toBeInTheDocument(); + }); + + it('downloads DNS zone file when button is clicked', async () => { + const { getByText } = renderWithTheme( + <DownloadDNSZoneFileButton domainLabel="test.com" domainId={1} /> + ); + fireEvent.click(getByText('Download DNS Zone File')); + await waitFor(() => expect(downloadFile).toHaveBeenCalledTimes(1)); + expect(downloadFile).toHaveBeenCalledWith( + 'test.com.txt', + 'example.com. 86400 IN SOA ns1.linode.com. test.example.com. 2013072519 14400 14400 1209600 86400' + ); + }); +}); diff --git a/packages/manager/src/features/Domains/DownloadDNSZoneFileButton.tsx b/packages/manager/src/features/Domains/DownloadDNSZoneFileButton.tsx new file mode 100644 index 00000000000..725bbc5ec98 --- /dev/null +++ b/packages/manager/src/features/Domains/DownloadDNSZoneFileButton.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import Button from 'src/components/Button'; +import { downloadFile } from 'src/utilities/downloadFile'; +import { getDNSZoneFile } from '@linode/api-v4/lib/domains'; + +type Props = { + domainId: number; + domainLabel: string; +}; + +export const DownloadDNSZoneFileButton = ({ domainId, domainLabel }: Props) => { + const handleClick = async () => { + const data = await getDNSZoneFile(domainId); + const zoneFileContent = data?.zone_file.join('\n'); + if (zoneFileContent) { + downloadFile(`${domainLabel}.txt`, zoneFileContent); + } + }; + + return ( + <Button onClick={handleClick} buttonType="secondary"> + Download DNS Zone File + </Button> + ); +}; diff --git a/packages/manager/src/features/Domains/EditDomainDrawer.tsx b/packages/manager/src/features/Domains/EditDomainDrawer.tsx index 004cbeb0845..52c670d1ae7 100644 --- a/packages/manager/src/features/Domains/EditDomainDrawer.tsx +++ b/packages/manager/src/features/Domains/EditDomainDrawer.tsx @@ -5,7 +5,7 @@ import FormControlLabel from 'src/components/core/FormControlLabel'; import RadioGroup from 'src/components/core/RadioGroup'; import Drawer from 'src/components/Drawer'; import MultipleIPInput from 'src/components/MultipleIPInput'; -import Notice from 'src/components/Notice/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; import TextField from 'src/components/TextField'; diff --git a/packages/manager/src/features/Domains/SortableTableHead.tsx b/packages/manager/src/features/Domains/SortableTableHead.tsx deleted file mode 100644 index 28a773e58d7..00000000000 --- a/packages/manager/src/features/Domains/SortableTableHead.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import TableHead from 'src/components/core/TableHead'; -import { OrderByProps } from 'src/components/OrderBy'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; - -type ClassNames = 'root' | 'label'; - -const styles = (theme: Theme) => - createStyles({ - root: {}, - label: { - paddingLeft: 65, - }, - }); - -type CombinedProps<T> = Omit<OrderByProps<T>, 'data'> & WithStyles<ClassNames>; - -const SortableTableHead = <T extends unknown>(props: CombinedProps<T>) => { - const { order, orderBy, handleOrderChange, classes } = props; - - const isActive = (label: string) => label === orderBy; - - return ( - <TableHead data-qa-table-head={order}> - <TableRow> - <TableSortCell - label="domain" - direction={order} - active={isActive('domain')} - handleClick={handleOrderChange} - data-qa-sort-domain={order} - className={classes.label} - > - Domain - </TableSortCell> - <TableSortCell - label="type" - direction={order} - active={isActive('type')} - handleClick={handleOrderChange} - data-qa-sort-type={order} - > - Type - </TableSortCell> - <TableSortCell - data-qa-domain-type-header={order} - active={orderBy === 'status'} - label="status" - direction={order} - handleClick={handleOrderChange} - > - Status - </TableSortCell> - <TableSortCell - data-qa-domain-type-header={order} - active={orderBy === 'updated'} - label="updated" - direction={order} - handleClick={handleOrderChange} - > - Last Modified - </TableSortCell> - <TableCell /> - </TableRow> - </TableHead> - ); -}; - -const styled = withStyles(styles); - -export default styled(SortableTableHead); diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx index efed81a1fb2..1a952d8fb15 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx @@ -6,7 +6,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { queryKey, useCreateTransfer } from 'src/queries/entityTransfers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { sendEntityTransferCreateEvent } from 'src/utilities/ga'; diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/LinodeTransferTable.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/LinodeTransferTable.tsx index 559bc349dcd..1c1d3199eff 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/LinodeTransferTable.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/LinodeTransferTable.tsx @@ -5,12 +5,12 @@ import { useTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import SelectableTableRow from 'src/components/SelectableTableRow'; -import TableCell from 'src/components/TableCell/TableCell'; +import { TableCell } from 'src/components/TableCell'; import TableContentWrapper from 'src/components/TableContentWrapper'; import { useSpecificTypes } from 'src/queries/types'; import { Entity, TransferEntity } from './transferReducer'; import TransferTable from './TransferTable'; -import { useLinodesByIdQuery } from 'src/queries/linodes'; +import { useLinodesQuery } from 'src/queries/linodes/linodes'; import { usePagination } from 'src/hooks/usePagination'; import { useRegionsQuery } from 'src/queries/regions'; import { extendType } from 'src/utilities/extendType'; @@ -22,19 +22,13 @@ interface Props { handleToggle: (linode: Entity) => void; } -export const LinodeTransferTable: React.FC<Props> = (props) => { +export const LinodeTransferTable = (props: Props) => { const { handleRemove, handleSelect, handleToggle, selectedLinodes } = props; const [searchText, setSearchText] = React.useState(''); const pagination = usePagination(); - const { - data, - isError, - isLoading, - error, - dataUpdatedAt, - } = useLinodesByIdQuery( + const { data, isError, isLoading, error, dataUpdatedAt } = useLinodesQuery( { page: pagination.page, page_size: pagination.pageSize, @@ -42,7 +36,8 @@ export const LinodeTransferTable: React.FC<Props> = (props) => { generateLinodeXFilter(searchText) ); - const linodesCurrentPage = Object.values(data?.linodes ?? {}); + const linodesCurrentPage = data?.data ?? []; + const hasSelectedAll = linodesCurrentPage.every((thisLinode) => Boolean(selectedLinodes[thisLinode.id]) diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferTable.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferTable.tsx index 422beb23404..590e7d51e5c 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferTable.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferTable.tsx @@ -2,14 +2,14 @@ import * as React from 'react'; import CheckBox from 'src/components/CheckBox'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; const useStyles = makeStyles((theme: Theme) => ({ root: { diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferCancelDialog.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferCancelDialog.tsx index 1fb9f84a006..5aa72f91271 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferCancelDialog.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferCancelDialog.tsx @@ -10,7 +10,7 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { makeStyles } from '@mui/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { queryKey } from 'src/queries/entityTransfers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { sendEntityTransferCancelEvent } from 'src/utilities/ga'; diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx index af5c858f693..55fa3894ff6 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx @@ -14,7 +14,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { queryKey, TRANSFER_FILTERS, diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx index ec9c50eb94c..e3b2b0cfc03 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx @@ -4,7 +4,7 @@ import { DateTime } from 'luxon'; import { update } from 'ramda'; import * as React from 'react'; import Button from 'src/components/Button'; -import CopyableTextField from 'src/components/CopyableTextField'; +import { CopyableTextField } from 'src/components/CopyableTextField/CopyableTextField'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import ToolTip from 'src/components/core/Tooltip'; diff --git a/packages/manager/src/features/EntityTransfers/RenderTransferRow.tsx b/packages/manager/src/features/EntityTransfers/RenderTransferRow.tsx index 82f2a647e4a..fdca4b8353c 100644 --- a/packages/manager/src/features/EntityTransfers/RenderTransferRow.tsx +++ b/packages/manager/src/features/EntityTransfers/RenderTransferRow.tsx @@ -5,8 +5,8 @@ import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { capitalize } from 'src/utilities/capitalize'; import { pluralize } from 'src/utilities/pluralize'; import ActionMenu from './TransfersPendingActionMenu'; diff --git a/packages/manager/src/features/EntityTransfers/TransfersTable.tsx b/packages/manager/src/features/EntityTransfers/TransfersTable.tsx index 3affcbd2b38..e29089aa391 100644 --- a/packages/manager/src/features/EntityTransfers/TransfersTable.tsx +++ b/packages/manager/src/features/EntityTransfers/TransfersTable.tsx @@ -8,13 +8,13 @@ import Accordion from 'src/components/Accordion'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; import TableContentWrapper from 'src/components/TableContentWrapper'; -import TableRow from 'src/components/TableRow'; +import { TableRow } from 'src/components/TableRow'; import { capitalize } from 'src/utilities/capitalize'; import ConfirmTransferCancelDialog from './EntityTransfersLanding/ConfirmTransferCancelDialog'; import TransferDetailsDialog from './EntityTransfersLanding/TransferDetailsDialog'; diff --git a/packages/manager/src/features/Events/EventRow.tsx b/packages/manager/src/features/Events/EventRow.tsx index e945cd47284..017b503ab5f 100644 --- a/packages/manager/src/features/Events/EventRow.tsx +++ b/packages/manager/src/features/Events/EventRow.tsx @@ -8,8 +8,8 @@ import { Theme } from '@mui/material/styles'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import HighlightedMarkdown from 'src/components/HighlightedMarkdown'; import renderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import eventMessageGenerator from 'src/eventMessageGenerator'; import { parseAPIDate } from 'src/utilities/date'; import { getEntityByIDFromStore } from 'src/utilities/getEntityByIDFromStore'; diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx index e07c590b868..e39925c57ac 100644 --- a/packages/manager/src/features/Events/EventsLanding.tsx +++ b/packages/manager/src/features/Events/EventsLanding.tsx @@ -9,13 +9,13 @@ import { compose } from 'recompose'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import H1Header from 'src/components/H1Header'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx index 168b752dda0..48d59b5676b 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx @@ -5,7 +5,7 @@ import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; import Link from 'src/components/Link'; import LinodeMultiSelect from 'src/components/LinodeMultiSelect/LinodeMultiSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { SupportLink } from 'src/components/SupportLink'; import { useGrants, useProfile } from 'src/queries/profile'; import { getEntityIdsByPermission } from 'src/utilities/grants'; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx index a569a43e635..bb184eb6be7 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import ActionMenu, { Props as ActionProps } from './FirewallDeviceActionMenu'; export const FirewallDeviceRow: React.FC<ActionProps> = (props) => { diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDevicesTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDevicesTable.tsx index fd7ed107201..7e3cefb54f8 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDevicesTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDevicesTable.tsx @@ -1,15 +1,15 @@ import { FirewallDevice } from '@linode/api-v4/lib/firewalls/types'; import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; import TableContentWrapper from 'src/components/TableContentWrapper'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import FirewallDeviceRow from './FirewallDeviceRow'; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallLinodesLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallLinodesLanding.tsx index 7335de16804..75dbb0ab82d 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallLinodesLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallLinodesLanding.tsx @@ -4,7 +4,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import AddDeviceDrawer from './AddDeviceDrawer'; import FirewallDevicesTable from './FirewallDevicesTable'; import RemoveDeviceDialog from './RemoveDeviceDialog'; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx index d0946852416..cbfec239d2c 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx @@ -22,7 +22,7 @@ import Drawer from 'src/components/Drawer'; import Select from 'src/components/EnhancedSelect'; import { Item } from 'src/components/EnhancedSelect/Select'; import MultipleIPInput from 'src/components/MultipleIPInput/MultipleIPInput'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import TextField from 'src/components/TextField'; import { diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx index 880c7449fd5..ff94292f98c 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRulesLanding.tsx @@ -11,7 +11,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Prompt from 'src/components/Prompt'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import FirewallRuleDrawer, { Mode } from './FirewallRuleDrawer'; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx index 500500ca124..a7945e99446 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx @@ -6,7 +6,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; import LinodeMultiSelect from 'src/components/LinodeMultiSelect/LinodeMultiSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; import { useGrants } from 'src/queries/profile'; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallEmptyState.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallEmptyState.tsx deleted file mode 100644 index ccd3171a4ac..00000000000 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallEmptyState.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import Typography from 'src/components/core/Typography'; -import Link from 'src/components/Link'; -import Placeholder from 'src/components/Placeholder'; -import FirewallIcon from 'src/assets/icons/entityIcons/firewall.svg'; - -interface Props { - openAddFirewallDrawer: () => void; -} - -const FirewallEmptyState: React.FC<Props> = (props) => { - const { openAddFirewallDrawer } = props; - return ( - <Placeholder - title={'Firewalls'} - icon={FirewallIcon} - isEntity - buttonProps={[ - { - onClick: openAddFirewallDrawer, - children: 'Create Firewall', - }, - ]} - > - <Typography variant="subtitle1"> - <div>Control network access to your Linodes from the Cloud.</div> - <Link to="https://www.linode.com/docs/guides/getting-started-with-cloud-firewall/"> - Get started with Cloud Firewalls. - </Link> - </Typography> - </Placeholder> - ); -}; - -export default React.memo(FirewallEmptyState); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx index 52afb293125..9429d7d4983 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx @@ -6,20 +6,20 @@ import { useFirewallsQuery } from 'src/queries/firewalls'; import CreateFirewallDrawer from './CreateFirewallDrawer'; import { ActionHandlers as FirewallHandlers } from './FirewallActionMenu'; import FirewallDialog, { Mode } from './FirewallDialog'; -import FirewallEmptyState from './FirewallEmptyState'; +import { FirewallLandingEmptyState } from './FirewallLandingEmptyState'; import FirewallRow from './FirewallRow'; import { usePagination } from 'src/hooks/usePagination'; import { useOrder } from 'src/hooks/useOrder'; import ErrorState from 'src/components/ErrorState/ErrorState'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import Table from 'src/components/Table/Table'; -import TableHead from 'src/components/core/TableHead'; -import TableRow from 'src/components/TableRow/TableRow'; +import { Table } from 'src/components/Table'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; -import TableCell from 'src/components/TableCell/TableCell'; -import TableBody from 'src/components/core/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableBody } from 'src/components/TableBody'; import Hidden from 'src/components/core/Hidden'; -import PaginationFooter from 'src/components/PaginationFooter'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; const preferenceKey = 'firewalls'; @@ -113,7 +113,7 @@ const FirewallLanding = () => { if (data?.results === 0) { return ( <> - <FirewallEmptyState openAddFirewallDrawer={onOpenCreateDrawer} /> + <FirewallLandingEmptyState openAddFirewallDrawer={onOpenCreateDrawer} /> <CreateFirewallDrawer open={isCreateFirewallDrawerOpen} onClose={() => setIsCreateFirewallDrawerOpen(false)} diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyResourcesData.ts b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyResourcesData.ts new file mode 100644 index 00000000000..40332e7b935 --- /dev/null +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyResourcesData.ts @@ -0,0 +1,69 @@ +import { + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + 'Control network traffic to and from Linode Compute Instances with a simple management interface', + + subtitle: 'Secure cloud-based firewall', + title: 'Firewalls', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: + 'https://www.linode.com/docs/products/networking/cloud-firewall/get-started/', + text: 'Getting Started with Cloud Firewalls', + }, + { + to: + 'https://www.linode.com/docs/products/networking/cloud-firewall/guides/manage-firewall-rules/', + text: 'Manage Firewall Rules', + }, + { + to: + 'https://www.linode.com/docs/products/networking/cloud-firewall/guides/comparing-firewalls/', + text: 'Comparing Cloud Firewalls to Linux Firewall Software', + }, + ], + moreInfo: { + to: + 'https://www.linode.com/docs/products/networking/cloud-firewall/guides/', + text: 'View additional Firewalls guides', + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=GsUUtsI_RSA', + text: + 'Linode Cloud Firewall Explained | Clear and Intuitive Network Control to and from All your Servers', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=H7wM5mDI1-k', + text: 'Simple Scalable Network Security | Linode Cloud Firewall', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'Firewall landing page empty', +}; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyState.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyState.tsx new file mode 100644 index 00000000000..016a7750e62 --- /dev/null +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLandingEmptyState.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import FirewallIcon from 'src/assets/icons/entityIcons/firewall.svg'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; +import { sendEvent } from 'src/utilities/ga'; +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './FirewallLandingEmptyResourcesData'; + +interface Props { + openAddFirewallDrawer: () => void; +} + +export const FirewallLandingEmptyState = (props: Props) => { + const { openAddFirewallDrawer } = props; + + return ( + <ResourcesSection + buttonProps={[ + { + onClick: () => { + sendEvent({ + category: linkGAEvent.category, + action: 'Click:button', + label: 'Create Firewall', + }); + openAddFirewallDrawer(); + }, + children: 'Create Firewall', + }, + ]} + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={FirewallIcon} + linkGAEvent={linkGAEvent} + youtubeLinkData={youtubeLinkData} + /> + ); +}; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index 2be5800e7be..b88e239c8fc 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -6,8 +6,8 @@ import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { useAllFirewallDevicesQuery } from 'src/queries/firewalls'; import { capitalize } from 'src/utilities/capitalize'; import ActionMenu, { ActionHandlers } from './FirewallActionMenu'; diff --git a/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx b/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx index 3ec87f5502f..4084055e3dc 100644 --- a/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx +++ b/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx @@ -7,7 +7,7 @@ import { Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useAccount, useMutateAccount } from 'src/queries/account'; import { useMutateProfile, useProfile } from 'src/queries/profile'; import { useNotificationsQuery } from 'src/queries/accountNotifications'; diff --git a/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.tsx b/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.tsx index 089c3edc8d8..84f479c4a81 100644 --- a/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.tsx +++ b/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.tsx @@ -2,7 +2,7 @@ import { Region } from '@linode/api-v4/lib/regions/types'; import * as React from 'react'; import Typography from 'src/components/core/Typography'; import ExternalLink from 'src/components/ExternalLink'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useRegionsQuery } from 'src/queries/regions'; export interface Props { diff --git a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx index 3c2132e8dc6..b8712770434 100644 --- a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx +++ b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx @@ -6,7 +6,7 @@ import { compose } from 'recompose'; import { createStyles, withStyles, WithStyles, WithTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import EnhancedSelect, { Item } from 'src/components/EnhancedSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { selectStyles } from 'src/features/TopMenu/SearchBar'; import windowIsNarrowerThan from 'src/utilities/breakpoints'; import withSearch, { AlgoliaState as AlgoliaProps } from '../SearchHOC'; diff --git a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx index eb1cf315f18..5a26e985a2c 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx @@ -8,7 +8,7 @@ import { Theme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import Box from '@mui/material/Box'; import H1Header from 'src/components/H1Header'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { COMMUNITY_SEARCH_URL, DOCS_SEARCH_URL } from 'src/constants'; import { getQueryParam } from 'src/utilities/queryParams'; diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index bb57f7d3079..79ec43fbfab 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -3,8 +3,8 @@ import { Image } from '@linode/api-v4/lib/images'; import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import Typography from 'src/components/core/Typography'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { useProfile } from 'src/queries/profile'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/src/features/Images/ImageUpload.tsx b/packages/manager/src/features/Images/ImageUpload.tsx index fc6127cf5e4..70a5ca55175 100644 --- a/packages/manager/src/features/Images/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImageUpload.tsx @@ -14,7 +14,7 @@ import { RegionSelect } from 'src/components/EnhancedSelect/variants/RegionSelec import FileUploader from 'src/components/FileUploader/FileUploader'; import Link from 'src/components/Link'; import LinodeCLIModal from 'src/components/LinodeCLIModal'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Prompt from 'src/components/Prompt'; import TextField from 'src/components/TextField'; import { Dispatch } from 'src/hooks/types'; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index 8ddba82c780..bebadebed2b 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -12,7 +12,7 @@ import Box from 'src/components/core/Box'; import Paper from 'src/components/core/Paper'; import Typography from 'src/components/core/Typography'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { resetEventsPolling } from 'src/eventsPolling'; import DiskSelect from 'src/features/linodes/DiskSelect'; diff --git a/packages/manager/src/features/Images/ImagesDrawer.tsx b/packages/manager/src/features/Images/ImagesDrawer.tsx index 3f4d4f559cb..d5305a591ac 100644 --- a/packages/manager/src/features/Images/ImagesDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesDrawer.tsx @@ -11,7 +11,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SectionErrorBoundary from 'src/components/SectionErrorBoundary'; import TextField from 'src/components/TextField'; import { IMAGE_DEFAULT_LIMIT } from 'src/constants'; diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 5bcaeded547..b088cfc5264 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -14,21 +14,21 @@ import Hidden from 'src/components/core/Hidden'; import Paper from 'src/components/core/Paper'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ErrorState from 'src/components/ErrorState'; import LandingHeader from 'src/components/LandingHeader'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; -import PaginationFooter from 'src/components/PaginationFooter/PaginationFooter'; +import { Notice } from 'src/components/Notice/Notice'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import Placeholder from 'src/components/Placeholder'; -import Table from 'src/components/Table/Table'; -import TableCell from 'src/components/TableCell/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; -import TableSortCell from 'src/components/TableSortCell/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { listToItemsByID } from 'src/queries/base'; diff --git a/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx b/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx index 5fdbdc2f2de..5a1886efd64 100644 --- a/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx +++ b/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx @@ -3,8 +3,8 @@ import Chip from 'src/components/core/Chip'; import Hidden from 'src/components/core/Hidden'; import Grid from '@mui/material/Unstable_Grid2'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import ActionMenu from './ClusterActionMenu'; import { Link } from 'react-router-dom'; import { makeStyles } from '@mui/styles'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 36a18450fd3..c750795cb8e 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -16,7 +16,7 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { RegionSelect } from 'src/components/EnhancedSelect/variants/RegionSelect'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { regionHelperText } from 'src/components/SelectRegionPanel/SelectRegionPanel'; import TextField from 'src/components/TextField'; import { diff --git a/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx b/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx index 7342dd82f12..e53225401be 100644 --- a/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx +++ b/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { CheckoutBar } from 'src/components/CheckoutBar/CheckoutBar'; import { CircleProgress } from 'src/components/CircleProgress'; import Divider from 'src/components/core/Divider'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import renderGuard from 'src/components/RenderGuard'; import EUAgreementCheckbox from 'src/features/Account/Agreements/EUAgreementCheckbox'; import { diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx index e9d764f92f1..ab01be88f44 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx @@ -4,7 +4,7 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; import TypeToConfirm from 'src/components/TypeToConfirm'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { usePreferences } from 'src/queries/preferences'; import { useDeleteKubernetesClusterMutation } from 'src/queries/kubernetes'; import { KubeNodePoolResponse } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx index a3baeddd751..83e693ffe4b 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx @@ -6,7 +6,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SelectPlanQuantityPanel from 'src/features/linodes/LinodesCreate/SelectPlanQuantityPanel'; import { useCreateNodePoolMutation } from 'src/queries/kubernetes'; import { useAllTypes } from 'src/queries/types'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx index 196be231f0c..16a0902a284 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx @@ -11,7 +11,7 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Grid from '@mui/material/Unstable_Grid2'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; import { AutoscaleSettings, KubeNodePoolResponse } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx index cebd0c4a2ed..57393fad749 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx @@ -60,6 +60,8 @@ const NodePool: React.FC<Props> = (props) => { alignItems="center" justifyContent="space-between" spacing={2} + data-qa-node-pool-section + data-qa-node-pool-id={poolId} > <Grid> <Typography variant="h2">{typeLabel}</Typography> diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx index 7651d3efce4..719361ab121 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx @@ -5,20 +5,20 @@ import { Link } from 'react-router-dom'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; +import { TableBody } from 'src/components/TableBody'; import TableFooter from 'src/components/core/TableFooter'; -import TableHead from 'src/components/core/TableHead'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; import TableContentWrapper from 'src/components/TableContentWrapper'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { transitionText } from 'src/features/linodes/transitions'; import useLinodes from 'src/hooks/useLinodes'; import { useReduxLoad } from 'src/hooks/useReduxLoad'; @@ -237,7 +237,11 @@ export const NodeRow: React.FC<NodeRowProps> = React.memo((props) => { const displayIP = ip ?? ''; return ( - <TableRow ariaLabel={label} className={classes.row}> + <TableRow + ariaLabel={label} + className={classes.row} + data-qa-node-row={nodeId} + > <TableCell> <Grid container wrap="nowrap" alignItems="center"> <Grid> diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/RecycleNodeDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/RecycleNodeDialog.tsx index 09ccdcb3a32..8f93e9543a3 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/RecycleNodeDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/RecycleNodeDialog.tsx @@ -27,6 +27,7 @@ export const RecycleNodeDialog = (props: Props) => { const onSubmit = () => { mutateAsync().then(() => { enqueueSnackbar('Node queued for recycling.', { variant: 'success' }); + onClose(); }); }; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/ResizeNodePoolDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/ResizeNodePoolDrawer.tsx index 80255c40d5e..a99d885c1c9 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/ResizeNodePoolDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/ResizeNodePoolDrawer.tsx @@ -8,7 +8,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; import EnhancedNumberInput from 'src/components/EnhancedNumberInput'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useUpdateNodePoolMutation } from 'src/queries/kubernetes'; import { useSpecificTypes } from 'src/queries/types'; import { extendType } from 'src/utilities/extendType'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/UpgradeClusterDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/UpgradeClusterDialog.tsx index 6b41a87c5fb..b21e01b8fe9 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/UpgradeClusterDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/UpgradeClusterDialog.tsx @@ -4,7 +4,7 @@ import Button from 'src/components/Button'; import CheckBox from 'src/components/CheckBox'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { HIGH_AVAILABILITY_PRICE } from 'src/constants'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx index 5e6b620c9b9..044fa07eab7 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx @@ -1,27 +1,28 @@ import * as React from 'react'; -import { CircleProgress } from 'src/components/CircleProgress'; -import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; + import ErrorState from 'src/components/ErrorState'; +import Hidden from 'src/components/core/Hidden'; import LandingHeader from 'src/components/LandingHeader'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; import TransferDisplay from 'src/components/TransferDisplay'; import UpgradeVersionModal from '../UpgradeVersionModal'; +import { CircleProgress } from 'src/components/CircleProgress'; import { DeleteKubernetesClusterDialog } from '../KubernetesClusterDetail/DeleteKubernetesClusterDialog'; -import { useOrder } from 'src/hooks/useOrder'; -import { usePagination } from 'src/hooks/usePagination'; -import { useKubernetesClustersQuery } from 'src/queries/kubernetes'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; +import { KubeNodePoolResponse } from '@linode/api-v4'; import { KubernetesClusterRow } from '../ClusterList/KubernetesClusterRow'; -import KubernetesEmptyState from './KubernetesLandingEmptyState'; -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { KubernetesEmptyState } from './KubernetesLandingEmptyState'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useHistory } from 'react-router-dom'; -import { KubeNodePoolResponse } from '@linode/api-v4'; +import { useKubernetesClustersQuery } from 'src/queries/kubernetes'; +import { useOrder } from 'src/hooks/useOrder'; +import { usePagination } from 'src/hooks/usePagination'; interface ClusterDialogState { open: boolean; diff --git a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyState.tsx b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyState.tsx index 7ab3b47d43a..9d04284b087 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyState.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyState.tsx @@ -1,129 +1,25 @@ import * as React from 'react'; -import { useHistory } from 'react-router-dom'; -import DocsIcon from 'src/assets/icons/docs.svg'; import KubernetesSvg from 'src/assets/icons/entityIcons/kubernetes.svg'; -import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; -import YoutubeIcon from 'src/assets/icons/youtube.svg'; -import List from 'src/components/core/List'; -import ListItem from 'src/components/core/ListItem'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; -import Link from 'src/components/Link'; -import Placeholder from 'src/components/Placeholder'; -import LinksSection from 'src/features/linodes/LinodesLanding/LinksSection'; -import LinkSubSection from 'src/features/linodes/LinodesLanding/LinksSubSection'; -import { - getLinkOnClick, - guidesMoreLinkText, - youtubeMoreLinkLabel, - youtubeMoreLinkText, -} from 'src/utilities/emptyStateLandingUtils'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; import { sendEvent } from 'src/utilities/ga'; +import { useHistory } from 'react-router-dom'; +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './KubernetesLandingEmptyStateData'; -const useStyles = makeStyles((theme: Theme) => ({ - placeholderAdjustment: { - padding: `${theme.spacing(2)} 0`, - [theme.breakpoints.up('md')]: { - padding: `${theme.spacing(10)} 0 ${theme.spacing(4)}`, - }, - }, -})); - -const guidesLinkData = [ - { - to: 'https://www.linode.com/docs/products/compute/kubernetes/get-started/', - text: 'Get Started with the Linode Kubernetes Engine (LKE)', - }, - { - to: - 'https://www.linode.com/docs/products/compute/kubernetes/guides/create-lke-cluster', - text: 'Create and Administer a Kubernetes Cluster on LKE', - }, - { - to: - 'https://www.linode.com/docs/guides/using-the-kubernetes-dashboard-on-lke/', - text: 'Using the Kubernetes Dashboard', - }, - { - to: 'https://www.linode.com/docs/guides/beginners-guide-to-kubernetes/', - text: 'A Beginner\u{2019}s Guide to Kubernetes', - }, -]; - -const youtubeLinkData = [ - { - to: 'https://www.youtube.com/watch?v=erthAqqdD_c', - text: 'Easily Deploy a Kubernetes Cluster on LKE', - }, - { - to: 'https://www.youtube.com/watch?v=VYUr_WvXCsY', - text: 'Enable High Availability on an LKE Cluster', - }, - { - to: 'https://www.youtube.com/watch?v=odPmyT5DONg', - text: 'Use a Load Balancer with an LKE Cluster', - }, - { - to: 'https://www.youtube.com/watch?v=1564_DrFRSE', - text: 'Use TOBS (The Observability Stack) with LKE', - }, -]; - -const gaCategory = 'Kubernetes landing page empty'; -const linkGAEventTemplate = { - category: gaCategory, - action: 'Click:link', -}; - -const guideLinks = ( - <List> - {guidesLinkData.map((linkData) => ( - <ListItem key={linkData.to}> - <Link - to={linkData.to} - onClick={getLinkOnClick(linkGAEventTemplate, linkData.text)} - > - {linkData.text} - </Link> - </ListItem> - ))} - </List> -); - -const youtubeLinks = ( - <List> - {youtubeLinkData.map((linkData) => ( - <ListItem key={linkData.to}> - <Link - onClick={getLinkOnClick(linkGAEventTemplate, linkData.text)} - to={linkData.to} - > - {linkData.text} - <ExternalLinkIcon /> - </Link> - </ListItem> - ))} - </List> -); - -const KubernetesEmptyState = () => { +export const KubernetesEmptyState = () => { const { push } = useHistory(); - const classes = useStyles(); return ( - <Placeholder - title="Kubernetes" - subtitle="Fully managed Kubernetes infrastructure" - className={classes.placeholderAdjustment} - icon={KubernetesSvg} - isEntity - showTransferDisplay + <ResourcesSection buttonProps={[ { onClick: () => { sendEvent({ - category: gaCategory, + category: linkGAEvent.category, action: 'Click:button', label: 'Create Cluster', }); @@ -132,56 +28,11 @@ const KubernetesEmptyState = () => { children: 'Create Cluster', }, ]} - linksSection={ - <LinksSection> - <LinkSubSection - title="Getting Started Guides" - icon={<DocsIcon />} - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick( - linkGAEventTemplate, - guidesMoreLinkText - )} - to="https://www.linode.com/docs/" - {...props} - > - {guidesMoreLinkText} - </Link> - )} - > - {guideLinks} - </LinkSubSection> - <LinkSubSection - title="Video Playlist" - icon={<YoutubeIcon />} - external - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick( - linkGAEventTemplate, - youtubeMoreLinkLabel - )} - to="https://www.youtube.com/playlist?list=PLTnRtjQN5ieb4XyvC9OUhp7nxzBENgCxJ" - {...props} - > - {youtubeMoreLinkText} - <ExternalLinkIcon style={{ marginLeft: 8 }} /> - </Link> - )} - > - {youtubeLinks} - </LinkSubSection> - </LinksSection> - } - > - {' '} - <Typography variant="subtitle1"> - Deploy and scale your applications with the Linode Kubernetes Engine - (LKE), a Kubernetes service equipped with a fully managed control plane. - </Typography> - </Placeholder> + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={KubernetesSvg} + linkGAEvent={linkGAEvent} + youtubeLinkData={youtubeLinkData} + /> ); }; - -export default React.memo(KubernetesEmptyState); diff --git a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyStateData.ts b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyStateData.ts new file mode 100644 index 00000000000..15ec5cec6c3 --- /dev/null +++ b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLandingEmptyStateData.ts @@ -0,0 +1,82 @@ +import { + docsLink, + guidesMoreLinkText, + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + 'Deploy and scale your applications with the Linode Kubernetes Engine (LKE), a Kubernetes service equipped with a fully managed control plane.', + subtitle: 'Fully managed Kubernetes infrastructure', + title: 'Kubernetes', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: + 'https://www.linode.com/docs/products/compute/kubernetes/get-started/', + text: 'Get Started with the Linode Kubernetes Engine (LKE)', + }, + { + to: + 'https://www.linode.com/docs/products/compute/kubernetes/guides/create-lke-cluster', + text: 'Create and Administer a Kubernetes Cluster on LKE', + }, + { + to: + 'https://www.linode.com/docs/guides/using-the-kubernetes-dashboard-on-lke/', + text: 'Using the Kubernetes Dashboard', + }, + { + to: 'https://www.linode.com/docs/guides/beginners-guide-to-kubernetes/', + text: 'A Beginner\u{2019}s Guide to Kubernetes', + }, + ], + moreInfo: { + to: docsLink, + text: guidesMoreLinkText, + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=erthAqqdD_c', + text: 'Easily Deploy a Kubernetes Cluster on LKE', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=VYUr_WvXCsY', + text: 'Enable High Availability on an LKE Cluster', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=odPmyT5DONg', + text: 'Use a Load Balancer with an LKE Cluster', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=1564_DrFRSE', + text: 'Use TOBS (The Observability Stack) with LKE', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'Kubernetes landing page empty', +}; diff --git a/packages/manager/src/features/LinodeConfigSelectionDrawer/LinodeConfigSelectionDrawer.tsx b/packages/manager/src/features/LinodeConfigSelectionDrawer/LinodeConfigSelectionDrawer.tsx index e48da3d6f5c..aa003f66e28 100644 --- a/packages/manager/src/features/LinodeConfigSelectionDrawer/LinodeConfigSelectionDrawer.tsx +++ b/packages/manager/src/features/LinodeConfigSelectionDrawer/LinodeConfigSelectionDrawer.tsx @@ -5,7 +5,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SelectionCard from 'src/components/SelectionCard'; export type LinodeConfigSelectionDrawerCallback = (id: number) => void; diff --git a/packages/manager/src/features/Lish/Lish.tsx b/packages/manager/src/features/Lish/Lish.tsx index a265557ca33..2d5c084034c 100644 --- a/packages/manager/src/features/Lish/Lish.tsx +++ b/packages/manager/src/features/Lish/Lish.tsx @@ -9,7 +9,10 @@ import ErrorState from 'src/components/ErrorState'; import SafeTabPanel from 'src/components/SafeTabPanel'; import TabLinkList from 'src/components/TabLinkList'; import { Tab } from 'src/components/TabLinkList/TabLinkList'; -import { useLinodeLishTokenQuery, useLinodeQuery } from 'src/queries/linodes'; +import { + useLinodeLishTokenQuery, + useLinodeQuery, +} from 'src/queries/linodes/linodes'; import Glish from './Glish'; import Weblish from './Weblish'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ActiveConnections.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ActiveConnections.tsx index 1e60937e5d9..cf4e8ad34cd 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ActiveConnections.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ActiveConnections.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from 'src/components/Grid'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { LongviewPort } from 'src/features/Longview/request.types'; import ConnectionRow from './ConnectionRow'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx index 0c08e65934d..fb74d6648d0 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { LongviewPort } from 'src/features/Longview/request.types'; interface Props { diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx index b5a777d7663..067a31b74d0 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx @@ -7,7 +7,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ExternalLink from 'src/components/ExternalLink'; import Grid from 'src/components/Grid'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { isToday as _isToday } from 'src/utilities/isToday'; import { WithStartAndEnd } from '../../../request.types'; import TimeRangeSelect from '../../../shared/TimeRangeSelect'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/ListeningServices.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/ListeningServices.tsx index edd46bdd86c..c14e8b7583f 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/ListeningServices.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/ListeningServices.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from 'src/components/Grid'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { LongviewService } from 'src/features/Longview/request.types'; import LongviewServiceRow from './LongviewServiceRow'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx index f944e1269ea..3cd537ba2cb 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { LongviewService } from 'src/features/Longview/request.types'; interface Props { diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx index 36431ad5deb..63791d2c848 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx @@ -7,7 +7,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ExternalLink from 'src/components/ExternalLink'; import Grid from 'src/components/Grid'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { isToday as _isToday } from 'src/utilities/isToday'; import { WithStartAndEnd } from '../../../request.types'; import TimeRangeSelect from '../../../shared/TimeRangeSelect'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx index 2a3a7e8fcd9..a6a93623066 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx @@ -7,7 +7,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ExternalLink from 'src/components/ExternalLink'; import Grid from 'src/components/Grid'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { isToday as _isToday } from 'src/utilities/isToday'; import { WithStartAndEnd } from '../../../request.types'; import TimeRangeSelect from '../../../shared/TimeRangeSelect'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx index b19ecf1f0d0..fa48759dbed 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx @@ -2,16 +2,16 @@ import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import OrderBy from 'src/components/OrderBy'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { formatCPU } from 'src/features/Longview/shared/formatters'; import { useWindowDimensions } from 'src/hooks/useWindowDimensions'; import { readableBytes } from 'src/utilities/unitConversions'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx index ce99d549209..107cef74ff9 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx @@ -3,18 +3,18 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import Box from 'src/components/core/Box'; import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from 'src/components/Grid'; import OrderBy from 'src/components/OrderBy'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { LongviewTopProcesses, TopProcessStat, diff --git a/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx b/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx index 38fe4f2f408..db2bda303de 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx @@ -11,7 +11,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import ErrorState from 'src/components/ErrorState'; import NotFound from 'src/components/NotFound'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SafeTabPanel from 'src/components/SafeTabPanel'; import SuspenseLoader from 'src/components/SuspenseLoader'; import TabLinkList from 'src/components/TabLinkList'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx index 5790afa777d..5376e339039 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx @@ -9,7 +9,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import ErrorState from 'src/components/ErrorState'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Props as LVProps } from 'src/containers/longview.container'; import LongviewRows from './LongviewListRows'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.tsx index 6670464f1fc..1b72a56e3be 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.tsx @@ -12,15 +12,15 @@ import CircularProgress from 'src/components/core/CircularProgress'; import Paper from 'src/components/core/Paper'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import { SupportLink } from 'src/components/SupportLink'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { useGrants, useProfile } from 'src/queries/profile'; diff --git a/packages/manager/src/features/Longview/LongviewPackageDrawer.tsx b/packages/manager/src/features/Longview/LongviewPackageDrawer.tsx index 4fbd654f787..7a67020a361 100644 --- a/packages/manager/src/features/Longview/LongviewPackageDrawer.tsx +++ b/packages/manager/src/features/Longview/LongviewPackageDrawer.tsx @@ -3,12 +3,11 @@ import * as React from 'react'; import { compose } from 'recompose'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableCell from 'src/components/core/TableCell'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Drawer from 'src/components/Drawer'; -import Table from 'src/components/Table'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import withLongviewStats, { DispatchProps, @@ -16,6 +15,7 @@ import withLongviewStats, { } from 'src/containers/longview.stats.container'; import LongviewPackageRow from './LongviewPackageRow'; import { LongviewPackage } from './request.types'; +import { TableCell } from 'src/components/TableCell'; const useStyles = makeStyles((theme: Theme) => ({ new: { diff --git a/packages/manager/src/features/Longview/LongviewPackageRow.tsx b/packages/manager/src/features/Longview/LongviewPackageRow.tsx index 43a00a3e983..09d0f233cb2 100644 --- a/packages/manager/src/features/Longview/LongviewPackageRow.tsx +++ b/packages/manager/src/features/Longview/LongviewPackageRow.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableCell from 'src/components/TableCell'; +import { TableCell } from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableRow } from 'src/components/TableRow'; import { LongviewPackage } from './request.types'; diff --git a/packages/manager/src/features/Managed/Contacts/Contacts.tsx b/packages/manager/src/features/Managed/Contacts/Contacts.tsx index 743d7ae3977..07b32708a81 100644 --- a/packages/manager/src/features/Managed/Contacts/Contacts.tsx +++ b/packages/manager/src/features/Managed/Contacts/Contacts.tsx @@ -5,19 +5,19 @@ import AddNewLink from 'src/components/AddNewLink'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { DeletionDialog } from 'src/components/DeletionDialog/DeletionDialog'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useDialog } from 'src/hooks/useDialog'; import useOpenClose from 'src/hooks/useOpenClose'; import { diff --git a/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx b/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx index 476ff879c18..4205239e190 100644 --- a/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx +++ b/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx @@ -8,7 +8,7 @@ import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; import Select from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { useCreateContactMutation, diff --git a/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx b/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx index 62a460e2bd5..f3a6603baa7 100644 --- a/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx +++ b/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx @@ -1,8 +1,8 @@ import { ManagedContact } from '@linode/api-v4/lib/managed'; import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import ActionMenu from './ContactsActionMenu'; interface Props { diff --git a/packages/manager/src/features/Managed/Credentials/AddCredentialDrawer.tsx b/packages/manager/src/features/Managed/Credentials/AddCredentialDrawer.tsx index 21d13cfb6a3..8977383a80a 100644 --- a/packages/manager/src/features/Managed/Credentials/AddCredentialDrawer.tsx +++ b/packages/manager/src/features/Managed/Credentials/AddCredentialDrawer.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SuspenseLoader from 'src/components/SuspenseLoader'; import TextField from 'src/components/TextField'; import { creationSchema } from './credential.schema'; diff --git a/packages/manager/src/features/Managed/Credentials/CredentialList.tsx b/packages/manager/src/features/Managed/Credentials/CredentialList.tsx index bc95615fba6..55d4a59d0b6 100644 --- a/packages/manager/src/features/Managed/Credentials/CredentialList.tsx +++ b/packages/manager/src/features/Managed/Credentials/CredentialList.tsx @@ -6,19 +6,19 @@ import * as React from 'react'; import AddNewLink from 'src/components/AddNewLink'; import { makeStyles } from 'tss-react/mui'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { DeletionDialog } from 'src/components/DeletionDialog/DeletionDialog'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useDialog } from 'src/hooks/useDialog'; import { useAllManagedCredentialsQuery, diff --git a/packages/manager/src/features/Managed/Credentials/CredentialRow.tsx b/packages/manager/src/features/Managed/Credentials/CredentialRow.tsx index 75e7aa817a8..f99cd9b7b4e 100644 --- a/packages/manager/src/features/Managed/Credentials/CredentialRow.tsx +++ b/packages/manager/src/features/Managed/Credentials/CredentialRow.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import { createStyles, makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import ActionMenu from './CredentialActionMenu'; const useStyles = makeStyles((theme: Theme) => diff --git a/packages/manager/src/features/Managed/Credentials/UpdateCredentialDrawer.tsx b/packages/manager/src/features/Managed/Credentials/UpdateCredentialDrawer.tsx index d9c3e234930..b128535e3f8 100644 --- a/packages/manager/src/features/Managed/Credentials/UpdateCredentialDrawer.tsx +++ b/packages/manager/src/features/Managed/Credentials/UpdateCredentialDrawer.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SuspenseLoader from 'src/components/SuspenseLoader'; import TextField from 'src/components/TextField'; import { updateLabelSchema, updatePasswordSchema } from './credential.schema'; diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx index 32ac91ab296..2743fd0dce9 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx @@ -120,6 +120,7 @@ const createTabs = ( <div className={classes.canvasContainer}> <LineGraph ariaLabel="CPU Usage Graph" + accessibleDataTable={{ unit: '%' }} timezone={timezone} chartHeight={chartHeight} showToday={true} @@ -151,6 +152,7 @@ const createTabs = ( showToday={true} nativeLegend unit="/s" + accessibleDataTable={{ unit: 'Kb/s"' }} formatData={convertNetworkData} formatTooltip={_formatTooltip} data={[ @@ -185,6 +187,7 @@ const createTabs = ( timezone={timezone} chartHeight={chartHeight} showToday={true} + accessibleDataTable={{ unit: 'op/s' }} data={[ { borderColor: 'transparent', diff --git a/packages/manager/src/features/Managed/MonitorDrawer.tsx b/packages/manager/src/features/Managed/MonitorDrawer.tsx index c928c3b6e27..7d38d2b92de 100644 --- a/packages/manager/src/features/Managed/MonitorDrawer.tsx +++ b/packages/manager/src/features/Managed/MonitorDrawer.tsx @@ -14,7 +14,7 @@ import InputAdornment from 'src/components/core/InputAdornment'; import Drawer from 'src/components/Drawer'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; export interface Props { diff --git a/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx b/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx index 3706ad06bee..f7eb7296031 100644 --- a/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx +++ b/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx @@ -7,8 +7,8 @@ import { Theme } from '@mui/material/styles'; import Tooltip from 'src/components/core/Tooltip'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { ExtendedIssue } from 'src/queries/managed/types'; import ActionMenu from './MonitorActionMenu'; import { statusIconMap, statusTextMap } from './monitorMaps'; diff --git a/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx b/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx index d1b12040a2f..d6630863fb3 100644 --- a/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx +++ b/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx @@ -6,18 +6,18 @@ import * as React from 'react'; import AddNewLink from 'src/components/AddNewLink'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import { DeletionDialog } from 'src/components/DeletionDialog/DeletionDialog'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useDialog } from 'src/hooks/useDialog'; import { useAllManagedContactsQuery, diff --git a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx index d8265cac452..2adfd6433b7 100644 --- a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx @@ -10,7 +10,7 @@ import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; import Grid from '@mui/material/Unstable_Grid2'; import IPSelect from 'src/components/IPSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; import { diff --git a/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx b/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx index b3daa298c17..eb49746a8c1 100644 --- a/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx @@ -1,8 +1,8 @@ import { ManagedLinodeSetting } from '@linode/api-v4/lib/managed'; import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import ActionMenu from './SSHAccessActionMenu'; interface Props { diff --git a/packages/manager/src/features/Managed/SSHAccess/SSHAccessTable.tsx b/packages/manager/src/features/Managed/SSHAccess/SSHAccessTable.tsx index 472b18587a3..5d829b754eb 100644 --- a/packages/manager/src/features/Managed/SSHAccess/SSHAccessTable.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/SSHAccessTable.tsx @@ -4,15 +4,15 @@ import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import useOpenClose from 'src/hooks/useOpenClose'; import { useAllLinodeSettingsQuery } from 'src/queries/managed/managed'; import { DEFAULTS } from './common'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx index 6c898c7bac1..8a6b5a7b4db 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx @@ -7,7 +7,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { getErrorMap } from 'src/utilities/errorUtils'; import SelectIP from './ConfigNodeIPSelect'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx index 486c4c67edc..a32fd5dbfe9 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx @@ -13,7 +13,7 @@ import Typography from 'src/components/core/Typography'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; import { getErrorMap } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index 8f79e031f53..acffebb8ee3 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -15,7 +15,7 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SelectRegionPanel from 'src/components/SelectRegionPanel'; import { TagsInput, Tag } from 'src/components/TagsInput/TagsInput'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx index 00eb758f23b..6661248fe75 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useHistory } from 'react-router-dom'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; import { useNodebalancerDeleteMutation } from 'src/queries/nodebalancers'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 4a561a6a54f..b611a044006 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -3,7 +3,7 @@ import { CircleProgress } from 'src/components/CircleProgress'; import TabPanels from 'src/components/core/ReachTabPanels'; import Tabs from 'src/components/core/ReachTabs'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SafeTabPanel from 'src/components/SafeTabPanel'; import TabLinkList from 'src/components/TabLinkList'; import NodeBalancerConfigurations from './NodeBalancerConfigurations'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx index 8fabdefde69..cf5a13a46e3 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx @@ -139,6 +139,7 @@ const TablesPanel = () => { ariaLabel="Connections Graph" timezone={timezone} showToday={true} + accessibleDataTable={{ unit: 'CXN/s' }} data={[ { label: 'Connections', @@ -207,6 +208,7 @@ const TablesPanel = () => { ariaLabel="Traffic Graph" timezone={timezone} showToday={true} + accessibleDataTable={{ unit: 'bits/s' }} data={[ { label: 'Traffic In', diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index b8e693b0c5f..289f1d340e6 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import { Link } from 'react-router-dom'; import Hidden from 'src/components/core/Hidden'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import IPAddress from 'src/features/linodes/LinodesLanding/IPAddress'; import RegionIndicator from 'src/features/linodes/LinodesLanding/RegionIndicator'; import { useAllNodeBalancerConfigsQuery } from 'src/queries/nodebalancers'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index e3a55410f2b..460a03507d4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import { useHistory } from 'react-router-dom'; import { CircleProgress } from 'src/components/CircleProgress'; import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import TableRow from 'src/components/core/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ErrorState from 'src/components/ErrorState'; import LandingHeader from 'src/components/LandingHeader'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table/Table'; -import TableCell from 'src/components/TableCell/TableCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; import TransferDisplay from 'src/components/TransferDisplay'; import { useOrder } from 'src/hooks/useOrder'; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx index 2e63eab5a65..49105d2faaf 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx @@ -13,7 +13,7 @@ import Button from 'src/components/Button'; import { CircleProgress } from 'src/components/CircleProgress'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { useAccountSettings } from 'src/queries/accountSettings'; import { diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx index beed322ad62..dd2e209c5b6 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx @@ -25,7 +25,7 @@ import { MODE, OpenAccessDrawer } from './types'; import ViewPermissionsDrawer from './ViewPermissionsDrawer'; import { useObjectStorageAccessKeys } from 'src/queries/objectStorage'; import { usePagination } from 'src/hooks/usePagination'; -import PaginationFooter from 'src/components/PaginationFooter'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; interface Props { isRestrictedUser: boolean; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable.tsx index e7f9c5c1711..e90ffdab803 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable.tsx @@ -3,12 +3,12 @@ import { ObjectStorageKey } from '@linode/api-v4/lib/object-storage'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import AccessKeyMenu from './AccessKeyMenu'; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/LimitedAccessControls.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/LimitedAccessControls.tsx index 9359d761681..7352d8872f7 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/LimitedAccessControls.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/LimitedAccessControls.tsx @@ -4,13 +4,13 @@ import * as React from 'react'; import FormControlLabel from 'src/components/core/FormControlLabel'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Radio from 'src/components/Radio'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { Toggle } from 'src/components/Toggle'; import AccessCell from './AccessCell'; import { MODE } from './types'; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx index 38aa28d713c..5d4c5f6b7b7 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx @@ -9,7 +9,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import EnhancedSelect from 'src/components/EnhancedSelect'; import ExternalLink from 'src/components/ExternalLink'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { Toggle } from 'src/components/Toggle'; import useOpenClose from 'src/hooks/useOpenClose'; import { capitalize } from 'src/utilities/capitalize'; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketDetail.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketDetail.tsx index 67fc9d8a67f..64bac26a640 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketDetail.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketDetail.tsx @@ -15,12 +15,12 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { prefixToQueryKey, queryKey, diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx index 82501040fd0..f0094469376 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx @@ -16,7 +16,7 @@ import Typography from 'src/components/core/Typography'; import ErrorState from 'src/components/ErrorState'; import ExternalLink from 'src/components/ExternalLink'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { useAPIRequest } from 'src/hooks/useAPIRequest'; import { getErrorMap } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx index 7b9d9d97dda..7e5d963d1a9 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx @@ -5,8 +5,8 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import EntityIcon from 'src/components/EntityIcon'; import Grid from '@mui/material/Unstable_Grid2'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { FolderActionMenu } from './FolderActionMenu'; const useStyles = makeStyles((theme: Theme) => ({ diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx index b9b387c1d93..9a99e2383e9 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx @@ -7,8 +7,8 @@ import Typography from 'src/components/core/Typography'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import EntityIcon from 'src/components/EntityIcon'; import Grid from '@mui/material/Unstable_Grid2'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { readableBytes } from 'src/utilities/unitConversions'; import ObjectActionMenu from './ObjectActionMenu'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx index dbcacc49fa1..4b5c86d6f72 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx @@ -13,7 +13,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ErrorState from 'src/components/ErrorState'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import OrderBy from 'src/components/OrderBy'; import Placeholder from 'src/components/Placeholder'; import TransferDisplay from 'src/components/TransferDisplay'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.tsx index 48e7ea8a244..7fb6732bdc0 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.tsx @@ -1,14 +1,14 @@ import { ObjectStorageBucket } from '@linode/api-v4/lib/object-storage'; import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import BucketTableRow from './BucketTableRow'; interface Props { diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTableRow.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTableRow.tsx index 6a48f45b059..3a998949538 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTableRow.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTableRow.tsx @@ -7,8 +7,8 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { useObjectStorageClusters } from 'src/queries/objectStorage'; import { useRegionsQuery } from 'src/queries/regions'; import { readableBytes } from 'src/utilities/unitConversions'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx new file mode 100644 index 00000000000..3fa1b692376 --- /dev/null +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { CreateBucketDrawer } from './CreateBucketDrawer'; +import { waitFor } from '@testing-library/react'; +import { renderWithTheme } from 'src/utilities/testHelpers'; +import userEvent from '@testing-library/user-event'; +import { rest, server } from 'src/mocks/testServer'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { + accountSettingsFactory, + objectStorageClusterFactory, + regionFactory, +} from 'src/factories'; + +const props = { + isOpen: true, + onClose: jest.fn(), +}; + +jest.mock('src/components/EnhancedSelect/Select'); + +describe('CreateBucketDrawer', () => { + it('Should show a general error notice if the API returns one', async () => { + server.use( + rest.post('*/object-storage/buckets', (req, res, ctx) => { + return res( + ctx.status(500), + ctx.json({ errors: [{ reason: 'Object Storage is offline!' }] }) + ); + }), + rest.get('*/regions', async (req, res, ctx) => { + return res( + ctx.json( + makeResourcePage( + regionFactory.buildList(1, { id: 'us-east', label: 'Newark, NJ' }) + ) + ) + ); + }), + rest.get('*object-storage/clusters', (req, res, ctx) => { + return res( + ctx.json( + makeResourcePage( + objectStorageClusterFactory.buildList(1, { + region: 'us-east', + id: 'us-east-1', + }) + ) + ) + ); + }), + rest.get('*/account/settings', (req, res, ctx) => { + return res( + ctx.json(accountSettingsFactory.build({ object_storage: 'active' })) + ); + }) + ); + + const { + getByTestId, + getByLabelText, + getByPlaceholderText, + findByText, + } = renderWithTheme(<CreateBucketDrawer {...props} />); + + userEvent.type(getByLabelText('Label'), 'my-test-bucket'); + + // We must waitFor because we need to load region and cluster data from the API + await waitFor(() => + userEvent.selectOptions( + getByPlaceholderText('Select a Region'), + 'Newark, NJ (us-east-1)' + ) + ); + + const saveButton = getByTestId('create-bucket-button'); + + userEvent.click(saveButton); + + await findByText('Object Storage is offline!'); + }); +}); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx index 8e9e1193a5b..70c7fff38d0 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx @@ -8,7 +8,7 @@ import { useObjectStorageClusters, } from 'src/queries/objectStorage'; import { isEURegion } from 'src/utilities/formatRegion'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { getErrorMap } from 'src/utilities/errorUtils'; import ClusterSelect from './ClusterSelect'; @@ -54,6 +54,7 @@ export const CreateBucketDrawer = (props: Props) => { error, reset, } = useCreateBucketMutation(); + const { data: agreements } = useAccountAgreements(); const { mutateAsync: updateAccountAgreements } = useMutateAccountAgreements(); const { data: accountSettings } = useAccountSettings(); @@ -121,6 +122,7 @@ export const CreateBucketDrawer = (props: Props) => { data-qa-permissions-notice /> )} + {Boolean(errorMap.none) && <Notice error text={errorMap.none} />} <TextField data-qa-cluster-label label="Label" @@ -134,7 +136,7 @@ export const CreateBucketDrawer = (props: Props) => { /> <ClusterSelect data-qa-cluster-select - error={formik.touched.cluster ? errorMap.cluster : undefined} + error={errorMap.cluster} onBlur={formik.handleBlur} onChange={(value) => formik.setFieldValue('cluster', value)} selectedCluster={formik.values.cluster} @@ -153,7 +155,12 @@ export const CreateBucketDrawer = (props: Props) => { <Button buttonType="secondary" onClick={onClose}> Cancel </Button> - <Button buttonType="primary" type="submit" loading={isLoading}> + <Button + buttonType="primary" + type="submit" + loading={isLoading} + data-testid="create-bucket-button" + > Create Bucket </Button> </ActionsPanel> diff --git a/packages/manager/src/features/OneClickApps/FakeSpec.ts b/packages/manager/src/features/OneClickApps/FakeSpec.ts index 3324bdd1703..2da48aa5179 100644 --- a/packages/manager/src/features/OneClickApps/FakeSpec.ts +++ b/packages/manager/src/features/OneClickApps/FakeSpec.ts @@ -313,7 +313,7 @@ export const oneClickApps: OCA[] = [ alt_description: 'SQL and NoSQL database interface and monitoring for MySQL, MongoDB, PostgreSQL, and more.', categories: ['Databases'], - description: `All-in-one interface for scripting and monitoring databases, including MySQL, MariaDB, Percona, MongoDB, PostgreSQL, Galera Cluster and more. Easily deploy database instances, manage with an included CLI, and automate performance monitoring.`, + description: `All-in-one interface for scripting and monitoring databases, including MySQL, MariaDB, Percona, PostgreSQL, Galera Cluster and more. Easily deploy database instances, manage with an included CLI, and automate performance monitoring.`, summary: 'All-in-one database deployment, management, and monitoring system.', related_guides: [ diff --git a/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx b/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx index 089d073ec31..c93779f5ccf 100644 --- a/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx +++ b/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx @@ -1,17 +1,17 @@ import * as React from 'react'; import AddNewLink from 'src/components/AddNewLink'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import SecretTokenDialog from 'src/features/Profile/SecretTokenDialog'; import { APITokenMenu } from './APITokenMenu'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; diff --git a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx index a7474cdd5cd..5ed8299ebd7 100644 --- a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx +++ b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx @@ -7,15 +7,15 @@ import FormControl from 'src/components/core/FormControl'; import FormHelperText from 'src/components/core/FormHelperText'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Drawer from 'src/components/Drawer'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TextField from 'src/components/TextField'; import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants'; import AccessCell from 'src/features/ObjectStorage/AccessKeyLanding/AccessCell'; diff --git a/packages/manager/src/features/Profile/APITokens/EditAPITokenDrawer.tsx b/packages/manager/src/features/Profile/APITokens/EditAPITokenDrawer.tsx index 7a53dc0daeb..b3a858fb7d5 100644 --- a/packages/manager/src/features/Profile/APITokens/EditAPITokenDrawer.tsx +++ b/packages/manager/src/features/Profile/APITokens/EditAPITokenDrawer.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { Token, TokenRequest } from '@linode/api-v4/lib/profile/types'; import { useFormik } from 'formik'; diff --git a/packages/manager/src/features/Profile/APITokens/ViewAPITokenDrawer.tsx b/packages/manager/src/features/Profile/APITokens/ViewAPITokenDrawer.tsx index 3d731e22299..5090285df69 100644 --- a/packages/manager/src/features/Profile/APITokens/ViewAPITokenDrawer.tsx +++ b/packages/manager/src/features/Profile/APITokens/ViewAPITokenDrawer.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Drawer from 'src/components/Drawer'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import AccessCell from 'src/features/ObjectStorage/AccessKeyLanding/AccessCell'; import { scopeStringToPermTuples, basePermNameMap } from './utils'; import { Token } from '@linode/api-v4/lib/profile/types'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx index 5532b4af34a..e80fa35b4a6 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx @@ -64,6 +64,7 @@ export const PhoneVerification = () => { const { mutateAsync: sendVerificationCode, + reset: resetCodeMutation, error: verifyError, } = useVerifyPhoneVerificationCodeMutation(); @@ -72,6 +73,7 @@ export const PhoneVerification = () => { const onSubmitPhoneNumber = async ( values: SendPhoneVerificationCodePayload ) => { + resetCodeMutation(); return await sendPhoneVerificationCode(values); }; @@ -145,8 +147,7 @@ export const PhoneVerification = () => { }; const onEnterDifferentPhoneNumber = () => { - resetSendCodeMutation(); - sendCodeForm.resetForm(); + reset(); }; const onResendVerificationCode = () => { diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx index e4cf760bfe5..17cd3c3f48d 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Button from 'src/components/Button'; import Box from 'src/components/core/Box'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import ActionsPanel from 'src/components/ActionsPanel'; import { makeStyles } from '@mui/styles'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx index d48ffddca92..00d18e76f6c 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx @@ -15,7 +15,7 @@ import Typography from 'src/components/core/Typography'; import ExternalLink from 'src/components/ExternalLink'; import Grid from '@mui/material/Unstable_Grid2'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import useFlags from 'src/hooks/useFlags'; import TPADialog from './TPADialog'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TrustedDevices.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TrustedDevices.tsx index b15b99d9563..1e98fd7dc51 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TrustedDevices.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TrustedDevices.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowError from 'src/components/TableRowError/TableRowError'; import TableRowEmpty from 'src/components/TableRowEmptyState'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import InlineMenuAction from 'src/components/InlineMenuAction'; import { Theme } from '@mui/material/styles'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/ConfirmToken.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/ConfirmToken.tsx index ae1fc677764..e291df78e16 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/ConfirmToken.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/ConfirmToken.tsx @@ -5,7 +5,7 @@ import Box from 'src/components/core/Box'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import TextField from 'src/components/TextField'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx index d5714bde383..8546b54cf0d 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx @@ -3,7 +3,7 @@ import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; import Divider from 'src/components/core/Divider'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; import ConfirmToken from './ConfirmToken'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/QRCodeForm.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/QRCodeForm.tsx index 54f93cd9c93..c0b429c010c 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/QRCodeForm.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/QRCodeForm.tsx @@ -1,7 +1,7 @@ import QRCode from 'qrcode.react'; import { compose } from 'ramda'; import * as React from 'react'; -import CopyableTextField from 'src/components/CopyableTextField'; +import { CopyableTextField } from 'src/components/CopyableTextField/CopyableTextField'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx index 13973fa219d..5354bf432a6 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx @@ -6,7 +6,7 @@ import FormControlLabel from 'src/components/core/FormControlLabel'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { Toggle } from 'src/components/Toggle'; import { queryKey } from 'src/queries/profile'; import { useSecurityQuestions } from 'src/queries/securityQuestions'; diff --git a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx index b169b211e37..f05b51b832d 100644 --- a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx +++ b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx @@ -11,7 +11,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { useMutateProfile, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx b/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx index c8f6dfaa46f..66356dc2d0b 100644 --- a/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx +++ b/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx @@ -5,7 +5,7 @@ import CheckBox from 'src/components/CheckBox'; import FormControl from 'src/components/core/FormControl'; import FormControlLabel from 'src/components/core/FormControlLabel'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; import { useFormik } from 'formik'; diff --git a/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx b/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx index ce7b5b0a313..4fc36cb98d8 100644 --- a/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx +++ b/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx @@ -5,7 +5,7 @@ import CheckBox from 'src/components/CheckBox'; import FormControl from 'src/components/core/FormControl'; import FormControlLabel from 'src/components/core/FormControlLabel'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; import { useFormik } from 'formik'; diff --git a/packages/manager/src/features/Profile/OAuthClients/OAuthClients.tsx b/packages/manager/src/features/Profile/OAuthClients/OAuthClients.tsx index b53c219ef54..0b97cde30c5 100644 --- a/packages/manager/src/features/Profile/OAuthClients/OAuthClients.tsx +++ b/packages/manager/src/features/Profile/OAuthClients/OAuthClients.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import Box from 'src/components/core/Box'; import AddNewLink from 'src/components/AddNewLink'; import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import ActionMenu from './OAuthClientActionMenu'; import SecretTokenDialog from '../SecretTokenDialog'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; diff --git a/packages/manager/src/features/Profile/Referrals/Referrals.tsx b/packages/manager/src/features/Profile/Referrals/Referrals.tsx index 7e8f2f499ca..9b30e92a837 100644 --- a/packages/manager/src/features/Profile/Referrals/Referrals.tsx +++ b/packages/manager/src/features/Profile/Referrals/Referrals.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Step1 from 'src/assets/referrals/step-1.svg'; import Step2 from 'src/assets/referrals/step-2.svg'; import Step3 from 'src/assets/referrals/step-3.svg'; -import CopyableTextField from 'src/components/CopyableTextField'; +import { CopyableTextField } from 'src/components/CopyableTextField/CopyableTextField'; import CircularProgress from 'src/components/core/CircularProgress'; import Paper from 'src/components/core/Paper'; import { makeStyles } from '@mui/styles'; @@ -11,7 +11,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx b/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx index d933ba00096..bae6261d462 100644 --- a/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx +++ b/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; import { useCreateSSHKeyMutation } from 'src/queries/profile'; diff --git a/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx b/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx index d33a23ba53f..8184b8c679f 100644 --- a/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx +++ b/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; import { useUpdateSSHKeyMutation } from 'src/queries/profile'; diff --git a/packages/manager/src/features/Profile/SSHKeys/SSHKeys.tsx b/packages/manager/src/features/Profile/SSHKeys/SSHKeys.tsx index 0808b453983..36a897ea78a 100644 --- a/packages/manager/src/features/Profile/SSHKeys/SSHKeys.tsx +++ b/packages/manager/src/features/Profile/SSHKeys/SSHKeys.tsx @@ -3,15 +3,15 @@ import AddNewLink from 'src/components/AddNewLink'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; diff --git a/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx b/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx index e82ac33bcb7..a2c27362e42 100644 --- a/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx +++ b/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ObjectStorageKey } from '@linode/api-v4/lib/object-storage'; import { makeStyles } from '@mui/styles'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import CopyableAndDownloadableTextField from 'src/components/CopyableAndDownloadableTextField'; import Box from 'src/components/core/Box'; diff --git a/packages/manager/src/features/Profile/Settings/PreferenceEditor.tsx b/packages/manager/src/features/Profile/Settings/PreferenceEditor.tsx index 63f32918a09..2bf0a82849e 100644 --- a/packages/manager/src/features/Profile/Settings/PreferenceEditor.tsx +++ b/packages/manager/src/features/Profile/Settings/PreferenceEditor.tsx @@ -6,7 +6,7 @@ import { import Typography from 'src/components/core/Typography'; import Link from 'src/components/Link'; import Button from 'src/components/Button'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useMutatePreferences, usePreferences } from 'src/queries/preferences'; type DialogProps = Pick<_DialogProps, 'onClose' | 'open'>; diff --git a/packages/manager/src/features/Search/ResultGroup.tsx b/packages/manager/src/features/Search/ResultGroup.tsx index 334e5858e0c..b6300370fb7 100644 --- a/packages/manager/src/features/Search/ResultGroup.tsx +++ b/packages/manager/src/features/Search/ResultGroup.tsx @@ -5,14 +5,14 @@ import Button from 'src/components/Button'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { capitalize } from 'src/utilities/capitalize'; import ResultRow from './ResultRow'; diff --git a/packages/manager/src/features/Search/ResultRow.tsx b/packages/manager/src/features/Search/ResultRow.tsx index a982aa3e01f..0237288be5b 100644 --- a/packages/manager/src/features/Search/ResultRow.tsx +++ b/packages/manager/src/features/Search/ResultRow.tsx @@ -6,8 +6,8 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Item } from 'src/components/EnhancedSelect/Select'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import Tags from 'src/components/Tags'; import RegionIndicator from 'src/features/linodes/LinodesLanding/RegionIndicator'; diff --git a/packages/manager/src/features/Search/SearchLanding.tsx b/packages/manager/src/features/Search/SearchLanding.tsx index 6bda58a2867..3488e2f60fa 100644 --- a/packages/manager/src/features/Search/SearchLanding.tsx +++ b/packages/manager/src/features/Search/SearchLanding.tsx @@ -10,7 +10,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import H1Header from 'src/components/H1Header'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { REFRESH_INTERVAL } from 'src/constants'; import useAPISearch from 'src/features/Search/useAPISearch'; import useAccountManagement from 'src/hooks/useAccountManagement'; diff --git a/packages/manager/src/features/Search/refinedSearch.test.ts b/packages/manager/src/features/Search/refinedSearch.test.ts index 4d1a001401e..89f9084ab54 100644 --- a/packages/manager/src/features/Search/refinedSearch.test.ts +++ b/packages/manager/src/features/Search/refinedSearch.test.ts @@ -4,6 +4,7 @@ import { searchableItems } from 'src/__data__/searchableItems'; import * as RefinedSearch from './refinedSearch'; import { QueryJSON } from './refinedSearch'; import { SearchableItem } from './search.interfaces'; +import { COMPRESSED_IPV6_REGEX } from './refinedSearch'; const { areAllTrue, @@ -297,25 +298,48 @@ describe('testItem', () => { describe('isSimpleQuery', () => { it('returns true if there are no specified search fields', () => { - let parsedQuery = searchString.parse('-hello world').getParsedQuery(); - expect(isSimpleQuery(parsedQuery)).toBe(true); + let query = '-hello world'; + let parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(true); - parsedQuery = searchString.parse('hello world').getParsedQuery(); - expect(isSimpleQuery(parsedQuery)).toBe(true); + query = '-hello world'; + parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(true); - parsedQuery = searchString.parse('hello -world').getParsedQuery(); - expect(isSimpleQuery(parsedQuery)).toBe(true); + query = 'hello -world'; + parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(true); }); it('returns false if there are specified search fields', () => { - let parsedQuery = searchString.parse('label:hello').getParsedQuery(); - expect(isSimpleQuery(parsedQuery)).toBe(false); + let query = 'label:hello'; + let parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(false); - parsedQuery = searchString.parse('tags:hello,world').getParsedQuery(); - expect(isSimpleQuery(parsedQuery)).toBe(false); + query = 'tags:hello,world'; + parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(false); - parsedQuery = searchString.parse('-label:hello').getParsedQuery(); - expect(isSimpleQuery(parsedQuery)).toBe(false); + query = '-label:hello'; + parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(false); + }); + + it('returns true if we match a shouldSkipFieldSearch rule', () => { + const query = '2001:db8:3c4d:15::1a2f:1a2b'; + const parsedQuery = searchString.parse(query).getParsedQuery(); + expect(isSimpleQuery(query, parsedQuery)).toBe(true); + }); +}); + +describe('IPv6 regex', () => { + it('matches compressed IPv6 addresses', () => { + expect( + '2001:db8:3c4d:15::1a2f:1a2b'.match(COMPRESSED_IPV6_REGEX) + ).toBeTruthy(); + expect('2001:db8::'.match(COMPRESSED_IPV6_REGEX)).toBeTruthy(); + expect('2001:db8::1234:5678'.match(COMPRESSED_IPV6_REGEX)).toBeTruthy(); + expect('::1234:5678'.match(COMPRESSED_IPV6_REGEX)).toBeTruthy(); }); }); diff --git a/packages/manager/src/features/Search/refinedSearch.ts b/packages/manager/src/features/Search/refinedSearch.ts index f1271d6d554..236ea9607e3 100644 --- a/packages/manager/src/features/Search/refinedSearch.ts +++ b/packages/manager/src/features/Search/refinedSearch.ts @@ -3,6 +3,7 @@ import { all, any, equals, isEmpty } from 'ramda'; import searchString from 'search-string'; import { SearchableItem, SearchField } from './search.interfaces'; +export const COMPRESSED_IPV6_REGEX = /^([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,7})?::([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,7})?$/; const DEFAULT_SEARCH_FIELDS = ['label', 'tags', 'ips']; // ============================================================================= @@ -108,7 +109,7 @@ export const testItem = (item: SearchableItem, query: string) => { const parsedQuery = searchString.parse(query).getParsedQuery(); // If there are no specified search fields, we search the default fields. - if (isSimpleQuery(parsedQuery)) { + if (isSimpleQuery(query, parsedQuery)) { return searchDefaultFields(item, query); } @@ -122,11 +123,25 @@ export const testItem = (item: SearchableItem, query: string) => { return areAllTrue(matchedSearchTerms); }; +// Force to skip field search (make a simple query) if there's a match +const shouldSkipFieldSearch = (query: string): boolean => { + const skipConditions = { + // matches a compressed ipv6 addresses. e.g. xxxx:xxxx::xxxx:xxxx:xxxx:xxxx + isIPV6: query.match(COMPRESSED_IPV6_REGEX), + }; + + return Object.values(skipConditions).some((condition) => condition); +}; + // Determines whether a query is "simple", i.e., doesn't contain any search fields, // like "tags:my-tag" or "-label:my-linode". -export const isSimpleQuery = (parsedQuery: any) => { +export const isSimpleQuery = (originalQuery: string, parsedQuery: any) => { const { exclude, ...include } = parsedQuery; - return isEmpty(exclude) && isEmpty(include); + + return ( + (isEmpty(exclude) && isEmpty(include)) || + shouldSkipFieldSearch(originalQuery) + ); }; export const searchDefaultFields = (item: SearchableItem, query: string) => { diff --git a/packages/manager/src/features/StackScripts/Partials/StackScriptTableHead.tsx b/packages/manager/src/features/StackScripts/Partials/StackScriptTableHead.tsx index ab664dd95e0..aee0831778e 100644 --- a/packages/manager/src/features/StackScripts/Partials/StackScriptTableHead.tsx +++ b/packages/manager/src/features/StackScripts/Partials/StackScriptTableHead.tsx @@ -3,10 +3,10 @@ import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableHead from 'src/components/core/TableHead'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; const useStyles = makeStyles((theme: Theme) => ({ root: { diff --git a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx index 300cbe3dd97..3b42e04c1b2 100644 --- a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx +++ b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx @@ -15,9 +15,9 @@ import Paper from 'src/components/core/Paper'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; -import Table from 'src/components/Table'; +import { Table } from 'src/components/Table'; import { withProfile, WithProfileProps, diff --git a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx index 8578c07f7ca..dfd1a2f63bc 100644 --- a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx +++ b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx @@ -3,13 +3,13 @@ import { StackScript } from '@linode/api-v4/lib/stackscripts'; import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableCell from 'src/components/core/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableRow } from 'src/components/TableRow'; import { formatDate } from 'src/utilities/formatDate'; import { truncate } from 'src/utilities/truncate'; import StackScriptSelectionRow from './StackScriptSelectionRow'; import { useProfile } from 'src/queries/profile'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; const useStyles = makeStyles(() => ({ loadingWrapper: { diff --git a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx index a8d1931bbd9..53a337a27a3 100644 --- a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx +++ b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx @@ -7,8 +7,8 @@ import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import Radio from 'src/components/Radio'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; -import TableCell from 'src/components/TableCell/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { openStackScriptDialog as openStackScriptDialogAction } from 'src/store/stackScriptDialog'; import { ClassNames, styles } from '../StackScriptRowHelpers'; diff --git a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts index 44da3ac4883..7710683e9ea 100644 --- a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts +++ b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts @@ -19,7 +19,6 @@ const styles = (theme: Theme) => }, emptyState: { color: theme.palette.text.primary, - textAlign: 'center', }, table: { backgroundColor: theme.bg.bgPaper, diff --git a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx index 26edc39b38b..d775a9b4557 100644 --- a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx @@ -1,38 +1,38 @@ -import { Image } from '@linode/api-v4/lib/images'; -import { StackScript } from '@linode/api-v4/lib/stackscripts'; -import { APIError, Filter, ResourcePage } from '@linode/api-v4/lib/types'; -import classNames from 'classnames'; -import { pathOr } from 'ramda'; import * as React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { Waypoint } from 'react-waypoint'; -import { compose } from 'recompose'; -import StackScriptsIcon from 'src/assets/icons/entityIcons/stackscript.svg'; +import { APIError, Filter, ResourcePage } from '@linode/api-v4/lib/types'; +import { + AcceptedFilters, + generateCatchAllFilter, + generateSpecificFilter, +} from '../stackScriptUtils'; import Button from 'src/components/Button'; +import classNames from 'classnames'; import { CircleProgress } from 'src/components/CircleProgress'; -import Typography from 'src/components/core/Typography'; +import { compose } from 'recompose'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Image } from '@linode/api-v4/lib/images'; +import { pathOr } from 'ramda'; +import { Notice } from 'src/components/Notice/Notice'; import Placeholder from 'src/components/Placeholder'; -import Table from 'src/components/Table'; -import { - withProfile, - WithProfileProps, -} from 'src/containers/profile.container'; -import { WithQueryClientProps } from 'src/containers/withQueryClient.container'; -import { isLinodeKubeImageId } from 'src/store/image/image.helpers'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; +import StackScriptsIcon from 'src/assets/icons/entityIcons/stackscript.svg'; +import StackScriptTableHead from '../Partials/StackScriptTableHead'; +import { StackScript } from '@linode/api-v4/lib/stackscripts'; +import { StackScriptsEmptyLandingState } from './StackScriptsEmptyLandingPage'; +import { StackScriptsRequest } from '../types'; +import { Table } from 'src/components/Table'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getDisplayName } from 'src/utilities/getDisplayName'; -import { handleUnauthorizedErrors } from 'src/utilities/handleUnauthorizedErrors'; import { getQueryParam } from 'src/utilities/queryParams'; -import StackScriptTableHead from '../Partials/StackScriptTableHead'; +import { handleUnauthorizedErrors } from 'src/utilities/handleUnauthorizedErrors'; +import { isLinodeKubeImageId } from 'src/store/image/image.helpers'; +import { Waypoint } from 'react-waypoint'; +import { WithQueryClientProps } from 'src/containers/withQueryClient.container'; import { - AcceptedFilters, - generateCatchAllFilter, - generateSpecificFilter, -} from '../stackScriptUtils'; -import { StackScriptsRequest } from '../types'; + withProfile, + WithProfileProps, +} from 'src/containers/profile.container'; import withStyles, { StyleProps } from './StackScriptBase.styles'; type CurrentFilter = 'label' | 'deploys' | 'revision'; @@ -493,44 +493,9 @@ const withStackScriptBase = (options: WithStackScriptBaseOptions) => ( You don’t have any StackScripts to select from. </Placeholder> ) : ( - <Placeholder - icon={StackScriptsIcon} - renderAsSecondary - isEntity - title="StackScripts" - buttonProps={[ - { - children: 'Create StackScript', - onClick: () => this.goToCreateStackScript(), - }, - ]} - className={classes.stackscriptPlaceholder} - > - <Typography variant="subtitle1"> - Automate Deployment with StackScripts! - </Typography> - <Typography variant="subtitle1"> - <a - href="https://linode.com/docs/platform/stackscripts-new-manager/" - target="_blank" - aria-describedby="external-site" - rel="noopener noreferrer" - className="h-u" - > - Learn more about getting started - </a> -  or  - <a - href="https://www.linode.com/docs/" - target="_blank" - aria-describedby="external-site" - rel="noopener noreferrer" - className="h-u" - > - visit our guides and tutorials. - </a> - </Typography> - </Placeholder> + <StackScriptsEmptyLandingState + goToCreateStackScript={this.goToCreateStackScript} + /> )} </div> ) : ( diff --git a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyLandingPage.tsx b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyLandingPage.tsx new file mode 100644 index 00000000000..51da185b8a5 --- /dev/null +++ b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyLandingPage.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import StackScriptsIcon from 'src/assets/icons/entityIcons/stackscript.svg'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; +import { sendEvent } from 'src/utilities/ga'; +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './StackScriptsEmptyResourcesData'; + +interface Props { + goToCreateStackScript: () => void; +} + +export const StackScriptsEmptyLandingState = (props: Props) => { + const { goToCreateStackScript } = props; + + return ( + <ResourcesSection + buttonProps={[ + { + onClick: () => { + sendEvent({ + category: linkGAEvent.category, + action: 'Click:button', + label: 'Create StackScript', + }); + goToCreateStackScript(); + }, + children: 'Create StackScript', + }, + ]} + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={StackScriptsIcon} + linkGAEvent={linkGAEvent} + youtubeLinkData={youtubeLinkData} + /> + ); +}; diff --git a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyResourcesData.ts b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyResourcesData.ts new file mode 100644 index 00000000000..42e969382b1 --- /dev/null +++ b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptsEmptyResourcesData.ts @@ -0,0 +1,72 @@ +import { + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + 'Run custom scripts to install and configure software when initializing Linode Compute Instances', + + subtitle: 'Automate deployment scripts', + title: 'StackScripts', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: + 'https://www.linode.com/docs/products/tools/stackscripts/get-started/', + text: 'Getting Started with StackScripts', + }, + { + to: + 'https://www.linode.com/docs/products/tools/stackscripts/guides/create/', + text: 'Create a StackScript', + }, + { + to: + 'https://www.linode.com/docs/products/tools/stackscripts/guides/write-a-custom-script/', + text: 'Write a Custom Script for Use with StackScripts', + }, + ], + moreInfo: { + to: 'https://www.linode.com/docs/products/tools/stackscripts/ ', + text: 'View additional Object Storage documentation', + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=nygChMc1hX4', + text: 'Automate Server Deployments Using Stackscripts', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=EbyA5rZwyRw', + text: 'Shell Scripts Explained', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=yM8v5i2Qjgg', + text: ' Linux for Programmers #7 | Environment Variables', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'StackScripts landing page empty', +}; diff --git a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx index d7c62c4a4c3..e832ad3e22f 100644 --- a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx @@ -20,7 +20,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Item } from 'src/components/EnhancedSelect/Select'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import withImages, { DefaultProps as ImagesProps, } from 'src/containers/images.container'; diff --git a/packages/manager/src/features/StackScripts/StackScriptDialog.tsx b/packages/manager/src/features/StackScripts/StackScriptDialog.tsx index 3a3d7de3121..f81a269ae72 100644 --- a/packages/manager/src/features/StackScripts/StackScriptDialog.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptDialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { connect, MapDispatchToProps } from 'react-redux'; import { Dialog } from 'src/components/Dialog/Dialog'; import { CircleProgress } from 'src/components/CircleProgress'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import _StackScript from 'src/components/StackScript'; import { ApplicationState } from 'src/store'; import { closeStackScriptDialog } from 'src/store/stackScriptDialog'; diff --git a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx index e974347bc81..32582a78eb3 100644 --- a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx @@ -10,7 +10,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import ImageSelect from 'src/features/Images/ImageSelect'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx index 68f04e03ab0..dc1eb99759b 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx @@ -5,8 +5,8 @@ import Hidden from 'src/components/core/Hidden'; import { withStyles, WithStyles } from '@mui/styles'; import Typography from 'src/components/core/Typography'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import StackScriptsActionMenu from 'src/features/StackScripts/StackScriptPanel/StackScriptActionMenu'; import { StackScriptCategory } from 'src/features/StackScripts/stackScriptUtils'; import { ClassNames, styles } from '../StackScriptRowHelpers'; diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx index 289c6ecd2d0..78e83d2251c 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx @@ -3,9 +3,9 @@ import { StackScript } from '@linode/api-v4/lib/stackscripts'; import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { canUserModifyAccountStackScript, StackScriptCategory, diff --git a/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx b/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx index 91d4daf7ce6..65e03b8da79 100644 --- a/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx @@ -5,7 +5,7 @@ import { CircleProgress } from 'src/components/CircleProgress'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; import LandingHeader from 'src/components/LandingHeader'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { listToItemsByID } from 'src/queries/base'; import { useAllImagesQuery } from 'src/queries/images'; import StackScriptPanel from './StackScriptPanel'; diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx index 830b74815cb..ee30c9b7ee0 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx @@ -1,7 +1,7 @@ import { UserDefinedField } from '@linode/api-v4/lib/stackscripts'; import * as React from 'react'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard from 'src/components/RenderGuard'; interface Props { diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx index 36a7eaa745c..a817c733f12 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx @@ -5,7 +5,7 @@ import InputLabel from 'src/components/core/InputLabel'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import MenuItem from 'src/components/MenuItem'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import TextField from 'src/components/TextField'; diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx index edcc005548e..540a4687083 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx @@ -10,7 +10,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import Divider from 'src/components/core/Divider'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard from 'src/components/RenderGuard'; import ShowMoreExpansion from 'src/components/ShowMoreExpansion'; import AppInfo from '../../linodes/LinodesCreate/AppInfo'; @@ -34,7 +34,6 @@ const useStyles = makeStyles((theme: Theme) => ({ username: { color: theme.color.grey1, }, - optionalFieldWrapper: {}, header: { display: 'flex', alignItems: 'center', @@ -252,18 +251,18 @@ const UserDefinedFieldsPanel = (props: CombinedProps) => { {/* Optional Fields */} {optionalUDFs.length !== 0 && ( <ShowMoreExpansion name="Advanced Options" defaultExpanded={true}> - <Typography variant="body1" className={classes.advDescription}> - These fields are additional configuration options and are not - required for creation. - </Typography> - <div - className={`${classes.optionalFieldWrapper} optionalFieldWrapper`} - > - {optionalUDFs.map((field: UserDefinedField) => { - const error = getError(field, errors); - return renderField(udf_data, handleChange, field, error); - })} - </div> + <> + <Typography variant="body1" className={classes.advDescription}> + These fields are additional configuration options and are not + required for creation. + </Typography> + <div> + {optionalUDFs.map((field: UserDefinedField) => { + const error = getError(field, errors); + return renderField(udf_data, handleChange, field, error); + })} + </div> + </> </ShowMoreExpansion> )} </Paper> diff --git a/packages/manager/src/features/Support/SupportTicketDetail/AttachmentError.tsx b/packages/manager/src/features/Support/SupportTicketDetail/AttachmentError.tsx index 16086cd989b..8b8eb9c08a8 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/AttachmentError.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/AttachmentError.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface Props { fileName: string; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx b/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx index 20c8bd68fe0..04ff4e38464 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx @@ -6,7 +6,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import scrollTo from 'src/utilities/scrollTo'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx index 09bb37be9e3..b7758cd0faf 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx @@ -22,7 +22,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ErrorState from 'src/components/ErrorState'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { withProfile, WithProfileProps, diff --git a/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx b/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx index 9fe180b00d5..da4d41bff52 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx @@ -11,7 +11,7 @@ import Accordion from 'src/components/Accordion'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; import { storage } from 'src/utilities/storage'; import { debounce } from 'throttle-debounce'; diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx index ddfc628a989..d7a9fa6f8d5 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx @@ -16,7 +16,7 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { Dialog } from 'src/components/Dialog/Dialog'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SectionErrorBoundary from 'src/components/SectionErrorBoundary'; import { EntityForTicketDetails } from 'src/components/SupportLink/SupportLink'; import TextField from 'src/components/TextField'; diff --git a/packages/manager/src/features/Support/SupportTickets/TicketList.tsx b/packages/manager/src/features/Support/SupportTickets/TicketList.tsx index cc1d53f42b8..fd3924952fc 100644 --- a/packages/manager/src/features/Support/SupportTickets/TicketList.tsx +++ b/packages/manager/src/features/Support/SupportTickets/TicketList.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; import { SupportTicket } from '@linode/api-v4/lib/support'; import Hidden from 'src/components/core/Hidden'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import TicketRow from './TicketRow'; import { useSupportTicketsQuery } from 'src/queries/support'; import { usePagination } from 'src/hooks/usePagination'; diff --git a/packages/manager/src/features/Support/SupportTickets/TicketRow.tsx b/packages/manager/src/features/Support/SupportTickets/TicketRow.tsx index 32358b5bb4b..8c50f1a4dc4 100644 --- a/packages/manager/src/features/Support/SupportTickets/TicketRow.tsx +++ b/packages/manager/src/features/Support/SupportTickets/TicketRow.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import Hidden from 'src/components/core/Hidden'; import Typography from 'src/components/core/Typography'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { SupportTicket } from '@linode/api-v4/lib/support'; import { Link } from 'react-router-dom'; import { makeStyles } from '@mui/styles'; diff --git a/packages/manager/src/features/TheApplicationIsOnFire.tsx b/packages/manager/src/features/TheApplicationIsOnFire.tsx index 9422d72d35f..23c112bddaa 100644 --- a/packages/manager/src/features/TheApplicationIsOnFire.tsx +++ b/packages/manager/src/features/TheApplicationIsOnFire.tsx @@ -4,7 +4,7 @@ import DialogContent from 'src/components/core/DialogContent'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import DialogTitle from 'src/components/DialogTitle'; +import { DialogTitle } from 'src/components/DialogTitle/DialogTitle'; const useStyles = makeStyles((theme: Theme) => ({ restartButton: { diff --git a/packages/manager/src/features/Users/CreateUserDrawer.tsx b/packages/manager/src/features/Users/CreateUserDrawer.tsx index 365bad1d61e..e5e7d3ecfa1 100644 --- a/packages/manager/src/features/Users/CreateUserDrawer.tsx +++ b/packages/manager/src/features/Users/CreateUserDrawer.tsx @@ -6,7 +6,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import FormControlLabel from 'src/components/core/FormControlLabel'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Users/UserDetail.tsx b/packages/manager/src/features/Users/UserDetail.tsx index 38998eca181..9bd7ef82173 100644 --- a/packages/manager/src/features/Users/UserDetail.tsx +++ b/packages/manager/src/features/Users/UserDetail.tsx @@ -12,7 +12,7 @@ import { import TabPanels from 'src/components/core/ReachTabPanels'; import Tabs from 'src/components/core/ReachTabs'; import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SafeTabPanel from 'src/components/SafeTabPanel'; import TabLinkList from 'src/components/TabLinkList'; import { queryKey } from 'src/queries/account'; diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index 38701358297..44c2068e2e8 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -22,7 +22,7 @@ import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SelectionCard from 'src/components/SelectionCard'; import { Toggle } from 'src/components/Toggle'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx index 9c82209e963..aaf85307c02 100644 --- a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx +++ b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { Grant, GrantLevel, GrantType } from '@linode/api-v4/lib/account'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Table from 'src/components/Table/Table'; -import TableHead from 'src/components/core/TableHead'; -import TableRow from 'src/components/TableRow/TableRow'; -import TableCell from 'src/components/TableCell/TableCell'; +import { Table } from 'src/components/Table'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; import Radio from 'src/components/Radio/Radio'; -import TableBody from 'src/components/core/TableBody'; -import PaginationFooter from 'src/components/PaginationFooter/PaginationFooter'; +import { TableBody } from 'src/components/TableBody'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { usePagination } from 'src/hooks/usePagination'; import { createDisplayPage } from 'src/components/Paginate'; import Typography from 'src/components/core/Typography'; diff --git a/packages/manager/src/features/Users/UserProfile.tsx b/packages/manager/src/features/Users/UserProfile.tsx index f616bd48d0e..fa959fc9312 100644 --- a/packages/manager/src/features/Users/UserProfile.tsx +++ b/packages/manager/src/features/Users/UserProfile.tsx @@ -12,7 +12,7 @@ import { Theme, useTheme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { useProfile } from 'src/queries/profile'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Users/UsersLanding.tsx b/packages/manager/src/features/Users/UsersLanding.tsx index fc1dfe3f413..0eb76a2ab19 100644 --- a/packages/manager/src/features/Users/UsersLanding.tsx +++ b/packages/manager/src/features/Users/UsersLanding.tsx @@ -5,15 +5,15 @@ import AddNewLink from 'src/components/AddNewLink'; import { makeStyles, useTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Notice } from 'src/components/Notice/Notice'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; diff --git a/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx b/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx index ea5fd4fbd2e..3fc9566ac6b 100644 --- a/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; import { resetEventsPolling } from 'src/eventsPolling'; import useLinodes from 'src/hooks/useLinodes'; diff --git a/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx b/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx index efe53b63222..9423b629784 100644 --- a/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx +++ b/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx @@ -7,10 +7,10 @@ import FormControl from 'src/components/core/FormControl'; import FormHelperText from 'src/components/core/FormHelperText'; import Drawer from 'src/components/Drawer'; import Select, { Item } from 'src/components/EnhancedSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { resetEventsPolling } from 'src/eventsPolling'; import LinodeSelect from 'src/features/linodes/LinodeSelect'; -import { useAllLinodeConfigsQuery } from 'src/queries/linodes'; +import { useAllLinodeConfigsQuery } from 'src/queries/linodes/linodes'; import { useGrants, useProfile } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx b/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx index 895e54c849d..0f593b78bfc 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx @@ -15,7 +15,7 @@ import { Theme, useTheme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { RegionSelect } from 'src/components/EnhancedSelect/variants/RegionSelect'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { MAX_VOLUME_SIZE } from 'src/constants'; import EUAgreementCheckbox from 'src/features/Account/Agreements/EUAgreementCheckbox'; import LinodeSelect from 'src/features/linodes/LinodeSelect'; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/AttachVolumeToLinodeForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/AttachVolumeToLinodeForm.tsx index 0b6dd6bdfab..e0625ad1934 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/AttachVolumeToLinodeForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/AttachVolumeToLinodeForm.tsx @@ -16,7 +16,7 @@ import ConfigSelect from './ConfigSelect'; import { modes } from './modes'; import { ModeSelection } from './ModeSelection'; import NoticePanel from './NoticePanel'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import VolumesActionsPanel from './VolumesActionsPanel'; import VolumeSelect from './VolumeSelect'; import { useAttachVolumeMutation } from 'src/queries/volumes'; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/EditVolumeForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/EditVolumeForm.tsx index 474a9a33fbc..1ca675d51fe 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/EditVolumeForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/EditVolumeForm.tsx @@ -1,7 +1,7 @@ import { UpdateVolumeSchema } from '@linode/validation/lib/volumes.schema'; import { useFormik } from 'formik'; import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { TagsInput, Tag } from 'src/components/TagsInput/TagsInput'; import { useUpdateVolumeMutation } from 'src/queries/volumes'; import { diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/NoticePanel.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/NoticePanel.tsx index 21cd4c9de97..959ac3834c3 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/NoticePanel.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/NoticePanel.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface Props { success?: string; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumeForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumeForm.tsx index 9d77f9d0b32..6bccb412c91 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumeForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumeForm.tsx @@ -2,7 +2,7 @@ import { ResizeVolumeSchema } from '@linode/validation/lib/volumes.schema'; import { Formik } from 'formik'; import * as React from 'react'; import Form from 'src/components/core/Form'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { resetEventsPolling } from 'src/eventsPolling'; import { useResizeVolumeMutation } from 'src/queries/volumes'; import { diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumesInstruction.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumesInstruction.tsx index 5776832b950..58eb38eb7db 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumesInstruction.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/ResizeVolumesInstruction.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; -import CopyableTextField from 'src/components/CopyableTextField'; +import { CopyableTextField } from 'src/components/CopyableTextField/CopyableTextField'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/VolumeConfigForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/VolumeConfigForm.tsx index e208fb484c9..c73316eb253 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/VolumeConfigForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/VolumeConfigForm.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import CopyableTextField from 'src/components/CopyableTextField'; +import { CopyableTextField } from 'src/components/CopyableTextField/CopyableTextField'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; diff --git a/packages/manager/src/features/Volumes/VolumeTableRow.tsx b/packages/manager/src/features/Volumes/VolumeTableRow.tsx index ea08b4d8ae7..80696ac52cc 100644 --- a/packages/manager/src/features/Volumes/VolumeTableRow.tsx +++ b/packages/manager/src/features/Volumes/VolumeTableRow.tsx @@ -7,8 +7,8 @@ import { makeStyles } from '@mui/styles'; import Typography from 'src/components/core/Typography'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import Box from '@mui/material/Box'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import VolumesActionMenu, { ActionHandlers } from './VolumesActionMenu'; import { Volume } from '@linode/api-v4/lib/volumes/types'; import { useRegionsQuery } from 'src/queries/regions'; diff --git a/packages/manager/src/features/Volumes/VolumesLanding.tsx b/packages/manager/src/features/Volumes/VolumesLanding.tsx index 8373ef01c7b..f42f2ee257d 100644 --- a/packages/manager/src/features/Volumes/VolumesLanding.tsx +++ b/packages/manager/src/features/Volumes/VolumesLanding.tsx @@ -5,22 +5,17 @@ import { connect } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { compose } from 'recompose'; import { bindActionCreators, Dispatch } from 'redux'; -import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; -import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import Typography from 'src/components/core/Typography'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import ErrorState from 'src/components/ErrorState'; import LandingHeader from 'src/components/LandingHeader'; import Loading from 'src/components/LandingLoading'; -import Link from 'src/components/Link'; -import PaginationFooter from 'src/components/PaginationFooter/PaginationFooter'; -import Placeholder from 'src/components/Placeholder'; -import Table from 'src/components/Table/Table'; -import TableCell from 'src/components/TableCell/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; -import TableSortCell from 'src/components/TableSortCell/TableSortCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useVolumesQuery } from 'src/queries/volumes'; @@ -38,6 +33,7 @@ import { DestructiveVolumeDialog } from './DestructiveVolumeDialog'; import { UpgradeVolumeDialog } from './UpgradeVolumeDialog'; import { VolumeAttachmentDrawer } from './VolumeAttachmentDrawer'; import { ActionHandlers as VolumeHandlers } from './VolumesActionMenu'; +import { VolumesLandingEmptyState } from './VolumesLandingEmptyState'; import VolumeTableRow from './VolumeTableRow'; interface Props { @@ -78,18 +74,9 @@ interface DispatchProps { type CombinedProps = Props & DispatchProps; -export const useStyles = makeStyles(() => ({ - empty: { - '& svg': { - transform: 'scale(0.75)', - }, - }, -})); - const preferenceKey = 'volumes'; -export const VolumesLanding: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); +export const VolumesLanding = (props: CombinedProps) => { const history = useHistory(); const pagination = usePagination(1, preferenceKey); @@ -226,32 +213,7 @@ export const VolumesLanding: React.FC<CombinedProps> = (props) => { } if (volumes?.results === 0) { - return ( - <> - <DocumentTitleSegment segment="Volumes" /> - <Placeholder - title="Volumes" - className={classes.empty} - icon={VolumeIcon} - isEntity - buttonProps={[ - { - onClick: () => history.push('/volumes/create'), - children: 'Create Volume', - }, - ]} - > - <Typography variant="subtitle1"> - Attach additional storage to your Linode. - </Typography> - <Typography variant="subtitle1"> - <Link to="https://www.linode.com/docs/products/storage/block-storage/"> - Learn more about Linode Block Storage Volumes. - </Link> - </Typography> - </Placeholder> - </> - ); + return <VolumesLandingEmptyState />; } const handlers: VolumeHandlers = { diff --git a/packages/manager/src/features/Volumes/VolumesLandingEmptyState.styles.ts b/packages/manager/src/features/Volumes/VolumesLandingEmptyState.styles.ts new file mode 100644 index 00000000000..b65fd81f0df --- /dev/null +++ b/packages/manager/src/features/Volumes/VolumesLandingEmptyState.styles.ts @@ -0,0 +1,8 @@ +import { styled } from '@mui/material/styles'; +import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; + +const StyledVolumeIcon = styled(VolumeIcon)(() => ({ + transform: 'scale(0.75)', +})); + +export { StyledVolumeIcon }; diff --git a/packages/manager/src/features/Volumes/VolumesLandingEmptyState.tsx b/packages/manager/src/features/Volumes/VolumesLandingEmptyState.tsx new file mode 100644 index 00000000000..b53a4751a2b --- /dev/null +++ b/packages/manager/src/features/Volumes/VolumesLandingEmptyState.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; +import { sendEvent } from 'src/utilities/ga'; +import { StyledVolumeIcon } from './VolumesLandingEmptyState.styles'; +import { useHistory } from 'react-router-dom'; +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './VolumesLandingEmptyStateData'; + +export const VolumesLandingEmptyState = () => { + const { push } = useHistory(); + + return ( + <ResourcesSection + buttonProps={[ + { + onClick: () => { + sendEvent({ + category: linkGAEvent.category, + action: 'Click:button', + label: 'Create Volume', + }); + push('/volumes/create'); + }, + children: 'Create Volume', + }, + ]} + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={StyledVolumeIcon} + linkGAEvent={linkGAEvent} + youtubeLinkData={youtubeLinkData} + /> + ); +}; diff --git a/packages/manager/src/features/Volumes/VolumesLandingEmptyStateData.ts b/packages/manager/src/features/Volumes/VolumesLandingEmptyStateData.ts new file mode 100644 index 00000000000..784335e4f76 --- /dev/null +++ b/packages/manager/src/features/Volumes/VolumesLandingEmptyStateData.ts @@ -0,0 +1,72 @@ +import { + docsLink, + guidesMoreLinkText, + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + 'Attach scalable, fault-tolerant, and performant block storage volumes to your Linode Compute Instances or Kubernetes Clusters.', + subtitle: 'NVM block storage service', + title: 'Volumes', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: 'https://www.linode.com/docs/products/storage/block-storage/', + text: 'Overview of Block Storage', + }, + { + to: 'https://www.linode.com/docs/products/storage/block-storage/guides/', + text: 'Create and Manage Block Storage Volumes', + }, + { + to: + 'https://www.linode.com/docs/products/storage/block-storage/guides/configure-volume/', + text: 'Configure a Volume on a Compute Instance', + }, + ], + moreInfo: { + to: docsLink, + text: guidesMoreLinkText, + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=7ti25oK7UMA', + text: 'How to Use Block Storage with Your Linode', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=8G0cNZZIxNc', + text: 'Block Storage Vs Object Storage', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=Z9jZv_IHO2s', + text: + 'How to use Block Storage to Increase Space on Your Nextcloud Instance', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'Volumes landing page empty', +}; diff --git a/packages/manager/src/features/linodes/CloneLanding/Configs.tsx b/packages/manager/src/features/linodes/CloneLanding/Configs.tsx index 4be7dd51e25..4bd8b8a4aa4 100644 --- a/packages/manager/src/features/linodes/CloneLanding/Configs.tsx +++ b/packages/manager/src/features/linodes/CloneLanding/Configs.tsx @@ -2,12 +2,12 @@ import { Config } from '@linode/api-v4/lib/linodes'; import * as React from 'react'; import CheckBox from 'src/components/CheckBox'; import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableCell from 'src/components/core/TableCell'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import { ConfigSelection } from './utilities'; diff --git a/packages/manager/src/features/linodes/CloneLanding/Details.tsx b/packages/manager/src/features/linodes/CloneLanding/Details.tsx index fe4292c605d..80be16f8cc3 100644 --- a/packages/manager/src/features/linodes/CloneLanding/Details.tsx +++ b/packages/manager/src/features/linodes/CloneLanding/Details.tsx @@ -9,7 +9,7 @@ import Paper from 'src/components/core/Paper'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useRegionsQuery } from 'src/queries/regions'; import LinodeSelect from '../LinodeSelect'; import { diff --git a/packages/manager/src/features/linodes/CloneLanding/Disks.tsx b/packages/manager/src/features/linodes/CloneLanding/Disks.tsx index a1a66dd99a1..85f0d155053 100644 --- a/packages/manager/src/features/linodes/CloneLanding/Disks.tsx +++ b/packages/manager/src/features/linodes/CloneLanding/Disks.tsx @@ -3,14 +3,14 @@ import { intersection, pathOr } from 'ramda'; import * as React from 'react'; import CheckBox from 'src/components/CheckBox'; import { makeStyles } from '@mui/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Grid from '@mui/material/Unstable_Grid2'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import { DiskSelection } from './utilities'; diff --git a/packages/manager/src/features/linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/linodes/LinodeEntityDetail.tsx index 82d2f96ee9d..c9e5b6524da 100644 --- a/packages/manager/src/features/linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/linodes/LinodeEntityDetail.tsx @@ -5,7 +5,6 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { Link } from 'react-router-dom'; import { HashLink } from 'react-router-hash-link'; -import { compose } from 'recompose'; import Button from 'src/components/Button'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import Box from 'src/components/core/Box'; @@ -13,18 +12,14 @@ import Chip from 'src/components/core/Chip'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Table from 'src/components/core/Table'; -import TableBody from 'src/components/core/TableBody'; -import TableCell from 'src/components/core/TableCell'; +import { TableBody } from 'src/components/TableBody'; import Typography, { TypographyProps } from 'src/components/core/Typography'; import EntityDetail from 'src/components/EntityDetail'; import Grid, { Grid2Props } from '@mui/material/Unstable_Grid2'; -import TableRow from 'src/components/TableRow'; +import { TableRow } from 'src/components/TableRow'; import TagCell from 'src/components/TagCell'; import LinodeActionMenu from 'src/features/linodes/LinodesLanding/LinodeActionMenu'; import { ProgressDisplay } from 'src/features/linodes/LinodesLanding/LinodeRow/LinodeRow'; -import { Action as BootAction } from 'src/features/linodes/PowerActionsDialogOrDrawer'; -import { OpenDialog } from 'src/features/linodes/types'; import { lishLaunch } from 'src/features/Lish/lishUtils'; import useLinodeActions from 'src/hooks/useLinodeActions'; import { useSpecificTypes } from 'src/queries/types'; @@ -38,9 +33,7 @@ import { pluralize } from 'src/utilities/pluralize'; import { ipv4TableID } from './LinodesDetail/LinodeNetworking/LinodeNetworking'; import { lishLink, sshLink } from './LinodesDetail/utilities'; import EntityHeader from 'src/components/EntityHeader'; -import withRecentEvent, { - WithRecentEvent, -} from './LinodesLanding/withRecentEvent'; +import { WithRecentEvent } from './LinodesLanding/withRecentEvent'; import { getProgressOrDefault, isEventWithSecondaryLinodeStatus, @@ -52,19 +45,16 @@ import useExtendedLinode from 'src/hooks/useExtendedLinode'; import { useTheme } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import { useProfile } from 'src/queries/profile'; +import { LinodeHandlers } from './LinodesLanding/LinodesLanding'; +// This component was built asuming an unmodified MUI <Table /> +import Table from '@mui/material/Table'; +import { TableCell } from 'src/components/TableCell'; interface LinodeEntityDetailProps { variant?: TypographyProps['variant']; id: number; linode: Linode; username?: string; - openDialog: OpenDialog; - openPowerActionDialog: ( - bootAction: BootAction, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => void; backups: LinodeBackups; linodeConfigs: Config[]; numVolumes: number; @@ -73,22 +63,14 @@ interface LinodeEntityDetailProps { isSummaryView?: boolean; } -interface StatusChange { - linodeConfigs: Config[]; - linodeId: number; - linodeLabel: string; - status: BootAction; -} - -export type CombinedProps = LinodeEntityDetailProps & WithRecentEvent; +export type CombinedProps = LinodeEntityDetailProps & + WithRecentEvent & { handlers: LinodeHandlers }; const LinodeEntityDetail: React.FC<CombinedProps> = (props) => { const { variant, linode, username, - openDialog, - openPowerActionDialog, backups, linodeConfigs, isSummaryView, @@ -96,6 +78,7 @@ const LinodeEntityDetail: React.FC<CombinedProps> = (props) => { openTagDrawer, openNotificationMenu, recentEvent, + handlers, } = props; const { data: images } = useAllImagesQuery({}, {}); @@ -140,8 +123,6 @@ const LinodeEntityDetail: React.FC<CombinedProps> = (props) => { linodeId={linode.id} linodeStatus={linode.status} linodePermissions={extendedLinode?._permissions} - openDialog={openDialog} - openPowerActionDialog={openPowerActionDialog} linodeRegionDisplay={linodeRegionDisplay} backups={backups} isSummaryView={isSummaryView} @@ -151,6 +132,7 @@ const LinodeEntityDetail: React.FC<CombinedProps> = (props) => { openNotificationMenu={openNotificationMenu || (() => null)} progress={progress} transitionText={transitionText} + handlers={handlers} /> } body={ @@ -176,19 +158,13 @@ const LinodeEntityDetail: React.FC<CombinedProps> = (props) => { linodeTags={linode.tags} linodeLabel={linode.label} openTagDrawer={openTagDrawer} - openDialog={openDialog} /> } /> ); }; -const enhanced = compose<CombinedProps, LinodeEntityDetailProps>( - withRecentEvent, - React.memo -); - -export default enhanced(LinodeEntityDetail); +export default LinodeEntityDetail; // ============================================================================= // Header @@ -200,13 +176,6 @@ export interface HeaderProps { linodeId: number; linodeStatus: Linode['status']; linodePermissions?: GrantLevel; - openDialog: OpenDialog; - openPowerActionDialog: ( - bootAction: BootAction, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => void; linodeRegionDisplay: string; backups: LinodeBackups; type: ExtendedType | null; @@ -285,7 +254,9 @@ const useHeaderStyles = makeStyles((theme: Theme) => ({ }, })); -const Header: React.FC<HeaderProps> = (props) => { +const Header: React.FC<HeaderProps & { handlers: LinodeHandlers }> = ( + props +) => { const classes = useHeaderStyles(); const theme = useTheme(); @@ -294,16 +265,14 @@ const Header: React.FC<HeaderProps> = (props) => { linodeId, linodeStatus, linodeRegionDisplay, - openDialog, - openPowerActionDialog, backups, type, variant, - linodeConfigs, isSummaryView, progress, transitionText, openNotificationMenu, + handlers, } = props; const isRunning = linodeStatus === 'running'; @@ -342,16 +311,6 @@ const Header: React.FC<HeaderProps> = (props) => { display: 'flex', }; - const handleStatusChange = ({ - linodeConfigs, - linodeId, - linodeLabel, - status, - }: StatusChange) => { - sendLinodeActionMenuItemEvent(`${status} Linode`); - openPowerActionDialog(status, linodeId, linodeLabel, linodeConfigs); - }; - return ( <EntityHeader title={ @@ -398,12 +357,7 @@ const Header: React.FC<HeaderProps> = (props) => { sx={sxActionItem} disabled={!(isRunning || isOffline)} onClick={() => - handleStatusChange({ - linodeConfigs, - linodeId, - linodeLabel, - status: isRunning ? 'Power Off' : 'Power On', - }) + handlers.onOpenPowerDialog(isRunning ? 'Power Off' : 'Power On') } > {isRunning ? 'Power Off' : 'Power On'} @@ -412,14 +366,7 @@ const Header: React.FC<HeaderProps> = (props) => { buttonType="secondary" sx={sxActionItem} disabled={isOffline} - onClick={() => - handleStatusChange({ - linodeConfigs, - linodeId, - linodeLabel, - status: 'Reboot', - }) - } + onClick={() => handlers.onOpenPowerDialog('Reboot')} > Reboot </Button> @@ -441,8 +388,7 @@ const Header: React.FC<HeaderProps> = (props) => { linodeRegion={linodeRegionDisplay} linodeStatus={linodeStatus} linodeType={type ?? undefined} - openDialog={openDialog} - openPowerActionDialog={openPowerActionDialog} + {...handlers} /> </Box> </EntityHeader> @@ -770,7 +716,6 @@ interface FooterProps { linodeTags: string[]; linodeLabel: string; openTagDrawer: (tags: string[]) => void; - openDialog: OpenDialog; } export const Footer: React.FC<FooterProps> = React.memo((props) => { diff --git a/packages/manager/src/features/linodes/LinodesCreate/ApiAwarenessModal/index.tsx b/packages/manager/src/features/linodes/LinodesCreate/ApiAwarenessModal/index.tsx index 3073a7c0e30..4259d972162 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/ApiAwarenessModal/index.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/ApiAwarenessModal/index.tsx @@ -8,7 +8,7 @@ import ExternalLink from 'src/components/ExternalLink'; import SafeTabPanel from 'src/components/SafeTabPanel'; import TabLinkList from 'src/components/TabLinkList'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Tabs from 'src/components/core/ReachTabs'; diff --git a/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx index 7da0da6494f..33ba036da71 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx @@ -21,7 +21,7 @@ import DocsLink from 'src/components/DocsLink'; import ErrorState from 'src/components/ErrorState'; import Grid from '@mui/material/Unstable_Grid2'; import LabelAndTagsPanel from 'src/components/LabelAndTagsPanel'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SafeTabPanel from 'src/components/SafeTabPanel'; import SelectRegionPanel from 'src/components/SelectRegionPanel'; import TabLinkList, { Tab } from 'src/components/TabLinkList'; diff --git a/packages/manager/src/features/linodes/LinodesCreate/Panel.tsx b/packages/manager/src/features/linodes/LinodesCreate/Panel.tsx index c1486c8e172..bc2fec7552e 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/Panel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/Panel.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import Paper from 'src/components/core/Paper'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface Props { error?: string; diff --git a/packages/manager/src/features/linodes/LinodesCreate/PasswordPanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/PasswordPanel.tsx index ac6b1689a6d..02b01fc508b 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/PasswordPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/PasswordPanel.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Paper from 'src/components/core/Paper'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; const PasswordInput = React.lazy(() => import('src/components/PasswordInput')); import RenderGuard from 'src/components/RenderGuard'; import SuspenseLoader from 'src/components/SuspenseLoader'; diff --git a/packages/manager/src/features/linodes/LinodesCreate/PremiumPlansAvailabilityNotice.tsx b/packages/manager/src/features/linodes/LinodesCreate/PremiumPlansAvailabilityNotice.tsx new file mode 100644 index 00000000000..011e25d5b1d --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesCreate/PremiumPlansAvailabilityNotice.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { Notice } from 'src/components/Notice/Notice'; +import useFlags from 'src/hooks/useFlags'; + +// Ideally we should add premium as a capability so the availability is returned by the API, +// but it's not there yet so we're using a flag for now. +export const PremiumPlansAvailabilityNotice = React.memo(() => { + const { premiumPlansAvailabilityNotice } = useFlags(); + + if (premiumPlansAvailabilityNotice) { + return <Notice warning>{premiumPlansAvailabilityNotice}</Notice>; + } + + return null; +}); diff --git a/packages/manager/src/features/linodes/LinodesCreate/SelectAppPanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/SelectAppPanel.tsx index 02a5103efa4..805c249db87 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/SelectAppPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/SelectAppPanel.tsx @@ -6,7 +6,7 @@ import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import ErrorState from 'src/components/ErrorState'; import Loading from 'src/components/LandingLoading'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import AppPanelSection from 'src/features/linodes/LinodesCreate/AppPanelSection'; import { getParamFromUrl } from 'src/utilities/queryParams'; import Panel from './Panel'; diff --git a/packages/manager/src/features/linodes/LinodesCreate/SelectBackupPanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/SelectBackupPanel.tsx index bf6b9aa23e2..c4a542855bd 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/SelectBackupPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/SelectBackupPanel.tsx @@ -11,16 +11,28 @@ import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import SelectionCard from 'src/components/SelectionCard'; -import { aggregateBackups } from 'src/features/linodes/LinodesDetail/LinodeBackup'; import { formatDate } from 'src/utilities/formatDate'; import { withProfile, WithProfileProps, } from 'src/containers/profile.container'; +export const aggregateBackups = ( + backups: LinodeBackupsResponse +): LinodeBackup[] => { + const manualSnapshot = + backups?.snapshot.in_progress?.status === 'needsPostProcessing' + ? backups?.snapshot.in_progress + : backups?.snapshot.current; + return ( + backups && + [...backups.automatic!, manualSnapshot!].filter((b) => Boolean(b)) + ); +}; + export interface LinodeWithBackups extends Linode { currentBackups: LinodeBackupsResponse; } diff --git a/packages/manager/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx index d6e37028647..a9f609bd355 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx @@ -6,9 +6,9 @@ import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import SelectionCard from 'src/components/SelectionCard'; diff --git a/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx index 71d77996929..b69ed8dd8dc 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx @@ -10,21 +10,21 @@ import FormControlLabel from 'src/components/core/FormControlLabel'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import { Currency } from 'src/components/Currency'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import RenderGuard from 'src/components/RenderGuard'; import SelectionCard from 'src/components/SelectionCard'; import TabbedPanel from 'src/components/TabbedPanel'; import { Tab } from 'src/components/TabbedPanel/TabbedPanel'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { LINODE_NETWORK_IN } from 'src/constants'; import arrayToList from 'src/utilities/arrayToDelimiterSeparatedList'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; @@ -32,6 +32,7 @@ import { gpuPlanText } from './utilities'; import { ExtendedType } from 'src/utilities/extendType'; import { ApplicationState } from 'src/store'; import { useRegionsQuery } from 'src/queries/regions'; +import { PremiumPlansAvailabilityNotice } from './PremiumPlansAvailabilityNotice'; const useStyles = makeStyles((theme: Theme) => ({ root: { @@ -93,6 +94,9 @@ const useStyles = makeStyles((theme: Theme) => ({ '& a:hover': { color: '#3683dc', }, + '& p': { + fontFamily: '"LatoWebBold", sans-serif', + }, }, })); @@ -507,7 +511,7 @@ export const SelectPlanPanel = (props: Props) => { {getRegionsWithCapability('GPU Linodes')}. </> ) : ( - <div className={classes.gpuGuideLink}>{gpuPlanText()}</div> + <div className={classes.gpuGuideLink}>{gpuPlanText(true)}</div> ); tabs.push({ render: () => { @@ -568,9 +572,7 @@ export const SelectPlanPanel = (props: Props) => { render: () => { return ( <> - <Notice warning> - This plan is only available in the Washington, DC region. - </Notice> + <PremiumPlansAvailabilityNotice /> <Typography data-qa-gpu className={classes.copy}> Premium CPU instances guarantee a minimum processor model, AMD Epyc<sup>TM</sup> 7713 or higher, to ensure consistent high diff --git a/packages/manager/src/features/linodes/LinodesCreate/SelectPlanQuantityPanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/SelectPlanQuantityPanel.tsx index 96ceafe94e9..ae97e884ce3 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/SelectPlanQuantityPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/SelectPlanQuantityPanel.tsx @@ -7,22 +7,23 @@ import Button from 'src/components/Button'; import Hidden from 'src/components/core/Hidden'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import EnhancedNumberInput from 'src/components/EnhancedNumberInput'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import SelectionCard from 'src/components/SelectionCard'; import TabbedPanel from 'src/components/TabbedPanel'; import { Tab } from 'src/components/TabbedPanel/TabbedPanel'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; import { gpuPlanText } from './utilities'; import { CreateNodePoolData } from '@linode/api-v4'; import { ExtendedType } from 'src/utilities/extendType'; +import { PremiumPlansAvailabilityNotice } from './PremiumPlansAvailabilityNotice'; type ClassNames = | 'root' @@ -378,9 +379,7 @@ export class SelectPlanQuantityPanel extends React.Component<CombinedProps> { render: () => { return ( <> - <Notice warning> - This plan is only available in the Washington, DC region. - </Notice> + <PremiumPlansAvailabilityNotice /> <Typography data-qa-gpu className={classes.copy}> Premium CPU instances guarantee a minimum processor model, AMD Epyc<sup>TM</sup> 7713 or higher, to ensure consistent high diff --git a/packages/manager/src/features/linodes/LinodesCreate/TabbedContent/ImageEmptyState.tsx b/packages/manager/src/features/linodes/LinodesCreate/TabbedContent/ImageEmptyState.tsx index 95521655bf6..e57c9639f2a 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/TabbedContent/ImageEmptyState.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/TabbedContent/ImageEmptyState.tsx @@ -3,7 +3,7 @@ import Paper from 'src/components/core/Paper'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; const useStyles = makeStyles((theme: Theme) => ({ emptyImagePanelText: { diff --git a/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordion.tsx b/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordion.tsx index 937e4f69c2b..b22494e1044 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordion.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordion.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Box from 'src/components/core/Box'; import Accordion from 'src/components/Accordion'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import Typography from 'src/components/core/Typography'; import { UserDataAccordionHeading } from './UserDataAccordionHeading'; diff --git a/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordionHeading.tsx b/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordionHeading.tsx index ca7ad9b61a8..b963010f2a1 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordionHeading.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/UserDataAccordion/UserDataAccordionHeading.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; import Box from 'src/components/core/Box'; import { CreateTypes } from 'src/store/linodeCreate/linodeCreate.actions'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/HostMaintenanceError.tsx b/packages/manager/src/features/linodes/LinodesDetail/HostMaintenanceError.tsx index c3c44b7649b..254705d37c8 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/HostMaintenanceError.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/HostMaintenanceError.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; const HostMaintenanceError = () => ( <Notice diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/ConfigRow.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/ConfigRow.tsx index 8ed332306f5..25f00a3245a 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/ConfigRow.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/ConfigRow.tsx @@ -3,8 +3,8 @@ import { isEmpty } from 'ramda'; import * as React from 'react'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { API_MAX_PAGE_SIZE } from 'src/constants'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; import LinodeConfigActionMenu from '../LinodeSettings/LinodeConfigActionMenu'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeConfigs.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeConfigs.tsx index 04c517789e3..55c480454a9 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeConfigs.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeConfigs.tsx @@ -16,18 +16,18 @@ import Button from 'src/components/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; import TableContentWrapper from 'src/components/TableContentWrapper'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import withFeatureFlags, { FeatureFlagConsumerProps, } from 'src/containers/withFeatureFlagConsumer.container'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskDrawer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskDrawer.tsx index bac7075aade..8546e6e512a 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskDrawer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskDrawer.tsx @@ -19,7 +19,7 @@ import { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; import { Link } from 'src/components/Link'; import ModeSelect, { Mode } from 'src/components/ModeSelect'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TextField from 'src/components/TextField'; import { TextTooltip } from 'src/components/TextTooltip'; import { diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskRow.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskRow.tsx index f6bd5f287c6..8e691cd9e64 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskRow.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDiskRow.tsx @@ -5,8 +5,8 @@ import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import useEvents from 'src/hooks/useEvents'; import LinodeDiskActionMenu from './LinodeDiskActionMenu'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDisks.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDisks.tsx index fda1360b586..f1c23317273 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDisks.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeDisks.tsx @@ -10,21 +10,21 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import Hidden from 'src/components/core/Hidden'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { resetEventsPolling } from 'src/eventsPolling'; import ImagesDrawer, { DrawerMode } from 'src/features/Images/ImagesDrawer'; import { diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeVolumes.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeVolumes.tsx index bc3b54a9fa5..3775ba55942 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeVolumes.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeAdvanced/LinodeVolumes.tsx @@ -6,18 +6,18 @@ import AddNewLink from 'src/components/AddNewLink'; import Hidden from 'src/components/core/Hidden'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import PaginationFooter from 'src/components/PaginationFooter/PaginationFooter'; -import Table from 'src/components/Table/Table'; -import TableCell from 'src/components/TableCell/TableCell'; -import TableRow from 'src/components/TableRow/TableRow'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import TableRowEmptyState from 'src/components/TableRowEmptyState/TableRowEmptyState'; import TableRowError from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import TableSortCell from 'src/components/TableSortCell'; +import { TableSortCell } from 'src/components/TableSortCell'; import { withLinodeDetailContext } from 'src/features/linodes/LinodesDetail/linodeDetailContext'; import { DestructiveVolumeDialog } from 'src/features/Volumes/DestructiveVolumeDialog'; import { VolumeAttachmentDrawer } from 'src/features/Volumes/VolumeAttachmentDrawer'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx index 5c9f815025d..9885401c9fb 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx @@ -1,19 +1,19 @@ import { LinodeBackup } from '@linode/api-v4/lib/linodes'; -import { Duration } from 'luxon'; +import { DateTime, Duration } from 'luxon'; import * as React from 'react'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { StatusIcon, Status } from 'src/components/StatusIcon/StatusIcon'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { parseAPIDate } from 'src/utilities/date'; import { formatDuration } from 'src/utilities/formatDuration'; -import LinodeBackupActionMenu from './LinodeBackupActionMenu'; +import { LinodeBackupActionMenu } from './LinodeBackupActionMenu'; interface Props { backup: LinodeBackup; disabled: boolean; - handleRestore: (backup: LinodeBackup) => void; - handleDeploy: (backup: LinodeBackup) => void; + handleRestore: () => void; + handleDeploy: () => void; } const typeMap = { @@ -41,12 +41,8 @@ const statusIconMap: Record<LinodeBackup['status'], Status> = { userAborted: 'error', }; -const BackupTableRow: React.FC<Props> = (props) => { - const { backup, disabled, handleRestore } = props; - - const onDeploy = () => { - props.handleDeploy(props.backup); - }; +const BackupTableRow = (props: Props) => { + const { backup, disabled, handleRestore, handleDeploy } = props; return ( <TableRow key={backup.id} data-qa-backup> @@ -71,7 +67,9 @@ const BackupTableRow: React.FC<Props> = (props) => { <TableCell parentColumn="Duration"> {formatDuration( Duration.fromMillis( - parseAPIDate(backup.finished).toMillis() - + (backup.finished + ? parseAPIDate(backup.finished).toMillis() + : DateTime.now().toMillis()) - parseAPIDate(backup.created).toMillis() ) )} @@ -91,7 +89,7 @@ const BackupTableRow: React.FC<Props> = (props) => { backup={backup} disabled={disabled} onRestore={handleRestore} - onDeploy={onDeploy} + onDeploy={handleDeploy} /> </TableCell> </TableRow> diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupsPlaceholder.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupsPlaceholder.tsx index af7db2cdeaf..511a2cd5070 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupsPlaceholder.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/BackupsPlaceholder.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; -import { makeStyles } from '@mui/styles'; +import { makeStyles } from 'tss-react/mui'; import Typography from 'src/components/core/Typography'; import { Currency } from 'src/components/Currency'; import Placeholder from 'src/components/Placeholder'; import LinodePermissionsError from '../LinodePermissionsError'; -import EnableBackupsDialog from './EnableBackupsDialog'; +import { EnableBackupsDialog } from './EnableBackupsDialog'; interface Props { backupsMonthlyPrice?: number; @@ -13,7 +13,7 @@ interface Props { linodeId: number; } -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles()(() => ({ empty: { '& svg': { transform: 'scale(0.75)', @@ -22,7 +22,7 @@ const useStyles = makeStyles(() => ({ })); export const BackupsPlaceholder = (props: Props) => { - const classes = useStyles(); + const { classes } = useStyles(); const { backupsMonthlyPrice, linodeId, disabled } = props; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx new file mode 100644 index 00000000000..d5872d7f3bd --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { useSnackbar } from 'notistack'; +import { resetEventsPolling } from 'src/eventsPolling'; +import { useLinodeBackupsCancelMutation } from 'src/queries/linodes/backups'; +import { sendBackupsDisabledEvent } from 'src/utilities/ga'; +import Typography from 'src/components/core/Typography'; +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import ActionsPanel from 'src/components/ActionsPanel/ActionsPanel'; +import Button from 'src/components/Button/Button'; + +interface Props { + isOpen: boolean; + onClose: () => void; + linodeId: number; +} + +export const CancelBackupsDialog = (props: Props) => { + const { isOpen, onClose, linodeId } = props; + const { enqueueSnackbar } = useSnackbar(); + + const { + mutateAsync: cancelBackups, + error, + isLoading, + } = useLinodeBackupsCancelMutation(linodeId); + + const onCancelBackups = async () => { + await cancelBackups(); + enqueueSnackbar('Backups are being canceled for this Linode', { + variant: 'info', + }); + onClose(); + resetEventsPolling(); + sendBackupsDisabledEvent(); + }; + + return ( + <ConfirmationDialog + open={isOpen} + onClose={onClose} + title="Confirm Cancellation" + error={error?.[0].reason} + actions={ + <ActionsPanel style={{ padding: 0 }}> + <Button + buttonType="secondary" + onClick={onClose} + data-qa-cancel-cancel + > + Close + </Button> + <Button + buttonType="primary" + onClick={onCancelBackups} + loading={isLoading} + data-qa-confirm-cancel + > + Cancel Backups + </Button> + </ActionsPanel> + } + > + <Typography> + Canceling backups associated with this Linode will delete all existing + backups. Are you sure? + </Typography> + <Typography style={{ marginTop: 12 }}> + <strong>Note: </strong> + Once backups for this Linode have been canceled, you cannot re-enable + them for 24 hours. + </Typography> + </ConfirmationDialog> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx new file mode 100644 index 00000000000..80433030a4a --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { renderWithTheme } from 'src/utilities/testHelpers'; +import { rest, server } from 'src/mocks/testServer'; +import { linodeFactory } from 'src/factories/linodes'; +import { CaptureSnapshot } from './CaptureSnapshot'; +import userEvent from '@testing-library/user-event'; + +describe('CaptureSnapshot', () => { + it('renders heading and copy', async () => { + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json(linodeFactory.build({ id: 1, backups: { enabled: true } })) + ); + }) + ); + + const { getByText } = renderWithTheme( + <CaptureSnapshot linodeId={1} isReadOnly={false} /> + ); + + getByText('Manual Snapshot'); + getByText( + /You can make a manual backup of your Linode by taking a snapshot./ + ); + }); + it('a confirmation dialog should open when you attempt to take a snapshot', async () => { + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json(linodeFactory.build({ id: 1, backups: { enabled: true } })) + ); + }) + ); + + const { getByLabelText, getByText } = renderWithTheme( + <CaptureSnapshot linodeId={1} isReadOnly={false} /> + ); + + userEvent.type(getByLabelText('Name Snapshot'), 'my-linode-snapshot'); + + userEvent.click(getByText('Take Snapshot')); + + expect( + getByText( + /Taking a snapshot will back up your Linode in its current state, overriding your previous snapshot. Are you sure?/ + ) + ).toBeVisible(); + }); +}); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx new file mode 100644 index 00000000000..b1ec29b6dfd --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import { Notice } from 'src/components/Notice/Notice'; +import FormControl from 'src/components/core/FormControl'; +import Paper from 'src/components/core/Paper'; +import Typography from 'src/components/core/Typography'; +import { Theme } from '@mui/material/styles'; +import { makeStyles } from 'tss-react/mui'; +import { useLinodeBackupSnapshotMutation } from 'src/queries/linodes/backups'; +import { useSnackbar } from 'notistack'; +import { useFormik } from 'formik'; +import TextField from 'src/components/TextField'; +import { CaptureSnapshotConfirmationDialog } from './CaptureSnapshotConfirmationDialog'; +import Button from 'src/components/Button'; +import { resetEventsPolling } from 'src/eventsPolling'; +import { getErrorMap } from 'src/utilities/errorUtils'; + +interface Props { + linodeId: number; + isReadOnly: boolean; +} + +const useStyles = makeStyles()((theme: Theme) => ({ + snapshotFormControl: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-end', + flexWrap: 'wrap', + '& > div': { + width: 'auto', + marginRight: theme.spacing(2), + }, + '& button': { + marginTop: theme.spacing(4), + }, + }, + snapshotNameField: { + minWidth: 275, + }, +})); + +export const CaptureSnapshot = ({ linodeId, isReadOnly }: Props) => { + const { classes } = useStyles(); + const { enqueueSnackbar } = useSnackbar(); + + const { + mutateAsync: takeSnapshot, + error: snapshotError, + isLoading: isSnapshotLoading, + } = useLinodeBackupSnapshotMutation(linodeId); + + const [ + isSnapshotConfirmationDialogOpen, + setIsSnapshotConfirmationDialogOpen, + ] = React.useState(false); + + const snapshotForm = useFormik({ + initialValues: { label: '' }, + async onSubmit(values, formikHelpers) { + await takeSnapshot(values); + enqueueSnackbar('Starting to capture snapshot', { + variant: 'info', + }); + setIsSnapshotConfirmationDialogOpen(false); + formikHelpers.resetForm(); + resetEventsPolling(); + }, + }); + + const hasErrorFor = getErrorMap(['label'], snapshotError); + + return ( + <Paper> + <Typography variant="h2" data-qa-manual-heading> + Manual Snapshot + </Typography> + <Typography variant="body1" data-qa-manual-desc marginTop={1}> + You can make a manual backup of your Linode by taking a snapshot. + Creating the manual snapshot can take several minutes, depending on the + size of your Linode and the amount of data you have stored on it. The + manual snapshot will not be overwritten by automatic backups. + </Typography> + <FormControl className={classes.snapshotFormControl}> + {hasErrorFor.none && ( + <Notice spacingBottom={8} error> + {hasErrorFor.none} + </Notice> + )} + <TextField + errorText={hasErrorFor.label} + label="Name Snapshot" + name="label" + value={snapshotForm.values.label} + onChange={snapshotForm.handleChange} + data-qa-manual-name + className={classes.snapshotNameField} + /> + <Button + buttonType="primary" + onClick={() => setIsSnapshotConfirmationDialogOpen(true)} + data-qa-snapshot-button + disabled={snapshotForm.values.label === '' || isReadOnly} + > + Take Snapshot + </Button> + </FormControl> + <CaptureSnapshotConfirmationDialog + open={isSnapshotConfirmationDialogOpen} + onClose={() => setIsSnapshotConfirmationDialogOpen(false)} + onSnapshot={() => snapshotForm.handleSubmit()} + loading={isSnapshotLoading} + /> + </Paper> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshotConfirmationDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshotConfirmationDialog.tsx new file mode 100644 index 00000000000..c270968a7f5 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/CaptureSnapshotConfirmationDialog.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import ActionsPanel from 'src/components/ActionsPanel'; +import Button from 'src/components/Button'; +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import Typography from 'src/components/core/Typography'; + +interface Props { + open: boolean; + error?: string; + loading: boolean; + onClose: () => void; + onSnapshot: () => void; +} + +export const CaptureSnapshotConfirmationDialog = (props: Props) => { + const { open, loading, onClose, error, onSnapshot } = props; + + const actions = ( + <ActionsPanel style={{ padding: 0 }}> + <Button buttonType="secondary" onClick={onClose} data-qa-cancel> + Cancel + </Button> + <Button + buttonType="primary" + onClick={onSnapshot} + loading={loading} + data-qa-confirm + > + Take Snapshot + </Button> + </ActionsPanel> + ); + + return ( + <ConfirmationDialog + open={open} + title="Take a snapshot?" + onClose={onClose} + actions={actions} + error={error} + > + <Typography> + Taking a snapshot will back up your Linode in its current state, + overriding your previous snapshot. Are you sure? + </Typography> + </ConfirmationDialog> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/DestructiveSnapshotDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/DestructiveSnapshotDialog.tsx deleted file mode 100644 index 6a0303dfb91..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/DestructiveSnapshotDialog.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import * as React from 'react'; -import ActionsPanel from 'src/components/ActionsPanel'; -import Button from 'src/components/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; - -type ClassNames = 'warningCopy'; - -const styles = (theme: Theme) => - createStyles({ - warningCopy: { - color: theme.color.red, - marginBottom: theme.spacing(2), - }, - }); - -interface Props { - open: boolean; - error?: string; - loading: boolean; - onClose: () => void; - onSnapshot: () => void; -} - -type CombinedProps = Props & WithStyles<ClassNames>; - -class DestructiveSnapshotDialog extends React.PureComponent<CombinedProps, {}> { - renderActions = () => { - return ( - <ActionsPanel style={{ padding: 0 }}> - <Button - buttonType="secondary" - onClick={this.props.onClose} - data-qa-cancel - > - Cancel - </Button> - <Button - buttonType="primary" - onClick={this.props.onSnapshot} - loading={this.props.loading} - data-qa-confirm - > - Take Snapshot - </Button> - </ActionsPanel> - ); - }; - - render() { - const title = 'Take a snapshot?'; - - return ( - <ConfirmationDialog - open={this.props.open} - title={`${title}`} - onClose={this.props.onClose} - actions={this.renderActions} - error={this.props.error} - > - <Typography> - Taking a snapshot will back up your Linode in its current state, - overriding your previous snapshot. Are you sure? - </Typography> - </ConfirmationDialog> - ); - } -} - -export default withStyles(styles)(DestructiveSnapshotDialog); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx index e41742246d1..6bb0dc3f661 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx @@ -1,84 +1,75 @@ -import { enableBackups } from '@linode/api-v4/lib/linodes'; -import { useSnackbar } from 'notistack'; import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import Typography from 'src/components/core/Typography'; +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Currency } from 'src/components/Currency'; -import Notice from 'src/components/Notice'; import { resetEventsPolling } from 'src/eventsPolling'; -import useLinodes from 'src/hooks/useLinodes'; -import { useSpecificTypes } from 'src/queries/types'; +import { useLinodeBackupsEnableMutation } from 'src/queries/linodes/backups'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { useTypeQuery } from 'src/queries/types'; +import { useSnackbar } from 'notistack'; interface Props { - linodeId: number; + linodeId: number | undefined; onClose: () => void; open: boolean; } export const EnableBackupsDialog = (props: Props) => { const { linodeId, onClose, open } = props; - /** - * Calculate the monthly backup price here. - * Since this component is used in LinodesLanding - * as well as detail, can't rely on parents knowing - * this information. - */ - const { linodes } = useLinodes(); - const thisLinode = linodes.itemsById[linodeId]; - const typesQuery = useSpecificTypes( - thisLinode?.type ? [thisLinode.type] : [] + + const { + mutateAsync: enableBackups, + reset, + isLoading, + error, + } = useLinodeBackupsEnableMutation(linodeId ?? -1); + + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + open && linodeId !== undefined && linodeId > 0 ); - const thisLinodeType = typesQuery[0]?.data; - const price = thisLinodeType?.addons.backups.price.monthly ?? 0; + const { data: type } = useTypeQuery( + linode?.type ?? '', + Boolean(linode?.type) + ); - const [submitting, setSubmitting] = React.useState(false); - const [error, setError] = React.useState<string | undefined>(); + const price = type?.addons?.backups?.price?.monthly ?? 0; const { enqueueSnackbar } = useSnackbar(); - const handleEnableBackups = React.useCallback(() => { - setSubmitting(true); - enableBackups(linodeId) - .then(() => { - setSubmitting(false); - resetEventsPolling(); - enqueueSnackbar('Backups are being enabled for this Linode.', { - variant: 'success', - }); - onClose(); - }) - .catch((error) => { - setError(error[0].reason); - setSubmitting(false); - }); - }, [linodeId, onClose, enqueueSnackbar]); + const handleEnableBackups = async () => { + await enableBackups(); + resetEventsPolling(); + enqueueSnackbar('Backups are being enabled for this Linode.', { + variant: 'success', + }); + onClose(); + }; React.useEffect(() => { if (open) { - setError(undefined); + reset(); } }, [open]); - const actions = React.useMemo(() => { - return ( - <ActionsPanel style={{ padding: 0 }}> - <Button buttonType="secondary" onClick={onClose} data-qa-cancel-cancel> - Close - </Button> - <Button - buttonType="primary" - onClick={handleEnableBackups} - loading={submitting} - data-qa-confirm-enable-backups - > - Enable Backups - </Button> - </ActionsPanel> - ); - }, [onClose, submitting, handleEnableBackups]); + const actions = ( + <ActionsPanel style={{ padding: 0 }}> + <Button buttonType="secondary" onClick={onClose} data-qa-cancel-cancel> + Close + </Button> + <Button + buttonType="primary" + onClick={handleEnableBackups} + loading={isLoading} + data-qa-confirm-enable-backups + > + Enable Backups + </Button> + </ActionsPanel> + ); return ( <ConfirmationDialog @@ -86,18 +77,14 @@ export const EnableBackupsDialog = (props: Props) => { actions={actions} open={open} onClose={onClose} + error={error?.[0].reason} > - <> - <Typography> - Are you sure you want to enable backups on this Linode?{` `} - This will add <Currency quantity={price} /> - {` `} - to your monthly bill. - </Typography> - {error && <Notice error text={error} spacingTop={8} />} - </> + <Typography> + Are you sure you want to enable backups on this Linode?{` `} + This will add <Currency quantity={price} /> + {` `} + to your monthly bill. + </Typography> </ConfirmationDialog> ); }; - -export default React.memo(EnableBackupsDialog); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackup.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackup.tsx deleted file mode 100644 index 615a2effd12..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackup.tsx +++ /dev/null @@ -1,886 +0,0 @@ -import { GrantLevel } from '@linode/api-v4/lib/account'; -import { - cancelBackups, - Day, - getLinodeBackups, - getType, - LinodeBackup, - LinodeBackupSchedule, - LinodeBackupsResponse, - takeSnapshot, - Window, -} from '@linode/api-v4/lib/linodes'; -import { APIError } from '@linode/api-v4/lib/types'; -import { withSnackbar, WithSnackbarProps } from 'notistack'; -import * as React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { compose } from 'recompose'; -import 'rxjs/add/operator/filter'; -import { Subscription } from 'rxjs/Subscription'; -import ActionsPanel from 'src/components/ActionsPanel'; -import Button from 'src/components/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import FormControl from 'src/components/core/FormControl'; -import FormHelperText from 'src/components/core/FormHelperText'; -import Paper from 'src/components/core/Paper'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; -import Tooltip from 'src/components/core/Tooltip'; -import Typography from 'src/components/core/Typography'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; -import PromiseLoader, { - PromiseLoaderResponse, -} from 'src/components/PromiseLoader'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TextField from 'src/components/TextField'; -import { events$ } from 'src/events'; -import { resetEventsPolling } from 'src/eventsPolling'; -import { linodeInTransition as isLinodeInTransition } from 'src/features/linodes/transitions'; -import { - LinodeActionsProps, - withLinodeActions, -} from 'src/store/linodes/linode.containers'; -import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; -import { ExtendedType } from 'src/utilities/extendType'; -import { formatDate } from 'src/utilities/formatDate'; -import { sendBackupsDisabledEvent } from 'src/utilities/ga'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; -import getUserTimezone from 'src/utilities/getUserTimezone'; -import { initWindows } from 'src/utilities/initWindows'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; -import { withLinodeDetailContext } from '../linodeDetailContext'; -import LinodePermissionsError from '../LinodePermissionsError'; -import BackupsPlaceholder from './BackupsPlaceholder'; -import BackupTableRow from './BackupTableRow'; -import DestructiveSnapshotDialog from './DestructiveSnapshotDialog'; -import RestoreToLinodeDrawer from './RestoreToLinodeDrawer'; -import { - withProfile, - WithProfileProps, -} from 'src/containers/profile.container'; - -type ClassNames = - | 'paper' - | 'subTitle' - | 'snapshotNameField' - | 'snapshotFormControl' - | 'snapshotGeneralError' - | 'scheduleAction' - | 'chooseDay' - | 'cancelButton' - | 'cancelCopy'; - -const styles = (theme: Theme) => - createStyles({ - paper: { - marginBottom: theme.spacing(3), - }, - subTitle: { - marginBottom: theme.spacing(1), - }, - snapshotFormControl: { - display: 'flex', - flexDirection: 'row', - alignItems: 'flex-end', - flexWrap: 'wrap', - '& > div': { - width: 'auto', - marginRight: theme.spacing(2), - }, - '& button': { - marginTop: theme.spacing(4), - }, - }, - scheduleAction: { - padding: 0, - '& button': { - marginLeft: 0, - marginTop: theme.spacing(2), - }, - }, - chooseDay: { - marginRight: theme.spacing(2), - minWidth: 150, - '& .react-select__menu-list': { - maxHeight: 'none', - }, - }, - cancelButton: { - marginBottom: theme.spacing(1), - [theme.breakpoints.down('md')]: { - marginLeft: theme.spacing(), - marginRight: theme.spacing(), - }, - }, - cancelCopy: { - [theme.breakpoints.down('md')]: { - marginLeft: theme.spacing(), - marginRight: theme.spacing(), - }, - }, - snapshotNameField: { - minWidth: 275, - }, - snapshotGeneralError: { - minWidth: '100%', - }, - }); - -interface ContextProps { - linodeID: number; - linodeRegion: string; - linodeType: null | string; - backupsEnabled: boolean; - backupsSchedule: LinodeBackupSchedule; - linodeInTransition: boolean; - linodeLabel: string; - permissions: GrantLevel; -} - -interface PreloadedProps { - backups: PromiseLoaderResponse<LinodeBackupsResponse>; - type: PromiseLoaderResponse<ExtendedType>; -} - -interface State { - backups: LinodeBackupsResponse; - getBackupsTimer: NodeJS.Timeout | null; - snapshotForm: { - label: string; - errors?: APIError[]; - }; - settingsForm: { - window: Window; - day: Day; - errors?: APIError[]; - loading: boolean; - }; - restoreDrawer: { - open: boolean; - backupCreated: string; - backupID?: number; - }; - dialogOpen: boolean; - dialogError?: string; - loading: boolean; - cancelBackupsAlertOpen: boolean; - enabling: boolean; -} - -type CombinedProps = PreloadedProps & - LinodeActionsProps & - WithStyles<ClassNames> & - RouteComponentProps<{}> & - ContextProps & - WithSnackbarProps & - WithProfileProps; - -const isReadOnly = (permissions: GrantLevel) => { - return permissions === 'read_only'; -}; - -export const aggregateBackups = ( - backups: LinodeBackupsResponse -): LinodeBackup[] => { - const manualSnapshot = - backups?.snapshot.in_progress?.status === 'needsPostProcessing' - ? backups?.snapshot.in_progress - : backups?.snapshot.current; - return ( - backups && - [...backups.automatic!, manualSnapshot!].filter((b) => Boolean(b)) - ); -}; - -/* tslint:disable-next-line */ -class _LinodeBackup extends React.Component<CombinedProps, State> { - state: State = { - backups: this.props.backups.response, - getBackupsTimer: null, - snapshotForm: { - label: '', - }, - settingsForm: { - window: this.props.backupsSchedule.window || 'Scheduling', - day: this.props.backupsSchedule.day || 'Scheduling', - loading: false, - }, - restoreDrawer: { - open: false, - backupCreated: '', - }, - dialogOpen: false, - dialogError: undefined, - loading: false, - cancelBackupsAlertOpen: false, - enabling: false, - }; - - windows: string[][] = []; - days: string[][] = []; - - eventSubscription: Subscription; - - mounted: boolean = false; - - componentDidMount() { - this.mounted = true; - this.eventSubscription = events$ - .filter(({ event }) => - [ - 'linode_snapshot', - 'backups_enable', - 'backups_cancel', - 'backups_restore', - ].includes(event.action) - ) - .filter(({ event }) => !event._initial && event.status === 'finished') - .subscribe((_) => { - getLinodeBackups(this.props.linodeID) - .then((data) => { - this.setState({ backups: data }); - }) - .catch(() => { - /* @todo: how do we want to display this error? */ - this.setState({ enabling: false }); - }); - }); - } - - // update backup status column from processing -> success/failure - componentDidUpdate() { - if (this.state.backups.snapshot.in_progress === null) { - return; - } - if ( - this.state.backups.snapshot.in_progress.status === - 'needsPostProcessing' && - this.state.getBackupsTimer === null - ) { - this.setState({ - getBackupsTimer: setTimeout( - () => - getLinodeBackups(this.props.linodeID).then((data) => { - this.setState({ - ...this.state, - backups: data, - getBackupsTimer: null, - }); - }), - 15000 - ), - }); - } - } - - componentWillUnmount() { - this.mounted = false; - this.eventSubscription.unsubscribe(); - } - - constructor(props: CombinedProps) { - super(props); - - this.windows = initWindows( - getUserTimezone(props.profile.data?.timezone), - true - ); - - this.days = [ - ['Choose a day', 'Scheduling'], - ['Sunday', 'Sunday'], - ['Monday', 'Monday'], - ['Tuesday', 'Tuesday'], - ['Wednesday', 'Wednesday'], - ['Thursday', 'Thursday'], - ['Friday', 'Friday'], - ['Saturday', 'Saturday'], - ]; - } - - cancelBackups = () => { - const { enqueueSnackbar } = this.props; - cancelBackups(this.props.linodeID) - .then(() => { - enqueueSnackbar('Backups are being canceled for this Linode', { - variant: 'info', - }); - // Just in case the user immediately disables backups - // and enabling is still true: - this.setState({ enabling: false }); - resetEventsPolling(); - // GA Event - sendBackupsDisabledEvent(); - }) - .catch((errorResponse) => { - getAPIErrorOrDefault( - errorResponse, - 'There was an error disabling backups' - ) - /** @todo move this error to the actual modal */ - .forEach((err: APIError) => - enqueueSnackbar(err.reason, { - variant: 'error', - }) - ); - }); - if (!this.mounted) { - return; - } - this.setState({ cancelBackupsAlertOpen: false }); - }; - - takeSnapshot = () => { - const { linodeID, enqueueSnackbar } = this.props; - const { snapshotForm } = this.state; - this.setState({ loading: true }); - takeSnapshot(linodeID, snapshotForm.label) - .then(() => { - enqueueSnackbar('A snapshot is being taken', { - variant: 'info', - }); - this.closeDestructiveDialog(); - this.setState({ - snapshotForm: { label: '', errors: undefined }, - loading: false, - }); - resetEventsPolling(); - }) - .catch((errorResponse) => { - this.setState({ - snapshotForm: { - ...this.state.snapshotForm, - errors: getAPIErrorOrDefault( - errorResponse, - 'There was an error taking a snapshot' - ), - }, - dialogOpen: this.state.dialogOpen, - loading: false, - dialogError: getAPIErrorOrDefault( - errorResponse, - 'There was an error taking a snapshot' - )[0].reason, - }); - }); - }; - - closeDestructiveDialog = () => { - this.setState({ - dialogOpen: false, - dialogError: undefined, - }); - }; - - saveSettings = () => { - const { - linodeID, - enqueueSnackbar, - linodeActions: { updateLinode }, - } = this.props; - const { settingsForm } = this.state; - - this.setState((state) => ({ - settingsForm: { ...state.settingsForm, loading: true, errors: undefined }, - })); - - updateLinode({ - linodeId: linodeID, - backups: { - enabled: true, - schedule: { - day: settingsForm.day, - window: settingsForm.window, - }, - }, - }) - .then(() => { - this.setState((state) => ({ - settingsForm: { ...state.settingsForm, loading: false }, - })); - - enqueueSnackbar('Backup settings saved', { - variant: 'success', - }); - }) - .catch((err) => { - this.setState( - (state) => ({ - settingsForm: { - ...state.settingsForm, - loading: false, - errors: getAPIErrorOrDefault(err), - }, - }), - () => { - scrollErrorIntoView(); - } - ); - }); - }; - - openRestoreDrawer = (backupID: number, backupCreated: string) => { - this.setState({ - restoreDrawer: { open: true, backupID, backupCreated }, - }); - }; - - closeRestoreDrawer = () => { - this.setState({ - restoreDrawer: { open: false, backupID: undefined, backupCreated: '' }, - }); - }; - - handleSelectBackupWindow = (e: Item) => { - this.setState({ - settingsForm: { - ...this.state.settingsForm, - window: e.value as Window, - }, - }); - }; - - handleSelectBackupTime = (e: Item) => { - this.setState({ - settingsForm: { - ...this.state.settingsForm, - day: e.value as Day, - }, - }); - }; - - inputHasChanged = ( - initialValue: LinodeBackupSchedule, - newValue: LinodeBackupSchedule - ) => { - return ( - newValue.day === 'Scheduling' || - newValue.window === 'Scheduling' || - (newValue.day === initialValue.day && - newValue.window === initialValue.window) - ); - }; - - handleSnapshotNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.setState({ snapshotForm: { label: e.target.value } }); - }; - - handleSnapshotDialogDisplay = () => { - // If there's no label, don't open the modal. Show an error in the form. - if (!this.state.snapshotForm.label) { - this.setState({ - snapshotForm: { - ...this.state.snapshotForm, - errors: [{ field: 'label', reason: 'Label is required.' }], - }, - }); - return; - } - this.setState({ - dialogOpen: true, - dialogError: undefined, - }); - }; - - handleCloseBackupsAlert = () => { - this.setState({ cancelBackupsAlertOpen: false }); - }; - - handleOpenBackupsAlert = () => { - this.setState({ cancelBackupsAlertOpen: true }); - }; - - handleDeploy = (backup: LinodeBackup) => { - const { history, linodeID, linodeType } = this.props; - history.push( - '/linodes/create' + - `?type=Backups&backupID=${backup.id}&linodeID=${linodeID}&typeID=${linodeType}` - ); - }; - - handleRestore = (backup: LinodeBackup) => { - this.openRestoreDrawer( - backup.id, - formatDate(backup.created, { - timezone: this.props.profile.data?.timezone, - }) - ); - }; - - handleRestoreSubmit = () => { - this.closeRestoreDrawer(); - this.props.enqueueSnackbar('Backup restore started', { - variant: 'info', - }); - }; - - Table = ({ backups }: { backups: LinodeBackup[] }): JSX.Element | null => { - const { classes, permissions } = this.props; - const disabled = isReadOnly(permissions); - - return ( - <Paper className={classes.paper} style={{ padding: 0 }}> - <Table aria-label="List of Backups"> - <TableHead> - <TableRow> - <TableCell>Label</TableCell> - <TableCell>Status</TableCell> - <TableCell>Date Created</TableCell> - <TableCell>Duration</TableCell> - <TableCell>Disks</TableCell> - <TableCell>Space Required</TableCell> - <TableCell /> - </TableRow> - </TableHead> - <TableBody> - {backups.map((backup: LinodeBackup, idx: number) => ( - <BackupTableRow - key={idx} - backup={backup} - disabled={disabled} - handleDeploy={this.handleDeploy} - handleRestore={this.handleRestore} - /> - ))} - </TableBody> - </Table> - </Paper> - ); - }; - - SnapshotForm = (): JSX.Element | null => { - const { classes, linodeInTransition, permissions } = this.props; - const { snapshotForm } = this.state; - const hasErrorFor = getErrorMap(['label'], snapshotForm.errors); - - const disabled = isReadOnly(permissions); - - return ( - <React.Fragment> - <Paper className={classes.paper}> - <Typography - variant="h2" - className={classes.subTitle} - data-qa-manual-heading - > - Manual Snapshot - </Typography> - <Typography variant="body1" data-qa-manual-desc> - You can make a manual backup of your Linode by taking a snapshot. - Creating the manual snapshot can take several minutes, depending on - the size of your Linode and the amount of data you have stored on - it. The manual snapshot will not be overwritten by automatic - backups. - </Typography> - <FormControl className={classes.snapshotFormControl}> - {hasErrorFor.none && ( - <Notice - spacingBottom={8} - className={classes.snapshotGeneralError} - error - > - {hasErrorFor.none} - </Notice> - )} - <TextField - errorText={hasErrorFor.label} - label="Name Snapshot" - value={snapshotForm.label || ''} - onChange={this.handleSnapshotNameChange} - data-qa-manual-name - className={classes.snapshotNameField} - /> - <Tooltip title={linodeInTransition ? 'This Linode is busy' : ''}> - <div> - <Button - buttonType="primary" - onClick={this.handleSnapshotDialogDisplay} - data-qa-snapshot-button - disabled={ - linodeInTransition || disabled || snapshotForm.label === '' - } - > - Take Snapshot - </Button> - </div> - </Tooltip> - </FormControl> - </Paper> - <DestructiveSnapshotDialog - open={this.state.dialogOpen} - error={this.state.dialogError} - onClose={this.closeDestructiveDialog} - onSnapshot={this.takeSnapshot} - loading={this.state.loading} - /> - </React.Fragment> - ); - }; - - SettingsForm = (): JSX.Element | null => { - const { classes, backupsSchedule, permissions, profile } = this.props; - const { settingsForm } = this.state; - const getErrorFor = getAPIErrorFor( - { - day: 'backups.day', - window: 'backups.window', - schedule: 'backups.schedule.window', - }, - settingsForm.errors - ); - const errorText = - getErrorFor('none') || - getErrorFor('backups.day') || - getErrorFor('backups.window') || - getErrorFor('backups.schedule.window') || - getErrorFor('backups.schedule.day'); - - const timeSelection = this.windows.map((window: Window[]) => { - const label = window[0]; - return { label, value: window[1] }; - }); - - const daySelection = this.days.map((day: string[]) => { - const label = day[0]; - return { label, value: day[1] }; - }); - - const defaultTimeSelection = timeSelection.find((eachOption) => { - return eachOption.value === settingsForm.window; - }); - - const defaultDaySelection = daySelection.find((eachOption) => { - return eachOption.value === settingsForm.day; - }); - - return ( - <Paper className={classes.paper}> - <Typography - variant="h2" - className={classes.subTitle} - data-qa-settings-heading - > - Settings - </Typography> - <Typography variant="body1" data-qa-settings-desc> - Configure when automatic backups are initiated. The Linode Backup - Service will generate a backup between the selected hours every day, - and will overwrite the previous daily backup. The selected day is when - the backup is promoted to the weekly slot. Up to two weekly backups - are saved. - </Typography> - <FormControl className={classes.chooseDay}> - <Select - textFieldProps={{ - dataAttrs: { - 'data-qa-weekday-select': true, - }, - }} - options={daySelection} - defaultValue={defaultDaySelection} - onChange={this.handleSelectBackupTime} - label="Day of Week" - placeholder="Choose a day" - isClearable={false} - menuPlacement="top" - name="Day of Week" - noMarginTop - /> - </FormControl> - <FormControl> - <Select - textFieldProps={{ - dataAttrs: { - 'data-qa-time-select': true, - }, - }} - options={timeSelection} - onChange={this.handleSelectBackupWindow} - label="Time of Day" - placeholder="Choose a time" - isClearable={false} - defaultValue={defaultTimeSelection} - menuPlacement="top" - name="Time of Day" - noMarginTop - /> - <FormHelperText> - Time displayed in{' '} - {getUserTimezone(profile.data?.timezone).replace('_', ' ')} - </FormHelperText> - </FormControl> - <ActionsPanel className={classes.scheduleAction}> - <Button - buttonType="primary" - onClick={this.saveSettings} - disabled={ - isReadOnly(permissions) || - this.inputHasChanged(backupsSchedule, settingsForm) - } - loading={this.state.settingsForm.loading} - data-qa-schedule - > - Save Schedule - </Button> - </ActionsPanel> - {errorText && <FormHelperText error>{errorText}</FormHelperText>} - </Paper> - ); - }; - - Management = (): JSX.Element | null => { - const { classes, linodeID, linodeRegion, permissions } = this.props; - const disabled = isReadOnly(permissions); - - const { backups: backupsResponse } = this.state; - const backups = aggregateBackups(backupsResponse); - - return ( - <React.Fragment> - {disabled && <LinodePermissionsError />} - {backups.length ? ( - <this.Table backups={backups} /> - ) : ( - <Paper className={classes.paper} data-qa-backup-description> - <Typography> - Automatic and manual backups will be listed here - </Typography> - </Paper> - )} - <this.SnapshotForm /> - <this.SettingsForm /> - <Button - buttonType="outlined" - className={classes.cancelButton} - disabled={disabled} - onClick={this.handleOpenBackupsAlert} - data-qa-cancel - > - Cancel Backups - </Button> - <Typography - className={classes.cancelCopy} - variant="body1" - data-qa-cancel-desc - > - Please note that when you cancel backups associated with this Linode, - this will remove all existing backups. - </Typography> - <RestoreToLinodeDrawer - open={this.state.restoreDrawer.open} - linodeID={linodeID} - linodeRegion={linodeRegion} - backupID={this.state.restoreDrawer.backupID} - backupCreated={this.state.restoreDrawer.backupCreated} - onClose={this.closeRestoreDrawer} - onSubmit={this.handleRestoreSubmit} - /> - <ConfirmationDialog - title="Confirm Cancellation" - actions={this.renderConfirmCancellationActions} - open={this.state.cancelBackupsAlertOpen} - onClose={this.handleCloseBackupsAlert} - > - <Typography> - Canceling backups associated with this Linode will delete all - existing backups. Are you sure? - </Typography> - <Typography style={{ marginTop: 12 }}> - <strong>Note: </strong> - Once backups for this Linode have been canceled, you cannot - re-enable them for 24 hours. - </Typography> - </ConfirmationDialog> - </React.Fragment> - ); - }; - - renderConfirmCancellationActions = () => { - return ( - <ActionsPanel style={{ padding: 0 }}> - <Button - buttonType="secondary" - onClick={this.handleCloseBackupsAlert} - data-qa-cancel-cancel - > - Close - </Button> - <Button - buttonType="primary" - onClick={this.cancelBackups} - data-qa-confirm-cancel - > - Cancel Backups - </Button> - </ActionsPanel> - ); - }; - - render() { - const { backupsEnabled, permissions, type } = this.props; - - if (this.props.backups.error) { - /** @todo remove promise loader and source backups from Redux */ - return ( - <ErrorState errorText="There was an issue retrieving your backups." /> - ); - } - - const backupsMonthlyPrice = - type.response?.addons?.backups?.price?.monthly ?? 0; - - return ( - <div> - {backupsEnabled ? ( - <this.Management /> - ) : ( - <BackupsPlaceholder - linodeId={this.props.linodeID} - backupsMonthlyPrice={backupsMonthlyPrice} - disabled={isReadOnly(permissions)} - /> - )} - </div> - ); - } -} - -const preloaded = PromiseLoader<ContextProps>({ - backups: (props) => getLinodeBackups(props.linodeID), - type: ({ linodeType }) => { - if (!linodeType) { - return Promise.resolve(undefined); - } - - return getType(linodeType); - }, -}); - -const styled = withStyles(styles); - -const linodeContext = withLinodeDetailContext(({ linode }) => ({ - backupsEnabled: linode.backups.enabled, - backupsSchedule: linode.backups.schedule, - linodeID: linode.id, - linodeInTransition: isLinodeInTransition(linode.status), - linodeLabel: linode.label, - linodeRegion: linode.region, - linodeType: linode.type, - permissions: linode._permissions, -})); - -export default compose<CombinedProps, {}>( - linodeContext, - preloaded, - styled, - withRouter, - withSnackbar, - withLinodeActions, - withProfile -)(_LinodeBackup); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackupActionMenu.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackupActionMenu.tsx index 06eb55da537..bf036fa0f8a 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackupActionMenu.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackupActionMenu.tsx @@ -1,19 +1,16 @@ -import { LinodeBackup } from '@linode/api-v4/lib/linodes'; import * as React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { LinodeBackup } from '@linode/api-v4/lib/linodes'; import ActionMenu, { Action } from 'src/components/ActionMenu'; interface Props { backup: LinodeBackup; disabled: boolean; - onRestore: (backup: LinodeBackup) => void; - onDeploy: (backup: LinodeBackup) => void; + onRestore: () => void; + onDeploy: () => void; } -type CombinedProps = Props & RouteComponentProps<{}>; - -export const LinodeBackupActionMenu: React.FC<CombinedProps> = (props) => { - const { backup, disabled, onRestore, onDeploy } = props; +export const LinodeBackupActionMenu = (props: Props) => { + const { disabled, onRestore, onDeploy } = props; const disabledProps = { disabled, tooltip: disabled @@ -25,14 +22,14 @@ export const LinodeBackupActionMenu: React.FC<CombinedProps> = (props) => { { title: 'Restore to Existing Linode', onClick: () => { - onRestore(backup); + onRestore(); }, ...disabledProps, }, { title: 'Deploy New Linode', onClick: () => { - onDeploy(backup); + onDeploy(); }, ...disabledProps, }, @@ -45,5 +42,3 @@ export const LinodeBackupActionMenu: React.FC<CombinedProps> = (props) => { /> ); }; - -export default withRouter(LinodeBackupActionMenu); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx new file mode 100644 index 00000000000..13771e0ea3a --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { renderWithTheme } from 'src/utilities/testHelpers'; +import { LinodeBackups } from './LinodeBackups'; +import { rest, server } from 'src/mocks/testServer'; +import { backupFactory, linodeFactory } from 'src/factories'; +import { LinodeBackupsResponse } from '@linode/api-v4'; + +// I'm so sorry, but I don't know a better way to mock react-router-dom params. +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(() => ({ linodeId: 1 })), +})); + +describe('LinodeBackups', () => { + it('renders a list of different types of backups if backups are enabled', async () => { + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json(linodeFactory.build({ id: 1, backups: { enabled: true } })) + ); + }), + rest.get('*/linode/instances/1/backups', (req, res, ctx) => { + const response: LinodeBackupsResponse = { + automatic: backupFactory.buildList(1, { label: null, type: 'auto' }), + snapshot: { + in_progress: backupFactory.build({ + label: 'in-progress-test-backup', + created: '2023-05-03T04:00:05', + finished: '2023-05-03T04:02:06', + type: 'snapshot', + }), + current: backupFactory.build({ + label: 'current-snapshot', + type: 'snapshot', + status: 'needsPostProcessing', + }), + }, + }; + return res(ctx.json(response)); + }) + ); + + const { findByText, getByText } = renderWithTheme(<LinodeBackups />); + + // Verify an automated backup renders + await findByText('current-snapshot'); + getByText('Automatic'); + + // Verify an `in_progress` snapshot renders + getByText('in-progress-test-backup'); + getByText('2 minutes, 1 second'); + + // Verify an `current` snapshot renders + getByText('current-snapshot'); + getByText('Processing'); + }); + + it('renders BackupsPlaceholder is backups are not enabled on this linode', async () => { + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json(linodeFactory.build({ id: 1, backups: { enabled: false } })) + ); + }) + ); + + const { findByText } = renderWithTheme(<LinodeBackups />); + + await findByText('Enable Backups'); + }); +}); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.tsx new file mode 100644 index 00000000000..8fd56ea67b7 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/LinodeBackups.tsx @@ -0,0 +1,216 @@ +import * as React from 'react'; +import { Table } from 'src/components/Table'; +import TableRowEmptyState from 'src/components/TableRowEmptyState'; +import Button from 'src/components/Button'; +import Paper from 'src/components/core/Paper'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import Typography from 'src/components/core/Typography'; +import ErrorState from 'src/components/ErrorState'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import LinodePermissionsError from '../LinodePermissionsError'; +import BackupsPlaceholder from './BackupsPlaceholder'; +import BackupTableRow from './BackupTableRow'; +import { Box, Stack } from '@mui/material'; +import { Theme } from '@mui/material/styles'; +import { LinodeBackup } from '@linode/api-v4/lib/linodes'; +import { useHistory, useParams } from 'react-router-dom'; +import { RestoreToLinodeDrawer } from './RestoreToLinodeDrawer'; +import { useTypeQuery } from 'src/queries/types'; +import { makeStyles } from 'tss-react/mui'; +import { CancelBackupsDialog } from './CancelBackupsDialog'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { ScheduleSettings } from './ScheduleSettings'; +import { useGrants, useProfile } from 'src/queries/profile'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { useLinodeBackupsQuery } from 'src/queries/linodes/backups'; +import { CaptureSnapshot } from './CaptureSnapshot'; + +const useStyles = makeStyles()((theme: Theme) => ({ + cancelButton: { + marginBottom: theme.spacing(1), + [theme.breakpoints.down('md')]: { + marginLeft: theme.spacing(), + marginRight: theme.spacing(), + }, + }, + cancelCopy: { + [theme.breakpoints.down('md')]: { + marginLeft: theme.spacing(), + marginRight: theme.spacing(), + }, + }, +})); + +export const LinodeBackups = () => { + const { linodeId } = useParams<{ linodeId: string }>(); + const id = Number(linodeId); + + const history = useHistory(); + const { classes } = useStyles(); + + const { data: profile } = useProfile(); + const { data: grants } = useGrants(); + + const doesNotHavePermission = + Boolean(profile?.restricted) && + grants?.linode.find( + (grant) => grant.id === linode?.id && grant.permissions !== 'read_write' + ) !== undefined; + + const { data: linode } = useLinodeQuery(id); + const { data: type } = useTypeQuery(linode?.type ?? '', linode !== undefined); + const { data: backups, error, isLoading } = useLinodeBackupsQuery( + id, + Boolean(linode?.backups.enabled) + ); + + const [isRestoreDrawerOpen, setIsRestoreDrawerOpen] = React.useState(false); + + const [ + isCancelBackupsDialogOpen, + setIsCancelBackupsDialogOpen, + ] = React.useState(false); + + const [selectedBackup, setSelectedBackup] = React.useState<LinodeBackup>(); + + const handleDeploy = (backup: LinodeBackup) => { + history.push( + '/linodes/create' + + `?type=Backups&backupID=${backup.id}&linodeID=${linode?.id}&typeID=${linode?.type}` + ); + }; + + const onRestoreBackup = (backup: LinodeBackup) => { + setIsRestoreDrawerOpen(true); + setSelectedBackup(backup); + }; + + if (error) { + return ( + <ErrorState errorText="There was an issue retrieving your backups." /> + ); + } + + const backupsMonthlyPrice = type?.addons?.backups?.price?.monthly ?? 0; + + if (isLoading) { + return <CircleProgress />; + } + + if (!linode?.backups.enabled) { + return ( + <BackupsPlaceholder + linodeId={id} + backupsMonthlyPrice={backupsMonthlyPrice} + disabled={doesNotHavePermission} + /> + ); + } + + const hasBackups = + backups !== undefined && + (backups?.automatic.length > 0 || + Boolean(backups?.snapshot.current) || + Boolean(backups?.snapshot.in_progress)); + + return ( + <Stack spacing={2}> + {doesNotHavePermission && <LinodePermissionsError />} + <Paper style={{ padding: 0 }}> + <Table aria-label="List of Backups"> + <TableHead> + <TableRow> + <TableCell>Label</TableCell> + <TableCell>Status</TableCell> + <TableCell>Date Created</TableCell> + <TableCell>Duration</TableCell> + <TableCell>Disks</TableCell> + <TableCell>Space Required</TableCell> + <TableCell /> + </TableRow> + </TableHead> + <TableBody> + {hasBackups ? ( + <> + {backups?.automatic.map((backup: LinodeBackup, idx: number) => ( + <BackupTableRow + key={idx} + backup={backup} + disabled={doesNotHavePermission} + handleDeploy={() => handleDeploy(backup)} + handleRestore={() => onRestoreBackup(backup)} + /> + ))} + {Boolean(backups?.snapshot.current) && ( + <BackupTableRow + backup={backups!.snapshot.current!} + disabled={doesNotHavePermission} + handleDeploy={() => + handleDeploy(backups!.snapshot.current!) + } + handleRestore={() => + onRestoreBackup(backups!.snapshot.current!) + } + /> + )} + {Boolean(backups?.snapshot.in_progress) && ( + <BackupTableRow + backup={backups!.snapshot.in_progress!} + disabled={doesNotHavePermission} + handleDeploy={() => + handleDeploy(backups!.snapshot.in_progress!) + } + handleRestore={() => + onRestoreBackup(backups!.snapshot.in_progress!) + } + /> + )} + </> + ) : ( + <TableRowEmptyState + colSpan={7} + message="Automatic and manual backups will be listed here" + /> + )} + </TableBody> + </Table> + </Paper> + <CaptureSnapshot linodeId={id} isReadOnly={doesNotHavePermission} /> + <ScheduleSettings linodeId={id} isReadOnly={doesNotHavePermission} /> + <Box> + <Button + buttonType="outlined" + className={classes.cancelButton} + disabled={doesNotHavePermission} + onClick={() => setIsCancelBackupsDialogOpen(true)} + data-qa-cancel + > + Cancel Backups + </Button> + <Typography + className={classes.cancelCopy} + variant="body1" + data-qa-cancel-desc + > + Please note that when you cancel backups associated with this Linode, + this will remove all existing backups. + </Typography> + </Box> + <RestoreToLinodeDrawer + open={isRestoreDrawerOpen} + linodeId={id} + backup={selectedBackup} + onClose={() => setIsRestoreDrawerOpen(false)} + /> + <CancelBackupsDialog + isOpen={isCancelBackupsDialogOpen} + onClose={() => setIsCancelBackupsDialogOpen(false)} + linodeId={id} + /> + </Stack> + ); +}; + +export default LinodeBackups; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx index 4896f82dcec..32cd10cd4b8 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx @@ -1,27 +1,30 @@ import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { CombinedProps, RestoreToLinodeDrawer } from './RestoreToLinodeDrawer'; +import { RestoreToLinodeDrawer } from './RestoreToLinodeDrawer'; +import { rest, server } from 'src/mocks/testServer'; +import { backupFactory, linodeFactory } from 'src/factories'; describe('RestoreToLinodeDrawer', () => { - const props: CombinedProps = { - open: true, - linodeID: 1234, - linodeRegion: 'us-east', - backupCreated: '12 hours ago', - onClose: jest.fn(), - onSubmit: jest.fn(), - linodesData: [], - linodesLastUpdated: 0, - linodesLoading: false, - getLinodes: jest.fn(), - linodesResults: 0, - }; - it('renders without crashing', async () => { - const { findByText } = renderWithTheme( - <RestoreToLinodeDrawer {...props} /> + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json(linodeFactory.build({ id: 1, backups: { enabled: true } })) + ); + }) + ); + + const backup = backupFactory.build({ created: '2023-05-03T04:00:47' }); + + const { getByText } = renderWithTheme( + <RestoreToLinodeDrawer + open={true} + linodeId={1} + backup={backup} + onClose={jest.fn()} + /> ); - await findByText(/Restore Backup from/); + getByText(`Restore Backup from ${backup.created}`); }); }); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx index 258c451185e..7b530d525d1 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx @@ -1,197 +1,167 @@ -import { restoreBackup } from '@linode/api-v4/lib/linodes'; -import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; -import { compose } from 'recompose'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import CheckBox from 'src/components/CheckBox'; import FormControl from 'src/components/core/FormControl'; import FormControlLabel from 'src/components/core/FormControlLabel'; import FormHelperText from 'src/components/core/FormHelperText'; -import InputLabel from 'src/components/core/InputLabel'; import Drawer from 'src/components/Drawer'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import Notice from 'src/components/Notice'; -import withLinodes, { - Props as LinodeProps, -} from 'src/containers/withLinodes.container'; -import { useGrants } from 'src/queries/profile'; -import { getPermissionsForLinode } from 'src/store/linodes/permissions/permissions.selector'; -import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; -import { Grants } from '@linode/api-v4/lib'; +import Select from 'src/components/EnhancedSelect/Select'; +import { Notice } from 'src/components/Notice/Notice'; +import { useFormik } from 'formik'; +import { LinodeBackup } from '@linode/api-v4/lib/linodes'; +import { useSnackbar } from 'notistack'; +import { resetEventsPolling } from 'src/eventsPolling'; +import { getErrorMap } from 'src/utilities/errorUtils'; +import { useLinodeBackupRestoreMutation } from 'src/queries/linodes/backups'; +import { + useAllLinodesQuery, + useLinodeQuery, +} from 'src/queries/linodes/linodes'; interface Props { open: boolean; - linodeID: number; - linodeRegion: string; - backupCreated: string; - backupID?: number; + linodeId: number; + backup: LinodeBackup | undefined; onClose: () => void; - onSubmit: () => void; } -export type CombinedProps = Props & LinodeProps; +export const RestoreToLinodeDrawer = (props: Props) => { + const { linodeId, backup, open, onClose } = props; + const { enqueueSnackbar } = useSnackbar(); + const { data: linode } = useLinodeQuery(linodeId, open); -const canEditLinode = ( - grants: Grants | undefined, - linodeId: number -): boolean => { - return getPermissionsForLinode(grants, linodeId) === 'read_only'; -}; - -export const RestoreToLinodeDrawer: React.FC<CombinedProps> = (props) => { const { - onSubmit, - linodeID, - backupID, - open, - backupCreated, - linodesData, - linodeRegion, - } = props; - - const { data: grants } = useGrants(); - - const [overwrite, setOverwrite] = React.useState<boolean>(false); - const [selectedLinodeId, setSelectedLinodeId] = React.useState<number | null>( - linodeID + data: linodes, + isLoading: linodesLoading, + error: linodeError, + } = useAllLinodesQuery( + {}, + { + region: linode?.region, + }, + open && linode !== undefined ); - const [errors, setErrors] = React.useState<APIError[]>([]); - - const reset = () => { - setOverwrite(false); - setSelectedLinodeId(null); - setErrors([]); - }; - const restoreToLinode = () => { - if (!selectedLinodeId) { - setErrors([ - ...errors, - ...[{ field: 'linode_id', reason: 'You must select a Linode' }], - ]); - scrollErrorIntoView(); - return; - } - restoreBackup(linodeID, Number(backupID), selectedLinodeId, overwrite) - .then(() => { - reset(); - onSubmit(); - }) - .catch((errResponse) => { - setErrors(getAPIErrorOrDefault(errResponse)); - scrollErrorIntoView(); + const { + mutateAsync: restoreBackup, + error, + isLoading, + reset: resetMutation, + } = useLinodeBackupRestoreMutation(); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + overwrite: false, + linode_id: linodeId, + }, + async onSubmit(values) { + await restoreBackup({ + linodeId, + backupId: backup?.id ?? -1, + targetLinodeId: values.linode_id ?? -1, + overwrite: values.overwrite, }); - }; - - const handleToggleOverwrite = () => { - setOverwrite((prevOverwrite) => !prevOverwrite); - }; - - const handleCloseDrawer = () => { - reset(); - props.onClose(); - }; - - const errorResources = { - linode_id: 'Linode', - overwrite: 'Overwrite', - }; - - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + enqueueSnackbar( + `Started restoring Linode ${selectedLinodeOption?.label} from a backup`, + { variant: 'info' } + ); + resetEventsPolling(); + onClose(); + }, + }); + + React.useEffect(() => { + if (open) { + formik.resetForm(); + resetMutation(); + } + }, [open]); - const linodeError = hasErrorFor('linode_id'); - const overwriteError = hasErrorFor('overwrite'); - const generalError = hasErrorFor('none'); + const linodeOptions = + linodes?.map(({ label, id }) => { + return { label, value: id }; + }) ?? []; - const readOnly = canEditLinode(grants, selectedLinodeId ?? -1); - const selectError = Boolean(linodeError) || readOnly; + const selectedLinodeOption = linodeOptions.find( + (option) => option.value === formik.values.linode_id + ); - const linodeOptions = linodesData - .filter((linode) => linode.region === linodeRegion) - .map(({ label, id }) => { - return { label, value: id }; - }); + const errorMap = getErrorMap(['linode_id', 'overwrite'], error); return ( <Drawer open={open} - onClose={handleCloseDrawer} - title={`Restore Backup from ${backupCreated}`} + onClose={onClose} + title={`Restore Backup from ${backup?.created}`} > - <FormControl fullWidth> - <InputLabel - htmlFor="linode" - disableAnimation - shrink={true} - error={Boolean(linodeError)} - > - Linode - </InputLabel> + <form onSubmit={formik.handleSubmit}> + {Boolean(errorMap.none) && <Notice error>{errorMap.none}</Notice>} <Select textFieldProps={{ dataAttrs: { 'data-qa-select-linode': true, }, }} - defaultValue={linodeOptions.find( - (option) => option.value === selectedLinodeId - )} + value={selectedLinodeOption} options={linodeOptions} - onChange={(item: Item<number>) => setSelectedLinodeId(item.value)} - errorText={linodeError} + onChange={(item) => formik.setFieldValue('linode_id', item.value)} + errorText={linodeError?.[0].reason ?? errorMap.linode_id} placeholder="Select a Linode" + label="Linode" isClearable={false} - label="Select a Linode" - hideLabel + isLoading={linodesLoading} /> - {selectError && ( - <FormHelperText error> - {linodeError || "You don't have permission to edit this Linode."} + <FormControl sx={{ paddingLeft: 0.4 }}> + <FormControlLabel + label="Overwrite Linode" + control={ + <CheckBox + name="overwrite" + checked={formik.values.overwrite} + onChange={formik.handleChange} + /> + } + /> + <FormHelperText sx={{ marginLeft: 0 }}> + Overwriting will delete all disks and configs on the target Linode + before restoring </FormHelperText> + </FormControl> + {Boolean(errorMap.overwrite) && ( + <Notice error>{errorMap.overwrite}</Notice> )} - </FormControl> - <FormControlLabel - control={ - <CheckBox checked={overwrite} onChange={handleToggleOverwrite} /> - } - label="Overwrite Linode" - /> - {overwrite && ( - <Notice - warning - text="This will delete all disks and configs on this Linode" - /> - )} - {Boolean(overwriteError) && ( - <FormHelperText error>{overwriteError}</FormHelperText> - )} - {Boolean(generalError) && ( - <FormHelperText error>{generalError}</FormHelperText> - )} - <ActionsPanel> - <Button - buttonType="secondary" - onClick={handleCloseDrawer} - data-qa-restore-cancel - > - Cancel - </Button> - <Button - buttonType="primary" - onClick={restoreToLinode} - data-qa-restore-submit - disabled={readOnly} - > - Restore - </Button> - </ActionsPanel> + {formik.values.overwrite && ( + <Notice + spacingTop={12} + spacingBottom={0} + warning + text={`This will delete all disks and configs on ${ + selectedLinodeOption + ? `Linode ${selectedLinodeOption.label}` + : 'the selcted Linode' + }`} + /> + )} + <ActionsPanel> + <Button + buttonType="secondary" + onClick={onClose} + data-qa-restore-cancel + > + Cancel + </Button> + <Button + buttonType="primary" + type="submit" + loading={isLoading} + data-qa-restore-submit + > + Restore + </Button> + </ActionsPanel> + </form> </Drawer> ); }; - -const enhanced = compose<CombinedProps, Props>(withLinodes()); - -export default enhanced(RestoreToLinodeDrawer); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx new file mode 100644 index 00000000000..734d98c465d --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { renderWithTheme } from 'src/utilities/testHelpers'; +import { ScheduleSettings } from './ScheduleSettings'; +import { rest, server } from 'src/mocks/testServer'; +import { linodeFactory } from 'src/factories/linodes'; +import { profileFactory } from 'src/factories'; + +describe('ScheduleSettings', () => { + it('renders heading and copy', async () => { + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json(linodeFactory.build({ id: 1, backups: { enabled: true } })) + ); + }) + ); + + const { getByText } = renderWithTheme( + <ScheduleSettings linodeId={1} isReadOnly={false} /> + ); + + getByText('Settings'); + getByText( + /Configure when automatic backups are initiated. The Linode Backup Service/ + ); + }); + + it('renders with the linode schedule taking into account the user timezone', async () => { + server.use( + rest.get('*/linode/instances/1', (req, res, ctx) => { + return res( + ctx.json( + linodeFactory.build({ + id: 1, + backups: { + enabled: true, + schedule: { + day: 'Monday', + window: 'W4', + }, + }, + }) + ) + ); + }), + rest.get('*/profile', (req, res, ctx) => { + return res( + ctx.json(profileFactory.build({ timezone: 'America/New_York' })) + ); + }) + ); + + const { findByText } = renderWithTheme( + <ScheduleSettings linodeId={1} isReadOnly={false} /> + ); + + await findByText('Monday'); + await findByText('00:00 - 02:00'); + + await findByText('America/New York', { exact: false }); + }); +}); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx new file mode 100644 index 00000000000..34e0ac05831 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx @@ -0,0 +1,174 @@ +import * as React from 'react'; +import ActionsPanel from 'src/components/ActionsPanel/ActionsPanel'; +import Button from 'src/components/Button/Button'; +import Select from 'src/components/EnhancedSelect/Select'; +import { Notice } from 'src/components/Notice/Notice'; +import FormControl from 'src/components/core/FormControl'; +import FormHelperText from 'src/components/core/FormHelperText'; +import Paper from 'src/components/core/Paper'; +import Typography from 'src/components/core/Typography'; +import getUserTimezone from 'src/utilities/getUserTimezone'; +import { Theme } from '@mui/material/styles'; +import { makeStyles } from 'tss-react/mui'; +import { useFormik } from 'formik'; +import { useSnackbar } from 'notistack'; +import { useProfile } from 'src/queries/profile'; +import { initWindows } from 'src/utilities/initWindows'; +import { + useLinodeQuery, + useLinodeUpdateMutation, +} from 'src/queries/linodes/linodes'; + +const useStyles = makeStyles()((theme: Theme) => ({ + scheduleAction: { + padding: 0, + '& button': { + marginLeft: 0, + marginTop: theme.spacing(2), + }, + }, + chooseDay: { + marginRight: theme.spacing(2), + minWidth: 150, + '& .react-select__menu-list': { + maxHeight: 'none', + }, + }, +})); + +interface Props { + linodeId: number; + isReadOnly: boolean; +} + +export const ScheduleSettings = ({ linodeId, isReadOnly }: Props) => { + const { classes } = useStyles(); + const { enqueueSnackbar } = useSnackbar(); + + const { data: profile } = useProfile(); + const { data: linode } = useLinodeQuery(linodeId); + + const { + mutateAsync: updateLinode, + error: updateLinodeError, + isLoading: isUpdating, + } = useLinodeUpdateMutation(linodeId); + + const settingsForm = useFormik({ + enableReinitialize: true, + initialValues: { + day: linode?.backups.schedule.day, + window: linode?.backups.schedule.window, + }, + async onSubmit(values) { + await updateLinode({ + backups: { + schedule: values, + }, + }); + + enqueueSnackbar('Backup settings saved', { + variant: 'success', + }); + }, + }); + + const days = [ + ['Choose a day', 'Scheduling'], + ['Sunday', 'Sunday'], + ['Monday', 'Monday'], + ['Tuesday', 'Tuesday'], + ['Wednesday', 'Wednesday'], + ['Thursday', 'Thursday'], + ['Friday', 'Friday'], + ['Saturday', 'Saturday'], + ]; + + const windows = initWindows(getUserTimezone(profile?.timezone), true); + + const windowOptions = windows.map((window) => ({ + label: window[0], + value: window[1], + })); + + const dayOptions = days.map((day) => ({ label: day[0], value: day[1] })); + + return ( + <Paper> + <form onSubmit={settingsForm.handleSubmit}> + <Typography variant="h2" data-qa-settings-heading> + Settings + </Typography> + <Typography variant="body1" data-qa-settings-desc marginTop={1}> + Configure when automatic backups are initiated. The Linode Backup + Service will generate a backup between the selected hours every day, + and will overwrite the previous daily backup. The selected day is when + the backup is promoted to the weekly slot. Up to two weekly backups + are saved. + </Typography> + {Boolean(updateLinodeError) && ( + <Notice error spacingTop={16} spacingBottom={0}> + {updateLinodeError?.[0].reason} + </Notice> + )} + <FormControl className={classes.chooseDay}> + <Select + textFieldProps={{ + dataAttrs: { + 'data-qa-weekday-select': true, + }, + }} + options={dayOptions} + onChange={(item) => settingsForm.setFieldValue('day', item.value)} + value={dayOptions.find( + (item) => item.value === settingsForm.values.day + )} + disabled={isReadOnly} + label="Day of Week" + placeholder="Choose a day" + isClearable={false} + name="Day of Week" + noMarginTop + /> + </FormControl> + <FormControl> + <Select + textFieldProps={{ + dataAttrs: { + 'data-qa-time-select': true, + }, + }} + options={windowOptions} + onChange={(item) => + settingsForm.setFieldValue('window', item.value) + } + value={windowOptions.find( + (item) => item.value === settingsForm.values.window + )} + label="Time of Day" + disabled={isReadOnly} + placeholder="Choose a time" + isClearable={false} + name="Time of Day" + noMarginTop + /> + <FormHelperText sx={{ marginLeft: 0 }}> + Time displayed in{' '} + {getUserTimezone(profile?.timezone).replace('_', ' ')} + </FormHelperText> + </FormControl> + <ActionsPanel className={classes.scheduleAction}> + <Button + buttonType="primary" + type="submit" + disabled={isReadOnly || !settingsForm.dirty} + loading={isUpdating} + data-qa-schedule + > + Save Schedule + </Button> + </ActionsPanel> + </form> + </Paper> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/index.ts b/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/index.ts deleted file mode 100644 index 088435e62ee..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeBackup/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import LinodeBackup from './LinodeBackup'; -export { aggregateBackups } from './LinodeBackup'; -export default LinodeBackup; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/AddIPDrawer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/AddIPDrawer.tsx index 8d46d1e0551..6aa404ee92c 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/AddIPDrawer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/AddIPDrawer.tsx @@ -15,7 +15,7 @@ import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; import { Item } from 'src/components/EnhancedSelect/Select'; import ExternalLink from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; const useStyles = makeStyles((theme: Theme) => ({ diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/CreateIPv4Drawer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/CreateIPv4Drawer.tsx index eb565199c3b..7fdc94f6b5e 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/CreateIPv4Drawer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/CreateIPv4Drawer.tsx @@ -5,7 +5,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx index 32395f016f7..6720e517bc5 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx @@ -19,12 +19,12 @@ import Typography from 'src/components/core/Typography'; import { Dialog } from 'src/components/Dialog/Dialog'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import TextField from 'src/components/TextField'; import useFlags from 'src/hooks/useFlags'; import { API_MAX_PAGE_SIZE } from 'src/constants'; -import { useAllLinodesQuery } from 'src/queries/linodes'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; import { areArraysEqual } from 'src/utilities/areArraysEqual'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx index 1ae9f7fb7b9..73879a6007a 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx @@ -23,13 +23,13 @@ import Typography from 'src/components/core/Typography'; import { Dialog } from 'src/components/Dialog/Dialog'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import usePrevious from 'src/hooks/usePrevious'; import { ipv6RangeQueryKey } from 'src/queries/networking'; import { queryKey as linodesQueryKey, useAllLinodesQuery, -} from 'src/queries/linodes'; +} from 'src/queries/linodes/linodes'; import { useIpv6RangesQuery } from 'src/queries/networking'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { debounce } from 'throttle-debounce'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx index 5aed1aad77b..a470c952941 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx @@ -24,15 +24,15 @@ import Hidden from 'src/components/core/Hidden'; import Paper from 'src/components/core/Paper'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableHead from 'src/components/core/TableHead'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; import Typography from 'src/components/core/Typography'; import ErrorState from 'src/components/ErrorState'; import OrderBy from 'src/components/OrderBy'; -import Table from 'src/components/Table'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; +import { Table } from 'src/components/Table'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; import withFeatureFlags, { FeatureFlagConsumerProps, } from 'src/containers/withFeatureFlagConsumer.container'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.tsx index b1b983de324..509ab6ada92 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.tsx @@ -6,7 +6,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useAPIRequest } from 'src/hooks/useAPIRequest'; import { useAccountTransfer } from 'src/queries/accountTransfer'; import { readableBytes } from 'src/utilities/unitConversions'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx index 14dc8faa66b..64262745db2 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx @@ -20,7 +20,7 @@ import { STATS_NOT_READY_MESSAGE, useLinodeStatsByDate, useLinodeTransferByDate, -} from 'src/queries/linodes'; +} from 'src/queries/linodes/stats'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { readableBytes } from 'src/utilities/unitConversions'; @@ -183,6 +183,7 @@ export const TransferHistory: React.FC<Props> = (props) => { timezone={profile?.timezone ?? 'UTC'} chartHeight={190} unit={`/s`} + accessibleDataTable={{ unit: 'Kb/s' }} formatData={convertNetworkData} formatTooltip={formatTooltip} showToday={true} diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodePermissionsError.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodePermissionsError.tsx index 565611dbcb8..62621076284 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodePermissionsError.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodePermissionsError.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; const LinodePermissionsError = () => ( <Notice diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodePowerControl/LinodePowerControl.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodePowerControl/LinodePowerControl.tsx index d46ec3767a1..9330ac886b0 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodePowerControl/LinodePowerControl.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodePowerControl/LinodePowerControl.tsx @@ -11,7 +11,7 @@ import { Theme } from '@mui/material/styles'; import EntityIcon from 'src/components/EntityIcon'; import MenuItem from 'src/components/MenuItem'; import { linodeInTransition } from 'src/features/linodes/transitions'; -import PowerDialogOrDrawer, { Action } from '../../PowerActionsDialogOrDrawer'; +import { PowerActionsDialog, Action } from '../../PowerActionsDialogOrDrawer'; type ClassNames = | 'root' @@ -307,13 +307,11 @@ export class LinodePowerButton extends React.Component<CombinedProps, State> { </MenuItem> )} </Menu> - <PowerDialogOrDrawer + <PowerActionsDialog isOpen={this.state.powerDialogOpen} - action={this.state.selectedBootAction} - linodeID={this.props.id} - linodeLabel={this.props.label} - close={this.closeDialog} - linodeConfigs={this.props.linodeConfigs} + action={this.state.selectedBootAction ?? 'Reboot'} + linodeId={this.props.id} + onClose={this.closeDialog} /> </React.Fragment> ); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/LinodeRebuildDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/LinodeRebuildDialog.tsx index 15e737057f2..77195a4d36b 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/LinodeRebuildDialog.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/LinodeRebuildDialog.tsx @@ -4,12 +4,13 @@ import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { Dialog } from 'src/components/Dialog/Dialog'; import EnhancedSelect, { Item } from 'src/components/EnhancedSelect/Select'; -import Notice from 'src/components/Notice'; -import useExtendedLinode from 'src/hooks/useExtendedLinode'; +import { Notice } from 'src/components/Notice/Notice'; import HostMaintenanceError from '../HostMaintenanceError'; import LinodePermissionsError from '../LinodePermissionsError'; import RebuildFromImage from './RebuildFromImage'; import RebuildFromStackScript from './RebuildFromStackScript'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { useGrants, useProfile } from 'src/queries/profile'; const useStyles = makeStyles((theme: Theme) => ({ root: { @@ -39,17 +40,16 @@ const useStyles = makeStyles((theme: Theme) => ({ })); interface Props { - linodeId: number; + linodeId: number | undefined; open: boolean; onClose: () => void; } -type CombinedProps = Props; - type MODES = | 'fromImage' | 'fromCommunityStackScript' | 'fromAccountStackScript'; + const options = [ { value: 'fromImage', label: 'From Image' }, { value: 'fromCommunityStackScript', label: 'From Community StackScript' }, @@ -58,15 +58,23 @@ const options = [ const passwordHelperText = 'Set a password for your rebuilt Linode.'; -const LinodeRebuildDialog: React.FC<CombinedProps> = (props) => { +export const LinodeRebuildDialog = (props: Props) => { const { linodeId, open, onClose } = props; - const linode = useExtendedLinode(linodeId); - const linodeLabel = linode?.label; - const linodeStatus = linode?.status; - const permissions = linode?._permissions; - const hostMaintenance = linodeStatus === 'stopped'; - const unauthorized = permissions === 'read_only'; + const { data: profile } = useProfile(); + const { data: grants } = useGrants(); + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined && open + ); + + const isReadOnly = + Boolean(profile?.restricted) && + grants?.linode.find((grant) => grant.id === linodeId)?.permissions === + 'read_only'; + + const hostMaintenance = linode?.status === 'stopped'; + const unauthorized = isReadOnly; const disabled = hostMaintenance || unauthorized; const classes = useStyles(); @@ -86,7 +94,7 @@ const LinodeRebuildDialog: React.FC<CombinedProps> = (props) => { return ( <Dialog - title={`Rebuild Linode ${linodeLabel ?? ''}`} + title={`Rebuild Linode ${linode?.label ?? ''}`} open={open} onClose={onClose} fullWidth @@ -124,8 +132,8 @@ const LinodeRebuildDialog: React.FC<CombinedProps> = (props) => { <RebuildFromImage passwordHelperText={passwordHelperText} disabled={disabled} - linodeId={linodeId} - linodeLabel={linodeLabel} + linodeId={linodeId ?? -1} + linodeLabel={linode?.label} handleRebuildError={handleRebuildError} onClose={onClose} /> @@ -135,8 +143,8 @@ const LinodeRebuildDialog: React.FC<CombinedProps> = (props) => { type="community" passwordHelperText={passwordHelperText} disabled={disabled} - linodeId={linodeId} - linodeLabel={linodeLabel} + linodeId={linodeId ?? -1} + linodeLabel={linode?.label} handleRebuildError={handleRebuildError} onClose={onClose} /> @@ -146,8 +154,8 @@ const LinodeRebuildDialog: React.FC<CombinedProps> = (props) => { type="account" passwordHelperText={passwordHelperText} disabled={disabled} - linodeId={linodeId} - linodeLabel={linodeLabel} + linodeId={linodeId ?? -1} + linodeLabel={linode?.label} handleRebuildError={handleRebuildError} onClose={onClose} /> @@ -155,5 +163,3 @@ const LinodeRebuildDialog: React.FC<CombinedProps> = (props) => { </Dialog> ); }; - -export default LinodeRebuildDialog; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildDialog.tsx deleted file mode 100644 index ecdef6cd4d4..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildDialog.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import ActionsPanel from 'src/components/ActionsPanel'; -import Button from 'src/components/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import Typography from 'src/components/core/Typography'; - -interface RebuildDialogProps { - isOpen: boolean; - isLoading: boolean; - handleClose: () => void; - handleSubmit: () => void; -} - -// During post-CMR cleanup, we should rename this component to something like "RebuildConfirmationDialog" or something similar, and rename LinodeRebuildDialog to "RebuildDialog" -export const RebuildDialog: React.FC<RebuildDialogProps> = (props) => { - const { isOpen, isLoading, handleClose, handleSubmit } = props; - - const actions = () => ( - <ActionsPanel> - <Button buttonType="secondary" onClick={handleClose} data-qa-cancel> - Cancel - </Button> - <Button - buttonType="primary" - onClick={handleSubmit} - loading={isLoading} - data-qa-submit-rebuild - > - Rebuild - </Button> - </ActionsPanel> - ); - - return ( - <ConfirmationDialog - open={isOpen} - onClose={handleClose} - title="Confirm Linode Rebuild" - actions={actions} - > - <Typography> - Are you sure you want to rebuild this Linode? This will result in - permanent data loss. - </Typography> - </ConfirmationDialog> - ); -}; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.styles.ts b/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.styles.ts index 3661dcfc230..4a4379ddc4d 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.styles.ts +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.styles.ts @@ -1,4 +1,4 @@ -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { styled } from '@mui/material/styles'; export const StyledNotice = styled(Notice)({ diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/index.ts b/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/index.ts deleted file mode 100644 index e0065d6b1f2..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRebuild/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @todo update this once the Bare Metal rebuild flow is merged -import LinodeRebuild from './LinodeRebuildDialog'; -export default LinodeRebuild; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx index 699861eb92b..c1ee5a082f8 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx @@ -7,19 +7,23 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { resetEventsPolling } from 'src/eventsPolling'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import RescueDescription from './RescueDescription'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; interface Props { - linodeID: number; - linodeLabel: string; + linodeId: number | undefined; isOpen: boolean; onClose: () => void; } -export const BareMetalRescue: React.FC<Props> = (props) => { - const { isOpen, onClose, linodeID, linodeLabel } = props; +export const BareMetalRescue = (props: Props) => { + const { isOpen, onClose, linodeId } = props; const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState<string | undefined>(undefined); const { enqueueSnackbar } = useSnackbar(); + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined && isOpen + ); React.useEffect(() => { if (isOpen) { @@ -31,7 +35,7 @@ export const BareMetalRescue: React.FC<Props> = (props) => { const handleSubmit = () => { setError(undefined); setLoading(true); - rescueMetalLinode(linodeID) + rescueMetalLinode(linodeId ?? -1) .then(() => { setLoading(false); enqueueSnackbar('Linode rescue started.', { @@ -61,15 +65,13 @@ export const BareMetalRescue: React.FC<Props> = (props) => { return ( <ConfirmationDialog - title={`Rescue Linode ${linodeLabel}`} + title={`Rescue Linode ${linode?.label ?? ''}`} open={isOpen} onClose={onClose} actions={actions} error={error} > - <RescueDescription linodeId={linodeID} isBareMetal /> + {linodeId ? <RescueDescription linodeId={linodeId} isBareMetal /> : null} </ConfirmationDialog> ); }; - -export default BareMetalRescue; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.tsx deleted file mode 100644 index bc9d007d4f7..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import useExtendedLinode from 'src/hooks/useExtendedLinode'; -import BareMetalRescue from './BareMetalRescue'; -import RescueDialog from './RescueDialog'; - -export interface Props { - open: boolean; - onClose: () => void; - linodeId: number; -} - -export const RescueContainer: React.FC<Props> = (props) => { - const { linodeId, open, onClose } = props; - const linode = useExtendedLinode(linodeId); - const isBareMetalInstance = linode?._type?.class === 'metal'; - const linodeLabel = linode?.label ?? ''; - - /** - * Bare Metal Linodes have a much simpler Rescue flow, - * since it's not possible to select disk mounts. Rather - * than conditionally handle everything in RescueDialog, - * we instead render a simple ConfirmationDialog for - * these instances. - */ - return isBareMetalInstance ? ( - <BareMetalRescue - linodeID={linodeId} - linodeLabel={linodeLabel} - isOpen={open} - onClose={onClose} - /> - ) : ( - /** For normal Linodes, load the standard rescue dialog. */ - <RescueDialog linodeId={linodeId} open={open} onClose={onClose} /> - ); -}; - -export default RescueContainer; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDescription.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDescription.tsx index 644450f7b05..03d0cf196ce 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDescription.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDescription.tsx @@ -3,9 +3,9 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import Link from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { lishLaunch } from 'src/features/Lish/lishUtils'; -import { useLinodeFirewalls } from 'src/queries/linodes'; +import { useLinodeFirewalls } from 'src/queries/linodes/firewalls'; const rescueDescription = { text: `If you suspect that your primary filesystem is corrupt, use the Linode Manager to boot your Linode into Rescue Mode. This is a safe environment for performing many system recovery and disk management tasks.`, diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.test.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx similarity index 51% rename from packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.test.tsx rename to packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx index 7a9b6f011ab..17ebfc7c0f2 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueContainer.test.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx @@ -1,10 +1,11 @@ -import { screen, waitFor } from '@testing-library/react'; import * as React from 'react'; +import { waitFor } from '@testing-library/react'; import { linodeFactory } from 'src/factories/linodes'; import { rest, server } from 'src/mocks/testServer'; import { typeFactory } from 'src/factories/types'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import RescueContainer, { Props } from './RescueContainer'; +import { RescueDialog, Props } from './RescueDialog'; +import { QueryClient } from 'react-query'; const standard = typeFactory.build({ id: 'g6-standard-1' }); const metal = typeFactory.build({ id: 'g6-metal-alpha-2', class: 'metal' }); @@ -18,48 +19,47 @@ const props: Props = { open: true, }; -const render = (propOverride?: Partial<Props>) => - renderWithTheme(<RescueContainer {...props} {...propOverride} />, { - customStore: { - __resources: { - linodes: { - itemsById: { - [normalLinode.id]: normalLinode, - [metalLinode.id]: metalLinode, - }, - }, - }, - }, - }); - -describe('RescueContainer', () => { - beforeEach(async () => { +describe('RescueDialog', () => { + it('should render the rescue modal for a normal instance', async () => { server.use( rest.get(`*/linode/types/${standard.id}`, (_, res, ctx) => { return res(ctx.json(standard)); + }), + rest.get(`*/linode/instances/${normalLinode.id}`, (_, res, ctx) => { + return res(ctx.json(normalLinode)); }) ); - - server.use( - rest.get(`*/linode/types/${metal.id}`, (_, res, ctx) => { - return res(ctx.json(metal)); - }) + const { queryByTestId, getByText } = renderWithTheme( + <RescueDialog {...props} />, + { + queryClient: new QueryClient(), + } ); - }); - it('should render the rescue modal for a normal instance', async () => { - render(); - expect(screen.getByText(/Rescue Linode/)).toBeInTheDocument(); + expect(getByText(/Rescue Linode/)).toBeInTheDocument(); + await waitFor(() => - expect(screen.queryByTestId('device-select')).toBeInTheDocument() + expect(queryByTestId('device-select')).toBeInTheDocument() ); }); it('should render a confirmation modal for a bare metal instance', async () => { - render({ linodeId: metalLinode.id }); - expect(screen.getByText(/Rescue Linode/)).toBeInTheDocument(); - await waitFor(() => - expect(screen.queryByTestId('device-select')).toBeNull() + server.use( + rest.get(`*/linode/types/${metal.id}`, (_, res, ctx) => { + return res(ctx.json(metal)); + }), + rest.get(`*/linode/instances/${metalLinode.id}`, (_, res, ctx) => { + return res(ctx.json(metalLinode)); + }) ); + const { queryByTestId, getByText } = renderWithTheme( + <RescueDialog {...props} linodeId={metalLinode.id} />, + { + queryClient: new QueryClient(), + } + ); + + expect(getByText(/Rescue Linode/)).toBeInTheDocument(); + await waitFor(() => expect(queryByTestId('device-select')).toBeNull()); }); }); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.tsx index 5bc7325f8af..f9093bd7766 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/RescueDialog.tsx @@ -1,268 +1,37 @@ -import { rescueLinode } from '@linode/api-v4/lib/linodes'; -import { APIError } from '@linode/api-v4/lib/types'; -import { useSnackbar } from 'notistack'; -import { assoc, clamp, equals, pathOr } from 'ramda'; import * as React from 'react'; -import { connect } from 'react-redux'; -import { RouteComponentProps } from 'react-router-dom'; -import { compose } from 'recompose'; -import { StyledActionPanel } from 'src/components/ActionsPanel/ActionsPanel'; -import Button from 'src/components/Button'; -import Paper from 'src/components/core/Paper'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import { Dialog } from 'src/components/Dialog/Dialog'; -import ErrorState from 'src/components/ErrorState'; -import Notice from 'src/components/Notice'; -import SectionErrorBoundary from 'src/components/SectionErrorBoundary'; -import { resetEventsPolling } from 'src/eventsPolling'; -import useExtendedLinode from 'src/hooks/useExtendedLinode'; -import usePrevious from 'src/hooks/usePrevious'; -import { useAllVolumesQuery } from 'src/queries/volumes'; -import { MapState } from 'src/store/types'; -import createDevicesFromStrings, { - DevicesAsStrings, -} from 'src/utilities/createDevicesFromStrings'; -import LinodePermissionsError from '../LinodePermissionsError'; -import DeviceSelection, { ExtendedDisk } from './DeviceSelection'; -import RescueDescription from './RescueDescription'; +import { BareMetalRescue } from './BareMetalRescue'; +import { StandardRescueDialog } from './StandardRescueDialog'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { useTypeQuery } from 'src/queries/types'; -const useStyles = makeStyles((theme: Theme) => ({ - root: { - padding: `${theme.spacing(3)} 0 ${theme.spacing(1)}`, - '& .iconTextLink': { - display: 'inline-flex', - margin: `${theme.spacing(3)} 0 0 0`, - }, - }, - button: { - marginTop: theme.spacing(), - }, -})); - -interface StateProps { - diskError?: APIError[]; -} - -interface Props { - linodeId: number; +export interface Props { open: boolean; onClose: () => void; + linodeId: number | undefined; } -type CombinedProps = Props & StateProps & RouteComponentProps; -interface DeviceMap { - sda?: string; - sdb?: string; - sdc?: string; - sdd?: string; - sde?: string; - sdf?: string; - sdg?: string; -} +export const RescueDialog = (props: Props) => { + const { linodeId, open, onClose } = props; -export const getDefaultDeviceMapAndCounter = ( - disks: ExtendedDisk[] -): [DeviceMap, number] => { - const defaultDisks = disks.map((thisDisk) => thisDisk._id); - const counter = defaultDisks.reduce( - (c, thisDisk) => (!!thisDisk ? c + 1 : c), - 0 + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined && open ); - /** - * This mimics the behavior of Classic: - * when you load the Rebuild tab, each - * device slot is filled with one of your - * disks, until you run out of either disks - * or slots. Note that defaultDisks[10000] - * will be `undefined`, which is the correct - * value for an empty slot, so this is a safe - * assignment. - */ - const deviceMap = { - sda: defaultDisks[0], - sdb: defaultDisks[1], - sdc: defaultDisks[2], - sdd: defaultDisks[3], - sde: defaultDisks[4], - sdf: defaultDisks[5], - sdg: defaultDisks[6], - }; - return [deviceMap, counter]; -}; - -const LinodeRescue: React.FC<CombinedProps> = (props) => { - const { diskError, open, onClose, linodeId } = props; - - const classes = useStyles(); + const { data: type } = useTypeQuery(linode?.type ?? '', linode !== undefined); - const linode = useExtendedLinode(linodeId); - const linodeRegion = linode?.region; - const linodeLabel = linode?.label; - const linodeDisks = linode?._disks.map((disk) => - assoc('_id', `disk-${disk.id}`, disk) - ); - - // We need the API to allow us to filter on `linode_id` - // const { data: volumes } = useAllVolumesQuery( - // {}, - // { - // '+or': [ - // { linode_id: props.linodeId }, - // { linode_id: null, region: linodeRegion }, - // ], - // }, - // open - // ); - - const { data: volumes, error: volumesError } = useAllVolumesQuery( - {}, - { region: linodeRegion }, - open - ); - - const filteredVolumes = - volumes?.filter((volume) => { - // whether volume is not attached to any Linode - const volumeIsUnattached = volume.linode_id === null; - // whether volume is attached to the current Linode we're viewing - const volumeIsAttachedToCurrentLinode = volume.linode_id === linodeId; - - return volumeIsAttachedToCurrentLinode || volumeIsUnattached; - }) ?? []; - - const [deviceMap, initialCounter] = getDefaultDeviceMapAndCounter( - linodeDisks ?? [] - ); + const isBareMetalInstance = type?.class === 'metal'; - const prevDeviceMap = usePrevious(deviceMap); - - const [counter, setCounter] = React.useState<number>(initialCounter); - const [rescueDevices, setRescueDevices] = React.useState<DevicesAsStrings>( - deviceMap - ); - - const { enqueueSnackbar } = useSnackbar(); - - const [APIError, setAPIError] = React.useState<string>(''); - - React.useEffect(() => { - if (!equals(deviceMap, prevDeviceMap)) { - setCounter(initialCounter); - setRescueDevices(deviceMap); - setAPIError(''); - } - }, [open, initialCounter, deviceMap, prevDeviceMap]); - - const devices = { - disks: linodeDisks ?? [], - volumes: - filteredVolumes.map((volume) => ({ - ...volume, - _id: `volume-${volume.id}`, - })) ?? [], - }; - - const unauthorized = linode?._permissions === 'read_only'; - const disabled = unauthorized; - - const onSubmit = () => { - rescueLinode(linodeId, createDevicesFromStrings(rescueDevices)) - .then((_) => { - enqueueSnackbar('Linode rescue started.', { - variant: 'info', - }); - resetEventsPolling(); - onClose(); - }) - .catch((errorResponse: APIError[]) => { - setAPIError(errorResponse[0].reason); - }); - }; - - const incrementCounter = () => { - setCounter(clamp(1, 6, counter + 1)); - }; - - /** string format is type-id */ - const onChange = (slot: string, _id: string) => - setRescueDevices((rescueDevices) => ({ - ...rescueDevices, - [slot]: _id, - })); - - return ( - <Dialog - title={`Rescue Linode ${linodeLabel ?? ''}`} - open={open} - onClose={() => { - setAPIError(''); - onClose(); - }} - fullWidth - fullHeight - maxWidth="md" - > - {APIError && <Notice error text={APIError} />} - {diskError ? ( - <div> - <ErrorState errorText="There was an error retrieving Disks information." /> - </div> - ) : volumesError ? ( - <div> - <ErrorState errorText="There was an error retrieving Volumes information." /> - </div> - ) : ( - <div> - <Paper className={classes.root}> - {unauthorized && <LinodePermissionsError />} - <RescueDescription linodeId={linodeId} /> - <DeviceSelection - slots={['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg']} - devices={devices} - onChange={onChange} - getSelected={(slot) => pathOr('', [slot], rescueDevices)} - counter={counter} - rescue - disabled={disabled} - /> - <Button - buttonType="secondary" - onClick={incrementCounter} - className={classes.button} - compactX - disabled={disabled || counter >= 6} - > - Add Disk - </Button> - <StyledActionPanel> - <Button - buttonType="primary" - disabled={disabled} - onClick={onSubmit} - data-qa-submit - > - Reboot into Rescue Mode - </Button> - </StyledActionPanel> - </Paper> - </div> - )} - </Dialog> + /** + * Bare Metal Linodes have a much simpler Rescue flow, + * since it's not possible to select disk mounts. Rather + * than conditionally handle everything in RescueDialog, + * we instead render a simple ConfirmationDialog for + * these instances. + */ + return isBareMetalInstance ? ( + <BareMetalRescue linodeId={linodeId} isOpen={open} onClose={onClose} /> + ) : ( + /** For normal Linodes, load the standard rescue dialog. */ + <StandardRescueDialog linodeId={linodeId} open={open} onClose={onClose} /> ); }; - -const mapStateToProps: MapState<StateProps, CombinedProps> = ( - state, - ownProps -) => ({ - diskError: state.__resources.linodeDisks[ownProps.linodeId]?.error?.read, -}); - -const connected = connect(mapStateToProps); - -export default compose<CombinedProps, Props>( - React.memo, - SectionErrorBoundary, - connected -)(LinodeRescue); diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx new file mode 100644 index 00000000000..87b13f84175 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -0,0 +1,257 @@ +import { rescueLinode } from '@linode/api-v4/lib/linodes'; +import { APIError } from '@linode/api-v4/lib/types'; +import { useSnackbar } from 'notistack'; +import { assoc, clamp, equals, pathOr } from 'ramda'; +import * as React from 'react'; +import { StyledActionPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import Button from 'src/components/Button'; +import Paper from 'src/components/core/Paper'; +import { makeStyles } from '@mui/styles'; +import { Theme } from '@mui/material/styles'; +import { Dialog } from 'src/components/Dialog/Dialog'; +import ErrorState from 'src/components/ErrorState'; +import { Notice } from 'src/components/Notice/Notice'; +import { resetEventsPolling } from 'src/eventsPolling'; +import usePrevious from 'src/hooks/usePrevious'; +import { useAllVolumesQuery } from 'src/queries/volumes'; +import createDevicesFromStrings, { + DevicesAsStrings, +} from 'src/utilities/createDevicesFromStrings'; +import LinodePermissionsError from '../LinodePermissionsError'; +import DeviceSelection, { ExtendedDisk } from './DeviceSelection'; +import RescueDescription from './RescueDescription'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; +import { useGrants, useProfile } from 'src/queries/profile'; + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + padding: `${theme.spacing(3)} 0 ${theme.spacing(1)}`, + '& .iconTextLink': { + display: 'inline-flex', + margin: `${theme.spacing(3)} 0 0 0`, + }, + }, + button: { + marginTop: theme.spacing(), + }, +})); + +interface Props { + linodeId: number | undefined; + open: boolean; + onClose: () => void; +} + +interface DeviceMap { + sda?: string; + sdb?: string; + sdc?: string; + sdd?: string; + sde?: string; + sdf?: string; + sdg?: string; +} + +export const getDefaultDeviceMapAndCounter = ( + disks: ExtendedDisk[] +): [DeviceMap, number] => { + const defaultDisks = disks.map((thisDisk) => thisDisk._id); + const counter = defaultDisks.reduce( + (c, thisDisk) => (!!thisDisk ? c + 1 : c), + 0 + ); + /** + * This mimics the behavior of Classic: + * when you load the Rebuild tab, each + * device slot is filled with one of your + * disks, until you run out of either disks + * or slots. Note that defaultDisks[10000] + * will be `undefined`, which is the correct + * value for an empty slot, so this is a safe + * assignment. + */ + const deviceMap: DeviceMap = { + sda: defaultDisks[0], + sdb: defaultDisks[1], + sdc: defaultDisks[2], + sdd: defaultDisks[3], + sde: defaultDisks[4], + sdf: defaultDisks[5], + sdg: defaultDisks[6], + }; + return [deviceMap, counter]; +}; + +export const StandardRescueDialog = (props: Props) => { + const { open, onClose, linodeId } = props; + + const classes = useStyles(); + + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined && open + ); + const { data: disks, error: disksError } = useAllLinodeDisksQuery( + linodeId ?? -1, + linodeId !== undefined && open + ); + const { data: volumes, error: volumesError } = useAllVolumesQuery( + {}, + { region: linode?.region }, + open + ); + + const { data: profile } = useProfile(); + const { data: grants } = useGrants(); + + const isReadOnly = + Boolean(profile?.restricted) && + grants?.linode.find((grant) => grant.id === linodeId)?.permissions === + 'read_only'; + + // We need the API to allow us to filter on `linode_id` + // const { data: volumes } = useAllVolumesQuery( + // {}, + // { + // '+or': [ + // { linode_id: props.linodeId }, + // { linode_id: null, region: linodeRegion }, + // ], + // }, + // open + // ); + + const linodeDisks = disks?.map((disk) => + assoc('_id', `disk-${disk.id}`, disk) + ); + + const filteredVolumes = + volumes?.filter((volume) => { + // whether volume is not attached to any Linode + const volumeIsUnattached = volume.linode_id === null; + // whether volume is attached to the current Linode we're viewing + const volumeIsAttachedToCurrentLinode = volume.linode_id === linodeId; + + return volumeIsAttachedToCurrentLinode || volumeIsUnattached; + }) ?? []; + + const [deviceMap, initialCounter] = getDefaultDeviceMapAndCounter( + linodeDisks ?? [] + ); + + const prevDeviceMap = usePrevious(deviceMap); + + const [counter, setCounter] = React.useState<number>(initialCounter); + const [rescueDevices, setRescueDevices] = React.useState<DevicesAsStrings>( + deviceMap + ); + + const { enqueueSnackbar } = useSnackbar(); + + const [APIError, setAPIError] = React.useState<string>(''); + + React.useEffect(() => { + if (!equals(deviceMap, prevDeviceMap)) { + setCounter(initialCounter); + setRescueDevices(deviceMap); + setAPIError(''); + } + }, [open, initialCounter, deviceMap, prevDeviceMap]); + + const devices = { + disks: linodeDisks ?? [], + volumes: + filteredVolumes.map((volume) => ({ + ...volume, + _id: `volume-${volume.id}`, + })) ?? [], + }; + + const disabled = isReadOnly; + + const onSubmit = () => { + rescueLinode(linodeId ?? -1, createDevicesFromStrings(rescueDevices)) + .then((_) => { + enqueueSnackbar('Linode rescue started.', { + variant: 'info', + }); + resetEventsPolling(); + onClose(); + }) + .catch((errorResponse: APIError[]) => { + setAPIError(errorResponse[0].reason); + }); + }; + + const incrementCounter = () => { + setCounter(clamp(1, 6, counter + 1)); + }; + + /** string format is type-id */ + const onChange = (slot: string, _id: string) => + setRescueDevices((rescueDevices) => ({ + ...rescueDevices, + [slot]: _id, + })); + + return ( + <Dialog + title={`Rescue Linode ${linode?.label ?? ''}`} + open={open} + onClose={() => { + setAPIError(''); + onClose(); + }} + fullWidth + fullHeight + maxWidth="md" + > + {APIError && <Notice error text={APIError} />} + {disksError ? ( + <div> + <ErrorState errorText="There was an error retrieving Disks information." /> + </div> + ) : volumesError ? ( + <div> + <ErrorState errorText="There was an error retrieving Volumes information." /> + </div> + ) : ( + <div> + <Paper className={classes.root}> + {isReadOnly && <LinodePermissionsError />} + {linodeId ? <RescueDescription linodeId={linodeId} /> : null} + <DeviceSelection + slots={['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg']} + devices={devices} + onChange={onChange} + getSelected={(slot) => pathOr('', [slot], rescueDevices)} + counter={counter} + rescue + disabled={disabled} + /> + <Button + buttonType="secondary" + onClick={incrementCounter} + className={classes.button} + compactX + disabled={disabled || counter >= 6} + > + Add Disk + </Button> + <StyledActionPanel> + <Button + buttonType="primary" + disabled={disabled} + onClick={onSubmit} + data-qa-submit + > + Reboot into Rescue Mode + </Button> + </StyledActionPanel> + </Paper> + </div> + )} + </Dialog> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/index.ts b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/index.ts deleted file mode 100644 index 18ee83d8863..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './RescueContainer'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index 7bba4fbc4be..0bfa77385e7 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -16,7 +16,7 @@ import Typography from 'src/components/core/Typography'; import { Dialog } from 'src/components/Dialog/Dialog'; import ExternalLink from 'src/components/ExternalLink'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import TypeToConfirm from 'src/components/TypeToConfirm'; import { withProfile, diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/index.ts b/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/index.ts deleted file mode 100644 index 3b80be26cd1..00000000000 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeResize/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import LinodeResize from './LinodeResize'; -export default LinodeResize; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeConfigDialog.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeConfigDialog.tsx index 0a3928edace..667bb5fdee4 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeConfigDialog.tsx @@ -31,7 +31,7 @@ import ErrorState from 'src/components/ErrorState'; import ExternalLink from 'src/components/ExternalLink'; import Grid from '@mui/material/Unstable_Grid2'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import Radio from 'src/components/Radio'; import TextField from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx index ed47f95cd1c..ad5b0a83d49 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx @@ -7,7 +7,7 @@ import { compose as rCompose } from 'recompose'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Accordion from 'src/components/Accordion'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import PanelErrorBoundary from 'src/components/PanelErrorBoundary'; import { withLinodeDetailContext } from 'src/features/linodes/LinodesDetail/linodeDetailContext'; import { diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx index b14e895564a..d5ec8e31f85 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx @@ -7,7 +7,7 @@ import { compose } from 'recompose'; import Accordion from 'src/components/Accordion'; import Button from 'src/components/Button'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import PanelErrorBoundary from 'src/components/PanelErrorBoundary'; import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; import { resetEventsPolling } from 'src/eventsPolling'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.tsx index beaa65cb4be..626ce7d01f0 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.tsx @@ -6,7 +6,7 @@ import { compose as recompose } from 'recompose'; import Accordion from 'src/components/Accordion'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import PanelErrorBoundary from 'src/components/PanelErrorBoundary'; import TextField from 'src/components/TextField'; import { diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsPasswordPanel.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsPasswordPanel.tsx index 93af9bfe7f3..33688d9c8a9 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsPasswordPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeSettingsPasswordPanel.tsx @@ -14,7 +14,7 @@ import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import EnhancedSelect, { Item } from 'src/components/EnhancedSelect/Select'; import Accordion from 'src/components/Accordion'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import PanelErrorBoundary from 'src/components/PanelErrorBoundary'; const PasswordInput = React.lazy(() => import('src/components/PasswordInput')); import SuspenseLoader from 'src/components/SuspenseLoader'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx index ba1975a13d8..ae26fc6843e 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx @@ -6,7 +6,7 @@ import Accordion from 'src/components/Accordion'; import FormControlLabel from 'src/components/core/FormControlLabel'; import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import PanelErrorBoundary from 'src/components/PanelErrorBoundary'; import { Toggle } from 'src/components/Toggle'; import { withLinodeDetailContext } from 'src/features/linodes/LinodesDetail/linodeDetailContext'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/LinodeSummary.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/LinodeSummary.tsx index 30a7c22de47..d01f3ab35c4 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/LinodeSummary.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/LinodeSummary.tsx @@ -16,7 +16,7 @@ import { STATS_NOT_READY_MESSAGE, useLinodeStats, useLinodeStatsByDate, -} from 'src/queries/linodes'; +} from 'src/queries/linodes/stats'; import { useProfile } from 'src/queries/profile'; import { setUpCharts } from 'src/utilities/charts'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -158,6 +158,7 @@ const LinodeSummary: React.FC<Props> = (props) => { return ( <LineGraph ariaLabel="CPU Usage Graph" + accessibleDataTable={{ unit: '%' }} timezone={timezone} chartHeight={chartHeight} showToday={rangeSelection === '24'} @@ -190,6 +191,7 @@ const LinodeSummary: React.FC<Props> = (props) => { return ( <LineGraph ariaLabel="Disk I/O Graph" + accessibleDataTable={{ unit: 'blocks/s' }} timezone={timezone} chartHeight={chartHeight} showToday={rangeSelection === '24'} diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx index 9036a85ecf8..aeca874e529 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx @@ -257,6 +257,7 @@ const Graph: React.FC<GraphProps> = (props) => { timezone={timezone} chartHeight={chartHeight} unit={`/s`} + accessibleDataTable={{ unit: 'Kb/s' }} formatData={convertNetworkData} formatTooltip={_formatTooltip} showToday={rangeSelection === '24'} diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/HostMaintenance.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/HostMaintenance.tsx index c144254bc0f..2b666b18b74 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/HostMaintenance.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/HostMaintenance.tsx @@ -1,7 +1,7 @@ import { LinodeStatus } from '@linode/api-v4/lib/linodes/types'; import * as React from 'react'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface Props { linodeStatus: LinodeStatus; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx index d7ed577521a..416e45d7ad0 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx @@ -1,33 +1,29 @@ import { Config, Disk, LinodeStatus } from '@linode/api-v4/lib/linodes'; -import { Volume } from '@linode/api-v4/lib/volumes'; import * as React from 'react'; import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import { compose } from 'recompose'; import TagDrawer from 'src/components/TagCell/TagDrawer'; import LinodeEntityDetail from 'src/features/linodes/LinodeEntityDetail'; -import PowerDialogOrDrawer, { - Action as BootAction, +import { + PowerActionsDialog, + Action, } from 'src/features/linodes/PowerActionsDialogOrDrawer'; -import { DialogType } from 'src/features/linodes/types'; -import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; import useLinodeActions from 'src/hooks/useLinodeActions'; import { useProfile } from 'src/queries/profile'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; import { parseQueryParams } from 'src/utilities/queryParams'; -import DeleteDialog from '../../LinodesLanding/DeleteDialog'; +import { DeleteLinodeDialog } from '../../LinodesLanding/DeleteLinodeDialog'; import { MigrateLinode } from 'src/features/linodes/MigrateLinode'; -import EnableBackupDialog from '../LinodeBackup/EnableBackupsDialog'; import { LinodeDetailContext, withLinodeDetailContext, } from '../linodeDetailContext'; -import LinodeRebuildDialog from '../LinodeRebuild/LinodeRebuildDialog'; -import RescueDialog from '../LinodeRescue'; +import { LinodeRebuildDialog } from '../LinodeRebuild/LinodeRebuildDialog'; +import { RescueDialog } from '../LinodeRescue/RescueDialog'; import LinodeResize from '../LinodeResize/LinodeResize'; import HostMaintenance from './HostMaintenance'; import MutationNotification from './MutationNotification'; import Notifications from './Notifications'; -import { UpgradeVolumesDialog } from './UpgradeVolumesDialog'; import LandingHeader from 'src/components/LandingHeader'; import { sendEvent } from 'src/utilities/ga'; import useEditableLabelState from 'src/hooks/useEditableLabelState'; @@ -35,7 +31,7 @@ import { APIError } from '@linode/api-v4/lib/types'; import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { ACCESS_LEVELS } from 'src/constants'; -import { useNotificationsQuery } from 'src/queries/accountNotifications'; +import { EnableBackupsDialog } from '../LinodeBackup/EnableBackupsDialog'; interface Props { numVolumes: number; @@ -48,20 +44,6 @@ interface TagDrawerProps { open: boolean; } -interface PowerDialogProps { - open: boolean; - linodeLabel: string; - linodeID: number; - bootAction?: BootAction; - linodeConfigs?: Config[]; -} - -interface DialogProps { - open: boolean; - linodeLabel?: string; - linodeID: number; -} - type CombinedProps = Props & LinodeDetailContext & LinodeContext; const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { @@ -79,143 +61,37 @@ const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { const matchedLinodeId = Number(match?.params?.linodeId ?? 0); - const { data: notifications } = useNotificationsQuery(); - - const notificationContext = React.useContext(_notificationContext); - const { linode, linodeStatus, linodeDisks, linodeConfigs } = props; - const [powerDialog, setPowerDialog] = React.useState<PowerDialogProps>({ - open: false, - linodeID: 0, - linodeLabel: '', - }); - - const [deleteDialog, setDeleteDialog] = React.useState<DialogProps>({ - open: false, - linodeID: 0, - linodeLabel: '', - }); - - const [resizeDialog, setResizeDialog] = React.useState<DialogProps>({ - open: queryParams.resize === 'true', - linodeID: matchedLinodeId, - }); - - const [migrateDialog, setMigrateDialog] = React.useState<DialogProps>({ - open: queryParams.migrate === 'true', - linodeID: matchedLinodeId, - }); - - const [rescueDialog, setRescueDialog] = React.useState<DialogProps>({ - open: queryParams.rescue === 'true', - linodeID: matchedLinodeId, - }); - - const [rebuildDialog, setRebuildDialog] = React.useState<DialogProps>({ - open: queryParams.rebuild === 'true', - linodeID: matchedLinodeId, - }); - - const [backupsDialog, setBackupsDialog] = React.useState<DialogProps>({ - open: false, - linodeID: 0, - }); - - const [ - upgradeVolumesDialog, - setUpgradeVolumesDialog, - ] = React.useState<DialogProps>({ - open: queryParams.upgrade === 'true', - linodeID: matchedLinodeId, - }); + const [powerAction, setPowerAction] = React.useState<Action>('Reboot'); + const [powerDialogOpen, setPowerDialogOpen] = React.useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState( + queryParams.delete === 'true' + ); + const [rebuildDialogOpen, setRebuildDialogOpen] = React.useState( + queryParams.rebuild === 'true' + ); + const [rescueDialogOpen, setRescueDialogOpen] = React.useState( + queryParams.rescue === 'true' + ); + const [resizeDialogOpen, setResizeDialogOpen] = React.useState( + queryParams.resize === 'true' + ); + const [migrateDialogOpen, setMigrateDialogOpen] = React.useState( + queryParams.migrate === 'true' + ); + const [enableBackupsDialogOpen, setEnableBackupsDialogOpen] = React.useState( + false + ); const [tagDrawer, setTagDrawer] = React.useState<TagDrawerProps>({ open: false, tags: [], }); - const { updateLinode, deleteLinode } = useLinodeActions(); + const { updateLinode } = useLinodeActions(); const history = useHistory(); - const openPowerActionDialog = ( - bootAction: BootAction, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => { - setPowerDialog({ - open: true, - bootAction, - linodeConfigs, - linodeID, - linodeLabel, - }); - }; - - const openDialog = ( - dialogType: DialogType, - linodeID: number, - linodeLabel?: string - ) => { - switch (dialogType) { - case 'delete': - setDeleteDialog((deleteDialog) => ({ - ...deleteDialog, - open: true, - linodeLabel, - linodeID, - })); - break; - case 'migrate': - setMigrateDialog((migrateDialog) => ({ - ...migrateDialog, - open: true, - linodeID, - })); - history.replace({ search: 'migrate=true' }); - break; - case 'resize': - setResizeDialog((resizeDialog) => ({ - ...resizeDialog, - open: true, - linodeID, - })); - history.replace({ search: 'resize=true' }); - break; - case 'rescue': - setRescueDialog((rescueDialog) => ({ - ...rescueDialog, - open: true, - linodeID, - })); - history.replace({ search: 'rescue=true' }); - break; - case 'rebuild': - setRebuildDialog((rebuildDialog) => ({ - ...rebuildDialog, - open: true, - linodeID, - })); - history.replace({ search: 'rebuild=true' }); - break; - case 'enable_backups': - setBackupsDialog((backupsDialog) => ({ - ...backupsDialog, - open: true, - linodeID, - })); - break; - case 'upgrade_volumes': - setUpgradeVolumesDialog((upgradeVolumesDialog) => ({ - ...upgradeVolumesDialog, - open: true, - })); - history.replace({ search: 'upgrade=true' }); - break; - } - }; - const closeDialogs = () => { // If the user is on a Linode detail tab with the modal open and they then close it, // change the URL to reflect just the tab they are on. @@ -229,17 +105,13 @@ const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { history.replace({ search: undefined }); } - setPowerDialog((powerDialog) => ({ ...powerDialog, open: false })); - setDeleteDialog((deleteDialog) => ({ ...deleteDialog, open: false })); - setResizeDialog((resizeDialog) => ({ ...resizeDialog, open: false })); - setMigrateDialog((migrateDialog) => ({ ...migrateDialog, open: false })); - setRescueDialog((rescueDialog) => ({ ...rescueDialog, open: false })); - setRebuildDialog((rebuildDialog) => ({ ...rebuildDialog, open: false })); - setBackupsDialog((backupsDialog) => ({ ...backupsDialog, open: false })); - setUpgradeVolumesDialog((upgradeVolumesDialog) => ({ - ...upgradeVolumesDialog, - open: false, - })); + setPowerDialogOpen(false); + setDeleteDialogOpen(false); + setResizeDialogOpen(false); + setMigrateDialogOpen(false); + setRescueDialogOpen(false); + setRebuildDialogOpen(false); + setEnableBackupsDialogOpen(false); }; const closeTagDrawer = () => { @@ -262,26 +134,8 @@ const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { const { data: profile } = useProfile(); const { data: volumesData } = useLinodeVolumesQuery(matchedLinodeId); - const volumesForLinode = volumesData?.data ?? []; const numAttachedVolumes = volumesData?.results ?? 0; - const handleDeleteLinode = (linodeId: number) => { - history.push('/linodes'); - return deleteLinode(linodeId); - }; - - const upgradeableVolumeIds = - notifications - ?.filter( - (notification) => - notification.type === 'volume_migration_scheduled' && - volumesForLinode.some( - (volume: Volume) => volume.id === notification?.entity?.id - ) - ) - // Non null assertion because we assume that these kinds of notifications will always have an entity attached. - .map((notification) => notification.entity!.id) ?? []; - const { editableLabelError, setEditableLabelError, @@ -317,6 +171,40 @@ const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { }); }; + const onOpenPowerDialog = (action: Action) => { + setPowerDialogOpen(true); + setPowerAction(action); + }; + + const onOpenDeleteDialog = () => { + setDeleteDialogOpen(true); + }; + + const onOpenResizeDialog = () => { + setResizeDialogOpen(true); + }; + + const onOpenRebuildDialog = () => { + setRebuildDialogOpen(true); + }; + + const onOpenRescueDialog = () => { + setRescueDialogOpen(true); + }; + + const onOpenMigrateDialog = () => { + setMigrateDialogOpen(true); + }; + + const handlers = { + onOpenPowerDialog, + onOpenDeleteDialog, + onOpenResizeDialog, + onOpenRebuildDialog, + onOpenRescueDialog, + onOpenMigrateDialog, + }; + return ( <> <HostMaintenance linodeStatus={linodeStatus} /> @@ -353,44 +241,38 @@ const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { linodeConfigs={linodeConfigs} backups={linode.backups} openTagDrawer={openTagDrawer} - openDialog={openDialog} - openPowerActionDialog={openPowerActionDialog} - openNotificationMenu={notificationContext.openMenu} + handlers={handlers} /> - <PowerDialogOrDrawer - isOpen={powerDialog.open} - action={powerDialog.bootAction} - linodeID={powerDialog.linodeID} - linodeLabel={powerDialog.linodeLabel} - close={closeDialogs} - linodeConfigs={powerDialog.linodeConfigs} + <PowerActionsDialog + isOpen={powerDialogOpen} + action={powerAction ?? 'Reboot'} + linodeId={matchedLinodeId} + onClose={closeDialogs} /> - <DeleteDialog - open={deleteDialog.open} + <DeleteLinodeDialog + open={deleteDialogOpen} onClose={closeDialogs} - linodeID={deleteDialog.linodeID} - linodeLabel={deleteDialog.linodeLabel} - handleDelete={handleDeleteLinode} + linodeId={matchedLinodeId} /> <LinodeResize - open={resizeDialog.open} + open={resizeDialogOpen} onClose={closeDialogs} - linodeId={resizeDialog.linodeID} + linodeId={matchedLinodeId} /> <LinodeRebuildDialog - open={rebuildDialog.open} + open={rebuildDialogOpen} onClose={closeDialogs} - linodeId={rebuildDialog.linodeID} + linodeId={matchedLinodeId} /> <RescueDialog - open={rescueDialog.open} + open={rescueDialogOpen} onClose={closeDialogs} - linodeId={rescueDialog.linodeID} + linodeId={matchedLinodeId} /> <MigrateLinode - open={migrateDialog.open} + open={migrateDialogOpen} onClose={closeDialogs} - linodeID={migrateDialog.linodeID} + linodeId={matchedLinodeId} /> <TagDrawer entityLabel={linode.label} @@ -399,15 +281,9 @@ const LinodeDetailHeader: React.FC<CombinedProps> = (props) => { updateTags={(tags) => updateTags(linode.id, tags)} onClose={closeTagDrawer} /> - <EnableBackupDialog - linodeId={backupsDialog.linodeID} - open={backupsDialog.open} - onClose={closeDialogs} - /> - <UpgradeVolumesDialog - open={upgradeVolumesDialog.open} - linode={linode} - upgradeableVolumeIds={upgradeableVolumeIds} + <EnableBackupsDialog + linodeId={matchedLinodeId} + open={enableBackupsDialogOpen} onClose={closeDialogs} /> </> diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.tsx index c6c01f8a37b..5e00fffd13d 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.tsx @@ -9,7 +9,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { useDialog } from 'src/hooks/useDialog'; import { capitalize } from 'src/utilities/capitalize'; import { parseAPIDate } from 'src/utilities/date'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx index f706392aeb7..66ee623fdc8 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx @@ -8,7 +8,7 @@ import { ThunkDispatch } from 'redux-thunk'; import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { MBpsIntraDC } from 'src/constants'; import { resetEventsPolling } from 'src/eventsPolling'; import { useSpecificTypes } from 'src/queries/types'; diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailNavigation.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailNavigation.tsx index 69c44502fd1..ef4e1276644 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailNavigation.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodesDetailNavigation.tsx @@ -22,7 +22,7 @@ const LinodeStorage = React.lazy(() => import('./LinodeStorage')); const LinodeConfigurations = React.lazy( () => import('./LinodeAdvanced/LinodeAdvancedConfigurationsPanel') ); -const LinodeBackup = React.lazy(() => import('./LinodeBackup')); +const LinodeBackup = React.lazy(() => import('./LinodeBackup/LinodeBackups')); const LinodeActivity = React.lazy( () => import('./LinodeActivity/LinodeActivity') ); diff --git a/packages/manager/src/features/linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx b/packages/manager/src/features/linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx index 10f91cc51a3..87f3c63119f 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx @@ -4,7 +4,7 @@ import Button from 'src/components/Button'; import ListItem from 'src/components/core/ListItem'; import Typography from 'src/components/core/Typography'; import Drawer from 'src/components/Drawer'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; interface MutateInfo { vcpus: number | null; diff --git a/packages/manager/src/features/linodes/LinodesLanding/AppsSection.tsx b/packages/manager/src/features/linodes/LinodesLanding/AppsSection.tsx index 8ef5c6688d3..502ef1ac22d 100644 --- a/packages/manager/src/features/linodes/LinodesLanding/AppsSection.tsx +++ b/packages/manager/src/features/linodes/LinodesLanding/AppsSection.tsx @@ -28,7 +28,7 @@ const useStyles = makeStyles((theme: Theme) => { display: 'flex', alignItems: 'center', gridColumn: 'span 1', - height: theme.spacing(4.25), + height: theme.spacing(4.75), maxWidth: theme.spacing(20), paddingLeft: theme.spacing(), justifyContent: 'space-between', @@ -70,8 +70,8 @@ const appsLinkData = [ }, { to: - '/linodes/create?type=One-Click&appID=869129&utm_source=marketplace&utm_medium=website&utm_campaign=aaPanel', - text: 'aaPanel', + '/linodes/create?type=One-Click&appID=912262&utm_source=marketplace&utm_medium=website&utm_campaign=Harbor', + text: 'Harbor', }, { to: @@ -80,18 +80,18 @@ const appsLinkData = [ }, { to: - '/linodes/create?type=One-Click&appID=691621&utm_source=marketplace&utm_medium=website&utm_campaign=Cloudron', - text: 'Cloudron', + '/linodes/create?type=One-Click&appID=1068726&utm_source=marketplace&utm_medium=website&utm_campaign=Postgres_Cluster', + text: 'Postgres Cluster', }, { to: - '/linodes/create?type=One-Click&appID=593835&utm_source=marketplace&utm_medium=website&utm_campaign=Plesk', - text: 'Plesk', + '/linodes/create?type=One-Click&appID=985364&utm_source=marketplace&utm_medium=website&utm_campaign=Prometheus_Grafana', + text: 'Prometheus & Grafana', }, { to: - '/linodes/create?type=One-Click&appID=985372&utm_source=marketplace&utm_medium=website&utm_campaign=Joomla', - text: 'Joomla', + '/linodes/create?type=One-Click&appID=1017300&utm_source=marketplace&utm_medium=website&utm_campaign=Kali', + text: 'Kali', }, ]; diff --git a/packages/manager/src/features/linodes/LinodesLanding/CardView.tsx b/packages/manager/src/features/linodes/LinodesLanding/CardView.tsx deleted file mode 100644 index 651d917c455..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/CardView.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from 'react'; -import { CircleProgress } from 'src/components/CircleProgress'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import TagDrawer, { TagDrawerProps } from 'src/components/TagCell/TagDrawer'; -import LinodeEntityDetail from 'src/features/linodes/LinodeEntityDetail'; -import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; -import useLinodeActions from 'src/hooks/useLinodeActions'; -import { useProfile } from 'src/queries/profile'; -import { getVolumesForLinode, useAllVolumesQuery } from 'src/queries/volumes'; -import { RenderLinodesProps } from './DisplayLinodes'; - -const useStyles = makeStyles((theme: Theme) => ({ - '@keyframes pulse': { - to: { - backgroundColor: `hsla(40, 100%, 55%, 0)`, - }, - }, - summaryOuter: { - backgroundColor: theme.bg.bgPaper, - margin: `${theme.spacing()} 0`, - marginBottom: 20, - '&.MuiGrid-item': { - padding: 0, - }, - '& .statusOther:before': { - animation: '$pulse 1.5s ease-in-out infinite', - }, - }, -})); - -const CardView: React.FC<RenderLinodesProps> = (props) => { - const classes = useStyles(); - const notificationContext = React.useContext(_notificationContext); - - const { updateLinode } = useLinodeActions(); - const { data: profile } = useProfile(); - - // When someone uses card view, sadly, this is the best way for us to populate volume counts. - const { data: volumes, isLoading } = useAllVolumesQuery(); - - const [tagDrawer, setTagDrawer] = React.useState<TagDrawerProps>({ - open: false, - tags: [], - label: '', - entityID: 0, - }); - - const closeTagDrawer = () => { - setTagDrawer({ ...tagDrawer, open: false }); - }; - - const openTagDrawer = (label: string, entityID: number, tags: string[]) => { - setTagDrawer({ - open: true, - label, - tags, - entityID, - }); - }; - - const updateTags = (linodeId: number, tags: string[]) => { - return updateLinode({ linodeId, tags }).then((_) => { - setTagDrawer({ ...tagDrawer, tags }); - }); - }; - - const { data, openDialog, openPowerActionDialog } = props; - - if (!profile?.username) { - return null; - } - - if (isLoading) { - return <CircleProgress />; - } - - if (data.length === 0) { - return ( - <Typography style={{ textAlign: 'center' }}> - No items to display. - </Typography> - ); - } - - const getVolumesByLinode = (linodeId: number) => - volumes ? getVolumesForLinode(volumes, linodeId).length : 0; - - return ( - <React.Fragment> - <Grid container className="m0" style={{ width: '100%' }}> - {data.map((linode, idx: number) => ( - <React.Fragment key={`linode-card-${idx}`}> - <Grid xs={12} className={`${classes.summaryOuter} py0`}> - <LinodeEntityDetail - id={linode.id} - linode={linode} - isSummaryView - numVolumes={getVolumesByLinode(linode.id)} - username={profile?.username} - linodeConfigs={linode._configs} - backups={linode.backups} - openTagDrawer={(tags) => - openTagDrawer(linode.label, linode.id, tags) - } - openDialog={openDialog} - openPowerActionDialog={openPowerActionDialog} - openNotificationMenu={notificationContext.openMenu} - /> - </Grid> - </React.Fragment> - ))} - </Grid> - <TagDrawer - entityLabel={tagDrawer.label} - open={tagDrawer.open} - tags={tagDrawer.tags} - updateTags={(tags) => updateTags(tagDrawer.entityID, tags)} - onClose={closeTagDrawer} - /> - </React.Fragment> - ); -}; - -export default CardView; diff --git a/packages/manager/src/features/linodes/LinodesLanding/DeleteDialog.tsx b/packages/manager/src/features/linodes/LinodesLanding/DeleteDialog.tsx deleted file mode 100644 index 0720e1b4334..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/DeleteDialog.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { APIError } from '@linode/api-v4/lib/types'; -import * as React from 'react'; -import { QueryClient, useQueryClient } from 'react-query'; -import { compose } from 'recompose'; -import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; -import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; - -interface Props { - linodeID?: number; - linodeLabel?: string; - open: boolean; - onClose: () => void; - handleDelete: (linodeID: number, queryClient: QueryClient) => Promise<{}>; -} - -type CombinedProps = Props; - -const DeleteLinodeDialog: React.FC<CombinedProps> = (props) => { - const { linodeID, linodeLabel, open, onClose, handleDelete } = props; - - const queryClient = useQueryClient(); - - const [isDeleting, setDeleting] = React.useState<boolean>(false); - const [errors, setErrors] = React.useState<APIError[] | undefined>(undefined); - - React.useEffect(() => { - if (open) { - /** - * reset error and loading states - */ - setErrors(undefined); - setDeleting(false); - } - }, [open]); - - const handleSubmit = () => { - if (!linodeID) { - return setErrors([{ reason: 'Something went wrong.' }]); - } - - setDeleting(true); - - handleDelete(linodeID, queryClient) - .then(() => { - onClose(); - }) - .catch((e) => { - setErrors(e); - setDeleting(false); - }); - }; - - return ( - <TypeToConfirmDialog - title={`Delete ${linodeLabel}?`} - entity={{ type: 'Linode', label: linodeLabel }} - open={open} - loading={isDeleting} - errors={errors} - onClose={onClose} - onClick={handleSubmit} - > - <Notice warning> - <Typography style={{ fontSize: '0.875rem' }}> - <strong>Warning:</strong> Deleting your Linode will result in - permanent data loss. - </Typography> - </Notice> - </TypeToConfirmDialog> - ); -}; - -export default compose<CombinedProps, Props>(React.memo)(DeleteLinodeDialog); diff --git a/packages/manager/src/features/linodes/LinodesLanding/DeleteLinodeDialog.tsx b/packages/manager/src/features/linodes/LinodesLanding/DeleteLinodeDialog.tsx new file mode 100644 index 00000000000..f72c38d3756 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesLanding/DeleteLinodeDialog.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import Typography from 'src/components/core/Typography'; +import { Notice } from 'src/components/Notice/Notice'; +import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; +import { + useDeleteLinodeMutation, + useLinodeQuery, +} from 'src/queries/linodes/linodes'; + +interface Props { + linodeId: number | undefined; + open: boolean; + onClose: () => void; +} + +export const DeleteLinodeDialog = (props: Props) => { + const { linodeId, open, onClose } = props; + + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined && open + ); + + const { mutateAsync, error, isLoading, reset } = useDeleteLinodeMutation( + linodeId ?? -1 + ); + + React.useEffect(() => { + if (open) { + reset(); + } + }, [open]); + + const onDelete = async () => { + await mutateAsync(); + onClose(); + }; + + return ( + <TypeToConfirmDialog + title={`Delete ${linode?.label ?? ''}?`} + entity={{ type: 'Linode', label: linode?.label }} + open={open} + loading={isLoading} + errors={error} + onClose={onClose} + onClick={onDelete} + > + <Notice warning> + <Typography style={{ fontSize: '0.875rem' }}> + <strong>Warning:</strong> Deleting your Linode will result in + permanent data loss. + </Typography> + </Notice> + </TypeToConfirmDialog> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesLanding/DisplayGroupedLinodes.tsx b/packages/manager/src/features/linodes/LinodesLanding/DisplayGroupedLinodes.tsx deleted file mode 100644 index 9210daa2f78..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/DisplayGroupedLinodes.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { Config } from '@linode/api-v4/lib/linodes'; -import { compose } from 'ramda'; -import * as React from 'react'; -import GroupByTag from 'src/assets/icons/group-by-tag.svg'; -import TableView from 'src/assets/icons/table-view.svg'; -import IconButton from 'src/components/core/IconButton'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import TableBody from 'src/components/core/TableBody'; -import TableCell from 'src/components/core/TableCell'; -import Tooltip from 'src/components/core/Tooltip'; -import Typography from 'src/components/core/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { OrderByProps } from 'src/components/OrderBy'; -import Paginate from 'src/components/Paginate'; -import PaginationFooter, { - MIN_PAGE_SIZE, -} from 'src/components/PaginationFooter'; -import { getMinimumPageSizeForNumberOfItems } from 'src/components/PaginationFooter/PaginationFooter'; -import TableRow from 'src/components/TableRow'; -import TableRowEmptyState from 'src/components/TableRowEmptyState'; -import { Action } from 'src/features/linodes/PowerActionsDialogOrDrawer'; -import { DialogType } from 'src/features/linodes/types'; -import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; -import { groupByTags, sortGroups } from 'src/utilities/groupByTags'; -import TableWrapper from './TableWrapper'; -import { LinodeWithMaintenance } from 'src/store/linodes/linodes.helpers'; -import { RenderLinodesProps } from './DisplayLinodes'; - -const useStyles = makeStyles((theme: Theme) => ({ - tagGridRow: { - marginBottom: 20, - }, - tagHeaderRow: { - backgroundColor: theme.bg.main, - height: 'auto', - '& td': { - // This is maintaining the spacing between groups because of how tables handle margin/padding. Adjust with care! - padding: `calc(${theme.spacing(2)} + 4px) 0 2px`, - borderBottom: 'none', - borderTop: 'none', - }, - }, - groupContainer: { - [theme.breakpoints.up('md')]: { - '& $tagHeaderRow > td': { - padding: '10px 0 2px', - borderTop: 'none', - }, - }, - }, - tagHeader: { - marginBottom: 2, - marginLeft: theme.spacing(), - }, - paginationCell: { - padding: 0, - }, - controlHeader: { - marginBottom: 28, - display: 'flex', - justifyContent: 'flex-end', - backgroundColor: theme.bg.tableHeader, - }, - toggleButton: { - color: '#d2d3d4', - padding: 10, - '&:focus': { - // Browser default until we get styling direction for focus states - outline: '1px dotted #999', - }, - }, -})); - -interface Props { - openDialog: (type: DialogType, linodeID: number, linodeLabel: string) => void; - openPowerActionDialog: ( - bootAction: Action, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => void; - display: 'grid' | 'list'; - component: React.ComponentType<RenderLinodesProps>; - data: LinodeWithMaintenance[]; - someLinodesHaveMaintenance: boolean; - toggleLinodeView: () => 'grid' | 'list'; - toggleGroupLinodes: () => boolean; - linodeViewPreference: 'grid' | 'list'; - linodesAreGrouped: boolean; - isVLAN?: boolean; -} - -type CombinedProps = Props & OrderByProps<LinodeWithMaintenance>; - -const DisplayGroupedLinodes: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); - - const { - data, - display, - component: Component, - order, - orderBy, - handleOrderChange, - toggleLinodeView, - toggleGroupLinodes, - linodeViewPreference, - linodesAreGrouped, - isVLAN, - ...rest - } = props; - - const dataLength = data.length; - - const orderedGroupedLinodes = compose(sortGroups, groupByTags)(data); - const tableWrapperProps = { - handleOrderChange, - order, - orderBy, - someLinodesHaveMaintenance: props.someLinodesHaveMaintenance, - dataLength, - isVLAN, - }; - - const { infinitePageSize, setInfinitePageSize } = useInfinitePageSize(); - const numberOfLinodesWithMaintenance = data.reduce((acc, thisLinode) => { - if (thisLinode.maintenance) { - acc++; - } - return acc; - }, 0); - - if (display === 'grid') { - return ( - <> - <Grid xs={12} className={'px0'}> - <div className={classes.controlHeader}> - <div id="displayViewDescription" className="visually-hidden"> - Currently in {linodeViewPreference} view - </div> - <Tooltip placement="top" title="List view"> - <IconButton - aria-label="Toggle display" - aria-describedby={'displayViewDescription'} - onClick={toggleLinodeView} - disableRipple - className={classes.toggleButton} - size="large" - > - <TableView /> - </IconButton> - </Tooltip> - - <div id="groupByDescription" className="visually-hidden"> - {linodesAreGrouped - ? 'group by tag is currently enabled' - : 'group by tag is currently disabled'} - </div> - <Tooltip placement="top-end" title="Ungroup by tag"> - <IconButton - aria-label={`Toggle group by tag`} - aria-describedby={'groupByDescription'} - onClick={toggleGroupLinodes} - disableRipple - className={classes.toggleButton} - size="large" - > - <GroupByTag /> - </IconButton> - </Tooltip> - </div> - </Grid> - {orderedGroupedLinodes.length === 0 ? ( - <Typography style={{ textAlign: 'center' }}> - No items to display. - </Typography> - ) : null} - {orderedGroupedLinodes.map(([tag, linodes]) => { - return ( - <div - key={tag} - className={classes.tagGridRow} - data-qa-tag-header={tag} - > - <Grid container> - <Grid xs={12}> - <Typography - variant="h2" - component="h3" - className={classes.tagHeader} - > - {tag} - </Typography> - </Grid> - </Grid> - <Paginate - data={linodes} - // If there are more Linodes with maintenance than the current page size, show the minimum - // page size needed to show ALL Linodes with maintenance. - pageSize={ - numberOfLinodesWithMaintenance > infinitePageSize - ? getMinimumPageSizeForNumberOfItems( - numberOfLinodesWithMaintenance - ) - : infinitePageSize - } - pageSizeSetter={setInfinitePageSize} - > - {({ - data: paginatedData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - count, - }) => { - const finalProps = { - ...rest, - data: paginatedData, - pageSize, - page, - handlePageSizeChange, - handlePageChange, - handleOrderChange, - order, - orderBy, - isVLAN, - count, - }; - return ( - <React.Fragment> - <Component {...finalProps} /> - <Grid xs={12}> - <PaginationFooter - count={count} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - pageSize={pageSize} - page={page} - eventCategory={'linodes landing'} - showAll - /> - </Grid> - </React.Fragment> - ); - }} - </Paginate> - </div> - ); - })} - </> - ); - } - - if (display === 'list') { - return ( - <TableWrapper - {...tableWrapperProps} - linodeViewPreference="list" - linodesAreGrouped={true} - toggleLinodeView={toggleLinodeView} - toggleGroupLinodes={toggleGroupLinodes} - > - {orderedGroupedLinodes.length === 0 ? ( - <TableBody> - <TableRowEmptyState colSpan={12} /> - </TableBody> - ) : null} - {orderedGroupedLinodes.map(([tag, linodes]) => { - return ( - <React.Fragment key={tag}> - <Paginate - data={linodes} - pageSize={infinitePageSize} - pageSizeSetter={setInfinitePageSize} - > - {({ - data: paginatedData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - count, - }) => { - const finalProps = { - ...rest, - data: paginatedData, - pageSize, - page, - handlePageSizeChange, - handlePageChange, - handleOrderChange, - order, - orderBy, - isVLAN, - count, - }; - return ( - <TableBody - className={classes.groupContainer} - data-qa-tag-header={tag} - > - <TableRow className={classes.tagHeaderRow}> - <TableCell colSpan={7}> - <Typography - variant="h2" - component="h3" - className={classes.tagHeader} - > - {tag} - </Typography> - </TableCell> - </TableRow> - <Component {...finalProps} /> - {count > MIN_PAGE_SIZE && ( - <TableRow> - <TableCell - colSpan={7} - className={classes.paginationCell} - > - <PaginationFooter - count={count} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - pageSize={pageSize} - page={page} - eventCategory={'linodes landing'} - // Disabling showAll as it is impacting page performance. - showAll={false} - /> - </TableCell> - </TableRow> - )} - </TableBody> - ); - }} - </Paginate> - </React.Fragment> - ); - })} - </TableWrapper> - ); - } - - return null; -}; - -export default DisplayGroupedLinodes; diff --git a/packages/manager/src/features/linodes/LinodesLanding/DisplayLinodes.tsx b/packages/manager/src/features/linodes/LinodesLanding/DisplayLinodes.tsx deleted file mode 100644 index ec1878ef5cb..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/DisplayLinodes.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { Config } from '@linode/api-v4/lib/linodes'; -import * as React from 'react'; -import { useLocation } from 'react-router-dom'; -import TableBody from 'src/components/core/TableBody'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Grid from '@mui/material/Unstable_Grid2'; -import { OrderByProps } from 'src/components/OrderBy'; -import Paginate, { PaginationProps } from 'src/components/Paginate'; -import PaginationFooter from 'src/components/PaginationFooter'; -import { getMinimumPageSizeForNumberOfItems } from 'src/components/PaginationFooter/PaginationFooter'; -import { Action } from 'src/features/linodes/PowerActionsDialogOrDrawer'; -import { DialogType } from 'src/features/linodes/types'; -import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; -import TableWrapper from './TableWrapper'; -import IconButton from 'src/components/core/IconButton'; -import Tooltip from 'src/components/core/Tooltip'; -import GroupByTag from 'src/assets/icons/group-by-tag.svg'; -import TableView from 'src/assets/icons/table-view.svg'; -import { getParamsFromUrl } from 'src/utilities/queryParams'; -import { LinodeWithMaintenanceAndDisplayStatus } from 'src/store/linodes/types'; -import { ExtendedLinode } from 'src/hooks/useExtendedLinode'; - -const useStyles = makeStyles((theme: Theme) => ({ - controlHeader: { - marginBottom: 28, - display: 'flex', - justifyContent: 'flex-end', - backgroundColor: theme.bg.tableHeader, - }, - toggleButton: { - color: '#d2d3d4', - padding: 10, - '&:focus': { - // Browser default until we get styling direction for focus states - outline: '1px dotted #999', - }, - }, - table: { - // tableLayout: 'fixed' - }, -})); - -export interface RenderLinodesProps extends PaginationProps { - data: Props['data']; - showHead?: boolean; - openDialog: Props['openDialog']; - openPowerActionDialog: Props['openPowerActionDialog']; -} - -interface Props { - openDialog: (type: DialogType, linodeID: number, linodeLabel: string) => void; - openPowerActionDialog: ( - bootAction: Action, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => void; - count: number; - display: 'grid' | 'list'; - component: React.ComponentType<RenderLinodesProps>; - data: (ExtendedLinode & LinodeWithMaintenanceAndDisplayStatus)[]; - someLinodesHaveMaintenance: boolean; - toggleLinodeView: () => 'grid' | 'list'; - toggleGroupLinodes: () => boolean; - linodeViewPreference: 'grid' | 'list'; - linodesAreGrouped: boolean; - updatePageUrl: (page: number) => void; -} - -type CombinedProps = Props & - OrderByProps<LinodeWithMaintenanceAndDisplayStatus>; - -const DisplayLinodes = (props: CombinedProps) => { - const classes = useStyles(); - const { - count, - data, - display, - component: Component, - order, - orderBy, - handleOrderChange, - toggleLinodeView, - toggleGroupLinodes, - linodeViewPreference, - linodesAreGrouped, - updatePageUrl, - ...rest - } = props; - - const { infinitePageSize, setInfinitePageSize } = useInfinitePageSize(); - const numberOfLinodesWithMaintenance = React.useMemo(() => { - return data.reduce((acc, thisLinode) => { - if (thisLinode.maintenance) { - acc++; - } - return acc; - }, 0); - }, [JSON.stringify(data)]); - const pageSize = - numberOfLinodesWithMaintenance > infinitePageSize - ? getMinimumPageSizeForNumberOfItems(numberOfLinodesWithMaintenance) - : infinitePageSize; - const maxPageNumber = Math.ceil(count / pageSize); - - const { search } = useLocation(); - const params = getParamsFromUrl(search); - const queryPage = Math.min(Number(params.page), maxPageNumber) || 1; - - return ( - <Paginate - data={data} - page={queryPage} - // If there are more Linodes with maintenance than the current page size, show the minimum - // page size needed to show ALL Linodes with maintenance. - pageSize={pageSize} - pageSizeSetter={setInfinitePageSize} - updatePageUrl={updatePageUrl} - > - {({ - data: paginatedData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => { - const componentProps = { - ...rest, - count, - data: paginatedData, - pageSize, - page, - handlePageSizeChange, - handlePageChange, - }; - const tableWrapperProps = { - handleOrderChange, - order, - orderBy, - someLinodesHaveMaintenance: props.someLinodesHaveMaintenance, - dataLength: paginatedData.length, - }; - return ( - <React.Fragment> - {display === 'list' && ( - <TableWrapper - {...tableWrapperProps} - linodeViewPreference={linodeViewPreference} - linodesAreGrouped={linodesAreGrouped} - toggleLinodeView={toggleLinodeView} - toggleGroupLinodes={toggleGroupLinodes} - tableProps={{ tableClass: classes.table }} - > - <TableBody> - <Component showHead {...componentProps} /> - </TableBody> - </TableWrapper> - )} - {display === 'grid' && ( - <> - <Grid xs={12} className={'px0'}> - <div className={classes.controlHeader}> - <div - id="displayViewDescription" - className="visually-hidden" - > - Currently in {linodeViewPreference} view - </div> - <Tooltip placement="top" title="List view"> - <IconButton - aria-label="Toggle display" - aria-describedby={'displayViewDescription'} - onClick={toggleLinodeView} - disableRipple - className={classes.toggleButton} - size="large" - > - <TableView /> - </IconButton> - </Tooltip> - - <div id="groupByDescription" className="visually-hidden"> - {linodesAreGrouped - ? 'group by tag is currently enabled' - : 'group by tag is currently disabled'} - </div> - <Tooltip placement="top-end" title="Group by tag"> - <IconButton - aria-label={`Toggle group by tag`} - aria-describedby={'groupByDescription'} - onClick={toggleGroupLinodes} - disableRipple - className={classes.toggleButton} - size="large" - > - <GroupByTag /> - </IconButton> - </Tooltip> - </div> - </Grid> - <Component showHead {...componentProps} /> - </> - )} - <Grid xs={12}> - { - <PaginationFooter - count={data.length} - handlePageChange={handlePageChange} - handleSizeChange={handlePageSizeChange} - pageSize={pageSize} - page={queryPage} - eventCategory={'linodes landing'} - // Disabling showAll as it is impacting page performance. - showAll={false} - /> - } - </Grid> - </React.Fragment> - ); - }} - </Paginate> - ); -}; - -export default React.memo(DisplayLinodes); diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinksSection.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinksSection.tsx deleted file mode 100644 index 00743b5d2e0..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinksSection.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; - -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; - -const useStyles = makeStyles((theme: Theme) => ({ - categoryWrapper: { - display: 'grid', - gridAutoColumns: '1fr', - gridAutoFlow: 'column', - columnGap: theme.spacing(5), - justifyItems: 'center', - [theme.breakpoints.down('md')]: { - gridAutoFlow: 'row', - rowGap: theme.spacing(8), - justifyItems: 'start', - }, - }, -})); - -interface Props { - children: JSX.Element[] | JSX.Element; -} - -const LinksSection = (props: Props) => { - const classes = useStyles(); - return <div className={classes.categoryWrapper}>{props.children}</div>; -}; - -export default LinksSection; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinksSubSection.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinksSubSection.tsx deleted file mode 100644 index be423d2ea46..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinksSubSection.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from 'react'; -import Typography from 'src/components/core/Typography'; - -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; - -const useStyles = makeStyles((theme: Theme) => ({ - linksSubSection: { - display: 'grid', - gridTemplateRows: `22px minmax(${theme.spacing(3)}, 100%) 1.125rem`, - rowGap: theme.spacing(2), - width: '100%', - '& > h2': { - color: theme.palette.text.primary, - }, - '& > h2 > svg': { - color: theme.palette.primary.main, - marginRight: theme.spacing(), - height: '1.125rem', - width: '1.125rem', - }, - '& > a': { - fontSize: '0.875rem', - fontWeight: 700, - display: 'flex', - color: theme.textColors.linkActiveLight, - '& > svg': { - color: theme.textColors.linkActiveLight, - marginLeft: theme.spacing(), - height: 12, - width: 12, - }, - }, - '& li': { - paddingLeft: 0, - paddingRight: 0, - '& > a': { - fontSize: '0.875rem', - color: theme.textColors.linkActiveLight, - '& > svg': { - color: theme.textColors.linkActiveLight, - marginLeft: theme.spacing(), - height: 12, - width: 12, - }, - }, - }, - }, - internalLink: { - alignItems: 'center', - }, - externalLink: { - alignItems: 'baseline', - }, -})); - -interface Props { - children?: JSX.Element[] | JSX.Element; - title: string; - icon: JSX.Element; - MoreLink: (props: { className: any }) => JSX.Element; - external?: boolean; -} - -const LinksSubSection = (props: Props) => { - const { title, icon, children, MoreLink, external } = props; - const classes = useStyles(); - const linkClassName = external ? classes.externalLink : classes.internalLink; - - return ( - <div className={classes.linksSubSection}> - <Typography variant="h2"> - {icon} {title} - </Typography> - {children} - <MoreLink className={linkClassName} /> - </div> - ); -}; - -export default LinksSubSection; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.test.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.test.tsx index ef1e537e689..dd16c42992a 100644 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.test.tsx +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.test.tsx @@ -17,8 +17,12 @@ const props: Props = { linodeLabel: 'test-linode', linodeStatus: 'running', linodeType: extendedTypes[0], - openDialog: jest.fn(), - openPowerActionDialog: jest.fn(), + onOpenPowerDialog: jest.fn(), + onOpenDeleteDialog: jest.fn(), + onOpenResizeDialog: jest.fn(), + onOpenRebuildDialog: jest.fn(), + onOpenRescueDialog: jest.fn(), + onOpenMigrateDialog: jest.fn(), }; describe('LinodeActionMenu', () => { diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.tsx index 1315aa747c0..133999269f8 100644 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.tsx +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodeActionMenu.tsx @@ -1,18 +1,11 @@ -import { - Config, - getLinodeConfigs, - LinodeBackups, -} from '@linode/api-v4/lib/linodes'; +import { LinodeBackups, LinodeType } from '@linode/api-v4/lib/linodes'; import { Region } from '@linode/api-v4/lib/regions'; -import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; import ActionMenu, { Action } from 'src/components/ActionMenu'; import { useTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; -import { Action as BootAction } from 'src/features/linodes/PowerActionsDialogOrDrawer'; -import { DialogType } from 'src/features/linodes/types'; import { lishLaunch } from 'src/features/Lish/lishUtils'; import { useSpecificTypes } from 'src/queries/types'; import { useGrants } from 'src/queries/profile'; @@ -24,25 +17,15 @@ import { sendMigrationNavigationEvent, } from 'src/utilities/ga'; import { ExtendedType, extendType } from 'src/utilities/extendType'; +import { LinodeHandlers } from './LinodesLanding'; -export interface Props { +export interface Props extends LinodeHandlers { linodeId: number; linodeLabel: string; linodeRegion: string; - linodeType?: ExtendedType; + linodeType?: LinodeType; linodeBackups: LinodeBackups; linodeStatus: string; - openDialog: ( - type: DialogType, - linodeID: number, - linodeLabel?: string - ) => void; - openPowerActionDialog: ( - bootAction: BootAction, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => void; inListView?: boolean; } @@ -76,13 +59,10 @@ export const buildQueryStringForLinodeClone = ( export const LinodeActionMenu: React.FC<Props> = (props) => { const { linodeId, - linodeLabel, linodeRegion, linodeStatus, linodeType, - openPowerActionDialog, inListView, - openDialog, } = props; const theme = useTheme<Theme>(); @@ -98,43 +78,14 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { const { data: grants } = useGrants(); const readOnly = getPermissionsForLinode(grants, linodeId) === 'read_only'; - - const [configs, setConfigs] = React.useState<Config[]>([]); - const [configsError, setConfigsError] = React.useState< - APIError[] | undefined - >(undefined); - const [ - hasMadeConfigsRequest, - setHasMadeConfigsRequest, - ] = React.useState<boolean>(false); - const toggleOpenActionMenu = () => { - if (!isBareMetalInstance) { - // Bare metal Linodes don't have configs that can be retrieved - getLinodeConfigs(props.linodeId) - .then((configs) => { - setConfigs(configs.data); - setConfigsError(undefined); - setHasMadeConfigsRequest(true); - }) - .catch((err) => { - setConfigsError(err); - setHasMadeConfigsRequest(true); - }); - } - sendLinodeActionEvent(); }; const handlePowerAction = () => { const action = linodeStatus === 'running' ? 'Power Off' : 'Power On'; sendLinodeActionMenuItemEvent(`${action} Linode`); - openPowerActionDialog( - `${action}` as BootAction, - linodeId, - linodeLabel, - linodeStatus === 'running' ? configs : [] - ); + props.onOpenPowerDialog(action); }; const hasHostMaintenance = linodeStatus === 'stopped'; @@ -167,19 +118,11 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { inListView || matchesSmDown ? { title: 'Reboot', - disabled: - linodeStatus !== 'running' || - (!hasMadeConfigsRequest && matchesSmDown) || - readOnly || - Boolean(configsError?.[0]?.reason), - tooltip: readOnly - ? noPermissionTooltipText - : configsError - ? 'Could not load configs for this Linode.' - : undefined, + disabled: linodeStatus !== 'running' || matchesSmDown || readOnly, + tooltip: readOnly ? noPermissionTooltipText : undefined, onClick: () => { sendLinodeActionMenuItemEvent('Reboot Linode'); - openPowerActionDialog('Reboot', linodeId, linodeLabel, configs); + props.onOpenPowerDialog('Reboot'); }, ...readOnlyProps, } @@ -219,7 +162,7 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { : { title: 'Resize', onClick: () => { - openDialog('resize', linodeId); + props.onOpenResizeDialog(); }, ...maintenanceProps, ...readOnlyProps, @@ -228,7 +171,7 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { title: 'Rebuild', onClick: () => { sendLinodeActionMenuItemEvent('Navigate to Rebuild Page'); - openDialog('rebuild', linodeId); + props.onOpenRebuildDialog(); }, ...maintenanceProps, ...readOnlyProps, @@ -237,7 +180,7 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { title: 'Rescue', onClick: () => { sendLinodeActionMenuItemEvent('Navigate to Rescue Page'); - openDialog('rescue', linodeId); + props.onOpenRescueDialog(); }, ...maintenanceProps, ...readOnlyProps, @@ -249,7 +192,7 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { onClick: () => { sendMigrationNavigationEvent('/linodes'); sendLinodeActionMenuItemEvent('Migrate'); - openDialog('migrate', linodeId); + props.onOpenMigrateDialog(); }, ...readOnlyProps, }, @@ -257,8 +200,7 @@ export const LinodeActionMenu: React.FC<Props> = (props) => { title: 'Delete', onClick: () => { sendLinodeActionMenuItemEvent('Delete Linode'); - - openDialog('delete', linodeId, linodeLabel); + props.onOpenDeleteDialog(); }, ...readOnlyProps, }, diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.style.ts b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.style.ts index 2256b340378..ab417b37eef 100644 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.style.ts +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.style.ts @@ -1,120 +1,76 @@ -import { createStyles, withStyles, WithStyles, WithTheme } from '@mui/styles'; import { Theme } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; -export type StyleProps = WithStyles<ClassNames> & WithTheme; - -type ClassNames = - | 'bodyRow' - | 'statusCell' - | 'statusCellMaintenance' - | 'statusLink' - | 'ipCell' - | 'ipCellWrapper' - | 'planCell' - | 'progressDisplay' - | 'regionCell' - | 'tagCell' - | 'maintenanceOuter' - | 'vlan_Status' - | 'maintenanceTooltip'; - -const styles = (theme: Theme) => - createStyles({ - bodyRow: { - height: 'auto', - '&:hover': { - backgroundColor: theme.bg.lightBlue1, - '& [data-qa-copy-ip] button > svg': { - opacity: 1, - }, - }, - '& [data-qa-copy-ip] button:focus > svg': { +export const useStyles = makeStyles((theme: Theme) => ({ + bodyRow: { + height: 'auto', + '&:hover': { + backgroundColor: theme.bg.lightBlue1, + '& [data-qa-copy-ip] button > svg': { opacity: 1, }, }, - progressDisplay: { - display: 'inline-block', + '& [data-qa-copy-ip] button:focus > svg': { + opacity: 1, }, - statusCell: { - width: '17%', + }, + progressDisplay: { + display: 'inline-block', + }, + statusCellMaintenance: { + [theme.breakpoints.up('md')]: { + width: '20%', }, - statusCellMaintenance: { + '& .data': { + display: 'flex', + alignItems: 'center', + lineHeight: 1.2, + marginRight: -12, [theme.breakpoints.up('md')]: { - width: '20%', - }, - '& .data': { - display: 'flex', - alignItems: 'center', - lineHeight: 1.2, - marginRight: -12, - [theme.breakpoints.up('md')]: { - minWidth: 200, - }, - }, - '& button': { - color: theme.textColors.linkActiveLight, - padding: '0 6px', - position: 'relative', + minWidth: 200, }, }, - statusLink: { - backgroundColor: 'transparent', - border: 'none', + '& button': { color: theme.textColors.linkActiveLight, - cursor: 'pointer', - padding: 0, - '& p': { - color: theme.textColors.linkActiveLight, - fontFamily: theme.font.bold, - }, - }, - planCell: { - whiteSpace: 'nowrap', - }, - ipCell: { - paddingRight: 0, - width: '14%', - }, - ipCellWrapper: { - display: 'inline-flex', - flexDirection: 'column', - - '& *': { - fontSize: '.875rem', - paddingTop: 0, - paddingBottom: 0, - }, - '& button:hover': { - backgroundColor: 'transparent', - }, - '& [data-qa-copy-ip] button > svg': { - opacity: 0, - }, - '& svg': { - marginTop: 2, - '&:hover': { - color: theme.palette.primary.main, - }, - }, + padding: '0 6px', + position: 'relative', }, - regionCell: { - width: '14%', + }, + statusLink: { + backgroundColor: 'transparent', + border: 'none', + color: theme.textColors.linkActiveLight, + cursor: 'pointer', + padding: 0, + '& p': { + color: theme.textColors.linkActiveLight, + fontFamily: theme.font.bold, }, - tagCell: { - borderRight: 'none', + }, + ipCellWrapper: { + '& *': { + fontSize: '.875rem', + paddingTop: 0, + paddingBottom: 0, }, - maintenanceOuter: { - display: 'flex', - alignItems: 'center', + '& button:hover': { + backgroundColor: 'transparent', }, - - // The "Status" cell in the VLAN Detail context. - vlan_Status: { - width: '14%', + '& [data-qa-copy-ip] button > svg': { + opacity: 0, }, - maintenanceTooltip: { - maxWidth: 300, + '& svg': { + marginTop: 2, + '&:hover': { + color: theme.palette.primary.main, + }, }, - }); - -export default withStyles(styles, { withTheme: true }); + }, + maintenanceOuter: { + display: 'flex', + alignItems: 'center', + }, + maintenanceTooltip: { + maxWidth: 300, + }, +})); diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.tsx index 9d32ebd2586..c438785a001 100644 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.tsx +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRow.tsx @@ -1,114 +1,75 @@ import { Notification } from '@linode/api-v4/lib/account'; -import { - Config, - LinodeBackups, - LinodeStatus, -} from '@linode/api-v4/lib/linodes'; +import { Linode } from '@linode/api-v4/lib/linodes'; import classNames from 'classnames'; import * as React from 'react'; import { Link } from 'react-router-dom'; -import { compose } from 'recompose'; import Flag from 'src/assets/icons/flag.svg'; import Hidden from 'src/components/core/Hidden'; import Tooltip from 'src/components/core/Tooltip'; import Typography from 'src/components/core/Typography'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import { Action } from 'src/features/linodes/PowerActionsDialogOrDrawer'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; import { getProgressOrDefault, linodeInTransition, transitionText, } from 'src/features/linodes/transitions'; -import { DialogType } from 'src/features/linodes/types'; -import { ExtendedType } from 'src/utilities/extendType'; -import { capitalize, capitalizeAllWords } from 'src/utilities/capitalize'; +import { capitalizeAllWords } from 'src/utilities/capitalize'; import IPAddress from '../IPAddress'; import LinodeActionMenu from '../LinodeActionMenu'; import RegionIndicator from '../RegionIndicator'; import { parseMaintenanceStartTime } from '../utils'; -import withRecentEvent, { WithRecentEvent } from '../withRecentEvent'; -import styled, { StyleProps } from './LinodeRow.style'; -import LinodeRowBackupCell from './LinodeRowBackupCell'; -import LinodeRowHeadCell from './LinodeRowHeadCell'; import { SxProps } from '@mui/system'; import { useNotificationsQuery } from 'src/queries/accountNotifications'; +import { LinodeHandlers } from '../LinodesLanding'; +import { useTypeQuery } from 'src/queries/types'; +import useEvents from 'src/hooks/useEvents'; +import { useStyles } from './LinodeRow.style'; +import { useAllAccountMaintenanceQuery } from 'src/queries/accountMaintenance'; +import { useNotificationContext } from 'src/features/NotificationCenter/NotificationContext'; +import { BackupStatus } from 'src/components/BackupStatus/BackupStatus'; -interface Props { - backups: LinodeBackups; - id: number; - image: string | null; - ipv4: string[]; - ipv6: string; - label: string; - maintenanceStartTime?: string | null; - region: string; - disk: number; - memory: number; - vcpus: number; - status: LinodeStatus; - displayStatus: string; - type?: ExtendedType; - tags: string[]; - mostRecentBackup: string | null; - openDialog: ( - type: DialogType, - linodeID: number, - linodeLabel?: string - ) => void; - openPowerActionDialog: ( - bootAction: Action, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => void; - openNotificationMenu: () => void; -} +type Props = Linode & { handlers: LinodeHandlers }; -export type CombinedProps = Props & WithRecentEvent & StyleProps; +export const LinodeRow = (props: Props) => { + const classes = useStyles(); + const { backups, id, ipv4, label, region, status, type, handlers } = props; -export const LinodeRow: React.FC<CombinedProps> = (props) => { - const { - // linode props - backups, - id, - ipv4, - ipv6, - maintenanceStartTime, - label, - region, - status, - displayStatus, - mostRecentBackup, - disk, - vcpus, - memory, - type, - tags, - image, - // other props - classes, - openDialog, - openPowerActionDialog, - openNotificationMenu, - recentEvent, - } = props; + const { openMenu } = useNotificationContext(); const { data: notifications } = useNotificationsQuery(); + const { data: accountMaintenanceData } = useAllAccountMaintenanceQuery( + {}, + { status: { '+or': ['pending, started'] } } + ); + + const maintenance = accountMaintenanceData?.find( + (m) => m.entity.id === id && m.entity.type === 'linode' + ); + const linodeNotifications = notifications?.filter( (notification) => notification.entity?.type === 'linode' && notification.entity?.id === id ) ?? []; - const isBareMetalInstance = type?.class === 'metal'; + const { data: linodeType } = useTypeQuery(type ?? '', type !== null); + + const { events } = useEvents(); + + const recentEvent = events.find( + (e) => e.entity?.id === id && e.entity.type === 'linode' + ); + + const isBareMetalInstance = linodeType?.class === 'metal'; const loading = linodeInTransition(status, recentEvent); + const parsedMaintenanceStartTime = parseMaintenanceStartTime( - maintenanceStartTime + maintenance?.when ); const MaintenanceText = () => { @@ -130,28 +91,6 @@ export const LinodeRow: React.FC<CombinedProps> = (props) => { ? 'inactive' : 'other'; - const headCell = ( - <LinodeRowHeadCell - loading={loading} - recentEvent={recentEvent} - backups={backups} - id={id} - ipv4={ipv4} - ipv6={ipv6} - label={label} - region={region} - status={status} - displayStatus={displayStatus} - tags={tags} - mostRecentBackup={mostRecentBackup} - disk={disk} - vcpus={vcpus} - memory={memory} - image={image} - maintenance={maintenanceStartTime} - /> - ); - return ( <TableRow key={id} @@ -160,23 +99,23 @@ export const LinodeRow: React.FC<CombinedProps> = (props) => { data-qa-linode={label} ariaLabel={label} > - {headCell} + <TableCell> + <Link to={`/linodes/${id}`} tabIndex={0}> + {label} + </Link> + </TableCell> <TableCell className={classNames({ - [classes.statusCell]: true, - [classes.statusCellMaintenance]: maintenanceStartTime, + [classes.statusCellMaintenance]: Boolean(maintenance), })} statusCell data-qa-status > - {!maintenanceStartTime ? ( + {!Boolean(maintenance) ? ( loading ? ( <> <StatusIcon status={iconStatus} /> - <button - className={classes.statusLink} - onClick={() => openNotificationMenu()} - > + <button className={classes.statusLink} onClick={() => openMenu()}> <ProgressDisplay className={classes.progressDisplay} progress={getProgressOrDefault(recentEvent)} @@ -187,9 +126,7 @@ export const LinodeRow: React.FC<CombinedProps> = (props) => { ) : ( <> <StatusIcon status={iconStatus} /> - {displayStatus.includes('_') - ? capitalizeAllWords(displayStatus.replace('_', ' ')) - : capitalize(displayStatus)} + {capitalizeAllWords(status.replace('_', ' '))} </> ) ) : ( @@ -205,34 +142,32 @@ export const LinodeRow: React.FC<CombinedProps> = (props) => { </div> )} </TableCell> - <Hidden smDown> - <TableCell className={classes.planCell} data-qa-ips> - <div className={classes.planCell}>{type?.formattedLabel}</div> - </TableCell> - <TableCell className={classes.ipCell} data-qa-ips> - <div className={classes.ipCellWrapper}> - <IPAddress ips={ipv4} /> - </div> + <TableCell noWrap>{linodeType?.label ?? type}</TableCell> + <TableCell data-qa-ips className={classes.ipCellWrapper}> + <IPAddress ips={ipv4} /> </TableCell> <Hidden lgDown> - <TableCell className={classes.regionCell} data-qa-region> + <TableCell data-qa-region> <RegionIndicator region={region} /> </TableCell> </Hidden> </Hidden> <Hidden lgDown> - <LinodeRowBackupCell - linodeId={id} - backupsEnabled={backups.enabled || false} - mostRecentBackup={mostRecentBackup || ''} - isBareMetalInstance={isBareMetalInstance} - /> + <TableCell> + <BackupStatus + linodeId={id} + backupsEnabled={backups.enabled} + mostRecentBackup={backups.last_successful} + isBareMetalInstance={isBareMetalInstance} + /> + </TableCell> </Hidden> - <TableCell actionCell data-qa-notifications> <RenderFlag - mutationAvailable={type?.isDeprecated ?? false} + mutationAvailable={ + linodeType !== undefined && linodeType?.successor !== null + } linodeNotifications={linodeNotifications} classes={classes} /> @@ -240,11 +175,10 @@ export const LinodeRow: React.FC<CombinedProps> = (props) => { linodeId={id} linodeLabel={label} linodeRegion={region} - linodeType={type} + linodeType={linodeType} linodeStatus={status} linodeBackups={backups} - openDialog={openDialog} - openPowerActionDialog={openPowerActionDialog} + {...handlers} inListView /> </TableCell> @@ -252,14 +186,6 @@ export const LinodeRow: React.FC<CombinedProps> = (props) => { ); }; -const enhanced = compose<CombinedProps, Props>( - withRecentEvent, - styled, - React.memo -); - -export default enhanced(LinodeRow); - export const RenderFlag: React.FC<{ mutationAvailable: boolean; linodeNotifications: Notification[]; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowBackupCell.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowBackupCell.tsx deleted file mode 100644 index 4a43714276e..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowBackupCell.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import { compose } from 'recompose'; -import { BackupStatus } from 'src/components/BackupStatus/BackupStatus'; -import { makeStyles } from '@mui/styles'; -import TableCell from 'src/components/TableCell'; - -const useStyles = makeStyles(() => ({ - root: { - borderTop: 'none', - padding: '10px 15px', - width: '14%', - }, -})); - -interface Props { - linodeId: number; - mostRecentBackup: string | null; - backupsEnabled: boolean; - isBareMetalInstance: boolean; -} - -type CombinedProps = Props; - -const LinodeRowBackupCell: React.FC<CombinedProps> = (props) => { - const classes = useStyles(); - - const { - linodeId, - backupsEnabled, - mostRecentBackup, - isBareMetalInstance, - } = props; - - return ( - <TableCell className={classes.root}> - <BackupStatus - linodeId={linodeId} - backupsEnabled={backupsEnabled} - mostRecentBackup={mostRecentBackup} - isBareMetalInstance={isBareMetalInstance} - /> - </TableCell> - ); -}; - -export default compose<CombinedProps, Props>(React.memo)(LinodeRowBackupCell); diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowHeadCell.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowHeadCell.tsx deleted file mode 100644 index b210b473572..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowHeadCell.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; -import { LinodeBackups, LinodeStatus } from '@linode/api-v4/lib/linodes'; -import classNames from 'classnames'; -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Grid from '@mui/material/Unstable_Grid2'; -import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; -import Notice from 'src/components/Notice'; -import TableCell from 'src/components/TableCell'; - -const useStyles = makeStyles((theme: Theme) => ({ - link: { - display: 'block', - fontSize: '.875rem', - lineHeight: '1.125rem', - color: theme.textColors.linkActiveLight, - '&:hover, &:focus': { - textDecoration: 'underline', - }, - }, - root: { - '& h3': { - transition: theme.transitions.create(['color']), - }, - [theme.breakpoints.up('lg')]: { - width: '20%', - }, - [theme.breakpoints.up('xl')]: { - width: '35%', - }, - }, - labelStatusWrapper: { - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - whiteSpace: 'nowrap', - '& a': { - overflow: 'hidden', - textOverflow: 'ellipsis', - display: 'inline-block', - }, - }, - maintenanceContainer: {}, - maintenanceNotice: { - paddingTop: 0, - paddingBottom: 0, - '& .noticeText': { - display: 'flex', - alignItems: 'center', - fontSize: '.9rem', - '& br': { - display: 'none', - }, - }, - }, - TooltipIcon: { - paddingTop: 0, - paddingBottom: 0, - }, -})); - -interface Props { - backups: LinodeBackups; - id: number; - image: string | null; - ipv4: string[]; - ipv6: string; - label: string; - region: string; - disk: number; - memory: number; - vcpus: number; - status: LinodeStatus; - displayStatus: string | null; - tags: string[]; - mostRecentBackup: string | null; - width?: number; - loading: boolean; - recentEvent?: Event; - maintenance?: string | null; - isDashboard?: boolean; - isVLAN?: boolean; -} - -type CombinedProps = Props; - -const LinodeRowHeadCell: React.FC<CombinedProps> = (props) => { - const { - // linode props - label, - id, - // other props - width, - maintenance, - isDashboard, - } = props; - - const classes = useStyles(); - - const style = width ? { width: `${width}%` } : {}; - const dateTime = maintenance && maintenance.split(' '); - const MaintenanceText = () => { - return ( - <> - For more information, please see your{' '} - <Link to="/support/tickets?type=open">open support tickets.</Link> - </> - ); - }; - - return ( - <TableCell - className={classNames({ - [classes.root]: true, - })} - style={style} - > - <Grid container wrap="nowrap" alignItems="center"> - {/* Hidden overflow is necessary for the wrapping of the label to work. */} - <Grid style={{ overflow: 'hidden' }}> - <div className={classes.labelStatusWrapper}> - <Link className={classes.link} to={`/linodes/${id}`} tabIndex={0}> - {label} - </Link> - </div> - {maintenance && dateTime && isDashboard && ( - <div className={classes.maintenanceContainer}> - <Notice - warning - spacingTop={8} - spacingBottom={0} - className={classes.maintenanceNotice} - > - Maintenance: <br /> - {dateTime[0]} at {dateTime[1]} - <TooltipIcon - status="help" - text={<MaintenanceText />} - tooltipPosition="top" - interactive - className={classes.TooltipIcon} - /> - </Notice> - </div> - )} - </Grid> - </Grid> - </TableCell> - ); -}; - -export default LinodeRowHeadCell; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowLoading.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowLoading.tsx deleted file mode 100644 index e884a86062b..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/LinodeRowLoading.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; -import { LinodeStatus } from '@linode/api-v4/lib/linodes'; -import * as React from 'react'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import LinearProgress from 'src/components/LinearProgress'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import { - getProgressOrDefault, - linodeInTransition, -} from 'src/features/linodes/transitions'; - -type ClassNames = 'bodyRow' | 'status' | 'bodyCell'; - -const styles = (theme: Theme) => - createStyles({ - bodyRow: { - height: 'auto', - '&:before': { - borderBottomColor: 'transparent', - }, - }, - bodyCell: { - border: 0, - paddingBottom: 0, - }, - status: { - textTransform: 'capitalize', - marginBottom: theme.spacing(1), - color: theme.palette.text.primary, - fontSize: '.92rem', - }, - }); - -interface Props { - linodeId: number; - linodeStatus: LinodeStatus; - linodeRecentEvent?: Event; -} - -type CombinedProps = Props & WithStyles<ClassNames>; - -const LinodeRowLoading: React.FC<CombinedProps> = (props) => { - const { - classes, - linodeId, - linodeStatus, - linodeRecentEvent, - children, - } = props; - - return ( - <TableRow - key={linodeId} - className={classes.bodyRow} - data-qa-linode={linodeId} - data-qa-loading - > - {children} - <TableCell colSpan={5} className={classes.bodyCell}> - {linodeInTransition(linodeStatus, linodeRecentEvent) && ( - <ProgressDisplay progress={getProgressOrDefault(linodeRecentEvent)} /> - )} - </TableCell> - </TableRow> - ); -}; - -const styled = withStyles(styles); - -export default styled(LinodeRowLoading); - -const ProgressDisplay: React.FC<{ - progress: null | number; -}> = (props) => { - const { progress } = props; - - return progress ? ( - <LinearProgress value={progress} /> - ) : ( - <LinearProgress variant="indeterminate" /> - ); -}; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/index.ts b/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/index.ts deleted file mode 100644 index d1e4ae770e3..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodeRow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, ProgressDisplay, RenderFlag } from './LinodeRow'; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.styles.ts b/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.styles.ts deleted file mode 100644 index 5c15ede908c..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.styles.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; - -type ClassNames = 'root' | 'CSVlinkContainer' | 'CSVlink' | 'CSVwrapper'; - -const styles = (theme: Theme) => - createStyles({ - root: { - margin: 0, - width: '100%', - '& > .MuiGrid-item': { - paddingLeft: 0, - paddingRight: 0, - }, - }, - CSVlink: { - color: theme.textColors.tableHeader, - fontSize: '.9rem', - '&:hover': { - textDecoration: 'underline', - }, - [theme.breakpoints.down('md')]: { - marginRight: theme.spacing(), - }, - }, - CSVlinkContainer: { - marginTop: theme.spacing(0.5), - '&.MuiGrid-item': { - paddingRight: 0, - }, - }, - CSVwrapper: { - marginLeft: 0, - marginRight: 0, - width: '100%', - }, - }); - -export type StyleProps = WithStyles<ClassNames>; - -export default withStyles(styles); diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.test.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.test.tsx deleted file mode 100644 index a07a0cad3c6..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Profile, APIError, Grants } from '@linode/api-v4'; -import { render } from '@testing-library/react'; -import * as React from 'react'; -import { UseQueryResult } from 'react-query'; -import { profileFactory } from 'src/factories'; -import { grantsFactory } from 'src/factories/grants'; -import { wrapWithTheme } from 'src/utilities/testHelpers'; -import { reactRouterProps } from 'src/__data__/reactRouterProps'; -import { ListLinodes } from './LinodesLanding'; - -describe('ListLinodes', () => { - const classes = { - root: '', - CSVlinkContainer: '', - CSVlink: '', - CSVwrapper: '', - }; - - it('renders without error', () => { - const { getByText } = render( - wrapWithTheme( - <ListLinodes - someLinodesHaveScheduledMaintenance={true} - linodesData={[]} - classes={classes} - enqueueSnackbar={jest.fn()} - linodesCount={0} - linodesRequestError={undefined} - linodesRequestLoading={false} - closeSnackbar={jest.fn()} - deleteLinode={jest.fn()} - {...reactRouterProps} - linodesInTransition={new Set<number>()} - profile={ - { data: profileFactory.build() } as UseQueryResult< - Profile, - APIError[] - > - } - grants={ - { data: grantsFactory.build() } as UseQueryResult< - Grants, - APIError[] - > - } - /> - ) - ); - - expect(getByText('Create Linode')).toBeInTheDocument(); - }); -}); diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.tsx index 0184b20dc7d..fb2c052cefd 100644 --- a/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.tsx +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodesLanding.tsx @@ -1,553 +1,231 @@ -import { Config } from '@linode/api-v4/lib/linodes/types'; -import { APIError } from '@linode/api-v4/lib/types'; -import { DateTime } from 'luxon'; -import { withSnackbar, WithSnackbarProps } from 'notistack'; import * as React from 'react'; -import { QueryClient } from 'react-query'; -import { connect, MapDispatchToProps } from 'react-redux'; -import { Link, RouteComponentProps, withRouter } from 'react-router-dom'; -import { compose } from 'recompose'; -import { AnyAction } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { CircleProgress } from 'src/components/CircleProgress'; -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import CSVLink from 'src/components/DownloadCSV'; +import Box from 'src/components/core/Box'; import ErrorState from 'src/components/ErrorState'; -import Grid from '@mui/material/Unstable_Grid2'; +import Hidden from 'src/components/core/Hidden'; import LandingHeader from 'src/components/LandingHeader'; +import LinodeResize from '../LinodesDetail/LinodeResize/LinodeResize'; import MaintenanceBanner from 'src/components/MaintenanceBanner'; -import OrderBy from 'src/components/OrderBy'; -import PreferenceToggle, { ToggleProps } from 'src/components/PreferenceToggle'; import TransferDisplay from 'src/components/TransferDisplay'; -import { - withProfile, - WithProfileProps, -} from 'src/containers/profile.container'; -import withFeatureFlagConsumer from 'src/containers/withFeatureFlagConsumer.container'; import { BackupsCTA } from 'src/features/Backups'; -import { DialogType } from 'src/features/linodes/types'; -import { ExtendedLinode } from 'src/hooks/useExtendedLinode'; -import { ApplicationState } from 'src/store'; -import { deleteLinode } from 'src/store/linodes/linode.requests'; -import { MapState } from 'src/store/types'; -import formatDate, { formatDateISO } from 'src/utilities/formatDate'; -import { - sendGroupByTagEnabledEvent, - sendLinodesViewEvent, -} from 'src/utilities/ga'; -import { getLinodeDescription } from 'src/utilities/getLinodeDescription'; -import EnableBackupsDialog from '../LinodesDetail/LinodeBackup/EnableBackupsDialog'; -import LinodeRebuildDialog from '../LinodesDetail/LinodeRebuild/LinodeRebuildDialog'; -import RescueDialog from '../LinodesDetail/LinodeRescue'; -import LinodeResize from '../LinodesDetail/LinodeResize'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { DeleteLinodeDialog } from './DeleteLinodeDialog'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { LinodeRebuildDialog } from '../LinodesDetail/LinodeRebuild/LinodeRebuildDialog'; +import { LinodeRow } from './LinodeRow/LinodeRow'; +import { LinodesLandingCSVDownload } from './LinodesLandingCSVDownload'; +import { LinodesLandingEmptyState } from './LinodesLandingEmptyState'; import { MigrateLinode } from 'src/features/linodes/MigrateLinode'; -import PowerDialogOrDrawer, { Action } from '../PowerActionsDialogOrDrawer'; -import { linodesInTransition as _linodesInTransition } from '../transitions'; -import CardView from './CardView'; -import DeleteDialog from './DeleteDialog'; -import DisplayGroupedLinodes from './DisplayGroupedLinodes'; -import DisplayLinodes from './DisplayLinodes'; -import styled, { StyleProps } from './LinodesLanding.styles'; -import ListLinodesEmptyState from './ListLinodesEmptyState'; -import ListView from './ListView'; -import { ExtendedStatus, statusToPriority } from './utils'; - -interface State { - powerDialogOpen: boolean; - powerDialogAction?: Action; - enableBackupsDialogOpen: boolean; - selectedLinodeConfigs?: Config[]; - selectedLinodeID?: number; - selectedLinodeLabel?: string; - deleteDialogOpen: boolean; - rebuildDialogOpen: boolean; - rescueDialogOpen: boolean; - groupByTag: boolean; - linodeResizeOpen: boolean; - linodeMigrateOpen: boolean; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { PowerActionsDialog, Action } from '../PowerActionsDialogOrDrawer'; +import { RescueDialog } from '../LinodesDetail/LinodeRescue/RescueDialog'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; +import { useHistory } from 'react-router-dom'; +import { useLinodesQuery } from 'src/queries/linodes/linodes'; +import { useOrder } from 'src/hooks/useOrder'; +import { usePagination } from 'src/hooks/usePagination'; + +export interface LinodeHandlers { + onOpenPowerDialog: (action: Action) => void; + onOpenDeleteDialog: () => void; + onOpenResizeDialog: () => void; + onOpenRebuildDialog: () => void; + onOpenRescueDialog: () => void; + onOpenMigrateDialog: () => void; } -interface Params { - view?: string; - groupByTag?: 'true' | 'false'; -} +const preferenceKey = 'linodes'; -type RouteProps = RouteComponentProps<Params>; +export const LinodesLanding = () => { + const history = useHistory(); -export interface Props { - LandingHeader?: React.ReactElement; - someLinodesHaveScheduledMaintenance: boolean; - linodesData: ExtendedLinode[]; - linodesRequestError?: APIError[]; - linodesRequestLoading: boolean; -} - -type CombinedProps = Props & - StateProps & - DispatchProps & - RouteProps & - StyleProps & - WithSnackbarProps & - WithProfileProps; + const [powerAction, setPowerAction] = React.useState<Action>('Reboot'); + const [powerDialogOpen, setPowerDialogOpen] = React.useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); + const [rebuildDialogOpen, setRebuildDialogOpen] = React.useState(false); + const [rescueDialogOpen, setRescueDialogOpen] = React.useState(false); + const [linodeResizeOpen, setResizeDialogOpen] = React.useState(false); + const [linodeMigrateOpen, setMigrateDialogOpen] = React.useState(false); -export class ListLinodes extends React.Component<CombinedProps, State> { - state: State = { - enableBackupsDialogOpen: false, - powerDialogOpen: false, - deleteDialogOpen: false, - rebuildDialogOpen: false, - rescueDialogOpen: false, - groupByTag: false, - linodeResizeOpen: false, - linodeMigrateOpen: false, - }; + const [selectedLinodeId, setSelectedLinodeId] = React.useState< + number | undefined + >(); - /** - * when you change the linode view, instantly update the query params - */ - changeViewInstant = (style: 'grid' | 'list') => { - const { history, location } = this.props; + const pagination = usePagination(1, preferenceKey); - const query = new URLSearchParams(location.search); + const { order, orderBy, handleOrderChange } = useOrder( + { + orderBy: 'label', + order: 'desc', + }, + `${preferenceKey}-order` + ); - query.set('view', style); - - history.push(`?${query.toString()}`); + const filter = { + ['+order_by']: orderBy, + ['+order']: order, }; - updatePageUrl = (page: number) => { - this.props.history.push(`?page=${page}`); + const { data, isLoading, error } = useLinodesQuery( + { + page: pagination.page, + page_size: pagination.pageSize, + }, + filter + ); + + const onOpenPowerDialog = (linodeId: number, action: Action) => { + setPowerDialogOpen(true); + setPowerAction(action); + setSelectedLinodeId(linodeId); }; - /** - * when you change the linode view, send an event to google analytics, debounced. - */ - changeViewDelayed = (style: 'grid' | 'list') => { - sendLinodesViewEvent(eventCategory, style); + const onOpenDeleteDialog = (linodeId: number) => { + setDeleteDialogOpen(true); + setSelectedLinodeId(linodeId); }; - openPowerDialog = ( - bootAction: Action, - linodeID: number, - linodeLabel: string, - linodeConfigs: Config[] - ) => { - this.setState({ - powerDialogOpen: true, - powerDialogAction: bootAction, - selectedLinodeConfigs: linodeConfigs, - selectedLinodeID: linodeID, - selectedLinodeLabel: linodeLabel, - }); + const onOpenResizeDialog = (linodeId: number) => { + setResizeDialogOpen(true); + setSelectedLinodeId(linodeId); }; - openDialog = (type: DialogType, linodeID: number, linodeLabel?: string) => { - switch (type) { - case 'delete': - this.setState({ - deleteDialogOpen: true, - }); - break; - case 'resize': - this.setState({ - linodeResizeOpen: true, - }); - break; - case 'migrate': - this.setState({ - linodeMigrateOpen: true, - }); - break; - case 'rebuild': - this.setState({ - rebuildDialogOpen: true, - }); - break; - case 'rescue': - this.setState({ - rescueDialogOpen: true, - }); - break; - case 'enable_backups': - this.setState({ - enableBackupsDialogOpen: true, - }); - break; - } - this.setState({ - selectedLinodeID: linodeID, - selectedLinodeLabel: linodeLabel, - }); + const onOpenRebuildDialog = (linodeId: number) => { + setRebuildDialogOpen(true); + setSelectedLinodeId(linodeId); }; - closeDialogs = () => { - this.setState({ - powerDialogOpen: false, - deleteDialogOpen: false, - rebuildDialogOpen: false, - rescueDialogOpen: false, - linodeResizeOpen: false, - linodeMigrateOpen: false, - enableBackupsDialogOpen: false, - }); + const onOpenRescueDialog = (linodeId: number) => { + setRescueDialogOpen(true); + setSelectedLinodeId(linodeId); }; - render() { - const { - linodesRequestError, - linodesRequestLoading, - linodesCount, - linodesData, - classes, - linodesInTransition, - profile, - } = this.props; - - const params = new URLSearchParams(this.props.location.search); - - const view = - params.has('view') && ['grid', 'list'].includes(params.get('view')!) - ? (params.get('view') as 'grid' | 'list') - : undefined; - - const componentProps = { - count: linodesCount, - someLinodesHaveMaintenance: this.props - .someLinodesHaveScheduledMaintenance, - openPowerActionDialog: this.openPowerDialog, - openDialog: this.openDialog, - }; - - if (linodesRequestError) { - let errorText: string | JSX.Element = - linodesRequestError?.[0]?.reason ?? 'Error loading Linodes'; - - if ( - typeof errorText === 'string' && - errorText.toLowerCase() === 'this linode has been suspended' - ) { - errorText = ( - <React.Fragment> - One or more of your Linodes is suspended. Please{' '} - <Link to="/support/tickets">open a support ticket </Link> - if you have questions. - </React.Fragment> - ); - } - - return ( - <React.Fragment> - <DocumentTitleSegment segment="Linodes" /> - <ErrorState errorText={errorText} /> - </React.Fragment> - ); - } + const onOpenMigrateDialog = (linodeId: number) => { + setMigrateDialogOpen(true); + setSelectedLinodeId(linodeId); + }; - if (linodesRequestLoading) { - return <CircleProgress />; - } + if (error) { + return <ErrorState errorText={error?.[0].reason} />; + } - if (this.props.linodesCount === 0) { - return <ListLinodesEmptyState />; - } + if (isLoading) { + return <CircleProgress />; + } - const headers = [ - { label: 'Label', key: 'linodeDescription' }, - { label: 'Linode ID', key: 'id' }, - { label: 'Image', key: 'image' }, - { label: 'Region', key: 'region' }, - { label: 'Created', key: 'created' }, - { label: 'Last Backup', key: 'lastBackup' }, - ]; + if (data?.results === 0) { + return <LinodesLandingEmptyState />; + } - return ( - <React.Fragment> - <LinodeResize - open={this.state.linodeResizeOpen} - onClose={this.closeDialogs} - linodeId={this.state.selectedLinodeID} - linodeLabel={ - this.props.linodesData.find( - (thisLinode) => thisLinode.id === this.state.selectedLinodeID - )?.label ?? undefined - } - /> - <MigrateLinode - open={this.state.linodeMigrateOpen} - onClose={this.closeDialogs} - linodeID={this.state.selectedLinodeID ?? -1} - /> - <LinodeRebuildDialog - open={this.state.rebuildDialogOpen} - onClose={this.closeDialogs} - linodeId={this.state.selectedLinodeID ?? -1} - /> - <RescueDialog - open={this.state.rescueDialogOpen} - onClose={this.closeDialogs} - linodeId={this.state.selectedLinodeID ?? -1} - /> - <EnableBackupsDialog - open={this.state.enableBackupsDialogOpen} - onClose={this.closeDialogs} - linodeId={this.state.selectedLinodeID ?? -1} - /> - {this.props.someLinodesHaveScheduledMaintenance && ( - <MaintenanceBanner /> - )} - <DocumentTitleSegment segment="Linodes" /> - <PreferenceToggle<boolean> - localStorageKey="GROUP_LINODES" - preferenceOptions={[false, true]} - preferenceKey="linodes_group_by_tag" - toggleCallbackFnDebounced={sendGroupByAnalytic} - > - {({ - preference: linodesAreGrouped, - togglePreference: toggleGroupLinodes, - }: ToggleProps<boolean>) => { - return ( - <PreferenceToggle<'grid' | 'list'> - preferenceKey="linodes_view_style" - localStorageKey="LINODE_VIEW" - preferenceOptions={['list', 'grid']} - toggleCallbackFnDebounced={this.changeViewDelayed} - toggleCallbackFn={this.changeViewInstant} - /** - * we want the URL query param to take priority here, but if it's - * undefined, just use the user preference - */ - value={view} + return ( + <React.Fragment> + <DocumentTitleSegment segment="Linodes" /> + <MaintenanceBanner /> + <BackupsCTA /> + <LandingHeader + title="Linodes" + entity="Linode" + onButtonClick={() => history.push('/linodes/create')} + docsLink="https://www.linode.com/docs/platform/billing-and-support/linode-beginners-guide/" + /> + <Table> + <TableHead> + <TableRow> + <TableSortCell + active={orderBy === 'label'} + direction={order} + label="label" + handleClick={handleOrderChange} + data-qa-sort-label={order} + > + Label + </TableSortCell> + <TableCell>Status</TableCell> + <Hidden smDown> + <TableCell>Plan</TableCell> + <TableCell>IP Address</TableCell> + </Hidden> + <Hidden lgDown> + <TableSortCell + active={orderBy === 'region'} + direction={order} + label="region" + handleClick={handleOrderChange} > - {({ - preference: linodeViewPreference, - togglePreference: toggleLinodeView, - }: ToggleProps<'list' | 'grid'>) => { - return ( - <React.Fragment> - <React.Fragment> - <BackupsCTA /> - {this.props.LandingHeader ? ( - this.props.LandingHeader - ) : ( - <div> - <LandingHeader - title="Linodes" - entity="Linode" - onButtonClick={() => - this.props.history.push('/linodes/create') - } - docsLink="https://www.linode.com/docs/platform/billing-and-support/linode-beginners-guide/" - /> - </div> - )} - </React.Fragment> - - <OrderBy - preferenceKey={'linodes-landing'} - data={linodesData.map((linode) => { - // Determine the priority of this Linode's status. - // We have to check for "Maintenance" and "Busy" since these are - // not actual Linode statuses (we derive them client-side). - let _status: ExtendedStatus = linode.status; - if (linode.maintenance) { - _status = 'maintenance'; - } else if (linodesInTransition.has(linode.id)) { - _status = 'busy'; - } - - return { - ...linode, - displayStatus: linode.maintenance - ? 'maintenance' - : linode.status, - _statusPriority: statusToPriority(_status), - }; - })} - // If there are Linodes with scheduled maintenance, default to - // sorting by status priority so they are more visible. - order="asc" - orderBy={ - this.props.someLinodesHaveScheduledMaintenance - ? '_statusPriority' - : 'label' - } - > - {({ data, handleOrderChange, order, orderBy }) => { - const finalProps = { - ...componentProps, - data, - handleOrderChange, - order, - orderBy, - }; - - return linodesAreGrouped ? ( - <DisplayGroupedLinodes - {...finalProps} - display={linodeViewPreference} - toggleLinodeView={toggleLinodeView} - toggleGroupLinodes={toggleGroupLinodes} - linodesAreGrouped={true} - linodeViewPreference={linodeViewPreference} - component={ - linodeViewPreference === 'grid' - ? CardView - : ListView - } - /> - ) : ( - <DisplayLinodes - {...finalProps} - display={linodeViewPreference} - toggleLinodeView={toggleLinodeView} - toggleGroupLinodes={toggleGroupLinodes} - updatePageUrl={this.updatePageUrl} - linodesAreGrouped={false} - linodeViewPreference={linodeViewPreference} - component={ - linodeViewPreference === 'grid' - ? CardView - : ListView - } - /> - ); - }} - </OrderBy> - <Grid - container - className={classes.CSVwrapper} - justifyContent="flex-end" - > - <Grid className={classes.CSVlinkContainer}> - <CSVLink - data={linodesData.map((e) => { - const maintenance = e.maintenance?.when - ? { - ...e.maintenance, - when: formatDateISO(e.maintenance?.when), - } - : { when: null }; - - const lastBackup = - e.backups.last_successful === null - ? e.backups.enabled - ? 'Scheduled' - : 'Never' - : e.backups.last_successful; - - return { - ...e, - lastBackup, - maintenance, - linodeDescription: getLinodeDescription( - e.label, - e.specs.memory, - e.specs.disk, - e.specs.vcpus, - '', - {} - ), - }; - })} - headers={ - this.props.someLinodesHaveScheduledMaintenance - ? [ - ...headers, - /** only add maintenance window to CSV if one Linode has a window */ - { - label: 'Maintenance Status', - key: 'maintenance.when', - }, - ] - : headers - } - filename={`linodes-${formatDate( - DateTime.local().toISO(), - { - timezone: profile.data?.timezone, - } - )}.csv`} - className={classes.CSVlink} - > - Download CSV - </CSVLink> - </Grid> - </Grid> - </React.Fragment> - ); - }} - </PreferenceToggle> - ); - }} - </PreferenceToggle> - <TransferDisplay /> - - {!!this.state.selectedLinodeID && !!this.state.selectedLinodeLabel && ( - <React.Fragment> - <PowerDialogOrDrawer - isOpen={this.state.powerDialogOpen} - action={this.state.powerDialogAction} - linodeID={this.state.selectedLinodeID} - linodeLabel={this.state.selectedLinodeLabel} - close={this.closeDialogs} - linodeConfigs={this.state.selectedLinodeConfigs} + Region + </TableSortCell> + <TableCell>Last Backup</TableCell> + </Hidden> + <TableCell></TableCell> + </TableRow> + </TableHead> + <TableBody> + {data?.data.map((linode) => ( + <LinodeRow + key={linode.id} + {...linode} + handlers={{ + onOpenDeleteDialog: () => onOpenDeleteDialog(linode.id), + onOpenMigrateDialog: () => onOpenMigrateDialog(linode.id), + onOpenPowerDialog: (action: Action) => + onOpenPowerDialog(linode.id, action), + onOpenRebuildDialog: () => onOpenRebuildDialog(linode.id), + onOpenRescueDialog: () => onOpenRescueDialog(linode.id), + onOpenResizeDialog: () => onOpenResizeDialog(linode.id), + }} /> - <DeleteDialog - open={this.state.deleteDialogOpen} - onClose={this.closeDialogs} - linodeID={this.state.selectedLinodeID} - linodeLabel={this.state.selectedLinodeLabel} - handleDelete={this.props.deleteLinode} - /> - </React.Fragment> - )} - </React.Fragment> - ); - } -} - -const eventCategory = 'linodes landing'; - -const sendGroupByAnalytic = (value: boolean) => { - sendGroupByTagEnabledEvent(eventCategory, value); + ))} + </TableBody> + </Table> + <PaginationFooter + count={data?.results ?? 0} + handlePageChange={pagination.handlePageChange} + handleSizeChange={pagination.handlePageSizeChange} + page={pagination.page} + pageSize={pagination.pageSize} + eventCategory="Linodes Table" + /> + <Box display="flex" justifyContent="flex-end" marginTop={1}> + <LinodesLandingCSVDownload /> + </Box> + <TransferDisplay /> + <PowerActionsDialog + isOpen={powerDialogOpen} + linodeId={selectedLinodeId} + onClose={() => setPowerDialogOpen(false)} + action={powerAction} + /> + <DeleteLinodeDialog + open={deleteDialogOpen} + onClose={() => setDeleteDialogOpen(false)} + linodeId={selectedLinodeId} + /> + <LinodeResize + open={linodeResizeOpen} + onClose={() => setResizeDialogOpen(false)} + linodeId={selectedLinodeId} + /> + <MigrateLinode + open={linodeMigrateOpen} + onClose={() => setMigrateDialogOpen(false)} + linodeId={selectedLinodeId} + /> + <LinodeRebuildDialog + open={rebuildDialogOpen} + onClose={() => setRebuildDialogOpen(false)} + linodeId={selectedLinodeId} + /> + <RescueDialog + open={rescueDialogOpen} + onClose={() => setRescueDialogOpen(false)} + linodeId={selectedLinodeId} + /> + </React.Fragment> + ); }; -interface StateProps { - linodesCount: number; - linodesInTransition: Set<number>; -} - -const mapStateToProps: MapState<StateProps, Props> = (state) => { - return { - linodesCount: state.__resources.linodes.results, - linodesInTransition: _linodesInTransition(state.events.events), - }; -}; - -interface DispatchProps { - deleteLinode: ( - linodeId: number, - queryClient: QueryClient - ) => Promise<Record<string, never>>; -} - -const mapDispatchToProps: MapDispatchToProps<DispatchProps, Props> = ( - dispatch: ThunkDispatch<ApplicationState, undefined, AnyAction> -) => ({ - deleteLinode: (linodeId: number, queryClient: QueryClient) => - dispatch(deleteLinode({ linodeId, queryClient })), -}); - -const connected = connect(mapStateToProps, mapDispatchToProps); - -export const enhanced = compose<CombinedProps, Props>( - withRouter, - withSnackbar, - connected, - styled, - withFeatureFlagConsumer, - withProfile -); - -export default enhanced(ListLinodes); +export default LinodesLanding; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingCSVDownload.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingCSVDownload.tsx new file mode 100644 index 00000000000..2ff6dd621a7 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingCSVDownload.tsx @@ -0,0 +1,75 @@ +/* eslint-disable jsx-a11y/anchor-is-valid */ +import * as React from 'react'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; +import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV'; +import { useAllAccountMaintenanceQuery } from 'src/queries/accountMaintenance'; +import { useFormattedDate } from 'src/hooks/useFormattedDate'; + +export const LinodesLandingCSVDownload = () => { + const csvRef = React.useRef<any>(); + const formattedDate = useFormattedDate(); + + const { data: linodes, refetch: getCSVData } = useAllLinodesQuery( + {}, + {}, + false + ); + + const { data: accountMaintenance } = useAllAccountMaintenanceQuery( + {}, + { status: { '+or': ['pending, started'] } } + ); + + const downloadCSV = async () => { + await getCSVData(); + csvRef.current.link.click(); + }; + + const headers = [ + { label: 'id', key: 'id' }, + { label: 'label', key: 'label' }, + { label: 'image', key: 'image' }, + { label: 'region', key: 'region' }, + { label: 'ipv4', key: 'ipv4' }, + { label: 'ipv6', key: 'ipv6' }, + { label: 'status', key: 'status' }, + { label: 'type', key: 'type' }, + { label: 'updated', key: 'updated' }, + { label: 'created', key: 'created' }, + { label: 'disk', key: 'specs.disk' }, + { label: 'gpus', key: 'specs.gpus' }, + { label: 'memory', key: 'specs.memory' }, + { label: 'transfer', key: 'specs.transfer' }, + { label: 'vcpus', key: 'specs.vcpus' }, + { label: 'tags', key: 'tags' }, + { label: 'hypervisor', key: 'hypervisor' }, + { label: 'host_uuid', key: 'host_uuid' }, + { label: 'watchdog_enabled', key: 'watchdog_enabled' }, + { label: 'backups_enabled', key: 'backups.enabled' }, + { label: 'last_backup', key: 'backups.last_successful' }, + { label: 'maintenance', key: 'maintenance' }, + ]; + + const data = + linodes?.map((linode) => { + const maintenanceForLinode = + accountMaintenance?.find( + (m) => m.entity.id === linode.id && m.entity.type === 'linode' + ) ?? null; + + return { + ...linode, + maintenance: maintenanceForLinode, + }; + }) ?? []; + + return ( + <DownloadCSV + csvRef={csvRef} + data={data} + filename={`linodes-${formattedDate}.csv`} + headers={headers} + onClick={downloadCSV} + /> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyState.tsx b/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyState.tsx new file mode 100644 index 00000000000..88b1a4c017f --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyState.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import AppsSection from './AppsSection'; +import LinodeSvg from 'src/assets/icons/entityIcons/linode.svg'; +import MarketplaceIcon from 'src/assets/icons/marketplace.svg'; +import PointerIcon from 'src/assets/icons/pointer.svg'; +import { getLinkOnClick } from 'src/utilities/emptyStateLandingUtils'; +import { ResourcesLinkIcon } from 'src/components/EmptyLandingPageResources/ResourcesLinkIcon'; +import { ResourcesLinksSubSection } from 'src/components/EmptyLandingPageResources/ResourcesLinksSubSection'; +import { ResourcesMoreLink } from 'src/components/EmptyLandingPageResources/ResourcesMoreLink'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; +import { sendEvent } from 'src/utilities/ga'; +import { useHistory } from 'react-router-dom'; +import { + gettingStartedGuides, + headers, + linkGAEvent, + youtubeLinkData, +} from './LinodesLandingEmptyStateData'; + +const APPS_MORE_LINKS_TEXT = 'See all Marketplace apps'; + +export const LinodesLandingEmptyState = () => { + const { push } = useHistory(); + + return ( + <ResourcesSection + buttonProps={[ + { + onClick: () => { + push('/linodes/create'); + sendEvent({ + category: linkGAEvent.category, + action: 'Click:button', + label: 'Create Linode', + }); + }, + children: 'Create Linode', + }, + ]} + CustomResource={() => ( + <ResourcesLinksSubSection + title="Deploy an App" + icon={<MarketplaceIcon />} + MoreLink={(props) => ( + <ResourcesMoreLink + onClick={getLinkOnClick(linkGAEvent, APPS_MORE_LINKS_TEXT)} + to="/linodes/create?type=One-Click" + {...props} + > + {APPS_MORE_LINKS_TEXT} + <ResourcesLinkIcon icon={<PointerIcon />} iconType="pointer" /> + </ResourcesMoreLink> + )} + > + <AppsSection /> + </ResourcesLinksSubSection> + )} + descriptionMaxWidth={500} + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={LinodeSvg} + linkGAEvent={linkGAEvent} + showTransferDisplay={true} + youtubeLinkData={youtubeLinkData} + wide={true} + /> + ); +}; diff --git a/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyStateData.ts b/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyStateData.ts new file mode 100644 index 00000000000..44b2511aa76 --- /dev/null +++ b/packages/manager/src/features/linodes/LinodesLanding/LinodesLandingEmptyStateData.ts @@ -0,0 +1,81 @@ +import { + docsLink, + guidesMoreLinkText, + youtubeChannelLink, + youtubeMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; +import type { + ResourcesHeaders, + ResourcesLinkSection, + ResourcesLinks, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + description: + 'Host your websites, applications, or any other Cloud-based workloads on a scalable and reliable platform.', + subtitle: 'Cloud-based virtual machines', + title: 'Linodes', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + to: 'https://www.linode.com/docs/guides/creating-a-compute-instance/', + text: 'Create a Compute Instance', + }, + { + to: 'https://www.linode.com/docs/guides/getting-started/', + text: 'Getting Started with Linode Compute Instances', + }, + { + to: + 'https://www.linode.com/docs/guides/understanding-billing-and-payments/', + text: 'Understanding Billing and Payment', + }, + { + to: 'https://www.linode.com/docs/guides/set-up-web-server-host-website/', + text: 'Hosting a Website or Application on Linode', + }, + ], + moreInfo: { + to: docsLink, + text: guidesMoreLinkText, + }, + title: 'Getting Started Guides', +}; + +export const youtubeLinkData: ResourcesLinkSection = { + links: [ + { + to: 'https://www.youtube.com/watch?v=KEK-ZxrGxMA', + text: 'Linode Getting Started Guide', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=AVXYq8aL47Q', + text: 'Common Linux Commands', + external: true, + }, + { + to: 'https://www.youtube.com/watch?v=lMC5VNoZFhg', + text: 'Copying Files to a Compute Instance', + external: true, + }, + { + to: + 'https://www.youtube.com/watch?v=ZVMckBHd7WA&list=PLTnRtjQN5ieb4XyvC9OUhp7nxzBENgCxJ&index=2', + text: 'How to use SSH', + external: true, + }, + ], + moreInfo: { + to: youtubeChannelLink, + text: youtubeMoreLinkText, + }, + title: 'Video Playlist', +}; + +export const linkGAEvent: ResourcesLinks['linkGAEvent'] = { + action: 'Click:link', + category: 'Linodes landing page empty', +}; diff --git a/packages/manager/src/features/linodes/LinodesLanding/ListLinodesEmptyState.tsx b/packages/manager/src/features/linodes/LinodesLanding/ListLinodesEmptyState.tsx deleted file mode 100644 index 2af496a8b53..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/ListLinodesEmptyState.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import * as React from 'react'; -import { useHistory } from 'react-router-dom'; -import DocsIcon from 'src/assets/icons/docs.svg'; -import LinodeSvg from 'src/assets/icons/entityIcons/linode.svg'; -import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; -import MarketplaceIcon from 'src/assets/icons/marketplace.svg'; -import PointerIcon from 'src/assets/icons/pointer.svg'; -import YoutubeIcon from 'src/assets/icons/youtube.svg'; -import List from 'src/components/core/List'; -import ListItem from 'src/components/core/ListItem'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; -import Link from 'src/components/Link'; -import Placeholder from 'src/components/Placeholder'; -import { - docsLink, - getLinkOnClick, - guidesMoreLinkText, - youtubeChannelLink, - youtubeMoreLinkLabel, - youtubeMoreLinkText, -} from 'src/utilities/emptyStateLandingUtils'; -import { sendEvent } from 'src/utilities/ga'; -import AppsSection from './AppsSection'; -import LinksSection from './LinksSection'; -import LinksSubSection from './LinksSubSection'; - -const gaCategory = 'Linodes landing page empty'; -const linkGAEventTemplate = { - category: gaCategory, - action: 'Click:link', -}; - -const gettingStartedGuideLinksData = [ - { - to: 'https://www.linode.com/docs/guides/creating-a-compute-instance/', - text: 'Create a Compute Instance', - }, - { - to: 'https://www.linode.com/docs/guides/getting-started/', - text: 'Getting Started with Linode Compute Instances', - }, - { - to: - 'https://www.linode.com/docs/guides/understanding-billing-and-payments/', - text: 'Understanding Billing and Payment', - }, - { - to: 'https://www.linode.com/docs/guides/set-up-web-server-host-website/', - text: 'Hosting a Website or Application on Linode', - }, -]; - -const youtubeLinksData = [ - { - to: 'https://www.youtube.com/watch?v=KEK-ZxrGxMA', - text: 'Linode Getting Started Guide', - }, - { - to: 'https://www.youtube.com/watch?v=AVXYq8aL47Q', - text: 'Common Linux Commands', - }, - { - to: 'https://www.youtube.com/watch?v=lMC5VNoZFhg', - text: 'Copying Files to a Compute Instance', - }, - { - to: - 'https://www.youtube.com/watch?v=ZVMckBHd7WA&list=PLTnRtjQN5ieb4XyvC9OUhp7nxzBENgCxJ&index=2', - text: 'How to use SSH', - }, -]; - -const guideLinks = ( - <List> - {gettingStartedGuideLinksData.map((linkData) => ( - <ListItem key={linkData.to}> - <Link - to={linkData.to} - onClick={getLinkOnClick(linkGAEventTemplate, linkData.text)} - > - {linkData.text} - </Link> - </ListItem> - ))} - </List> -); - -const appsMoreLinkText = 'See all Marketplace apps'; - -const youtubeLinks = ( - <List> - {youtubeLinksData.map((linkData) => ( - <ListItem key={linkData.to}> - <Link - onClick={getLinkOnClick(linkGAEventTemplate, linkData.text)} - to={linkData.to} - > - {linkData.text} - <ExternalLinkIcon /> - </Link> - </ListItem> - ))} - </List> -); - -const useStyles = makeStyles((theme: Theme) => ({ - placeholderAdjustment: { - padding: `${theme.spacing(2)} 0`, - [theme.breakpoints.up('md')]: { - padding: `${theme.spacing(10)} 0 ${theme.spacing(4)}`, - }, - }, -})); - -export const ListLinodesEmptyState: React.FC<{}> = (_) => { - const classes = useStyles(); - - const { push } = useHistory(); - - return ( - <Placeholder - title={'Linodes'} - subtitle="Cloud-based virtual machines" - icon={LinodeSvg} - isEntity - className={classes.placeholderAdjustment} - buttonProps={[ - { - onClick: () => { - push('/linodes/create'); - sendEvent({ - category: gaCategory, - action: 'Click:button', - label: 'Create Linode', - }); - }, - children: 'Create Linode', - }, - ]} - linksSection={ - <LinksSection> - <LinksSubSection - title="Getting Started Guides" - icon={<DocsIcon />} - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick( - linkGAEventTemplate, - guidesMoreLinkText - )} - to={docsLink} - {...props} - > - {guidesMoreLinkText} - <PointerIcon /> - </Link> - )} - > - {guideLinks} - </LinksSubSection> - <LinksSubSection - title="Deploy an App" - icon={<MarketplaceIcon />} - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick(linkGAEventTemplate, appsMoreLinkText)} - to="/linodes/create?type=One-Click" - {...props} - > - {appsMoreLinkText} - <PointerIcon /> - </Link> - )} - > - <AppsSection /> - </LinksSubSection> - <LinksSubSection - title="Video Playlist" - icon={<YoutubeIcon />} - external - MoreLink={(props) => ( - <Link - onClick={getLinkOnClick( - linkGAEventTemplate, - youtubeMoreLinkLabel - )} - to={youtubeChannelLink} - {...props} - > - {youtubeMoreLinkText} - <ExternalLinkIcon /> - </Link> - )} - > - {youtubeLinks} - </LinksSubSection> - </LinksSection> - } - showTransferDisplay - > - <Typography - style={{ fontSize: '1.125rem', lineHeight: '1.75rem', maxWidth: 541 }} - > - Host your websites, applications, or any other Cloud-based workloads on - a scalable and reliable platform. - </Typography> - </Placeholder> - ); -}; - -export default React.memo(ListLinodesEmptyState); diff --git a/packages/manager/src/features/linodes/LinodesLanding/ListView.tsx b/packages/manager/src/features/linodes/LinodesLanding/ListView.tsx deleted file mode 100644 index 1abb08967d4..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/ListView.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react'; -import TableRowEmptyState from 'src/components/TableRowEmptyState'; -import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; -import formatDate from 'src/utilities/formatDate'; -import LinodeRow from './LinodeRow'; -import { RenderLinodesProps } from './DisplayLinodes'; -import { useProfile } from 'src/queries/profile'; - -export const ListView: React.FC<RenderLinodesProps> = (props) => { - const { data, openDialog, openPowerActionDialog } = props; - - const { data: profile } = useProfile(); - - const notificationContext = React.useContext(_notificationContext); - - // This won't happen in the normal Linodes Landing context (a custom empty - // state is shown higher up in the tree). This is specifically for the case of - // VLAN Details, where we want to show the table even if there's nothing attached. - if (data.length === 0) { - return <TableRowEmptyState colSpan={12} />; - } - - return ( - // eslint-disable-next-line - <> - {/* @todo: fix this "any" typing once https://github.com/linode/manager/pull/6999 is merged. */} - {data.map((linode, idx: number) => ( - <LinodeRow - backups={linode.backups} - id={linode.id} - ipv4={linode.ipv4} - maintenanceStartTime={ - linode.maintenance?.when - ? formatDate(linode.maintenance.when, { - timezone: profile?.timezone, - }) - : '' - } - ipv6={linode.ipv6 || ''} - label={linode.label} - region={linode.region} - status={linode.status} - displayStatus={linode.displayStatus || ''} - tags={linode.tags} - mostRecentBackup={linode.backups.last_successful} - disk={linode.specs.disk} - vcpus={linode.specs.vcpus} - memory={linode.specs.memory} - type={linode._type ?? undefined} - image={linode.image} - key={`linode-row-${idx}`} - openDialog={openDialog} - openNotificationMenu={notificationContext.openMenu} - openPowerActionDialog={openPowerActionDialog} - /> - ))} - </> - ); -}; - -export default ListView; diff --git a/packages/manager/src/features/linodes/LinodesLanding/SortableTableHead.tsx b/packages/manager/src/features/linodes/LinodesLanding/SortableTableHead.tsx deleted file mode 100644 index 117c5ad29ff..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/SortableTableHead.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import * as React from 'react'; -import GridView from 'src/assets/icons/grid-view.svg'; -import Hidden from 'src/components/core/Hidden'; -import IconButton from 'src/components/core/IconButton'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import TableHead from 'src/components/core/TableHead'; -import Tooltip from 'src/components/core/Tooltip'; -import { GroupByTagToggle } from 'src/components/EntityTable/EntityTableHeader'; -import { OrderByProps } from 'src/components/OrderBy'; -import TableCell from 'src/components/TableCell'; -import TableRow from 'src/components/TableRow'; -import TableSortCell from 'src/components/TableSortCell'; - -const useStyles = makeStyles((theme: Theme) => ({ - controlHeader: { - display: 'flex', - justifyContent: 'flex-end', - backgroundColor: theme.bg.tableHeader, - }, - toggleButton: { - color: '#d2d3d4', - padding: '0 10px', - '&:focus': { - outline: '1px dotted #999', - }, - }, - // There's nothing very scientific about the widths across the breakpoints - // here, just a lot of trial and error based on maximum expected column sizes. - labelCell: { - ...theme.applyTableHeaderStyles, - width: '24%', - [theme.breakpoints.down('lg')]: { - width: '20%', - }, - }, - statusCell: { - ...theme.applyTableHeaderStyles, - width: '20%', - [theme.breakpoints.only('md')]: { - width: '27%', - }, - [theme.breakpoints.down('md')]: { - width: '25%', - }, - }, - planCell: { - ...theme.applyTableHeaderStyles, - width: '14%', - [theme.breakpoints.only('sm')]: { - width: '15%', - }, - }, - regionCell: { - ...theme.applyTableHeaderStyles, - width: '14%', - [theme.breakpoints.down('sm')]: { - width: '18%', - }, - }, - lastBackupCell: { - ...theme.applyTableHeaderStyles, - width: '14%', - [theme.breakpoints.down('sm')]: { - width: '18%', - }, - }, -})); - -interface Props { - toggleLinodeView: () => 'list' | 'grid'; - linodeViewPreference: 'list' | 'grid'; - toggleGroupLinodes: () => boolean; - linodesAreGrouped: boolean; - isVLAN?: boolean; -} - -type CombinedProps<T> = Props & Omit<OrderByProps<T>, 'data'>; - -const SortableTableHead = <T extends unknown>(props: CombinedProps<T>) => { - const classes = useStyles(); - - const { - handleOrderChange, - order, - orderBy, - toggleLinodeView, - linodeViewPreference, - toggleGroupLinodes, - linodesAreGrouped, - isVLAN, - } = props; - - const isActive = (label: string) => - label.toLowerCase() === orderBy.toLowerCase(); - - return ( - <TableHead role="rowgroup" data-qa-table-head> - <TableRow> - <TableSortCell - label="label" - direction={order} - active={isActive('label')} - handleClick={handleOrderChange} - className={classes.labelCell} - data-qa-sort-label={order} - > - Label - </TableSortCell> - <TableSortCell - noWrap - label="_statusPriority" - direction={order} - active={isActive('_statusPriority')} - className={classes.statusCell} - handleClick={handleOrderChange} - > - Status - </TableSortCell> - {isVLAN ? ( - <TableSortCell - label="_vlanIP" - active={isActive('_vlanIP')} - handleClick={handleOrderChange} - direction={order} - > - VLAN IP - </TableSortCell> - ) : null} - {isVLAN ? null : ( - <> - <Hidden smDown> - <TableSortCell - label="type" - active={isActive('type')} - handleClick={handleOrderChange} - direction={order} - className={classes.planCell} - > - Plan - </TableSortCell> - <TableSortCell - label="ipv4[0]" // we want to sort by the first ipv4 - active={isActive('ipv4[0]')} - handleClick={handleOrderChange} - direction={order} - > - IP Address - </TableSortCell> - <Hidden lgDown> - <TableSortCell - label="region" - direction={order} - active={isActive('region')} - handleClick={handleOrderChange} - className={classes.regionCell} - data-qa-sort-region={order} - > - Region - </TableSortCell> - </Hidden> - </Hidden> - <Hidden lgDown> - <TableSortCell - noWrap - label="backups:last_successful" - direction={order} - active={isActive('backups:last_successful')} - className={classes.lastBackupCell} - handleClick={handleOrderChange} - > - Last Backup - </TableSortCell> - </Hidden> - </> - )} - <TableCell> - <div className={classes.controlHeader}> - <div id="displayViewDescription" className="visually-hidden"> - Currently in {linodeViewPreference} view - </div> - <Tooltip placement="top" title="Summary view"> - <IconButton - aria-label="Toggle display" - aria-describedby={'displayViewDescription'} - onClick={toggleLinodeView} - disableRipple - className={classes.toggleButton} - size="large" - > - <GridView /> - </IconButton> - </Tooltip> - <GroupByTagToggle - toggleGroupByTag={toggleGroupLinodes} - isGroupedByTag={linodesAreGrouped} - /> - </div> - </TableCell> - </TableRow> - </TableHead> - ); -}; - -export default SortableTableHead; diff --git a/packages/manager/src/features/linodes/LinodesLanding/TableWrapper.tsx b/packages/manager/src/features/linodes/LinodesLanding/TableWrapper.tsx deleted file mode 100644 index d6fc35adeb8..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/TableWrapper.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react'; -import Grid from '@mui/material/Unstable_Grid2'; -import { OrderByProps } from 'src/components/OrderBy'; -import Table, { TableProps } from 'src/components/Table'; -import SortableTableHead from './SortableTableHead'; - -interface Props { - dataLength: number; - toggleLinodeView: () => 'list' | 'grid'; - linodeViewPreference: 'list' | 'grid'; - toggleGroupLinodes: () => boolean; - linodesAreGrouped: boolean; - isVLAN?: boolean; - tableProps?: TableProps; - children: React.ReactNode; -} - -type CombinedProps<T> = Omit<OrderByProps<T>, 'data'> & Props; - -const TableWrapper = <T extends unknown>(props: CombinedProps<T>) => { - const { - dataLength, - order, - orderBy, - handleOrderChange, - toggleLinodeView, - linodeViewPreference, - toggleGroupLinodes, - linodesAreGrouped, - isVLAN, - tableProps, - } = props; - - return ( - <Grid container className="m0" spacing={0} style={{ width: '100%' }}> - <Grid xs={12} className="p0"> - <Table - aria-label="List of Linodes" - rowCount={dataLength} - colCount={5} - stickyHeader - {...tableProps} - > - <SortableTableHead - order={order} - orderBy={orderBy} - handleOrderChange={handleOrderChange} - toggleGroupLinodes={toggleGroupLinodes} - linodeViewPreference={linodeViewPreference} - toggleLinodeView={toggleLinodeView} - linodesAreGrouped={linodesAreGrouped} - isVLAN={isVLAN} - /> - {props.children} - </Table> - </Grid> - </Grid> - ); -}; - -export default TableWrapper; diff --git a/packages/manager/src/features/linodes/LinodesLanding/ToggleBox.tsx b/packages/manager/src/features/linodes/LinodesLanding/ToggleBox.tsx deleted file mode 100644 index 50c3b23d489..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/ToggleBox.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import ViewList from '@mui/icons-material/ViewList'; -import ViewModule from '@mui/icons-material/ViewModule'; -import * as React from 'react'; -import { compose } from 'redux'; -import Button from 'src/components/Button'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; - -type CSSClasses = - | 'root' - | 'button' - | 'buttonActive' - | 'buttonLeft' - | 'buttonRight' - | 'icon'; - -const styles = (theme: Theme) => - createStyles({ - root: { - margin: 8, - }, - button: { - borderWidth: 1, - borderStyle: 'solid', - borderColor: theme.color.boxShadow, - borderRadius: 0, - fontFamily: theme.font.bold, - textTransform: 'inherit', - width: 80, - minWidth: 80, - padding: '6px 14px 5px 12px', - minHeight: 'inherit', - fontSize: '1rem', - lineHeight: '1.3em', - color: theme.palette.text.primary, - '&:focus': { - backgroundColor: theme.color.white, - }, - '&:hover': { - backgroundColor: 'transparent', - '& $icon': { - opacity: 1, - }, - }, - }, - buttonActive: { - backgroundColor: theme.color.white, - '&:hover': { - backgroundColor: theme.color.white, - }, - }, - buttonLeft: { - width: 79, - }, - buttonRight: { - borderLeftWidth: 0, - }, - icon: { - marginRight: 6, - width: 18, - height: 18, - opacity: 0.4, - transition: 'opacity 400ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - }, - }); - -interface Props { - handleClick: (v: 'grid' | 'list') => void; - status: 'grid' | 'list'; -} - -const styled = withStyles(styles); - -type CombinedProps = Props & WithStyles<CSSClasses>; - -export const ToggleBox: React.FC<CombinedProps> = (props) => { - const { classes, handleClick, status } = props; - - return ( - <div className={classes.root} data-qa-active-view={props.status}> - <Button - /* in other words, don't toggle the view if we're clicking the already selected view button */ - onClick={() => (status === 'grid' ? handleClick('list') : null)} - className={` - ${!status || (status === 'list' && classes.buttonActive)} - ${classes.button} - ${classes.buttonLeft}`} - data-qa-view="list" - > - <ViewList className={classes.icon} /> - List - </Button> - <Button - /* in other words, don't toggle the view if we're clicking the already selected view button */ - onClick={() => (status === 'list' ? handleClick('grid') : null)} - className={` - ${status === 'grid' && classes.buttonActive} - ${classes.button} - ${classes.buttonRight}`} - data-qa-view="grid" - > - <ViewModule className={classes.icon} /> - Grid - </Button> - </div> - ); -}; - -export default compose(styled)(ToggleBox); diff --git a/packages/manager/src/features/linodes/LinodesLanding/index.tsx b/packages/manager/src/features/linodes/LinodesLanding/index.tsx deleted file mode 100644 index ec1ea304f4b..00000000000 --- a/packages/manager/src/features/linodes/LinodesLanding/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { CircleProgress } from 'src/components/CircleProgress'; -import { useReduxLoad } from 'src/hooks/useReduxLoad'; - -const LinodesLanding: React.FC<Props> = (props) => { - const { _loading } = useReduxLoad(['linodes']); - - return _loading ? <CircleProgress /> : <_LinodesLanding {...props} />; -}; - -import _LinodesLanding, { Props } from './LinodesLanding'; -export default LinodesLanding; diff --git a/packages/manager/src/features/linodes/MigrateLinode/CautionNotice.tsx b/packages/manager/src/features/linodes/MigrateLinode/CautionNotice.tsx index 3b870899155..f4fabf250ff 100644 --- a/packages/manager/src/features/linodes/MigrateLinode/CautionNotice.tsx +++ b/packages/manager/src/features/linodes/MigrateLinode/CautionNotice.tsx @@ -5,7 +5,7 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { Link } from 'src/components/Link'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { API_MAX_PAGE_SIZE } from 'src/constants'; import { useAccount } from 'src/queries/account'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; @@ -45,11 +45,11 @@ interface Props { setConfirmed: (value: boolean) => void; error?: string; migrationTimeInMins: number; - linodeId: number; + linodeId: number | undefined; metadataWarning?: string; } -const CautionNotice: React.FC<Props> = (props) => { +const CautionNotice = (props: Props) => { const classes = useStyles(); const { data: account } = useAccount(); @@ -60,9 +60,14 @@ const CautionNotice: React.FC<Props> = (props) => { // the React Query store in a paginated shape. We want to keep data in a paginated shape // because the event handler automatically updates stored paginated data. // We can safely do this because linodes can't have more than 64 volumes. - const { data: volumesData } = useLinodeVolumesQuery(props.linodeId, { - page_size: API_MAX_PAGE_SIZE, - }); + const { data: volumesData } = useLinodeVolumesQuery( + props.linodeId ?? -1, + { + page_size: API_MAX_PAGE_SIZE, + }, + {}, + props.linodeId !== undefined + ); const amountOfAttachedVolumes = volumesData?.results ?? 0; diff --git a/packages/manager/src/features/linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/linodes/MigrateLinode/MigrateLinode.tsx index 2610435ee36..c5eb42993b3 100644 --- a/packages/manager/src/features/linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/linodes/MigrateLinode/MigrateLinode.tsx @@ -7,7 +7,7 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import Button from 'src/components/Button'; import { Dialog } from 'src/components/Dialog/Dialog'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; import Box from 'src/components/core/Box'; import Typography from 'src/components/core/Typography'; @@ -71,17 +71,17 @@ const useStyles = makeStyles((theme: Theme) => ({ })); interface Props { - linodeID: number; + linodeId: number | undefined; open: boolean; onClose: () => void; } const MigrateLinode = React.memo((props: Props) => { - const { linodeID, onClose, open } = props; + const { linodeId, onClose, open } = props; const classes = useStyles(); const { enqueueSnackbar } = useSnackbar(); - const linode = useExtendedLinode(linodeID); + const linode = useExtendedLinode(linodeId ?? -1); const typesQuery = useSpecificTypes(linode?.type ? [linode.type] : []); const type = typesQuery[0]?.data ? extendType(typesQuery[0].data) : undefined; @@ -172,7 +172,7 @@ const MigrateLinode = React.memo((props: Props) => { setLoading(true); - return scheduleOrQueueMigration(linodeID, { + return scheduleOrQueueMigration(linodeId ?? -1, { region: selectedRegion, }) .then(() => { @@ -216,7 +216,7 @@ const MigrateLinode = React.memo((props: Props) => { const disabledText = getDisabledReason( linode._events, linode.status, - linodeID + linodeId ?? -1 ); /** how long will this take to migrate when the migration starts */ @@ -247,7 +247,7 @@ const MigrateLinode = React.memo((props: Props) => { notifications={notifications} /> */} <CautionNotice - linodeId={linodeID} + linodeId={linodeId} setConfirmed={setConfirmed} hasConfirmed={hasConfirmed} error={acceptError} diff --git a/packages/manager/src/features/linodes/MigrateLinode/MigrationImminentNotice.tsx b/packages/manager/src/features/linodes/MigrateLinode/MigrationImminentNotice.tsx index a9dfd83bcef..cd2a09684bc 100644 --- a/packages/manager/src/features/linodes/MigrateLinode/MigrationImminentNotice.tsx +++ b/packages/manager/src/features/linodes/MigrateLinode/MigrationImminentNotice.tsx @@ -2,7 +2,7 @@ import { Notification } from '@linode/api-v4/lib/account'; import * as React from 'react'; import { compose } from 'recompose'; -import Notice from 'src/components/Notice'; +import { Notice } from 'src/components/Notice/Notice'; import { SupportLink } from 'src/components/SupportLink'; interface Props { diff --git a/packages/manager/src/features/linodes/PowerActionsDialogOrDrawer.tsx b/packages/manager/src/features/linodes/PowerActionsDialogOrDrawer.tsx index 5de79428801..26c9fa52cae 100644 --- a/packages/manager/src/features/linodes/PowerActionsDialogOrDrawer.tsx +++ b/packages/manager/src/features/linodes/PowerActionsDialogOrDrawer.tsx @@ -1,22 +1,21 @@ -import { - Config, - linodeBoot, - linodeReboot, - linodeShutdown, -} from '@linode/api-v4/lib/linodes'; -import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; -import { compose } from 'recompose'; +import Select from 'src/components/EnhancedSelect/Select'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; +import Typography from 'src/components/core/Typography'; +import ExternalLink from 'src/components/ExternalLink'; +import { Notice } from 'src/components/Notice/Notice'; +import { Config } from '@linode/api-v4/lib/linodes'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; -import ExternalLink from 'src/components/ExternalLink'; -import Notice from 'src/components/Notice'; -import { resetEventsPolling } from 'src/eventsPolling'; -import LinodeConfigDrawer from 'src/features/LinodeConfigSelectionDrawer'; +import { + useAllLinodeConfigsQuery, + useBootLinodeMutation, + useLinodeQuery, + useRebootLinodeMutation, + useShutdownLinodeMutation, +} from 'src/queries/linodes/linodes'; export type Action = 'Reboot' | 'Power Off' | 'Power On'; @@ -42,17 +41,12 @@ const useStyles = makeStyles((theme: Theme) => ({ })); interface Props { - action?: Action; - linodeID: number; - linodeLabel: string; + linodeId: number | undefined; isOpen: boolean; - close: () => void; - /** if a Linode has multiple configs, we need to boot a specific config */ - linodeConfigs?: Config[]; + onClose: () => void; + action: Action; } -type CombinedProps = Props; - /** * In special cases, such as Rescue mode, the API's method * for determining the last booted config doesn't work as @@ -64,105 +58,123 @@ type CombinedProps = Props; export const selectDefaultConfig = (configs?: Config[]) => configs?.length === 1 ? configs[0].id : undefined; -const PowerActionsDialogOrDrawer: React.FC<CombinedProps> = (props) => { - const { linodeConfigs } = props; +export const PowerActionsDialog = (props: Props) => { + const { onClose, linodeId, isOpen, action } = props; const classes = useStyles(); - const [isTakingAction, setTakingAction] = React.useState<boolean>(false); - const [errors, setErrors] = React.useState<APIError[] | undefined>(undefined); - const [selectedConfigID, selectConfigID] = React.useState<number | undefined>( - selectDefaultConfig(linodeConfigs) + + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined && isOpen ); - const hasMoreThanOneConfigOnSelectedLinode = - !!props.linodeConfigs && props.linodeConfigs.length > 1; - - React.useEffect(() => { - if (props.isOpen) { - /** - * reset error and loading state when we open the modal - */ - setErrors(undefined); - setTakingAction(false); - selectConfigID(selectDefaultConfig(linodeConfigs)); - } - }, [props.isOpen]); + const { + data: configs, + isLoading: configsLoading, + error: configsError, + } = useAllLinodeConfigsQuery( + linodeId ?? -1, + linodeId !== undefined && isOpen + ); - const handleSubmit = () => { - /** this will never happen but handle gracefully */ - if (!props.action || !props.linodeID) { - return setErrors([{ reason: 'An unexpected error occurred.' }]); - } + const { + mutateAsync: bootLinode, + isLoading: isBooting, + error: bootError, + } = useBootLinodeMutation(linodeId ?? -1); + + const { + mutateAsync: rebootLinode, + isLoading: isRebooting, + error: rebootError, + } = useRebootLinodeMutation(linodeId ?? -1); + + const { + mutateAsync: shutdownLinode, + isLoading: isShuttingDown, + error: shutdownError, + } = useShutdownLinodeMutation(linodeId ?? -1); + + const [selectedConfigID, setSelectConfigID] = React.useState<number | null>( + null + ); - /** throw an error if we have need to select a config and haven't */ - if ( - hasMoreThanOneConfigOnSelectedLinode && - ((props.action === 'Power On' && !selectedConfigID) || - (props.action === 'Reboot' && !selectedConfigID)) - ) { - /** force the user into selecting a config when they boot */ - return setErrors([ - { reason: 'Please select a Config Profile to boot with.' }, - ]); - } + const mutationMap = { + Reboot: rebootLinode, + 'Power Off': shutdownLinode, + 'Power On': bootLinode, + } as const; + + const errorMap = { + Reboot: rebootError, + 'Power Off': shutdownError, + 'Power On': bootError, + }; + + const loadingMap = { + Reboot: isRebooting, + 'Power Off': isShuttingDown, + 'Power On': isBooting, + }; + + const error = errorMap[action]; + const isLoading = loadingMap[action]; - setTakingAction(true); - determineBootPromise(props.action)(props.linodeID, selectedConfigID) - .then(() => { - resetEventsPolling(); - props.close(); - }) - .catch((e) => { - setTakingAction(false); - setErrors(e); + const onSubmit = async () => { + if (props.action === 'Power On' || props.action === 'Reboot') { + const mutateAsync = mutationMap[action as 'Power On' | 'Reboot']; + await mutateAsync({ + config_id: selectedConfigID ?? selectDefaultConfig(configs), }); + } else { + const mutateAsync = mutationMap[action as 'Power Off']; + await mutateAsync(); + } + onClose(); }; - /** - * if we're rebooting or booting a Linode with many configs, we need the user to - * confirm which config they actually want to boot, rather than - * confirming the action with a dialog message. - */ - if ( - (props.action === 'Power On' && hasMoreThanOneConfigOnSelectedLinode) || - (props.action === 'Reboot' && hasMoreThanOneConfigOnSelectedLinode) - ) { - return ( - <LinodeConfigDrawer - loading={isTakingAction} - error={errors} - isOpen={props.isOpen} - onSelectConfig={selectConfigID} - onSubmit={handleSubmit} - onClose={props.close} - linodeConfigs={props.linodeConfigs ?? []} - selectedConfigID={selectedConfigID} - /> - ); - } - - if (!props.action) { - return null; - } + const showConfigSelect = + configs !== undefined && + configs?.length > 1 && + (props.action === 'Power On' || props.action === 'Reboot'); + + const configOptions = + configs?.map((config) => ({ + label: config.label, + value: config.id, + })) ?? []; return ( <ConfirmationDialog className={classes.dialog} - open={props.isOpen} - title={`${props.action} Linode ${props.linodeLabel}?`} - onClose={props.close} - error={errors ? errors[0].reason : ''} + open={isOpen} + title={`${action} Linode ${linode?.label ?? ''}?`} + onClose={onClose} + error={error?.[0].reason} actions={ - <Actions - onClose={props.close} - loading={isTakingAction} - onSubmit={handleSubmit} - action={props.action} - /> + <ActionsPanel> + <Button buttonType="secondary" onClick={props.onClose}> + Cancel + </Button> + <Button buttonType="primary" onClick={onSubmit} loading={isLoading}> + {props.action} Linode + </Button> + </ActionsPanel> } > <Typography className={classes.root}> Are you sure you want to {props.action.toLowerCase()} your Linode? </Typography> + {showConfigSelect && ( + <Select + label="Config" + options={configOptions} + value={configOptions.find((o) => o.value === selectedConfigID)} + onChange={(o) => setSelectConfigID(o === null ? null : o.value)} + isLoading={configsLoading} + errorText={configsError?.[0].reason} + overflowPortal + /> + )} {props.action === 'Power Off' && ( <span> <Notice warning important className={classes.notice}> @@ -180,44 +192,3 @@ const PowerActionsDialogOrDrawer: React.FC<CombinedProps> = (props) => { </ConfirmationDialog> ); }; - -interface ActionsProps { - onClose: () => void; - onSubmit: () => void; - loading: boolean; - action: Action; -} - -const Actions: React.FC<ActionsProps> = (props) => { - return ( - <ActionsPanel> - <Button buttonType="secondary" onClick={props.onClose}> - Cancel - </Button> - <Button - buttonType="primary" - onClick={props.onSubmit} - loading={props.loading} - > - {props.action} Linode - </Button> - </ActionsPanel> - ); -}; - -const determineBootPromise = (action: Action) => { - switch (action) { - case 'Reboot': - return linodeReboot; - case 'Power On': - return linodeBoot; - case 'Power Off': - return linodeShutdown; - default: - return linodeReboot; - } -}; - -export default compose<CombinedProps, Props>(React.memo)( - PowerActionsDialogOrDrawer -); diff --git a/packages/manager/src/features/linodes/index.tsx b/packages/manager/src/features/linodes/index.tsx index 64342bd458b..ac361f4dad1 100644 --- a/packages/manager/src/features/linodes/index.tsx +++ b/packages/manager/src/features/linodes/index.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import SuspenseLoader from 'src/components/SuspenseLoader'; -import { useAllAccountMaintenanceQuery } from 'src/queries/accountMaintenance'; -import { useExtendedLinodes } from 'src/hooks/useExtendedLinode'; -import useLinodes from 'src/hooks/useLinodes'; -const LinodesLanding = React.lazy(() => import('./LinodesLanding')); +const LinodesLanding = React.lazy( + () => import('./LinodesLanding/LinodesLanding') +); const LinodesDetail = React.lazy(() => import('./LinodesDetail')); const LinodesCreate = React.lazy( () => import('./LinodesCreate/LinodeCreateContainer') @@ -17,7 +16,7 @@ const LinodesRoutes: React.FC = () => { <Switch> <Route component={LinodesCreate} path="/linodes/create" /> <Route component={LinodesDetail} path="/linodes/:linodeId" /> - <Route component={LinodesLandingWrapper} path="/linodes" exact strict /> + <Route component={LinodesLanding} path="/linodes" exact strict /> <Redirect to="/linodes" /> </Switch> </React.Suspense> @@ -25,34 +24,3 @@ const LinodesRoutes: React.FC = () => { }; export default LinodesRoutes; - -// Light wrapper around LinodesLanding that injects "extended" Linodes (with -// plan type and maintenance information). This extra data used to come from -// mapStateToProps, but since I wanted to use a query (for accountMaintenance) -// I needed a Function Component. It seemed safer to do it this way instead of -// refactoring LinodesLanding. -const LinodesLandingWrapper: React.FC = React.memo(() => { - const { data: accountMaintenanceData } = useAllAccountMaintenanceQuery( - {}, - { status: { '+or': ['pending, started'] } } - ); - const { linodes } = useLinodes(); - const extendedLinodes = useExtendedLinodes(); - - const someLinodesHaveScheduledMaintenance = accountMaintenanceData?.some( - (thisAccountMaintenance) => { - return linodes.itemsById[thisAccountMaintenance.entity.id]; - } - ); - - return ( - <LinodesLanding - someLinodesHaveScheduledMaintenance={Boolean( - someLinodesHaveScheduledMaintenance - )} - linodesData={extendedLinodes} - linodesRequestLoading={linodes.loading} - linodesRequestError={linodes.error.read} - /> - ); -}); diff --git a/packages/manager/src/hooks/useFormattedDate.ts b/packages/manager/src/hooks/useFormattedDate.ts new file mode 100644 index 00000000000..a27ddfa2527 --- /dev/null +++ b/packages/manager/src/hooks/useFormattedDate.ts @@ -0,0 +1,9 @@ +import { useMemo } from 'react'; +import { DateTime } from 'luxon'; + +export const useFormattedDate = () => { + return useMemo(() => { + const now = DateTime.local(); + return now.toFormat('yyyy-MM-dd'); + }, []); +}; diff --git a/packages/manager/src/hooks/usePagination.ts b/packages/manager/src/hooks/usePagination.ts index be1c9266ad4..d951c8ac1d7 100644 --- a/packages/manager/src/hooks/usePagination.ts +++ b/packages/manager/src/hooks/usePagination.ts @@ -1,5 +1,5 @@ import { useHistory, useLocation } from 'react-router-dom'; -import { MIN_PAGE_SIZE } from 'src/components/PaginationFooter'; +import { MIN_PAGE_SIZE } from 'src/components/PaginationFooter/PaginationFooter'; import { useMutatePreferences, usePreferences } from 'src/queries/preferences'; export interface PaginationProps { diff --git a/packages/manager/src/index.css b/packages/manager/src/index.css index 3c2876751e8..07af05d4999 100644 --- a/packages/manager/src/index.css +++ b/packages/manager/src/index.css @@ -68,10 +68,6 @@ body.searchOverlay #main-content { color: #000 !important; } - .tableWrapper { - border: 1px solid #f4f4f4 !important; - } - thead { display: table-header-group !important; border-bottom: 1px solid #f4f4f4 !important; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index d4b8464b5bf..3d067ed28c9 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -635,6 +635,9 @@ export const handlers = [ const buckets = objectStorageBucketFactory.buildList(10); return res(ctx.json(makeResourcePage(buckets))); }), + rest.post('*/object-storage/buckets', (req, res, ctx) => { + return res(ctx.json(objectStorageBucketFactory.build())); + }), rest.get('*object-storage/clusters', (req, res, ctx) => { const clusters = objectStorageClusterFactory.buildList(3); return res(ctx.json(makeResourcePage(clusters))); diff --git a/packages/manager/src/queries/linodes.ts b/packages/manager/src/queries/linodes.ts deleted file mode 100644 index fbced3e707f..00000000000 --- a/packages/manager/src/queries/linodes.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { NetworkTransfer } from '@linode/api-v4/lib/account/types'; -import { useInfiniteQuery, useQuery } from 'react-query'; -import { - APIError, - Filter, - Params, - ResourcePage, -} from '@linode/api-v4/lib/types'; -import { getAll } from 'src/utilities/getAll'; -import { listToItemsByID, queryPresets } from './base'; -import { parseAPIDate } from 'src/utilities/date'; -import { DateTime } from 'luxon'; -import { Firewall } from '@linode/api-v4'; -import { - Linode, - getLinodes, - getLinodeStatsByDate, - Stats, - getLinodeTransferByDate, - getLinodeStats, - getLinode, - getLinodeLishToken, - getLinodeConfigs, - Config, - getLinodeFirewalls, -} from '@linode/api-v4/lib/linodes'; - -export const STATS_NOT_READY_API_MESSAGE = - 'Stats are unavailable at this time.'; -export const STATS_NOT_READY_MESSAGE = - 'Stats for this Linode are not available yet'; - -export const queryKey = 'linode'; - -interface LinodeData { - results: number; - linodes: Record<string, Linode>; -} - -export const useLinodesQuery = ( - params: Params = {}, - filter: Filter = {}, - enabled: boolean = true -) => { - return useQuery<ResourcePage<Linode>, APIError[]>( - [queryKey, params, filter], - () => getLinodes(params, filter), - { ...queryPresets.longLived, enabled } - ); -}; - -// @todo get rid of "byId". It adds yet another manipulation of API that a dev must understand -export const useLinodesByIdQuery = ( - params: Params = {}, - filter: Filter = {}, - enabled: boolean = true -) => { - return useQuery<LinodeData, APIError[]>( - [queryKey, params, filter], - () => getLinodesRequest(params, filter), - { ...queryPresets.longLived, enabled } - ); -}; - -export const useAllLinodesQuery = ( - params: Params = {}, - filter: Filter = {}, - enabled: boolean = true -) => { - return useQuery<Linode[], APIError[]>( - [`${queryKey}-all`, params, filter], - () => getAllLinodesRequest(params, filter), - { ...queryPresets.longLived, enabled } - ); -}; - -export const useInfiniteLinodesQuery = (filter: Filter) => - useInfiniteQuery<ResourcePage<Linode>, APIError[]>( - [queryKey, filter], - ({ pageParam }) => getLinodes({ page: pageParam, page_size: 25 }, filter), - { - getNextPageParam: ({ page, pages }) => { - if (page === pages) { - return undefined; - } - return page + 1; - }, - } - ); - -const getIsTooEarlyForStats = (linodeCreated?: string) => { - if (!linodeCreated) { - return false; - } - - return parseAPIDate(linodeCreated) > DateTime.local().minus({ minutes: 7 }); -}; - -export const useLinodeStats = ( - id: number, - enabled = true, - linodeCreated?: string -) => { - return useQuery<Stats, APIError[]>( - [queryKey, id, 'stats'], - getIsTooEarlyForStats(linodeCreated) - ? () => Promise.reject([{ reason: STATS_NOT_READY_MESSAGE }]) - : () => getLinodeStats(id), - // We need to disable retries because the API will - // error if stats are not ready. If the default retry policy - // is used, a "stats not ready" state can't be shown because the - // query is still trying to request. - { enabled, refetchInterval: 30000, retry: false } - ); -}; - -export const useLinodeStatsByDate = ( - id: number, - year: string, - month: string, - enabled = true, - linodeCreated?: string -) => { - return useQuery<Stats, APIError[]>( - [queryKey, id, 'stats', 'date', year, month], - getIsTooEarlyForStats(linodeCreated) - ? () => Promise.reject([{ reason: STATS_NOT_READY_MESSAGE }]) - : () => getLinodeStatsByDate(id, year, month), - // We need to disable retries because the API will - // error if stats are not ready. If the default retry policy - // is used, a "stats not ready" state can't be shown because the - // query is still trying to request. - { enabled, refetchInterval: 30000, retry: false } - ); -}; - -export const useLinodeTransferByDate = ( - id: number, - year: string, - month: string, - enabled = true -) => { - return useQuery<NetworkTransfer, APIError[]>( - [queryKey, id, 'transfer', year, month], - () => getLinodeTransferByDate(id, year, month), - { enabled } - ); -}; - -export const useLinodeQuery = (id: number, enabled = true) => { - return useQuery<Linode, APIError[]>([queryKey, id], () => getLinode(id), { - enabled, - }); -}; - -export const useAllLinodeConfigsQuery = (id: number, enabled = true) => { - return useQuery<Config[], APIError[]>( - [queryKey, id, 'configs'], - () => getAllLinodeConfigs(id), - { enabled } - ); -}; - -export const useLinodeLishTokenQuery = (id: number) => { - return useQuery<{ lish_token: string }, APIError[]>( - [queryKey, id, 'lish-token'], - () => getLinodeLishToken(id), - { staleTime: Infinity } - ); -}; - -export const useLinodeFirewalls = (linodeID: number) => - useQuery<ResourcePage<Firewall>, APIError[]>( - [queryKey, linodeID, 'firewalls'], - () => getLinodeFirewalls(linodeID), - queryPresets.oneTimeFetch - ); - -/** Use with care; originally added to request all Linodes in a given region for IP sharing and transfer */ -const getAllLinodesRequest = ( - passedParams: Params = {}, - passedFilter: Filter = {} -) => - getAll<Linode>((params, filter) => - getLinodes({ ...params, ...passedParams }, { ...filter, ...passedFilter }) - )().then((data) => data.data); - -const getLinodesRequest = ( - passedParams: Params = {}, - passedFilter: Filter = {} -) => - getLinodes(passedParams, passedFilter).then((data) => ({ - linodes: listToItemsByID(data.data), - results: data.results, - })); - -const getAllLinodeConfigs = (id: number) => - getAll<Config>((params, filter) => - getLinodeConfigs(id, params, filter) - )().then((data) => data.data); - -export const getAllLinodeFirewalls = ( - linodeId: number, - passedParams: Params = {}, - passedFilter: Filter = {} -) => - getAll<Firewall>((params, filter) => - getLinodeFirewalls( - linodeId, - { ...params, ...passedParams }, - { ...filter, ...passedFilter } - ) - )().then((data) => data.data); diff --git a/packages/manager/src/queries/linodes/backups.ts b/packages/manager/src/queries/linodes/backups.ts new file mode 100644 index 00000000000..dbda7549442 --- /dev/null +++ b/packages/manager/src/queries/linodes/backups.ts @@ -0,0 +1,58 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { queryKey } from './linodes'; +import { + APIError, + cancelBackups, + enableBackups, + getLinodeBackups, + LinodeBackupsResponse, + restoreBackup, + takeSnapshot, +} from '@linode/api-v4'; + +export const useLinodeBackupsQuery = (id: number, enabled = true) => { + return useQuery<LinodeBackupsResponse, APIError[]>( + [queryKey, 'linode', id, 'backups'], + () => getLinodeBackups(id), + { enabled } + ); +}; + +export const useLinodeBackupsEnableMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[]>(() => enableBackups(id), { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + }); +}; + +export const useLinodeBackupsCancelMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[]>(() => cancelBackups(id), { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + }); +}; + +export const useLinodeBackupSnapshotMutation = (id: number) => { + return useMutation<{}, APIError[], { label: string }>(({ label }) => + takeSnapshot(id, label) + ); +}; + +export const useLinodeBackupRestoreMutation = () => { + return useMutation< + {}, + APIError[], + { + linodeId: number; + backupId: number; + targetLinodeId: number; + overwrite: boolean; + } + >(({ linodeId, backupId, targetLinodeId, overwrite }) => + restoreBackup(linodeId, backupId, targetLinodeId, overwrite) + ); +}; diff --git a/packages/manager/src/queries/linodes/disks.ts b/packages/manager/src/queries/linodes/disks.ts new file mode 100644 index 00000000000..58ab186f6ac --- /dev/null +++ b/packages/manager/src/queries/linodes/disks.ts @@ -0,0 +1,17 @@ +import { APIError, Disk, getLinodeDisks } from '@linode/api-v4'; +import { useQuery } from 'react-query'; +import { queryKey } from './linodes'; +import { getAll } from 'src/utilities/getAll'; + +export const useAllLinodeDisksQuery = (id: number, enabled = true) => { + return useQuery<Disk[], APIError[]>( + [queryKey, 'linode', id, 'disks', 'all'], + () => getAllLinodeDisks(id), + { enabled } + ); +}; + +const getAllLinodeDisks = (id: number) => + getAll<Disk>((params, filter) => getLinodeDisks(id, params, filter))().then( + (data) => data.data + ); diff --git a/packages/manager/src/queries/linodes/events.ts b/packages/manager/src/queries/linodes/events.ts new file mode 100644 index 00000000000..3213d50f42b --- /dev/null +++ b/packages/manager/src/queries/linodes/events.ts @@ -0,0 +1,7 @@ +import { EventWithStore } from 'src/events'; +import { queryKey } from './linodes'; + +export const linodeEventsHandler = ({ event, queryClient }: EventWithStore) => { + // For now, invalidate any linode query. We can fine tune later. + queryClient.invalidateQueries([queryKey]); +}; diff --git a/packages/manager/src/queries/linodes/firewalls.ts b/packages/manager/src/queries/linodes/firewalls.ts new file mode 100644 index 00000000000..364067f4981 --- /dev/null +++ b/packages/manager/src/queries/linodes/firewalls.ts @@ -0,0 +1,32 @@ +import { useQuery } from 'react-query'; +import { queryKey } from './linodes'; +import { queryPresets } from '../base'; +import { getAll } from 'src/utilities/getAll'; +import { + APIError, + Filter, + Firewall, + Params, + ResourcePage, + getLinodeFirewalls, +} from '@linode/api-v4'; + +export const useLinodeFirewalls = (linodeID: number) => + useQuery<ResourcePage<Firewall>, APIError[]>( + [queryKey, 'linode', linodeID, 'firewalls'], + () => getLinodeFirewalls(linodeID), + queryPresets.oneTimeFetch + ); + +export const getAllLinodeFirewalls = ( + linodeId: number, + passedParams: Params = {}, + passedFilter: Filter = {} +) => + getAll<Firewall>((params, filter) => + getLinodeFirewalls( + linodeId, + { ...params, ...passedParams }, + { ...filter, ...passedFilter } + ) + )().then((data) => data.data); diff --git a/packages/manager/src/queries/linodes/linodes.ts b/packages/manager/src/queries/linodes/linodes.ts new file mode 100644 index 00000000000..d9898b402d6 --- /dev/null +++ b/packages/manager/src/queries/linodes/linodes.ts @@ -0,0 +1,163 @@ +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from 'react-query'; +import { getAll } from 'src/utilities/getAll'; +import { queryPresets } from '../base'; +import { + APIError, + DeepPartial, + Filter, + Params, + ResourcePage, +} from '@linode/api-v4/lib/types'; +import { + Linode, + getLinodes, + getLinode, + getLinodeLishToken, + getLinodeConfigs, + Config, + updateLinode, + deleteLinode, + linodeBoot, + linodeReboot, + linodeShutdown, +} from '@linode/api-v4/lib/linodes'; + +export const queryKey = 'linodes'; + +export const useLinodesQuery = ( + params: Params = {}, + filter: Filter = {}, + enabled: boolean = true +) => { + return useQuery<ResourcePage<Linode>, APIError[]>( + [queryKey, 'paginated', params, filter], + () => getLinodes(params, filter), + { ...queryPresets.longLived, enabled, keepPreviousData: true } + ); +}; + +export const useAllLinodesQuery = ( + params: Params = {}, + filter: Filter = {}, + enabled: boolean = true +) => { + return useQuery<Linode[], APIError[]>( + [queryKey, 'all', params, filter], + () => getAllLinodesRequest(params, filter), + { ...queryPresets.longLived, enabled } + ); +}; + +export const useInfiniteLinodesQuery = (filter: Filter) => + useInfiniteQuery<ResourcePage<Linode>, APIError[]>( + [queryKey, 'infinite', filter], + ({ pageParam }) => getLinodes({ page: pageParam, page_size: 25 }, filter), + { + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + } + ); + +export const useLinodeQuery = (id: number, enabled = true) => { + return useQuery<Linode, APIError[]>( + [queryKey, 'linode', id], + () => getLinode(id), + { + enabled, + } + ); +}; + +export const useLinodeUpdateMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<Linode, APIError[], DeepPartial<Linode>>( + (data) => updateLinode(id, data), + { + onSuccess(linode) { + queryClient.invalidateQueries([queryKey]); + queryClient.setQueryData([queryKey, 'linode', id], linode); + }, + } + ); +}; + +export const useAllLinodeConfigsQuery = (id: number, enabled = true) => { + return useQuery<Config[], APIError[]>( + [queryKey, 'linode', id, 'configs'], + () => getAllLinodeConfigs(id), + { enabled } + ); +}; + +export const useLinodeLishTokenQuery = (id: number) => { + return useQuery<{ lish_token: string }, APIError[]>( + [queryKey, 'linode', id, 'lish-token'], + () => getLinodeLishToken(id), + { staleTime: Infinity } + ); +}; + +/** Use with care; originally added to request all Linodes in a given region for IP sharing and transfer */ +const getAllLinodesRequest = ( + passedParams: Params = {}, + passedFilter: Filter = {} +) => + getAll<Linode>((params, filter) => + getLinodes({ ...params, ...passedParams }, { ...filter, ...passedFilter }) + )().then((data) => data.data); + +const getAllLinodeConfigs = (id: number) => + getAll<Config>((params, filter) => + getLinodeConfigs(id, params, filter) + )().then((data) => data.data); + +export const useDeleteLinodeMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[]>(() => deleteLinode(id), { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + }); +}; + +export const useBootLinodeMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[], { config_id?: number }>( + ({ config_id }) => linodeBoot(id, config_id), + { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + } + ); +}; + +export const useRebootLinodeMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[], { config_id?: number }>( + ({ config_id }) => linodeReboot(id, config_id), + { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + } + ); +}; + +export const useShutdownLinodeMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[]>(() => linodeShutdown(id), { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + }); +}; diff --git a/packages/manager/src/queries/linodes/stats.ts b/packages/manager/src/queries/linodes/stats.ts new file mode 100644 index 00000000000..ddbcc25c4ca --- /dev/null +++ b/packages/manager/src/queries/linodes/stats.ts @@ -0,0 +1,76 @@ +import { + APIError, + NetworkTransfer, + Stats, + getLinodeStats, + getLinodeStatsByDate, + getLinodeTransferByDate, +} from '@linode/api-v4'; +import { DateTime } from 'luxon'; +import { useQuery } from 'react-query'; +import { parseAPIDate } from 'src/utilities/date'; +import { queryKey } from './linodes'; + +export const STATS_NOT_READY_API_MESSAGE = + 'Stats are unavailable at this time.'; +export const STATS_NOT_READY_MESSAGE = + 'Stats for this Linode are not available yet'; + +const getIsTooEarlyForStats = (linodeCreated?: string) => { + if (!linodeCreated) { + return false; + } + + return parseAPIDate(linodeCreated) > DateTime.local().minus({ minutes: 7 }); +}; + +export const useLinodeStats = ( + id: number, + enabled = true, + linodeCreated?: string +) => { + return useQuery<Stats, APIError[]>( + [queryKey, 'linode', id, 'stats'], + getIsTooEarlyForStats(linodeCreated) + ? () => Promise.reject([{ reason: STATS_NOT_READY_MESSAGE }]) + : () => getLinodeStats(id), + // We need to disable retries because the API will + // error if stats are not ready. If the default retry policy + // is used, a "stats not ready" state can't be shown because the + // query is still trying to request. + { enabled, refetchInterval: 30000, retry: false } + ); +}; + +export const useLinodeStatsByDate = ( + id: number, + year: string, + month: string, + enabled = true, + linodeCreated?: string +) => { + return useQuery<Stats, APIError[]>( + [queryKey, 'linode', id, 'stats', 'date', year, month], + getIsTooEarlyForStats(linodeCreated) + ? () => Promise.reject([{ reason: STATS_NOT_READY_MESSAGE }]) + : () => getLinodeStatsByDate(id, year, month), + // We need to disable retries because the API will + // error if stats are not ready. If the default retry policy + // is used, a "stats not ready" state can't be shown because the + // query is still trying to request. + { enabled, refetchInterval: 30000, retry: false } + ); +}; + +export const useLinodeTransferByDate = ( + id: number, + year: string, + month: string, + enabled = true +) => { + return useQuery<NetworkTransfer, APIError[]>( + [queryKey, 'linode', id, 'transfer', year, month], + () => getLinodeTransferByDate(id, year, month), + { enabled } + ); +}; diff --git a/packages/manager/src/queries/objectStorage.ts b/packages/manager/src/queries/objectStorage.ts index af627ef922d..7032395aa29 100644 --- a/packages/manager/src/queries/objectStorage.ts +++ b/packages/manager/src/queries/objectStorage.ts @@ -26,7 +26,7 @@ import { getObjectURL, ObjectStorageObjectURLOptions, ObjectStorageObjectURL, -} from '@linode/api-v4/lib/object-storage'; +} from '@linode/api-v4'; import { queryKey as accountSettingsQueryKey } from './accountSettings'; export interface BucketError { diff --git a/packages/manager/src/queries/types.ts b/packages/manager/src/queries/types.ts index 7a180136f33..47763a92dff 100644 --- a/packages/manager/src/queries/types.ts +++ b/packages/manager/src/queries/types.ts @@ -48,3 +48,12 @@ export const useSpecificTypes = (types: string[], enabled = true) => { })) ); }; + +export const useTypeQuery = (type: string, enabled = true) => { + return useQuery<LinodeType, APIError[]>({ + queryKey: specificTypesQueryKey(type), + queryFn: () => getType(type), + ...queryPresets.oneTimeFetch, + enabled, + }); +}; diff --git a/packages/manager/src/queries/volumes.ts b/packages/manager/src/queries/volumes.ts index 6e7d8c4a785..8e90d043d67 100644 --- a/packages/manager/src/queries/volumes.ts +++ b/packages/manager/src/queries/volumes.ts @@ -1,16 +1,14 @@ import { APIError, ResourcePage } from '@linode/api-v4/lib/types'; +import { Filter, Params } from '@linode/api-v4/src/types'; +import { EventWithStore } from 'src/events'; +import { getAll } from 'src/utilities/getAll'; +import { updateInPaginatedStore } from './base'; import { useInfiniteQuery, useMutation, useQuery, useQueryClient, } from 'react-query'; -import { getAll } from 'src/utilities/getAll'; -import { - doesItemExistInPaginatedStore, - getItemInPaginatedStore, - updateInPaginatedStore, -} from './base'; import { attachVolume, AttachVolumePayload, @@ -28,32 +26,19 @@ import { createVolume, getLinodeVolumes, } from '@linode/api-v4'; -import { Filter, Params } from '@linode/api-v4/src/types'; -import { EventWithStore } from 'src/events'; - -/** - * For Volumes, we must maintain the following stores to keep our cache up to date. - * When we manually mutate our cache, we must keep data under the following queryKeys up to date. - * - * Query Key Prefixes: - * - `volumes-all` - Contains an array of all volumes - * - Only use this when absolutely necessary - * - `volumes-list` - Contains ResourcePage of Paginated Volumes - * - [`volumes-list`, 'linode', id] - Conatins Paginated Volumes for a Specifc Linode - */ export const queryKey = 'volumes'; export const useVolumesQuery = (params: Params, filters: Filter) => useQuery<ResourcePage<Volume>, APIError[]>( - [`${queryKey}-list`, params, filters], + [queryKey, 'paginated', params, filters], () => getVolumes(params, filters), { keepPreviousData: true } ); export const useInfiniteVolumesQuery = (filter: Filter) => useInfiniteQuery<ResourcePage<Volume>, APIError[]>( - [queryKey, filter], + [queryKey, 'infinite', filter], ({ pageParam }) => getVolumes({ page: pageParam, page_size: 25 }, filter), { getNextPageParam: ({ page, pages }) => { @@ -65,30 +50,31 @@ export const useInfiniteVolumesQuery = (filter: Filter) => } ); -export const useLinodeVolumesQuery = ( - linodeId: number, - params: Params = {}, - filters: Filter = {}, - enabled = true -) => - useQuery<ResourcePage<Volume>, APIError[]>( - [`${queryKey}-list`, 'linode', linodeId, params, filters], - () => getLinodeVolumes(linodeId, params, filters), - { keepPreviousData: true, enabled } - ); export const useAllVolumesQuery = ( params: Params = {}, filters: Filter = {}, enabled = true ) => useQuery<Volume[], APIError[]>( - [`${queryKey}-all`, params, filters], + [queryKey, 'all', params, filters], () => getAllVolumes(params, filters), { enabled, } ); +export const useLinodeVolumesQuery = ( + linodeId: number, + params: Params = {}, + filters: Filter = {}, + enabled = true +) => + useQuery<ResourcePage<Volume>, APIError[]>( + [queryKey, 'linode', linodeId, params, filters], + () => getLinodeVolumes(linodeId, params, filters), + { keepPreviousData: true, enabled } + ); + export const useResizeVolumeMutation = () => { const queryClient = useQueryClient(); return useMutation< @@ -98,7 +84,7 @@ export const useResizeVolumeMutation = () => { >(({ volumeId, ...data }) => resizeVolume(volumeId, data), { onSuccess(volume) { updateInPaginatedStore<Volume>( - `${queryKey}-list`, + [queryKey, 'paginated'], volume.id, volume, queryClient @@ -115,7 +101,7 @@ export const useCloneVolumeMutation = () => { { volumeId: number } & CloneVolumePayload >(({ volumeId, ...data }) => cloneVolume(volumeId, data), { onSuccess() { - queryClient.invalidateQueries(`${queryKey}-list`); + queryClient.invalidateQueries([queryKey]); }, }); }; @@ -126,7 +112,7 @@ export const useDeleteVolumeMutation = () => { ({ id }) => deleteVolume(id), { onSuccess() { - queryClient.invalidateQueries(`${queryKey}-list`); + queryClient.invalidateQueries([queryKey]); }, } ); @@ -136,7 +122,7 @@ export const useCreateVolumeMutation = () => { const queryClient = useQueryClient(); return useMutation<Volume, APIError[], VolumeRequestPayload>(createVolume, { onSuccess() { - queryClient.invalidateQueries(`${queryKey}-list`); + queryClient.invalidateQueries([queryKey]); }, }); }; @@ -150,11 +136,14 @@ export const useUpdateVolumeMutation = () => { >(({ volumeId, ...data }) => updateVolume(volumeId, data), { onSuccess(volume) { updateInPaginatedStore<Volume>( - `${queryKey}-list`, + [queryKey, 'paginated'], volume.id, volume, queryClient ); + if (volume.linode_id) { + queryClient.invalidateQueries([queryKey, 'linode', volume.linode_id]); + } }, }); }; @@ -168,16 +157,14 @@ export const useAttachVolumeMutation = () => { >(({ volumeId, ...data }) => attachVolume(volumeId, data), { onSuccess(volume) { updateInPaginatedStore<Volume>( - `${queryKey}-list`, + [queryKey, 'paginated'], volume.id, volume, queryClient ); - queryClient.invalidateQueries([ - `${queryKey}-list`, - 'linode', - volume.linode_id, - ]); + if (volume.linode_id) { + queryClient.invalidateQueries([queryKey, 'linode', volume.linode_id]); + } }, }); }; @@ -185,123 +172,17 @@ export const useAttachVolumeMutation = () => { export const useDetachVolumeMutation = () => useMutation<{}, APIError[], { id: number }>(({ id }) => detachVolume(id)); -export const volumeEventsHandler = (event: EventWithStore) => { - const { - event: { action, status, entity }, - queryClient, - } = event; - - // Keep the getAll query up to date so that when we have to use it, it contains accurate data - queryClient.invalidateQueries(`${queryKey}-all`); +export const volumeEventsHandler = ({ event, queryClient }: EventWithStore) => { + if (['finished', 'failed', 'notification'].includes(event.status)) { + queryClient.invalidateQueries([queryKey]); + } - switch (action) { - case 'volume_create': - switch (status) { - case 'started': - case 'scheduled': - return; - case 'failed': - case 'finished': - case 'notification': - queryClient.invalidateQueries(`${queryKey}-list`); - return; - } - case 'volume_attach': - switch (status) { - case 'scheduled': - case 'started': - case 'notification': - return; - case 'finished': - const volume = getItemInPaginatedStore<Volume>( - `${queryKey}-list`, - entity!.id, - queryClient - ); - if (volume && volume.linode_id === null) { - queryClient.invalidateQueries(`${queryKey}-list`); - } - return; - case 'failed': - // This means a attach was unsuccessful. Remove associated Linode. - updateInPaginatedStore<Volume>( - `${queryKey}-list`, - entity!.id, - { - linode_id: null, - linode_label: null, - }, - queryClient - ); - return; - } - case 'volume_update': - return; - case 'volume_detach': - switch (status) { - case 'scheduled': - case 'failed': - case 'started': - return; - case 'notification': - case 'finished': - const volume = getItemInPaginatedStore<Volume>( - `${queryKey}-list`, - entity!.id, - queryClient - ); - updateInPaginatedStore<Volume>( - `${queryKey}-list`, - entity!.id, - { - linode_id: null, - linode_label: null, - }, - queryClient - ); - if (volume && volume.linode_id !== null) { - queryClient.invalidateQueries([ - `${queryKey}-list`, - 'linode', - volume.linode_id, - ]); - } - return; - } - case 'volume_resize': - // This means a resize was successful. Transition from 'resizing' to 'active'. - updateInPaginatedStore<Volume>( - `${queryKey}-list`, - entity!.id, - { - status: 'active', - }, - queryClient - ); - return; - case 'volume_clone': - // This is very hacky, but we have no way to know when a cloned volume should transition - // from 'creating' to 'active' so we will wait a bit after a volume is cloned, then refresh - // and hopefully the volume is active. - setTimeout(() => { - queryClient.invalidateQueries(`${queryKey}-list`); - }, 5000); - return; - case 'volume_delete': - if ( - doesItemExistInPaginatedStore( - `${queryKey}-list`, - entity!.id, - queryClient - ) - ) { - queryClient.invalidateQueries(`${queryKey}-list`); - } - return; - case 'volume_migrate': - return; - default: - return; + if (event.action === 'volume_clone') { + // The API gives us no way to know when a cloned volume transitions from + // creating to active, so we will just refresh after 10 seconds + setTimeout(() => { + queryClient.invalidateQueries([queryKey]); + }, 10000); } }; diff --git a/packages/manager/src/store/linodes/linode.requests.ts b/packages/manager/src/store/linodes/linode.requests.ts index 85ef5245c9e..e391173cfce 100644 --- a/packages/manager/src/store/linodes/linode.requests.ts +++ b/packages/manager/src/store/linodes/linode.requests.ts @@ -9,7 +9,7 @@ import { updateLinode as _updateLinode, } from '@linode/api-v4/lib/linodes'; import { queryKey as firewallsQueryKey } from 'src/queries/firewalls'; -import { getAllLinodeFirewalls } from 'src/queries/linodes'; +import { getAllLinodeFirewalls } from 'src/queries/linodes/firewalls'; import { getAll } from 'src/utilities/getAll'; import { createRequestThunk } from '../store.helpers'; import { ThunkActionCreator } from '../types'; @@ -58,11 +58,9 @@ export const deleteLinode = createRequestThunk( } // Removed unneeded volume stores - queryClient.removeQueries([`${volumesQueryKey}-list`, 'linode', linodeId]); - - // A Linode that was deleted may have had volumes attached. - // Invalidate paginated volume stores so they will reflect there is no attached Linode. - queryClient.invalidateQueries([`${volumesQueryKey}-list`]); + queryClient.removeQueries([volumesQueryKey, 'linode', linodeId]); + // Invalidate volume stores so they will reflect there is no attached Linode. + queryClient.invalidateQueries([volumesQueryKey]); return response; } diff --git a/packages/manager/src/themes.ts b/packages/manager/src/themes.ts index 673ef70e5e8..3e747e3a776 100644 --- a/packages/manager/src/themes.ts +++ b/packages/manager/src/themes.ts @@ -382,15 +382,6 @@ const darkThemeOptions: ThemeOptions = { }, }, }, - MuiIconButton: { - styleOverrides: { - root: { - '&:hover': { - color: primaryColors.light, - }, - }, - }, - }, MuiInput: { styleOverrides: { input: { diff --git a/packages/manager/src/utilities/ga.ts b/packages/manager/src/utilities/ga.ts index dda9e4c3cb2..4b3a53d99eb 100644 --- a/packages/manager/src/utilities/ga.ts +++ b/packages/manager/src/utilities/ga.ts @@ -1,5 +1,6 @@ import { event } from 'react-ga'; -import { GA_ID } from 'src/constants'; +import { GA_ID, ADOBE_ANALYTICS_URL } from 'src/constants'; +import { reportException } from 'src/exceptionReporting'; interface AnalyticsEvent { category: string; @@ -8,10 +9,26 @@ interface AnalyticsEvent { value?: number; } -/* - * Will throw error unless analytics is initialized - */ export const sendEvent = (eventPayload: AnalyticsEvent): void => { + if (!ADOBE_ANALYTICS_URL) { + return; + } + + // Send a Direct Call Rule if our environment is configured with an Adobe Launch script + try { + (window as any)._satellite.track('custom event', { + category: eventPayload.category, + action: eventPayload.action, + label: eventPayload.label, + value: eventPayload.value, + }); + } catch (error) { + reportException(error, { + message: + 'An error occurred when tracking a custom event. Adobe Launch script not loaded correctly; no analytics will be sent.', + }); + } + /** only send events if we have a GA ID */ return !!GA_ID ? event(eventPayload) : undefined; }; diff --git a/packages/manager/src/utilities/testHelpers.tsx b/packages/manager/src/utilities/testHelpers.tsx index 93926a4a632..315daccda04 100644 --- a/packages/manager/src/utilities/testHelpers.tsx +++ b/packages/manager/src/utilities/testHelpers.tsx @@ -13,6 +13,7 @@ import thunk from 'redux-thunk'; import { FlagSet } from 'src/featureFlags'; import LinodeThemeWrapper from 'src/LinodeThemeWrapper'; import { queryClientFactory } from 'src/queries/base'; +import { setupInterceptors } from 'src/request'; import { storeFactory, ApplicationState, @@ -55,6 +56,13 @@ export const wrapWithTheme = (ui: any, options: Options = {}) => { const storeToPass = customStore ? baseStore(customStore) : storeFactory(queryClient); + + // we have to call setupInterceptors so that our API error normalization works as expected + // I'm sorry that it makes us pass it the "ApplicationStore" + setupInterceptors( + configureStore<ApplicationState>([thunk])(defaultState) + ); + return ( <Provider store={storeToPass}> <QueryClientProvider client={passedQueryClient || queryClient}> diff --git a/packages/validation/src/account.schema.ts b/packages/validation/src/account.schema.ts index e2bd71213d9..ff600b9ce78 100644 --- a/packages/validation/src/account.schema.ts +++ b/packages/validation/src/account.schema.ts @@ -32,19 +32,6 @@ export const updateOAuthClientSchema = object({ redirect_uri: string(), }); -export const StagePaypalPaymentSchema = object({ - cancel_url: string().required( - 'You must provide a URL to redirect on cancel.' - ), - redirect_url: string().required('You must provide a redirect URL.'), - usd: string().required('USD payment amount is required.'), -}); - -export const ExecutePaypalPaymentSchema = object({ - payer_id: string().required('You must provide a payer ID.'), - payment_id: string().required('You must provide a payment ID (from Paypal).'), -}); - export const PaymentSchema = object({ usd: string().required('USD payment amount is required.'), }); diff --git a/yarn.lock b/yarn.lock index f852c682859..e6b50071f2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2654,14 +2654,14 @@ memoizerific "^1.11.3" prop-types "^15.7.2" -"@storybook/addons@^7.0.0-beta.60": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-7.0.0-beta.62.tgz#8c97f775ac09ca599120a01dba9d9d44bb4c425b" - integrity sha512-KjYEdwzgeencUnmegMA4iTW/Rwa3sYvhKBIscHjJ+KAbb8rwYZyL4phJkhglWGQtLA20prNyI7Qir6lxhheWPw== +"@storybook/addons@^7.0.0": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-7.0.7.tgz#cef9a022bcdd14e79d4fc364fb3eb8e44e94ed18" + integrity sha512-it8NWXsdm3dhjc237d9jj7dGJf6eHDfuDv12nirV64J1dzWrnW+lONeZMPMgxxdLlgYfxH52fLgjcw/dAC/E+Q== dependencies: - "@storybook/manager-api" "7.0.0-beta.62" - "@storybook/preview-api" "7.0.0-beta.62" - "@storybook/types" "7.0.0-beta.62" + "@storybook/manager-api" "7.0.7" + "@storybook/preview-api" "7.0.7" + "@storybook/types" "7.0.7" "@storybook/addons@~7.0.6": version "7.0.6" @@ -2672,13 +2672,13 @@ "@storybook/preview-api" "7.0.6" "@storybook/types" "7.0.6" -"@storybook/api@^7.0.0-beta.60": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-7.0.0-beta.62.tgz#e090558f8b6f1ba6cbeefce6bd69c28e59517ed9" - integrity sha512-RiMONl88tRm0lS1HvPvRYqGY1+hUi/UVZPuUDLxQLs/GB8qQK9+/E1PyKWNhDlxyZG4+aHUgqL7x8LVaUKLBsg== +"@storybook/api@^7.0.0": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-7.0.7.tgz#ae2c3f435b025f0aac86da1033ad3f0cbceb6ef4" + integrity sha512-0++LcK6PX1Z2HsI9fyZyqvmeFrB5NDMcsbmIvJfA2NfK92UW8y7t6Ft2fq/2jUCJcWT8Jp3xpatUvYb28irfwg== dependencies: - "@storybook/client-logger" "7.0.0-beta.62" - "@storybook/manager-api" "7.0.0-beta.62" + "@storybook/client-logger" "7.0.7" + "@storybook/manager-api" "7.0.7" "@storybook/blocks@7.0.6": version "7.0.6" @@ -2756,18 +2756,6 @@ remark-slug "^6.0.0" rollup "^2.25.0 || ^3.3.0" -"@storybook/channel-postmessage@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-7.0.0-beta.62.tgz#4778dc9b35bde3e3c0a892b5cc3ca1c249870918" - integrity sha512-tqc2Zgpt0GK6bMx8GWSA+8OR33AK2Mnglh4WMhCVbvDwhooRxY2r6jcCYCsfLzHm3c3/NJ6x9RxORNusnqvZUw== - dependencies: - "@storybook/channels" "7.0.0-beta.62" - "@storybook/client-logger" "7.0.0-beta.62" - "@storybook/core-events" "7.0.0-beta.62" - "@storybook/global" "^5.0.0" - qs "^6.10.0" - telejson "^7.0.3" - "@storybook/channel-postmessage@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-7.0.6.tgz#46f2887579ea7065480c127a55d48a7d1a935614" @@ -2780,6 +2768,18 @@ qs "^6.10.0" telejson "^7.0.3" +"@storybook/channel-postmessage@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-7.0.7.tgz#8b00a45f3a078946169f7222c320d8d1e17b67c2" + integrity sha512-XMtYfcaE0UoY/V7K1cTu9PcWETD4iyWb/Yswc4F9VrPw0Ui4UwGS1j4iaAu8DC06yyoJs4XvxYFBMlCQmKja6A== + dependencies: + "@storybook/channels" "7.0.7" + "@storybook/client-logger" "7.0.7" + "@storybook/core-events" "7.0.7" + "@storybook/global" "^5.0.0" + qs "^6.10.0" + telejson "^7.0.3" + "@storybook/channel-websocket@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-7.0.6.tgz#281663622eb1deb6c73271ddf5bb1a33c91afdaf" @@ -2790,16 +2790,16 @@ "@storybook/global" "^5.0.0" telejson "^7.0.3" -"@storybook/channels@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-7.0.0-beta.62.tgz#e22558a066fbbd562d17e9fb1420b2c68bf148ab" - integrity sha512-WG0bH5EYIi2Eh7iRNq9ScvQlzuBZqtlg0CV8FdLdEeDp6LuYRgIabdjON0PDGagwpir0ozDAqF5sRMwYkhuVPg== - "@storybook/channels@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-7.0.6.tgz#c025eeb45072a89b82e111bea021b09f4cc80b96" integrity sha512-+34cVmrXZ3lb1s5tDK+OWd5HLtEPSUMas0VKFJ0k9LBpFlVl9aiCZBJRvSYmWL7beauUfa+HSmJgjlD6228ChQ== +"@storybook/channels@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-7.0.7.tgz#3f3962be97b447752db99a78ef7beea9f94d75a4" + integrity sha512-Om4ovBLNw8pVrBu83MpOKgAuGO9Dpr1Coh2qp8t64WRPkejX1mxOY9IgH723//zH3igx8LCkf9rvBvcrsyaScQ== + "@storybook/cli@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-7.0.6.tgz#ccf52234208e999f2f884f201375b82304ddd269" @@ -2852,20 +2852,6 @@ "@storybook/client-logger" "7.0.6" "@storybook/preview-api" "7.0.6" -"@storybook/client-logger@7.0.0-beta.60": - version "7.0.0-beta.60" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.0.0-beta.60.tgz#a307518cd2ac97e321d3432394fb2c5dababdac2" - integrity sha512-L9aT6KnbIwZ9MuH77YpkIOtf2vTDeDvVe+8jUm59ov5L0XKZ6RTKdyKonAvp+CBuyp2XQS8+E3A/lnezACIzLQ== - dependencies: - "@storybook/global" "^5.0.0" - -"@storybook/client-logger@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.0.0-beta.62.tgz#c39b84eba62cb880ca38500a52f80c8267aeddee" - integrity sha512-Frp6aqRgFbqyT5LzBSjuxx6rzZeGmDcGRJkIy8BSccmkRx+PQs0sJlkRfDk08u9pseaEpOJZPAvD1SuGUowO6g== - dependencies: - "@storybook/global" "^5.0.0" - "@storybook/client-logger@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.0.6.tgz#2e4a419b498efaf9a4eca69abc2c879d45a764bb" @@ -2873,6 +2859,13 @@ dependencies: "@storybook/global" "^5.0.0" +"@storybook/client-logger@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.0.7.tgz#1f91eb2785111c94b7946bda2d32199c11045d71" + integrity sha512-EclHjDs5HwHMKB4X2orn/KKA0DTIDmp4AXAUJGRfxb5ArpKEb7tXLHsgrRBlaoz1j5LAwKTmEyZOONh9G3etjg== + dependencies: + "@storybook/global" "^5.0.0" + "@storybook/codemod@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-7.0.6.tgz#e5904c72261422b21da6ec3ef88d32daa4e96e64" @@ -2906,16 +2899,16 @@ use-resize-observer "^9.1.0" util-deprecate "^1.0.2" -"@storybook/components@^7.0.0-beta.60": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-7.0.0-beta.62.tgz#86811075e7dcb65a1c464c04396c6a0e4f4bcc4e" - integrity sha512-PjAt9vCIomJodxwbgsdnqFsE20ka90C1s37oosXF3/9XLZ1BP/MOEEJKB/Gb8DWcgy4wnPgwDfJJspw/HkZ4VQ== +"@storybook/components@^7.0.0": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-7.0.7.tgz#3b4829e8287e81a6850417c40f6f28e6ea5580bc" + integrity sha512-6PLs9LMkBuhH/w4bSJ72tYgICMbOOIHuoB/fQdVlzhsdnXL2fM/v4RVW2N7v+Oz3lYXp/JtV8V9Ub8h6eDQKXg== dependencies: - "@storybook/client-logger" "7.0.0-beta.62" - "@storybook/csf" next + "@storybook/client-logger" "7.0.7" + "@storybook/csf" "^0.1.0" "@storybook/global" "^5.0.0" - "@storybook/theming" "7.0.0-beta.62" - "@storybook/types" "7.0.0-beta.62" + "@storybook/theming" "7.0.7" + "@storybook/types" "7.0.7" memoizerific "^1.11.3" use-resize-observer "^9.1.0" util-deprecate "^1.0.2" @@ -2953,16 +2946,16 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/core-events@7.0.0-beta.62", "@storybook/core-events@^7.0.0-beta.60": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.0.0-beta.62.tgz#66806bb1aad633468382ca88cc2a912fd5c5cb19" - integrity sha512-/301tBWbUnhv4tTg5feTR3r1iZ+ep+drEX3ii61SfQNWLugeo9sLIldtxRuEElT6X3Xypw4nCnd+fc3IKDs/Xw== - "@storybook/core-events@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.0.6.tgz#f3b4876cb9003a6bbdbd9602d017e2b37ec86f78" integrity sha512-kGrtjlYtjd4iTVk+Phb4CymZaVkB+MGscKAgcO8gfgJ/Q/gq8HQLVZSIzeoCDcDSHOGlBzbg2WVtdHIHhCKlOQ== +"@storybook/core-events@7.0.7", "@storybook/core-events@^7.0.0": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.0.7.tgz#9acb6425d0a2a3d25becc21980c2c678c6c5548e" + integrity sha512-XNsR2RgaL2vBwuqsu+KA1DzGmB1UFfrAhpxhmyWTKDCniwtTLlaXgfKbqwcrOrPu/o1YswgIup/9UHepRHaf4A== + "@storybook/core-server@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.0.6.tgz#eb82f1b6490e73c28598e6a99957071649f4d6ab" @@ -3041,13 +3034,6 @@ dependencies: type-fest "^2.19.0" -"@storybook/csf@next": - version "0.0.2-next.10" - resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.2-next.10.tgz#be71280e08bafae97134770ed9d0e5c75bc02f6c" - integrity sha512-m2PFgBP/xRIF85VrDhvesn9ktaD2pN3VUjvMqkAL/cINp/3qXsCyI81uw7N5VEOkQAbWrY2FcydnvEPDEdE8fA== - dependencies: - type-fest "^2.19.0" - "@storybook/docs-mdx@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@storybook/docs-mdx/-/docs-mdx-0.1.0.tgz#33ba0e39d1461caf048b57db354b2cc410705316" @@ -3071,27 +3057,6 @@ resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== -"@storybook/manager-api@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-7.0.0-beta.62.tgz#494fd87c172282cae3de5f5d312316417039a910" - integrity sha512-Ec8QMKbwYbAdstc/SQ8t4L1eNPSEhKeAgYLEygFLF2WH3FZZ0VwU/+5NM8Ksi78MolXT4UehfVFskuf5dud1lQ== - dependencies: - "@storybook/channels" "7.0.0-beta.62" - "@storybook/client-logger" "7.0.0-beta.62" - "@storybook/core-events" "7.0.0-beta.62" - "@storybook/csf" next - "@storybook/global" "^5.0.0" - "@storybook/router" "7.0.0-beta.62" - "@storybook/theming" "7.0.0-beta.62" - "@storybook/types" "7.0.0-beta.62" - dequal "^2.0.2" - lodash "^4.17.21" - memoizerific "^1.11.3" - semver "^7.3.7" - store2 "^2.14.2" - telejson "^7.0.3" - ts-dedent "^2.0.0" - "@storybook/manager-api@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-7.0.6.tgz#7a1539ff0d6d3fbd7527ff5e7357898abf092d14" @@ -3113,6 +3078,27 @@ telejson "^7.0.3" ts-dedent "^2.0.0" +"@storybook/manager-api@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-7.0.7.tgz#0b3a839f4c48c84424ce0462739afb5c7e08d1b7" + integrity sha512-QTd/P72peAhofKqK+8yzIO9iWAEfPn8WUGGveV2KGaTlSlgbr87RLHEKilcXMZcYhBWC9izFRmjKum9ROdskrQ== + dependencies: + "@storybook/channels" "7.0.7" + "@storybook/client-logger" "7.0.7" + "@storybook/core-events" "7.0.7" + "@storybook/csf" "^0.1.0" + "@storybook/global" "^5.0.0" + "@storybook/router" "7.0.7" + "@storybook/theming" "7.0.7" + "@storybook/types" "7.0.7" + dequal "^2.0.2" + lodash "^4.17.21" + memoizerific "^1.11.3" + semver "^7.3.7" + store2 "^2.14.2" + telejson "^7.0.3" + ts-dedent "^2.0.0" + "@storybook/manager@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/manager/-/manager-7.0.6.tgz#5342a926c43cb1c36ee9fde742ec457e95cf3afa" @@ -3138,40 +3124,39 @@ resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-7.0.6.tgz#9aa1d5f679fdb29f20820b5d08e255bf624811d6" integrity sha512-NDAA2I2LqDKXqnCMgnNNpwU87rNYmf5tjLg0MK9NFR79zSdjPryy+64oBWoNjGdub342Y9fyc3gTV7OIQdvH0Q== -"@storybook/preview-api@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.0.0-beta.62.tgz#0e2ee8c020751310c79194552dd9e99733465b0d" - integrity sha512-i8zaFqGojcKxkIcztTvX+jnd7XF1LeaXZl6OLW+WORByA0ytIFQOhJfAES+tfdblTK1h7Xg6mC8uhb9B6aAjPw== +"@storybook/preview-api@7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.0.6.tgz#2f12a0035243496e8777cb8122bc0b2db5820d16" + integrity sha512-uNsedNyiEccBV2EDUC/xcKTbmiNCYuVHbgOoWTmBz0ZqFo9bX0jxkpyYWHEhJM79qqVqmrpiQ5jbS8QKn8TIxQ== dependencies: - "@storybook/channel-postmessage" "7.0.0-beta.62" - "@storybook/channels" "7.0.0-beta.62" - "@storybook/client-logger" "7.0.0-beta.62" - "@storybook/core-events" "7.0.0-beta.62" - "@storybook/csf" next + "@storybook/channel-postmessage" "7.0.6" + "@storybook/channels" "7.0.6" + "@storybook/client-logger" "7.0.6" + "@storybook/core-events" "7.0.6" + "@storybook/csf" "^0.1.0" "@storybook/global" "^5.0.0" - "@storybook/types" "7.0.0-beta.62" + "@storybook/types" "7.0.6" "@types/qs" "^6.9.5" dequal "^2.0.2" lodash "^4.17.21" memoizerific "^1.11.3" qs "^6.10.0" - slash "^3.0.0" synchronous-promise "^2.0.15" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/preview-api@7.0.6": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.0.6.tgz#2f12a0035243496e8777cb8122bc0b2db5820d16" - integrity sha512-uNsedNyiEccBV2EDUC/xcKTbmiNCYuVHbgOoWTmBz0ZqFo9bX0jxkpyYWHEhJM79qqVqmrpiQ5jbS8QKn8TIxQ== +"@storybook/preview-api@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.0.7.tgz#8f8da37418d91dd3d9009df914cb26add87a8e67" + integrity sha512-R5pmGTodpu6hbwEg2RM2ulWtW3d426YzsisHrZJ+FT9lecWauN1y9xHCz7HdNzEFhT8r4YOa24L9ZS3mosZ7hA== dependencies: - "@storybook/channel-postmessage" "7.0.6" - "@storybook/channels" "7.0.6" - "@storybook/client-logger" "7.0.6" - "@storybook/core-events" "7.0.6" + "@storybook/channel-postmessage" "7.0.7" + "@storybook/channels" "7.0.7" + "@storybook/client-logger" "7.0.7" + "@storybook/core-events" "7.0.7" "@storybook/csf" "^0.1.0" "@storybook/global" "^5.0.0" - "@storybook/types" "7.0.6" + "@storybook/types" "7.0.7" "@types/qs" "^6.9.5" dequal "^2.0.2" lodash "^4.17.21" @@ -3232,15 +3217,6 @@ type-fest "^2.19.0" util-deprecate "^1.0.2" -"@storybook/router@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-7.0.0-beta.62.tgz#ff1f35d662ff6e67eb6218dd30192931fd423c37" - integrity sha512-y8ZfuU128RukhJNJka0IY5CjQFgOMSALTyRma8gV53OfOPYN4MNQ8AH/4nqAuZ05yES8yqsfoIOKCajIC2CTlg== - dependencies: - "@storybook/client-logger" "7.0.0-beta.62" - memoizerific "^1.11.3" - qs "^6.10.0" - "@storybook/router@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/router/-/router-7.0.6.tgz#051db34924b9b248a93455fbdd8b8f7f20c09543" @@ -3250,6 +3226,15 @@ memoizerific "^1.11.3" qs "^6.10.0" +"@storybook/router@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-7.0.7.tgz#9f60dc63f083b3a24e40e2461cc18eb531dfd451" + integrity sha512-/lM8/NHQKeshfnC3ayFuO8Y9TCSHnCAPRhIsVxvanBzcj+ILbCIyZ+TspvB3hT4MbX/Ez+JR8VrMbjXIGwmH8w== + dependencies: + "@storybook/client-logger" "7.0.7" + memoizerific "^1.11.3" + qs "^6.10.0" + "@storybook/telemetry@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-7.0.6.tgz#0e60403f6fa07c73b4ce2bd74984c2951656fb1a" @@ -3265,16 +3250,6 @@ nanoid "^3.3.1" read-pkg-up "^7.0.1" -"@storybook/theming@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-7.0.0-beta.62.tgz#d352fcb7423407b49b58de29b3e62ee8c1b2cd09" - integrity sha512-aHH6Aqt4BifegojoKczSandUCHruuUJb9I7iZOAf40RpXNubpuKDDmUC6Za98WqxPFBMCwZ6eWxPxHLnj7yFUg== - dependencies: - "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" - "@storybook/client-logger" "7.0.0-beta.62" - "@storybook/global" "^5.0.0" - memoizerific "^1.11.3" - "@storybook/theming@7.0.6", "@storybook/theming@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-7.0.6.tgz#b42c1840c12a1c0198c704ce746992223ffa4e8b" @@ -3285,26 +3260,16 @@ "@storybook/global" "^5.0.0" memoizerific "^1.11.3" -"@storybook/theming@^7.0.0-beta.60": - version "7.0.0-beta.60" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-7.0.0-beta.60.tgz#5c40f09b49262b1ffd4b5b29cd0751ce563c9e21" - integrity sha512-BBaHioOI+eO+D0tUGKkqM+5jd/4yWANy+NIlaIg0dxkF1094amn19ziwH0Bx43NIdkdlpFwFDMdcNvOamTpHbw== +"@storybook/theming@7.0.7", "@storybook/theming@^7.0.0": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-7.0.7.tgz#263c162825e3c0ff4eb53e204e586077e596037e" + integrity sha512-InTZe+Sgco1NsxgiG+cyUKWQe3GsjlIyU/o5qDdtOTXcZ64HzyBuAZlAequSddqfDeMDqxRFPc2w1J28MAUHxA== dependencies: "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" - "@storybook/client-logger" "7.0.0-beta.60" + "@storybook/client-logger" "7.0.7" "@storybook/global" "^5.0.0" memoizerific "^1.11.3" -"@storybook/types@7.0.0-beta.62": - version "7.0.0-beta.62" - resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.0.0-beta.62.tgz#0001d709101cfbe043f9753798226404779c3635" - integrity sha512-TqZTTd5V75wW9fTTz4D8JVfbCxZ8wjfJ385M6czB/gCjpvFUq15VCySEvK1xzhTiXWeyHZ2IXI5kTrj9vH3S1w== - dependencies: - "@storybook/channels" "7.0.0-beta.62" - "@types/babel__core" "^7.0.0" - "@types/express" "^4.7.0" - file-system-cache "^2.0.0" - "@storybook/types@7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.0.6.tgz#3bdc0f4ade0c21548a3b6423b64c8ae1e797ba35" @@ -3315,6 +3280,16 @@ "@types/express" "^4.7.0" file-system-cache "^2.0.0" +"@storybook/types@7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.0.7.tgz#e42ab7e44b3d7d49f50592f0d9d03023f44af079" + integrity sha512-v9piuwp8FvTiHXIOOi5lEyTEJKhnbcbhVxgJ3VFhhXYFd0DTz6Bst0XIIgkgs21ITb3xhkfPbCRUueMcbXO1MA== + dependencies: + "@storybook/channels" "7.0.7" + "@types/babel__core" "^7.0.0" + "@types/express" "^4.7.0" + file-system-cache "^2.0.0" + "@svgr/babel-plugin-add-jsx-attribute@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" @@ -5981,11 +5956,6 @@ commander@^4.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -6345,10 +6315,10 @@ cypress-vite@^1.3.2: dependencies: debug "^4.3.4" -cypress@^12.2.0: - version "12.6.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.6.0.tgz#d71a82639756173c0682b3d467eb9f0523460e91" - integrity sha512-WdHSVaS1lumSd5XpVTslZd8ui9GIGphrzvXq9+3DtVhqjRZC5M70gu5SW/Y/SLPq3D1wiXGZoHC6HJ7ESVE2lw== +cypress@^12.11.0: + version "12.11.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.11.0.tgz#b46dc6a1d0387f59a4b5c6a18cc03884fd61876e" + integrity sha512-TJE+CCWI26Hwr5Msb9GpQhFLubdYooW0fmlPwTsfiyxmngqc7+SZGLPeIkj2dTSSZSEtpQVzOzvcnzH0o8G7Vw== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -6364,7 +6334,7 @@ cypress@^12.2.0: check-more-types "^2.24.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" - commander "^5.1.0" + commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" debug "^4.3.4" @@ -6382,7 +6352,7 @@ cypress@^12.2.0: listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.6" + minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" proxy-from-env "1.0.0" @@ -11273,7 +11243,7 @@ minimatch@3.1.2, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -13824,17 +13794,17 @@ store2@^2.14.2: resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.2.tgz#56138d200f9fe5f582ad63bc2704dbc0e4a45068" integrity sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w== -storybook-dark-mode-v7@3.0.0-alpha.0: - version "3.0.0-alpha.0" - resolved "https://registry.yarnpkg.com/storybook-dark-mode-v7/-/storybook-dark-mode-v7-3.0.0-alpha.0.tgz#ffb4a7ebcd87c91242cc6d2245e8e4c728bd8f09" - integrity sha512-zAxHayO9ufgtr7g+SaV6nJI73ubEtWC3Z4P0SY80QqVonLVSdLWNIIRMUK88ImCVB7VSG4QyZDTW16BgJ+86Ew== +storybook-dark-mode@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/storybook-dark-mode/-/storybook-dark-mode-3.0.0.tgz#4c9d59de28c84a0c159ddc3d18e4e8e33db779a9" + integrity sha512-aeAvqP/mmdccEiCsvx6aw3M0i7mZSiXROsrAsEQN8vl1lAg3FZN+y3Xu/f+ye59wLMRuKJC/JBp7E3/H7vLBRQ== dependencies: - "@storybook/addons" "^7.0.0-beta.60" - "@storybook/api" "^7.0.0-beta.60" - "@storybook/components" "^7.0.0-beta.60" - "@storybook/core-events" "^7.0.0-beta.60" + "@storybook/addons" "^7.0.0" + "@storybook/api" "^7.0.0" + "@storybook/components" "^7.0.0" + "@storybook/core-events" "^7.0.0" "@storybook/global" "^5.0.0" - "@storybook/theming" "^7.0.0-beta.60" + "@storybook/theming" "^7.0.0" fast-deep-equal "^3.1.3" memoizerific "^1.11.3"