From 4dcfcb636c72bb1fda47a310d190d1a78f622906 Mon Sep 17 00:00:00 2001 From: mduda-akamai Date: Thu, 15 May 2025 13:25:38 +0200 Subject: [PATCH 01/59] upcoming: [DPS-33104] DataStream: routes, feature flag, tabs (#12155) * upcoming: [DPS-33104] DataStream: routes, feature flag, tabs * [DPS-33104] CR: Fix braces * [DPS-33104] CR: remove braces, idx, default export * [DPS-33104] CR: update header's spacing * [DPS-33104] CR: remove react-router-dom dependencies * [DPS-33104] CR: replace TabLinkList with TanStackTabLinkList --------- Co-authored-by: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> --- ...r-12155-upcoming-features-1746446424705.md | 5 ++ packages/manager/eslint.config.js | 1 + .../src/components/PrimaryNav/PrimaryNav.tsx | 7 ++ .../manager/src/dev-tools/FeatureFlagTool.tsx | 1 + packages/manager/src/featureFlags.ts | 2 + .../features/DataStream/DataStreamLanding.tsx | 67 +++++++++++++++++++ .../DataStream/Destinations/Destinations.tsx | 5 ++ .../features/DataStream/Streams/Streams.tsx | 5 ++ .../src/routes/datastream/DataStreamRoute.tsx | 16 +++++ .../routes/datastream/dataStreamLazyRoutes.ts | 7 ++ .../manager/src/routes/datastream/index.ts | 40 +++++++++++ packages/manager/src/routes/index.tsx | 3 + 12 files changed, 159 insertions(+) create mode 100644 packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md create mode 100644 packages/manager/src/features/DataStream/DataStreamLanding.tsx create mode 100644 packages/manager/src/features/DataStream/Destinations/Destinations.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/Streams.tsx create mode 100644 packages/manager/src/routes/datastream/DataStreamRoute.tsx create mode 100644 packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts create mode 100644 packages/manager/src/routes/datastream/index.ts diff --git a/packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md b/packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md new file mode 100644 index 00000000000..7d32e4d1d26 --- /dev/null +++ b/packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DataStream: routes, feature flag, tabs ([#12155](https://github.com/linode/manager/pull/12155)) diff --git a/packages/manager/eslint.config.js b/packages/manager/eslint.config.js index 1f82e028d70..e1a650d46a9 100644 --- a/packages/manager/eslint.config.js +++ b/packages/manager/eslint.config.js @@ -403,6 +403,7 @@ export const baseConfig = [ // for each new features added to the migration router, add its directory here 'src/features/Betas/**/*', 'src/features/Domains/**/*', + 'src/features/DataStream/**/*', 'src/features/Firewalls/**/*', 'src/features/Images/**/*', 'src/features/Longview/**/*', diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index fc90a238d65..cbc9573592e 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -38,6 +38,7 @@ export type NavEntity = | 'Cloud Load Balancers' | 'Dashboard' | 'Databases' + | 'DataStream' | 'Domains' | 'Firewalls' | 'Help & Support' @@ -239,6 +240,12 @@ export const PrimaryNav = (props: PrimaryNavProps) => { display: 'Longview', href: '/longview', }, + { + display: 'DataStream', + hide: !flags.aclpLogs?.enabled, + href: '/datastream', + isBeta: flags.aclpLogs?.beta, + }, ], name: 'Monitor', }, diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 2442dad99cd..baab8e30e64 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -22,6 +22,7 @@ const options: { flag: keyof Flags; label: string }[] = [ { flag: 'aclp', label: 'CloudPulse' }, { flag: 'aclpAlerting', label: 'CloudPulse Alerting' }, { flag: 'aclpIntegration', label: 'ACLP Integration' }, + { flag: 'aclpLogs', label: 'ACLP Logs' }, { flag: 'apl', label: 'Akamai App Platform' }, { flag: 'blockStorageEncryption', label: 'Block Storage Encryption (BSE)' }, { flag: 'disableLargestGbPlans', label: 'Disable Largest GB Plans' }, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 4fc91fd8873..f718a289ddd 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -110,6 +110,7 @@ export interface Flags { aclpAlerting: AclpAlerting; aclpAlertServiceTypeConfig: AclpAlertServiceTypeConfig[]; aclpIntegration: boolean; + aclpLogs: BetaFeatureFlag; aclpReadEndpoint: string; aclpResourceTypeMap: CloudPulseResourceTypeMapFlag[]; apicliButtonCopy: string; @@ -244,6 +245,7 @@ export type ProductInformationBannerLocation = | 'Account' | 'Betas' | 'Databases' + | 'DataStream' | 'Domains' | 'Firewalls' | 'Identity and Access' diff --git a/packages/manager/src/features/DataStream/DataStreamLanding.tsx b/packages/manager/src/features/DataStream/DataStreamLanding.tsx new file mode 100644 index 00000000000..8f747d39e28 --- /dev/null +++ b/packages/manager/src/features/DataStream/DataStreamLanding.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; + +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { LandingHeader } from 'src/components/LandingHeader'; +import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; +import { TabPanels } from 'src/components/Tabs/TabPanels'; +import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; +import { useTabs } from 'src/hooks/useTabs'; + +const Destinations = React.lazy(() => + import('./Destinations/Destinations').then((module) => ({ + default: module.Destinations, + })) +); + +const Streams = React.lazy(() => + import('./Streams/Streams').then((module) => ({ + default: module.Streams, + })) +); + +export const DataStreamLanding = React.memo(() => { + const landingHeaderProps = { + breadcrumbProps: { + pathname: '/datastream', + }, + entity: 'DataStream', + title: 'DataStream', + }; + + const { handleTabChange, tabIndex, tabs } = useTabs([ + { + to: '/datastream/streams', + title: 'Streams', + }, + { + to: '/datastream/destinations', + title: 'Destinations', + }, + ]); + + return ( + <> + + + + + + + + }> + + + + + + + + + + + + ); +}); diff --git a/packages/manager/src/features/DataStream/Destinations/Destinations.tsx b/packages/manager/src/features/DataStream/Destinations/Destinations.tsx new file mode 100644 index 00000000000..72bb42fa7a8 --- /dev/null +++ b/packages/manager/src/features/DataStream/Destinations/Destinations.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const Destinations = () => { + return

Content for Destinations tab

; +}; diff --git a/packages/manager/src/features/DataStream/Streams/Streams.tsx b/packages/manager/src/features/DataStream/Streams/Streams.tsx new file mode 100644 index 00000000000..f9caa96733e --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/Streams.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const Streams = () => { + return

Content for Streams tab

; +}; diff --git a/packages/manager/src/routes/datastream/DataStreamRoute.tsx b/packages/manager/src/routes/datastream/DataStreamRoute.tsx new file mode 100644 index 00000000000..8e2c14ef033 --- /dev/null +++ b/packages/manager/src/routes/datastream/DataStreamRoute.tsx @@ -0,0 +1,16 @@ +import { NotFound } from '@linode/ui'; +import { Outlet } from '@tanstack/react-router'; +import React from 'react'; + +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { useFlags } from 'src/hooks/useFlags'; + +export const DataStreamRoute = () => { + const flags = useFlags(); + const { aclpLogs } = flags; + return ( + }> + {aclpLogs?.enabled ? : } + + ); +}; diff --git a/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts b/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts new file mode 100644 index 00000000000..4cc2a268043 --- /dev/null +++ b/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts @@ -0,0 +1,7 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import { DataStreamLanding } from 'src/features/DataStream/DataStreamLanding'; + +export const dataStreamLandingLazyRoute = createLazyRoute('/datastream')({ + component: DataStreamLanding, +}); diff --git a/packages/manager/src/routes/datastream/index.ts b/packages/manager/src/routes/datastream/index.ts new file mode 100644 index 00000000000..cdd5473bc6f --- /dev/null +++ b/packages/manager/src/routes/datastream/index.ts @@ -0,0 +1,40 @@ +import { createRoute, redirect } from '@tanstack/react-router'; + +import { rootRoute } from '../root'; +import { DataStreamRoute } from './DataStreamRoute'; + +export const dataStreamRoute = createRoute({ + component: DataStreamRoute, + getParentRoute: () => rootRoute, + path: 'datastream', +}); + +const dataStreamLandingRoute = createRoute({ + beforeLoad: () => { + throw redirect({ to: '/datastream/streams' }); + }, + getParentRoute: () => dataStreamRoute, + path: '/', +}).lazy(() => + import('./dataStreamLazyRoutes').then((m) => m.dataStreamLandingLazyRoute) +); + +const streamsRoute = createRoute({ + getParentRoute: () => dataStreamRoute, + path: 'streams', +}).lazy(() => + import('./dataStreamLazyRoutes').then((m) => m.dataStreamLandingLazyRoute) +); + +const destinationsRoute = createRoute({ + getParentRoute: () => dataStreamRoute, + path: 'destinations', +}).lazy(() => + import('./dataStreamLazyRoutes').then((m) => m.dataStreamLandingLazyRoute) +); + +export const dataStreamRouteTree = dataStreamRoute.addChildren([ + dataStreamLandingRoute, + streamsRoute, + destinationsRoute, +]); diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index f8ec870898e..cf725715d52 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -9,6 +9,7 @@ import { accountRouteTree } from './account'; import { cloudPulseAlertsRouteTree } from './alerts'; import { betaRouteTree } from './betas'; import { databasesRouteTree } from './databases'; +import { dataStreamRouteTree } from './datastream'; import { domainsRouteTree } from './domains'; import { eventsRouteTree } from './events'; import { firewallsRouteTree } from './firewalls'; @@ -46,6 +47,7 @@ export const routeTree = rootRoute.addChildren([ cloudPulseAlertsRouteTree, cloudPulseMetricsRouteTree, databasesRouteTree, + dataStreamRouteTree, domainsRouteTree, eventsRouteTree, firewallsRouteTree, @@ -90,6 +92,7 @@ declare module '@tanstack/react-router' { export const migrationRouteTree = migrationRootRoute.addChildren([ betaRouteTree, domainsRouteTree, + dataStreamRouteTree, firewallsRouteTree, imagesRouteTree, longviewRouteTree, From 146bb08c992c1b0c6856f445c8dfbd56fcf3d3c0 Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Thu, 15 May 2025 17:04:12 +0530 Subject: [PATCH 02/59] change: [M3-9947] - Remove Accordion wrapper from default Alerts tab on Linode details page (#12215) * Remove Accordion wrapper from Alerts tab on Linode details page * Added changeset: Remove the `Accordion` wrapper from the default Alerts tab and replace it with `Paper` on the Linode details page * Minor fix * console error fix when clearing field manually * Added changeset: Manual clearing of default Alerts fields now resets values to zero, preventing empty string/NaN and ensuring consistency with toggle off * Update changeset * Update `Paper` padding to match ADS spacing --- .../pr-12215-changed-1747223125401.md | 5 + .../pr-12215-fixed-1747230804629.md | 5 + .../LinodeSettings/AlertSection.tsx | 174 +++++++++--------- .../LinodeSettingsAlertsPanel.tsx | 63 ++++--- packages/ui/src/components/Paper/Paper.tsx | 4 +- 5 files changed, 136 insertions(+), 115 deletions(-) create mode 100644 packages/manager/.changeset/pr-12215-changed-1747223125401.md create mode 100644 packages/manager/.changeset/pr-12215-fixed-1747230804629.md diff --git a/packages/manager/.changeset/pr-12215-changed-1747223125401.md b/packages/manager/.changeset/pr-12215-changed-1747223125401.md new file mode 100644 index 00000000000..c85f18440bb --- /dev/null +++ b/packages/manager/.changeset/pr-12215-changed-1747223125401.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Remove the `Accordion` wrapper from the default Alerts tab and replace it with `Paper` on the Linode details page ([#12215](https://github.com/linode/manager/pull/12215)) diff --git a/packages/manager/.changeset/pr-12215-fixed-1747230804629.md b/packages/manager/.changeset/pr-12215-fixed-1747230804629.md new file mode 100644 index 00000000000..325b8148f49 --- /dev/null +++ b/packages/manager/.changeset/pr-12215-fixed-1747230804629.md @@ -0,0 +1,5 @@ +--- +'@linode/manager': Fixed +--- + +Manual clearing of default Alerts fields now resets values to zero, preventing empty string/NaN and ensuring consistency with toggle off state ([#12215](https://github.com/linode/manager/pull/12215)) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx index 57ebbc5678d..64a4126ff90 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx @@ -1,6 +1,5 @@ import { Box, - Divider, fadeIn, FormControlLabel, InputAdornment, @@ -43,106 +42,103 @@ export const AlertSection = (props: Props) => { } = props; return ( - <> + - - - - } - data-qa-alert={title} - label={title} - sx={{ - '& > span:last-child': { - ...theme.typography.h3, - }, - '.MuiFormControlLabel-label': { - paddingLeft: '12px', - }, - }} - /> - - + + } + data-qa-alert={title} + label={title} sx={{ - paddingLeft: '70px', - [theme.breakpoints.down('md')]: { - marginTop: '-12px', + '& > span:last-child': { + ...theme.typography.h3, + }, + '.MuiFormControlLabel-label': { + paddingLeft: '12px', }, }} - > - {copy} - - - + + - {endAdornment} - ), - }} - label={textTitle} - max={Infinity} - min={0} - onChange={onValueChange} - sx={{ - '.MuiInput-root': { - animation: `${fadeIn} .3s ease-in-out forwards`, - marginTop: 0, - maxWidth: 150, - }, - }} - type="number" - value={value} - /> - + {copy} + + + + {endAdornment} + ), + }} + label={textTitle} + max={Infinity} + min={0} + onChange={onValueChange} + sx={{ + '.MuiInput-root': { + animation: `${fadeIn} .3s ease-in-out forwards`, + marginTop: 0, + maxWidth: 150, + }, + }} + type="number" + value={value} + /> - - + ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx index 593fc9a81ce..2576a9014df 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx @@ -1,5 +1,5 @@ import { useLinodeQuery, useLinodeUpdateMutation } from '@linode/queries'; -import { Accordion, ActionsPanel, Notice } from '@linode/ui'; +import { ActionsPanel, Divider, Notice, Paper, Typography } from '@linode/ui'; import { styled } from '@mui/material/styles'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; @@ -93,7 +93,10 @@ export const LinodeSettingsAlertsPanel = (props: Props) => { : 0 ), onValueChange: (e: React.ChangeEvent) => - formik.setFieldValue('cpu', e.target.valueAsNumber), + formik.setFieldValue( + 'cpu', + !Number.isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : 0 + ), radioInputLabel: 'cpu_usage_state', state: formik.values.cpu > 0, textInputLabel: 'cpu_usage_threshold', @@ -115,7 +118,10 @@ export const LinodeSettingsAlertsPanel = (props: Props) => { checked ? (linode?.alerts.io ? linode?.alerts.io : 10000) : 0 ), onValueChange: (e: React.ChangeEvent) => - formik.setFieldValue('io', e.target.valueAsNumber), + formik.setFieldValue( + 'io', + !Number.isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : 0 + ), radioInputLabel: 'disk_io_state', state: formik.values.io > 0, textInputLabel: 'disk_io_threshold', @@ -141,7 +147,10 @@ export const LinodeSettingsAlertsPanel = (props: Props) => { : 0 ), onValueChange: (e: React.ChangeEvent) => - formik.setFieldValue('network_in', e.target.valueAsNumber), + formik.setFieldValue( + 'network_in', + !Number.isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : 0 + ), radioInputLabel: 'incoming_traffic_state', state: formik.values.network_in > 0, textInputLabel: 'incoming_traffic_threshold', @@ -167,7 +176,10 @@ export const LinodeSettingsAlertsPanel = (props: Props) => { : 0 ), onValueChange: (e: React.ChangeEvent) => - formik.setFieldValue('network_out', e.target.valueAsNumber), + formik.setFieldValue( + 'network_out', + !Number.isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : 0 + ), radioInputLabel: 'outbound_traffic_state', state: formik.values.network_out > 0, textInputLabel: 'outbound_traffic_threshold', @@ -193,7 +205,10 @@ export const LinodeSettingsAlertsPanel = (props: Props) => { : 0 ), onValueChange: (e: React.ChangeEvent) => - formik.setFieldValue('transfer_quota', e.target.valueAsNumber), + formik.setFieldValue( + 'transfer_quota', + !Number.isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : 0 + ), radioInputLabel: 'transfer_quota_state', state: formik.values.transfer_quota > 0, textInputLabel: 'transfer_quota_threshold', @@ -203,8 +218,23 @@ export const LinodeSettingsAlertsPanel = (props: Props) => { }, ].filter((thisAlert) => !thisAlert.hidden); - const renderExpansionActions = () => { - return ( + const generalError = hasErrorFor('none'); + + return ( + ({ pb: theme.spacingFunction(16) })}> + ({ mb: theme.spacingFunction(12) })} + variant="h2" + > + Alerts + + {generalError && {generalError}} + {alertSections.map((p, idx) => ( + + + {idx !== alertSections.length - 1 ? : null} + + ))} { onClick: () => formik.handleSubmit(), }} /> - ); - }; - - const generalError = hasErrorFor('none'); - - return ( - - {generalError && {generalError}} - {alertSections.map((p, idx) => ( - - ))} - + ); }; diff --git a/packages/ui/src/components/Paper/Paper.tsx b/packages/ui/src/components/Paper/Paper.tsx index caa909adadf..22efa21d8be 100644 --- a/packages/ui/src/components/Paper/Paper.tsx +++ b/packages/ui/src/components/Paper/Paper.tsx @@ -41,8 +41,8 @@ const StyledPaper = styled(_Paper, { shouldForwardProp: (prop) => prop !== 'error', })(({ theme, ...props }) => ({ borderColor: props.error ? theme.palette.error.dark : undefined, - padding: theme.spacing(3), - paddingTop: 17, + padding: theme.spacingFunction(24), + paddingTop: theme.spacingFunction(16), })); const StyledErrorText = styled(FormHelperText)(({ theme }) => ({ From 78a9864c2e461de0cdb52173c6781b6914769715 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Thu, 15 May 2025 11:52:55 -0400 Subject: [PATCH 03/59] chore: [M3-9973] - MUI v7 Update (#12211) * package bump + styles * GridLegacy instances * e2e hopeful fix * e2e hopeful fix * feedback @pmakode-akamai * fix after merging develop --- .../e2e/core/account/service-transfer.spec.ts | 2 +- packages/manager/package.json | 6 +- packages/manager/src/MainContent.tsx | 2 +- packages/manager/src/Root.tsx | 2 +- .../AbuseTicketBanner/AbuseTicketBanner.tsx | 2 +- .../CheckoutSummary/CheckoutSummary.tsx | 4 +- .../CheckoutSummary/SummaryItem.tsx | 4 +- .../components/ColorPalette/ColorPalette.tsx | 2 +- .../components/DatePicker/DateTimePicker.tsx | 14 +- .../DescriptionList/DescriptionList.styles.ts | 6 +- .../EditableEntityLabel.tsx | 2 +- .../components/EntityDetail/EntityDetail.tsx | 2 +- .../EntityIcon/EntityIcon.stories.tsx | 2 +- .../LandingHeader/LandingHeader.tsx | 2 +- .../MultipleIPInput/MultipleIPInput.tsx | 2 +- .../PasswordInput/StrengthIndicator.tsx | 2 +- .../DeletePaymentMethodDialog.tsx | 2 +- .../SelectionCard/CardBase.styles.ts | 2 +- .../SelectionCard/SelectionCard.tsx | 6 +- .../src/components/TagCell/TagCell.tsx | 2 +- .../TransferDisplay/TransferDisplayUsage.tsx | 2 +- .../src/dev-tools/EnvironmentToggleTool.tsx | 2 +- .../manager/src/dev-tools/ThemeSelector.tsx | 2 +- .../src/features/Billing/BillingDetail.tsx | 2 +- .../BillingActivityPanel.tsx | 2 +- .../BillingSummary/BillingSummary.tsx | 2 +- .../PaymentDrawer/GooglePayButton.tsx | 2 +- .../PaymentDrawer/PayPalButton.tsx | 2 +- .../PaymentDrawer/PaymentDrawer.tsx | 2 +- .../ContactInfoPanel/ContactInformation.tsx | 2 +- .../UpdateContactInformationForm.tsx | 2 +- .../AddCreditCardForm.tsx | 4 +- .../AddPaymentMethodDrawer.tsx | 2 +- .../PaymentInfoPanel/PaymentInformation.tsx | 2 +- .../PaymentInfoPanel/PaymentMethods.tsx | 2 +- .../Billing/InvoiceDetail/InvoiceDetail.tsx | 2 +- .../AlertsDetail/AlertDetailCriteria.tsx | 14 +- .../AlertsDetail/AlertDetailNotification.tsx | 18 +- .../Alerts/AlertsDetail/AlertDetailRow.tsx | 14 +- .../AlertsDetail/DisplayAlertDetailChips.tsx | 22 +-- .../RenderAlertsMetricsAndDimensions.tsx | 14 +- .../Alerts/AlertsListing/AlertListTable.tsx | 6 +- .../AlertsResources/AlertsResources.tsx | 38 ++-- .../Criteria/DimensionFilterField.tsx | 18 +- .../Alerts/CreateAlert/Criteria/Metric.tsx | 22 +-- .../Criteria/TriggerConditions.tsx | 18 +- .../AddNotificationChannelDrawer.tsx | 14 +- .../Dashboard/CloudPulseDashboard.tsx | 6 +- .../Dashboard/CloudPulseDashboardLanding.tsx | 10 +- .../CloudPulseDashboardWithFilters.tsx | 18 +- .../CloudPulse/Overview/GlobalFilters.tsx | 14 +- .../CloudPulse/Widget/CloudPulseWidget.tsx | 8 +- .../Widget/CloudPulseWidgetRenderer.tsx | 10 +- .../CloudPulseComponentRenderer.test.tsx | 18 +- .../CloudPulseDashboardFilterBuilder.tsx | 24 ++- .../shared/CloudPulseErrorPlaceholder.tsx | 6 +- .../DatabaseCreate/DatabaseClusterData.tsx | 2 +- .../DatabaseCreate/DatabaseCreate.tsx | 2 +- .../DatabaseCreateAccessControls.tsx | 2 +- .../DatabaseCreate/DatabaseEngineSelect.tsx | 2 +- .../DatabaseAdvancedConfiguration.tsx | 2 +- .../DatabaseAdvancedConfigurationDrawer.tsx | 2 +- .../DatabaseBackups/DatabaseBackups.tsx | 18 +- .../DatabaseResize/DatabaseResize.style.ts | 2 +- .../DatabaseSettingsMaintenance.tsx | 14 +- .../DatabaseSummary/DatabaseSummary.tsx | 2 +- ...tabaseSummaryClusterConfiguration.style.ts | 8 +- .../DatabaseSummaryClusterConfiguration.tsx | 2 +- .../DatabaseSummaryConnectionDetails.tsx | 2 +- .../Domains/CreateDomain/CreateDomain.tsx | 2 +- .../Domains/DomainDetail/DomainDetail.tsx | 2 +- .../DomainRecords/DomainRecords.styles.ts | 2 +- .../DomainRecords/DomainRecords.tsx | 2 +- .../EntityTransferCreate.styles.ts | 2 +- .../EntityTransfersCreate.tsx | 2 +- .../TransferControls.styles.ts | 2 +- .../Devices/FirewallDeviceLanding.tsx | 2 +- .../EmailBounce.styles.tsx | 2 +- .../GlobalNotifications/EmailBounce.tsx | 2 +- .../src/features/Help/Panels/OtherWays.tsx | 2 +- .../src/features/Help/Panels/PopularPosts.tsx | 2 +- .../SupportSearchLanding/HelpResources.tsx | 2 +- .../SupportSearchLanding.tsx | 2 +- .../RolesTable/AssignSelectedRolesDrawer.tsx | 2 +- .../IAM/Roles/RolesTable/RolesTable.tsx | 2 +- .../AssignedRolesTable/AssignedRolesTable.tsx | 2 +- .../Shared/Permissions/Permissions.style.ts | 4 +- .../IAM/Shared/Permissions/Permissions.tsx | 2 +- .../Users/UserDetails/UserDetailsPanel.tsx | 2 +- .../UserEntities/AssignedEntitiesTable.tsx | 2 +- .../Users/UserRoles/AssignNewRoleDrawer.tsx | 2 +- .../ImagesCreate/ImageCreateContainer.tsx | 2 +- .../CreateCluster/ClusterTierPanel.tsx | 6 +- .../CreateCluster/ControlPlaneACLPane.tsx | 2 +- .../CreateCluster/CreateCluster.tsx | 2 +- .../CreateCluster/NodePoolPanel.tsx | 2 +- .../APLSummaryPanel.tsx | 2 +- .../KubeClusterSpecs.tsx | 2 +- .../KubeEntityDetailFooter.tsx | 2 +- .../KubeSummaryPanel.styles.tsx | 2 +- .../NodePoolsDisplay/AutoscalePoolDialog.tsx | 2 +- .../NodePoolsDisplay/NodeRow.tsx | 2 +- .../KubernetesPlanContainer.tsx | 2 +- .../KubernetesPlanSelection.tsx | 2 +- .../src/features/Linodes/AccessTable.tsx | 2 +- .../Linodes/CloneLanding/CloneLanding.tsx | 2 +- .../features/Linodes/CloneLanding/Disks.tsx | 2 +- .../LinodeCreate/Networking/InterfaceType.tsx | 6 +- .../Tabs/Backups/BackupSelect.tsx | 2 +- .../Tabs/Marketplace/AppSection.tsx | 2 +- .../Tabs/Marketplace/AppsList.tsx | 2 +- .../LinodeCreate/shared/LinodeSelectTable.tsx | 2 +- .../LinodeCreate/shared/SelectLinodeCard.tsx | 2 +- .../Linodes/LinodeEntityDetail.styles.ts | 2 +- .../Linodes/LinodeEntityDetailBody.tsx | 2 +- .../Linodes/LinodeEntityDetailFooter.tsx | 2 +- .../LinodeConfigs/LinodeConfigDialog.tsx | 2 +- .../LinodeSummary/LinodeSummary.tsx | 2 +- .../LinodeSummary/NetworkGraphs.tsx | 2 +- .../LinodeNetworking/IPSharing.tsx | 2 +- .../LinodeNetworking/IPTransfer.tsx | 2 +- .../NetworkingSummaryPanel/DNSResolvers.tsx | 2 +- .../NetworkingSummaryPanel.tsx | 2 +- .../TransferContent.tsx | 2 +- .../LinodeSettings/AlertSection.tsx | 2 +- .../LinodeSettings/InterfaceSelect.tsx | 2 +- .../LinodeSettings/LinodeWatchdogPanel.tsx | 2 +- .../LinodeStorage/LinodeDisks.tsx | 2 +- .../LinodesDetail/LinodesDetailNavigation.tsx | 2 +- .../Linodes/LinodesLanding/CardView.tsx | 2 +- .../LinodesLanding/DisplayGroupedLinodes.tsx | 2 +- .../Linodes/LinodesLanding/DisplayLinodes.tsx | 2 +- .../LinodesLanding/LinodesLanding.styles.ts | 2 +- .../Linodes/LinodesLanding/TableWrapper.tsx | 2 +- .../src/features/Linodes/RenderIPs.tsx | 2 +- .../ActiveConnections/ActiveConnections.tsx | 2 +- .../DetailTabs/Apache/Apache.tsx | 2 +- .../DetailTabs/Apache/ApacheGraphs.tsx | 2 +- .../DetailTabs/CommonStyles.styles.tsx | 2 +- .../DetailTabs/GaugesSection.tsx | 2 +- .../LongviewDetail/DetailTabs/IconSection.tsx | 2 +- .../ListeningServices/ListeningServices.tsx | 2 +- .../DetailTabs/LongviewDetailOverview.tsx | 2 +- .../DetailTabs/MySQL/MySQLGraphs.tsx | 2 +- .../DetailTabs/MySQL/MySQLLanding.tsx | 2 +- .../LongviewDetail/DetailTabs/NGINX/NGINX.tsx | 2 +- .../DetailTabs/NGINX/NGINXGraphs.tsx | 2 +- .../DetailTabs/Network/NetworkLanding.tsx | 2 +- .../OverviewGraphs/OverviewGraphs.tsx | 2 +- .../DetailTabs/ProcessGraphs.tsx | 2 +- .../DetailTabs/Processes/ProcessesLanding.tsx | 2 +- .../DetailTabs/TopProcesses.tsx | 2 +- .../LongviewClientHeader.styles.ts | 2 +- .../LongviewLanding/LongviewClientHeader.tsx | 2 +- .../LongviewClientInstructions.tsx | 2 +- .../LongviewLanding/LongviewClientRow.tsx | 2 +- .../LongviewLanding/LongviewClients.styles.ts | 2 +- .../shared/InstallationInstructions.styles.ts | 2 +- .../shared/InstallationInstructions.tsx | 2 +- .../Managed/Contacts/Contacts.styles.tsx | 2 +- .../Managed/Contacts/ContactsDrawer.tsx | 2 +- .../DashboardCard.styles.tsx | 2 +- .../ManagedDashboardCard/DashboardCard.tsx | 2 +- .../ManagedDashboardCard.styles.tsx | 2 +- .../ManagedDashboardCard.tsx | 2 +- .../MonitorStatus.styles.tsx | 2 +- .../ManagedDashboardCard/MonitorStatus.tsx | 2 +- .../MonitorTickets.styles.tsx | 2 +- .../ManagedDashboardCard/MonitorTickets.tsx | 2 +- .../src/features/Managed/MonitorDrawer.tsx | 2 +- .../Managed/Monitors/IssueDay.styles.tsx | 2 +- .../features/Managed/Monitors/IssueDay.tsx | 2 +- .../Managed/Monitors/MonitorRow.styles.tsx | 2 +- .../features/Managed/Monitors/MonitorRow.tsx | 2 +- .../Managed/Monitors/MonitorTable.styles.tsx | 2 +- .../Managed/Monitors/MonitorTable.tsx | 2 +- .../Managed/SSHAccess/EditSSHAccessDrawer.tsx | 2 +- .../Managed/SSHAccess/LinodePubKey.styles.tsx | 2 +- .../Managed/SSHAccess/LinodePubKey.tsx | 2 +- .../NodeBalancers/NodeBalancerActiveCheck.tsx | 2 +- .../NodeBalancers/NodeBalancerConfigNode.tsx | 2 +- .../NodeBalancers/NodeBalancerConfigPanel.tsx | 2 +- .../NodeBalancerSummary.tsx | 2 +- .../NodeBalancerPassiveCheck.tsx | 2 +- .../ObjectStorage/BucketDetail/BucketSSL.tsx | 2 +- .../BucketDetail/FolderTableRow.tsx | 2 +- .../BucketDetail/ObjectTableRow.tsx | 2 +- .../BucketLanding/BucketLanding.tsx | 2 +- .../BucketLanding/OMC_BucketLanding.tsx | 2 +- .../PlacementGroupsLinodes.tsx | 2 +- .../AuthenticationSettings/TPAProviders.tsx | 2 +- .../Profile/Referrals/Referrals.styles.ts | 2 +- .../features/Profile/Referrals/Referrals.tsx | 2 +- .../src/features/Search/ResultGroup.tsx | 2 +- .../StackScripts/CommonStackScript.styles.ts | 2 +- .../features/Support/AttachFileListItem.tsx | 2 +- .../Support/ExpandableTicketPanel.tsx | 2 +- .../SupportTicketDetail.tsx | 2 +- .../TabbedReply/ReplyContainer.tsx | 2 +- .../SupportTicketDetail/TicketStatus.tsx | 2 +- .../features/Support/TicketAttachmentList.tsx | 2 +- .../src/features/Support/TicketDetailText.tsx | 2 +- .../TopMenu/UserMenu/UserMenuPopover.tsx | 2 +- .../features/Users/UserPermissions.styles.ts | 2 +- .../src/features/Users/UserPermissions.tsx | 2 +- .../Users/UserProfile/UserDetailsPanel.tsx | 2 +- .../VPCs/VPCCreate/MultipleSubnetInput.tsx | 2 +- .../features/VPCs/VPCCreate/SubnetNode.tsx | 2 +- .../src/features/VPCs/VPCCreate/VPCCreate.tsx | 2 +- .../VPCs/VPCCreateDrawer/VPCCreateDrawer.tsx | 2 +- .../components/PlansPanel/PlanContainer.tsx | 2 +- packages/ui/package.json | 6 +- .../ui/src/components/Accordion/Accordion.tsx | 2 +- packages/ui/src/components/Drawer/Drawer.tsx | 2 +- .../src/components/ErrorState/ErrorState.tsx | 2 +- packages/ui/src/foundations/themes/index.ts | 2 +- pnpm-lock.yaml | 165 +++++++++++------- 217 files changed, 499 insertions(+), 448 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 1012c840caf..620e66bb792 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -546,7 +546,7 @@ describe('Account service transfers', () => { .within(() => { cy.get(`[data-qa-panel-summary="${transfer}"]`).click(); // Error Icon should shows up. - cy.findByTestId('ErrorOutlineIcon').should('be.visible'); + cy.findByTestId('error-state').should('be.visible'); // Error message should be visible. cy.findByText(serviceTransferErrorMessage, { exact: false }).should( 'be.visible' diff --git a/packages/manager/package.json b/packages/manager/package.json index 8d3edf9d16f..ecbb32e5567 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -32,9 +32,9 @@ "@linode/utilities": "workspace:*", "@linode/validation": "workspace:*", "@lukemorales/query-key-factory": "^1.3.4", - "@mui/icons-material": "^6.4.5", - "@mui/material": "^6.4.5", - "@mui/utils": "^6.4.3", + "@mui/icons-material": "^7.1.0", + "@mui/material": "^7.1.0", + "@mui/utils": "^7.1.0", "@mui/x-date-pickers": "^7.27.0", "@paypal/react-paypal-js": "^8.8.3", "@reach/tabs": "^0.18.0", diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 0b39ef1a519..a3d0a793e1b 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -6,7 +6,7 @@ import { } from '@linode/queries'; import { Box } from '@linode/ui'; import { useMediaQuery } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/Root.tsx b/packages/manager/src/Root.tsx index 415901c3f2e..054c4c55c3e 100644 --- a/packages/manager/src/Root.tsx +++ b/packages/manager/src/Root.tsx @@ -11,7 +11,7 @@ import { useProfile, } from '@linode/queries'; import { Box } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { Outlet } from '@tanstack/react-router'; import React from 'react'; diff --git a/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx b/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx index 22dfac315c3..98a8488c7c9 100644 --- a/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx +++ b/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx @@ -1,6 +1,6 @@ import { useNotificationsQuery } from '@linode/queries'; import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { DateTime } from 'luxon'; import * as React from 'react'; import { useLocation } from 'react-router-dom'; diff --git a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx index 9a0c6439668..8cfb23885ed 100644 --- a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx +++ b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx @@ -1,6 +1,6 @@ import { Paper, Typography } from '@linode/ui'; import { useTheme } from '@mui/material'; -import Grid2 from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; @@ -71,7 +71,7 @@ const StyledHeading = styled(Typography)(({ theme }) => ({ marginBottom: theme.spacing(3), })); -const StyledSummary = styled(Grid2)(({ theme }) => ({ +const StyledSummary = styled(Grid)(({ theme }) => ({ [theme.breakpoints.up('md')]: { '& > div': { '&:first-child': { diff --git a/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx b/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx index 307eec624f3..7240c30b795 100644 --- a/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx +++ b/packages/manager/src/components/CheckoutSummary/SummaryItem.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid2 from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import React from 'react'; @@ -27,7 +27,7 @@ export const SummaryItem = ({ details, title }: Props) => { ); }; -const StyledGrid = styled(Grid2)(({ theme }) => ({ +const StyledGrid = styled(Grid)(({ theme }) => ({ marginBottom: `${theme.spacing()} !important`, marginTop: `${theme.spacing()} !important`, paddingBottom: '0 !important', diff --git a/packages/manager/src/components/ColorPalette/ColorPalette.tsx b/packages/manager/src/components/ColorPalette/ColorPalette.tsx index f9017a88e21..a7ee81b07c0 100644 --- a/packages/manager/src/components/ColorPalette/ColorPalette.tsx +++ b/packages/manager/src/components/ColorPalette/ColorPalette.tsx @@ -1,7 +1,7 @@ import { Typography as FontTypography } from '@linode/design-language-system'; import { Typography } from '@linode/ui'; import { useTheme } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx index 4adab2e34d2..c4ab9722276 100644 --- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx +++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx @@ -2,7 +2,7 @@ import { ActionsPanel, InputAdornment, TextField } from '@linode/ui'; import { Divider } from '@linode/ui'; import { Box } from '@linode/ui'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; -import { Grid, Popover } from '@mui/material'; +import { GridLegacy, Popover } from '@mui/material'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; @@ -220,13 +220,13 @@ export const DateTimePicker = ({ borderWidth: '0px', })} /> - {showTime && ( - + - + )} {showTimeZone && ( - + - + )} - + diff --git a/packages/manager/src/components/DescriptionList/DescriptionList.styles.ts b/packages/manager/src/components/DescriptionList/DescriptionList.styles.ts index 9a602e3ff2d..60e826b298a 100644 --- a/packages/manager/src/components/DescriptionList/DescriptionList.styles.ts +++ b/packages/manager/src/components/DescriptionList/DescriptionList.styles.ts @@ -1,13 +1,13 @@ import { omittedProps, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import type { DescriptionListProps } from './DescriptionList'; import type { TypographyProps } from '@mui/material'; -import type { Grid2Props } from '@mui/material/Grid2'; +import type { GridProps } from '@mui/material/Grid'; interface StyledDLProps extends Omit { - component: Grid2Props['component']; + component: GridProps['component']; gridColumns?: number; isStacked: boolean; } diff --git a/packages/manager/src/components/EditableEntityLabel/EditableEntityLabel.tsx b/packages/manager/src/components/EditableEntityLabel/EditableEntityLabel.tsx index c669f1f7508..3eccdc51055 100644 --- a/packages/manager/src/components/EditableEntityLabel/EditableEntityLabel.tsx +++ b/packages/manager/src/components/EditableEntityLabel/EditableEntityLabel.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/components/EntityDetail/EntityDetail.tsx b/packages/manager/src/components/EntityDetail/EntityDetail.tsx index 7d6524ae473..4a9b86e113f 100644 --- a/packages/manager/src/components/EntityDetail/EntityDetail.tsx +++ b/packages/manager/src/components/EntityDetail/EntityDetail.tsx @@ -1,5 +1,5 @@ import { omittedProps } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/components/EntityIcon/EntityIcon.stories.tsx b/packages/manager/src/components/EntityIcon/EntityIcon.stories.tsx index b66e56752a6..0c7d23271d2 100644 --- a/packages/manager/src/components/EntityIcon/EntityIcon.stories.tsx +++ b/packages/manager/src/components/EntityIcon/EntityIcon.stories.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import React from 'react'; diff --git a/packages/manager/src/components/LandingHeader/LandingHeader.tsx b/packages/manager/src/components/LandingHeader/LandingHeader.tsx index 4deb8bbe978..c57cfa1c057 100644 --- a/packages/manager/src/components/LandingHeader/LandingHeader.tsx +++ b/packages/manager/src/components/LandingHeader/LandingHeader.tsx @@ -1,5 +1,5 @@ import { Button } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 96dc1fa8cc3..08a5b00bf1b 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -7,7 +7,7 @@ import { TooltipIcon, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/components/PasswordInput/StrengthIndicator.tsx b/packages/manager/src/components/PasswordInput/StrengthIndicator.tsx index ee1613bce39..b794ab30d4b 100644 --- a/packages/manager/src/components/PasswordInput/StrengthIndicator.tsx +++ b/packages/manager/src/components/PasswordInput/StrengthIndicator.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx b/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx index 2e19905b2fa..b513bfc088b 100644 --- a/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx +++ b/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx @@ -1,5 +1,5 @@ import { ActionsPanel } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/components/SelectionCard/CardBase.styles.ts b/packages/manager/src/components/SelectionCard/CardBase.styles.ts index e67a01b2643..c4ba1d46fc4 100644 --- a/packages/manager/src/components/SelectionCard/CardBase.styles.ts +++ b/packages/manager/src/components/SelectionCard/CardBase.styles.ts @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import type { CardBaseProps } from './CardBase'; diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index fd659ce617b..6798ada6f8f 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -1,12 +1,12 @@ import { Tooltip } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { CardBase } from './CardBase'; import type { TooltipProps } from '@linode/ui'; -import type { Grid2Props } from '@mui/material/Grid2'; +import type { GridProps } from '@mui/material/Grid'; import type { SxProps, Theme } from '@mui/material/styles'; export interface SelectionCardProps { @@ -33,7 +33,7 @@ export interface SelectionCardProps { * Optionally override the grid item's size * @default { lg: 4, sm: 6, xl: 3, xs: 12 } */ - gridSize?: Grid2Props['size']; + gridSize?: GridProps['size']; /** * The heading of the card. * @example Linode 1GB diff --git a/packages/manager/src/components/TagCell/TagCell.tsx b/packages/manager/src/components/TagCell/TagCell.tsx index 06f4eb3873f..d912c7e92a9 100644 --- a/packages/manager/src/components/TagCell/TagCell.tsx +++ b/packages/manager/src/components/TagCell/TagCell.tsx @@ -6,7 +6,7 @@ import { StyledTagButton, } from '@linode/ui'; import MoreHoriz from '@mui/icons-material/MoreHoriz'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/components/TransferDisplay/TransferDisplayUsage.tsx b/packages/manager/src/components/TransferDisplay/TransferDisplayUsage.tsx index 283ccbbec94..1b55209136d 100644 --- a/packages/manager/src/components/TransferDisplay/TransferDisplayUsage.tsx +++ b/packages/manager/src/components/TransferDisplay/TransferDisplayUsage.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx b/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx index 6349bac5274..4acd11b72b1 100644 --- a/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx +++ b/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { storage } from 'src/utilities/storage'; diff --git a/packages/manager/src/dev-tools/ThemeSelector.tsx b/packages/manager/src/dev-tools/ThemeSelector.tsx index 9c497ebf49f..ecd03136dd1 100644 --- a/packages/manager/src/dev-tools/ThemeSelector.tsx +++ b/packages/manager/src/dev-tools/ThemeSelector.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { getStorage, setStorage } from 'src/utilities/storage'; diff --git a/packages/manager/src/features/Billing/BillingDetail.tsx b/packages/manager/src/features/Billing/BillingDetail.tsx index dab10c24980..50b0ff6c636 100644 --- a/packages/manager/src/features/Billing/BillingDetail.tsx +++ b/packages/manager/src/features/Billing/BillingDetail.tsx @@ -4,7 +4,7 @@ import { useProfile, } from '@linode/queries'; import { Button, CircleProgress, ErrorState } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; import { styled } from '@mui/material/styles'; import { PayPalScriptProvider } from '@paypal/react-paypal-js'; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx index e6ea37695cf..a3dd9d6612b 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx @@ -8,7 +8,7 @@ import { } from '@linode/queries'; import { Autocomplete, Typography } from '@linode/ui'; import { getAll, useSet } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; import { styled } from '@mui/material/styles'; import { DateTime } from 'luxon'; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/BillingSummary.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/BillingSummary.tsx index de438dd6bb4..915b69b7c76 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/BillingSummary.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/BillingSummary.tsx @@ -1,6 +1,6 @@ import { useGrants, useNotificationsQuery } from '@linode/queries'; import { Box, Button, Divider, TooltipIcon, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx index c6375a3030d..a48830919e5 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx @@ -1,7 +1,7 @@ import { useAccount, useClientToken } from '@linode/queries'; import { CircleProgress, Tooltip } from '@linode/ui'; import { useScript } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; 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 0e32dbad49c..d2e0f998c08 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx @@ -1,7 +1,7 @@ import { makePayment } from '@linode/api-v4/lib/account/payments'; import { accountQueries, useAccount, useClientToken } from '@linode/queries'; import { CircleProgress, Tooltip } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { BraintreePayPalButtons, DISPATCH_ACTION, 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 6ca41bb1804..af36073255d 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx @@ -12,7 +12,7 @@ import { TooltipIcon, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx index 7242acb834d..7af5ef663f2 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx @@ -1,6 +1,6 @@ import { useNotificationsQuery, usePreferences } from '@linode/queries'; import { Box, TooltipIcon, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { allCountries } from 'country-region-data'; import * as React from 'react'; import { useState } from 'react'; 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 81124135dec..6a538ab6215 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx @@ -14,7 +14,7 @@ import { TextField, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import { allCountries } from 'country-region-data'; import { useFormik } from 'formik'; 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 e2b23107b55..06fa6b3d9a4 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx @@ -1,7 +1,7 @@ import { useAddPaymentMethodMutation } from '@linode/queries'; import { ActionsPanel, Notice, TextField } from '@linode/ui'; import { CreditCardSchema } from '@linode/validation'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useFormik, yupToFormErrors } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; @@ -12,7 +12,7 @@ import { makeStyles } from 'tss-react/mui'; import { parseExpiryYear } from 'src/utilities/creditCard'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; -import type { InputBaseComponentProps } from '@mui/material/InputBase/InputBase'; +import type { InputBaseComponentProps } from '@mui/material'; import type { Theme } from '@mui/material/styles'; const useStyles = makeStyles()((theme: Theme) => ({ 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 751431ab6d8..0d61a2b520d 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx @@ -7,7 +7,7 @@ import { TooltipIcon, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { LinearProgress } from 'src/components/LinearProgress'; diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx index 20e2f11308f..81ef5829f33 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx @@ -1,7 +1,7 @@ import { deletePaymentMethod } from '@linode/api-v4/lib/account'; import { accountQueries } from '@linode/queries'; import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import * as React from 'react'; import { useHistory, useRouteMatch } from 'react-router-dom'; diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentMethods.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentMethods.tsx index 72115b9823b..efbc179be8b 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentMethods.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentMethods.tsx @@ -1,5 +1,5 @@ import { CircleProgress, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { PaymentMethodRow } from 'src/components/PaymentMethodRow/PaymentMethodRow'; diff --git a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx index 2f97fdac2cb..aa018b056f9 100644 --- a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx +++ b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx @@ -3,7 +3,7 @@ import { useAccount, useRegionsQuery } from '@linode/queries'; import { Box, Button, IconButton, Notice, Paper, Typography } from '@linode/ui'; import { getAll } from '@linode/utilities'; import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailCriteria.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailCriteria.tsx index 2a1d2c2a9c2..80c8a4b32d3 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailCriteria.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailCriteria.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import { Grid, useTheme } from '@mui/material'; +import { GridLegacy, useTheme } from '@mui/material'; import React from 'react'; import { convertSecondsToMinutes } from '../Utils/utils'; @@ -30,15 +30,15 @@ export const AlertDetailCriteria = React.memo((props: CriteriaProps) => { const renderTriggerCriteria = React.useMemo( () => ( <> - + Trigger Alert When: - - + { consecutive occurrences. - + ), [theme, triggerOccurrences] @@ -78,7 +78,7 @@ export const AlertDetailCriteria = React.memo((props: CriteriaProps) => { Criteria - { values={[convertSecondsToMinutes(evaluationPeriod)]} /> {renderTriggerCriteria} {/** Render the trigger criteria */} - + ); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailNotification.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailNotification.tsx index bb7a407a361..7289424ab01 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailNotification.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailNotification.tsx @@ -1,5 +1,5 @@ import { CircleProgress, ErrorState, Stack, Typography } from '@linode/ui'; -import { Divider, Grid } from '@mui/material'; +import { Divider, GridLegacy } from '@mui/material'; import React from 'react'; import EntityIcon from 'src/assets/icons/entityIcons/alerts.svg'; @@ -58,7 +58,7 @@ export const AlertDetailNotification = React.memo( Notification Channels - { const { channel_type, id, label } = notificationChannel; return ( - + - + - + {channels.length > 1 && index !== channels.length - 1 && ( - + - + )} - + ); })} - + ); } diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailRow.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailRow.tsx index f82e12f3655..23b02468c37 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailRow.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetailRow.tsx @@ -1,4 +1,4 @@ -import { Grid, useTheme } from '@mui/material'; +import { GridLegacy, useTheme } from '@mui/material'; import React from 'react'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; @@ -45,13 +45,13 @@ export const AlertDetailRow = React.memo((props: AlertDetailRowProps) => { const theme = useTheme(); return ( - - + + {label}: - - + + {status && ( { /> )} {value} - - + + ); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/DisplayAlertDetailChips.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/DisplayAlertDetailChips.tsx index 411257efa94..a774100456a 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/DisplayAlertDetailChips.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/DisplayAlertDetailChips.tsx @@ -1,4 +1,4 @@ -import { Grid, useTheme } from '@mui/material'; +import { GridLegacy, useTheme } from '@mui/material'; import React from 'react'; import { getAlertChipBorderRadius } from '../Utils/utils'; @@ -49,18 +49,18 @@ export const DisplayAlertDetailChips = React.memo( : []; const theme = useTheme(); return ( - + {chipValues.map((value, index) => ( - + {index === 0 && ( {label}: )} - - - + + {value.map((label, index) => ( - - + ))} - - + + ))} - + ); } ); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/RenderAlertsMetricsAndDimensions.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/RenderAlertsMetricsAndDimensions.tsx index e217eb7151b..6daade31a0e 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/RenderAlertsMetricsAndDimensions.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/RenderAlertsMetricsAndDimensions.tsx @@ -1,6 +1,6 @@ import { Divider } from '@linode/ui'; import { capitalize } from '@linode/utilities'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import NullComponent from 'src/components/NullComponent'; @@ -44,7 +44,7 @@ export const RenderAlertMetricsAndDimensions = React.memo( index ) => ( - + - + {dimensionFilters && dimensionFilters.length > 0 && ( - + - + )} - + - + ) ); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx index 563b3c183d3..d5cb3d20e25 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx @@ -6,7 +6,7 @@ import { } from '@linode/api-v4'; import { Notice, Typography } from '@linode/ui'; import { groupByTags, sortGroups } from '@linode/utilities'; -import { Grid2, TableBody, TableHead, TableRow } from '@mui/material'; +import { GridLegacy, TableBody, TableHead, TableRow } from '@mui/material'; import { enqueueSnackbar } from 'notistack'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; @@ -239,7 +239,7 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => { return ( <> - + { /> )}
-
+ {!isGroupedByTag && ( { alert for. )} - - + { }} xs={12} > - + { }} value={searchText || ''} /> - + {/* Dynamically render service type based filters */} {filtersToRender.map(({ component, filterKey }, index) => ( - + { ), })} /> - + ))} - + {isSelectionsNeeded && ( - + { text="Show Selected Only" value="Show Selected" /> - + )} {errorText?.length && ( - + - + )} {maxSelectionCount !== undefined && ( - + - + )} {isSelectionsNeeded && !isDataLoadingError && resources && resources.length > 0 && ( - + - + )} - + { selectionsRemaining={selectionsRemaining} serviceType={serviceType} /> - - + + ); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx index 79e18bc088d..2b30ccb746d 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx @@ -1,6 +1,6 @@ import { Autocomplete, Box, TextField } from '@linode/ui'; import { capitalize } from '@linode/utilities'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import type { FieldPathByValue } from 'react-hook-form'; @@ -92,14 +92,14 @@ export const DimensionFilterField = (props: DimensionFilterFieldProps) => { ? textFieldOperators.includes(dimensionOperatorWatcher) : false; return ( - - + { /> )} /> - - + + { /> )} /> - - + + {
- - + + ); }; diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx index 06c38e61bee..b638c61691c 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx @@ -1,6 +1,6 @@ import { Autocomplete, Box } from '@linode/ui'; import { TextField, Typography } from '@linode/ui'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import type { FieldPathByValue } from 'react-hook-form'; @@ -129,8 +129,8 @@ export const Metric = (props: MetricCriteriaProps) => { {showDeleteIcon && } - - + + { /> )} /> - - + + { /> )} /> - - + + { /> )} /> - - + + { {unit} - - + + { })} > Trigger Conditions - - + { /> )} /> - - + + { /> )} /> - - + { > consecutive occurrence(s). - - + + ); }; diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddNotificationChannelDrawer.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddNotificationChannelDrawer.tsx index cced81ec99f..3c4ca191e09 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddNotificationChannelDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddNotificationChannelDrawer.tsx @@ -6,7 +6,7 @@ import { Drawer, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; @@ -194,11 +194,11 @@ export const AddNotificationChannelDrawer = ( {selectedTemplate && selectedTemplate.channel_type === 'email' && ( - - + + To: - - + - - + + )} diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx index b5bae381f73..8d4d21dd011 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -1,5 +1,5 @@ import { CircleProgress, ErrorState } from '@linode/ui'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards'; @@ -154,8 +154,8 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { */ const renderErrorState = (errorMessage: string) => { return ( - + - + ); }; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index 29b7948d4bd..b7409301d30 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -1,5 +1,5 @@ import { Box, Paper } from '@linode/ui'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { Redirect } from 'react-router-dom'; @@ -87,8 +87,8 @@ export const CloudPulseDashboardLanding = () => { docsLabel="Docs" docsLink="https://techdocs.akamai.com/cloud-computing/docs/akamai-cloud-pulse" /> - - + + { )} - + - + ); diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx index 3d86db12a1d..61382d06154 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx @@ -1,5 +1,5 @@ import { Box, CircleProgress, Divider, ErrorState, Paper } from '@linode/ui'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards'; @@ -129,8 +129,8 @@ export const CloudPulseDashboardWithFilters = React.memo( padding: 0, }} > - - + + - + - + ({ borderColor: theme.color.grey5, margin: 0, })} /> - + {isFilterBuilderNeeded && ( )} - )} - - + + {isMandatoryFiltersSelected ? ( { }, []); return ( - - + + { - + {selectedDashboard && ( - + ({ borderColor: theme.color.grey5, margin: 0, })} /> - + )} {selectedDashboard && ( @@ -154,6 +154,6 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { preferences={preferences} /> )} - + ); }); diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index f137b451199..ff7a182e3aa 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -1,6 +1,6 @@ import { useProfile } from '@linode/queries'; -import { Paper, Typography } from '@linode/ui'; -import { Box, Grid, Stack, useTheme } from '@mui/material'; +import { Box, Paper, Typography } from '@linode/ui'; +import { GridLegacy, Stack, useTheme } from '@mui/material'; import { DateTime } from 'luxon'; import React from 'react'; @@ -280,7 +280,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { const hours = end.diff(start, 'hours').hours; const tickFormat = hours <= 24 ? 'hh:mm a' : 'LLL dd'; return ( - + { /> - + ); }; diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx index 452c4546ee7..cf07acc0a3f 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx @@ -1,4 +1,4 @@ -import { Grid, Paper } from '@mui/material'; +import { GridLegacy, Paper } from '@mui/material'; import React from 'react'; import { CloudPulseErrorPlaceholder } from '../shared/CloudPulseErrorPlaceholder'; @@ -42,11 +42,11 @@ interface WidgetProps { const renderPlaceHolder = (subtitle: string) => { return ( - + - + ); }; @@ -138,7 +138,7 @@ export const RenderWidgets = React.memo( // maintain a copy const newDashboard: Dashboard = createObjectCopy(dashboard)!; return ( - + {{ ...newDashboard }.widgets.map((widget, index) => { // check if widget metric definition is available or not if (widget) { @@ -174,7 +174,7 @@ export const RenderWidgets = React.memo( return ; } })} - + ); }, (oldProps: WidgetProps, newProps: WidgetProps) => { diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx index 45e58792447..685cc7eef2f 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx @@ -1,4 +1,4 @@ -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import React from 'react'; import { dashboardFactory } from 'src/factories'; @@ -37,7 +37,7 @@ describe('ComponentRenderer component tests', () => { }); const { getByPlaceholderText } = renderWithTheme( - + {RenderComponent({ componentKey: 'tags', componentProps: { @@ -52,7 +52,7 @@ describe('ComponentRenderer component tests', () => { }, key: 'tags', })} - + ); expect(getByPlaceholderText('Select Tags')).toBeDefined(); @@ -76,7 +76,7 @@ describe('ComponentRenderer component tests', () => { }); const { getByPlaceholderText } = renderWithTheme( - + {RenderComponent({ componentKey: 'region', componentProps: { @@ -91,7 +91,7 @@ describe('ComponentRenderer component tests', () => { }, key: 'region', })} - + ); expect(getByPlaceholderText('Select a Region')).toBeDefined(); @@ -118,7 +118,7 @@ describe('ComponentRenderer component tests', () => { }); const { getByPlaceholderText } = renderWithTheme( - + {RenderComponent({ componentKey: 'resource_id', componentProps: { @@ -134,7 +134,7 @@ describe('ComponentRenderer component tests', () => { }, key: 'resource_id', })} - + ); expect(getByPlaceholderText('Select Resources')).toBeDefined(); }); @@ -163,7 +163,7 @@ describe('ComponentRenderer component tests', () => { }); const { getByPlaceholderText } = renderWithTheme( - + {RenderComponent({ componentKey: 'node_type', componentProps: { @@ -179,7 +179,7 @@ describe('ComponentRenderer component tests', () => { }, key: 'node_type', })} - + ); expect(getByPlaceholderText('Select a Node Type')).toBeDefined(); }); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx index e919ac3e035..ba04c1e56dc 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx @@ -1,5 +1,5 @@ import { Button, ErrorState, Typography } from '@linode/ui'; -import { Grid, useTheme } from '@mui/material'; +import { GridLegacy, useTheme } from '@mui/material'; import * as React from 'react'; import KeyboardCaretDownIcon from 'src/assets/icons/caret_down.svg'; @@ -322,7 +322,13 @@ export const CloudPulseDashboardFilterBuilder = React.memo( } return filters.map((filter, index) => ( - + {RenderComponent({ componentKey: filter.configuration.type !== undefined @@ -331,7 +337,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( componentProps: { ...getProps(filter) }, key: index + filter.configuration.filterKey, })} - + )); }, [dashboard, getProps, isServiceAnalyticsIntegration]); @@ -344,7 +350,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( } return ( - - Filters - - + - - + + ); }, compareProps diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx index ee48340adce..4875c182954 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx @@ -1,4 +1,4 @@ -import { Grid, Paper } from '@mui/material'; +import { GridLegacy, Paper } from '@mui/material'; import React from 'react'; import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg'; @@ -8,7 +8,7 @@ export const CloudPulseErrorPlaceholder = React.memo( (props: { errorMessage: string }) => { const { errorMessage } = props; return ( - + - + ); } ); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx index 19a17b667bd..259ef2f7203 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx @@ -1,6 +1,6 @@ import { useIsGeckoEnabled } from '@linode/shared'; import { Divider, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 4e37b6285b5..ea01b8ff886 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -2,7 +2,7 @@ import { useRegionsQuery } from '@linode/queries'; import { CircleProgress, Divider, ErrorState, Notice, Paper } from '@linode/ui'; import { formatStorageUnits, scrollErrorIntoViewV2 } from '@linode/utilities'; import { createDatabaseSchema } from '@linode/validation/lib/databases.schema'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { createLazyRoute } from '@tanstack/react-router'; import { useFormik } from 'formik'; import * as React from 'react'; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx index fb20e96e15a..935f0cb8c48 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx @@ -5,7 +5,7 @@ import { RadioGroup, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useState } from 'react'; import * as React from 'react'; import type { ChangeEvent } from 'react'; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx index e9a0fc6c986..bcdee109fc6 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx @@ -1,5 +1,5 @@ import { Autocomplete, Box } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { getEngineOptions } from 'src/features/Databases/DatabaseCreate/utilities'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx index bc8642088da..690720026cf 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx @@ -1,5 +1,5 @@ import { Box, Button, Paper, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx index 6dcc1afa452..4f8a0839c78 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx @@ -11,7 +11,7 @@ import { } from '@linode/ui'; import { scrollErrorIntoViewV2 } from '@linode/utilities'; import { createDynamicAdvancedConfigSchema } from '@linode/validation'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { enqueueSnackbar } from 'notistack'; import React, { useEffect, useMemo, useState } from 'react'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx index 4fa04520297..8d73e04df74 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx @@ -13,7 +13,7 @@ import { Radio, RadioGroup, } from '@mui/material'; -import Grid from '@mui/material/Grid'; +import { GridLegacy } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { DateTime } from 'luxon'; @@ -187,14 +187,14 @@ export const DatabaseBackups = (props: Props) => { /> )} - - + Date { value={selectedDate} /> - - + + Time (UTC) {/* TODO: Replace Time Select to the own custom date-time picker component when it's ready */} @@ -247,9 +247,9 @@ export const DatabaseBackups = (props: Props) => { value={selectedTime} /> - - - + + + - + {database && ( { const hasUpdates = hasPendingUpdates(databasePendingUpdates); return ( - - + + Maintenance Version {engineVersion} @@ -60,7 +60,7 @@ export const DatabaseSettingsMaintenance = (props: Props) => { } /> )} - + {/* TODO Uncomment and provide value when the EOL is returned by the API. Currently, it is not supported, however they are working on returning it since it has value to the end user @@ -68,7 +68,7 @@ export const DatabaseSettingsMaintenance = (props: Props) => { End of life */} - + Maintenance updates {hasUpdates ? ( @@ -84,8 +84,8 @@ export const DatabaseSettingsMaintenance = (props: Props) => { maintenance window.{' '} )} - - + + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx index c97e81f0d26..96c80996d90 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx @@ -1,5 +1,5 @@ import { Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import ClusterConfiguration from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts index dbc39604847..8d775843f5e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts @@ -1,8 +1,8 @@ import { Typography } from '@linode/ui'; -import Grid2 from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; -export const StyledGridContainer = styled(Grid2, { +export const StyledGridContainer = styled(Grid, { label: 'StyledGridContainer', })(({ theme }) => ({ '&>*:nth-of-type(even)': { @@ -35,7 +35,7 @@ export const StyledLabelTypography = styled(Typography, { padding: `${theme.spacing(0.5)} 15px`, })); -export const StyledValueGrid = styled(Grid2, { +export const StyledValueGrid = styled(Grid, { label: 'StyledValueGrid', })(({ theme }) => ({ alignItems: 'center', @@ -43,5 +43,3 @@ export const StyledValueGrid = styled(Grid2, { display: 'flex', padding: `0 ${theme.spacing()}`, })); - -// theme.spacing() 8 diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx index f34111513e8..3e49fc058d2 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx @@ -1,7 +1,7 @@ import { useRegionsQuery } from '@linode/queries'; import { TooltipIcon, Typography } from '@linode/ui'; import { convertMegabytesTo, formatStorageUnits } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx index e9076344646..82fb4fe4854 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx @@ -1,7 +1,7 @@ import { getSSLFields } from '@linode/api-v4/lib/databases/databases'; import { Button, CircleProgress, TooltipIcon, Typography } from '@linode/ui'; import { downloadFile } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 5f7c9f855a6..a12c98c990e 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -13,7 +13,7 @@ import { } from '@linode/ui'; import { scrollErrorIntoView } from '@linode/utilities'; import { createDomainSchema } from '@linode/validation/lib/domains.schema'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { useNavigate } from '@tanstack/react-router'; import { useFormik } from 'formik'; diff --git a/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx b/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx index 7856b1c50db..c39d13aed53 100644 --- a/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx +++ b/packages/manager/src/features/Domains/DomainDetail/DomainDetail.tsx @@ -6,7 +6,7 @@ import { Stack, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { useLocation, useNavigate, useParams } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.styles.ts b/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.styles.ts index 8d19fdb2473..81cca80a3ab 100644 --- a/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.styles.ts +++ b/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.styles.ts @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { TableCell } from 'src/components/TableCell'; diff --git a/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.tsx b/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.tsx index 0a384dddc61..f4de9f3e3e2 100644 --- a/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.tsx +++ b/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecords.tsx @@ -1,7 +1,7 @@ import { deleteDomainRecord as _deleteDomainRecord } from '@linode/api-v4/lib/domains'; import { ActionsPanel, Stack, Typography } from '@linode/ui'; import { scrollErrorIntoViewV2 } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransferCreate.styles.ts b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransferCreate.styles.ts index 80b25d3d0b5..4d76e6e5dba 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransferCreate.styles.ts +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransferCreate.styles.ts @@ -1,5 +1,5 @@ import { Notice } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledNotice = styled(Notice, { diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx index 4302bb60a99..2ae39b6cc49 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/EntityTransfersCreate.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import { createLazyRoute } from '@tanstack/react-router'; import { curry } from 'ramda'; diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/TransferControls.styles.ts b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/TransferControls.styles.ts index 81ac813b561..09b845f766d 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/TransferControls.styles.ts +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/TransferControls.styles.ts @@ -1,5 +1,5 @@ import { Button, TextField, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; // sm = 600, md = 960, lg = 1280 diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx index df1e13eb293..aef63548003 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx @@ -1,5 +1,5 @@ import { Button, Notice, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/features/GlobalNotifications/EmailBounce.styles.tsx b/packages/manager/src/features/GlobalNotifications/EmailBounce.styles.tsx index aa59af34f4c..188c62ff8e5 100644 --- a/packages/manager/src/features/GlobalNotifications/EmailBounce.styles.tsx +++ b/packages/manager/src/features/GlobalNotifications/EmailBounce.styles.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledGrid = styled(Grid, { label: 'StyledGrid' })( diff --git a/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx b/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx index 540942d3b12..26b12b3f38c 100644 --- a/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx +++ b/packages/manager/src/features/GlobalNotifications/EmailBounce.tsx @@ -6,7 +6,7 @@ import { useProfile, } from '@linode/queries'; import { Button, Notice, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useSnackbar } from 'notistack'; diff --git a/packages/manager/src/features/Help/Panels/OtherWays.tsx b/packages/manager/src/features/Help/Panels/OtherWays.tsx index d6224534c06..5c069fb5109 100644 --- a/packages/manager/src/features/Help/Panels/OtherWays.tsx +++ b/packages/manager/src/features/Help/Panels/OtherWays.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Help/Panels/PopularPosts.tsx b/packages/manager/src/features/Help/Panels/PopularPosts.tsx index d7981604513..71b6fedbd78 100644 --- a/packages/manager/src/features/Help/Panels/PopularPosts.tsx +++ b/packages/manager/src/features/Help/Panels/PopularPosts.tsx @@ -1,5 +1,5 @@ import { Paper, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx b/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx index 842c32cd33c..1a6afe1e2cb 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx index 5b54eae2623..d6175b1b3f4 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx @@ -1,7 +1,7 @@ import { Box, H1Header, InputAdornment, Notice, TextField } from '@linode/ui'; import { getQueryParamFromQueryString } from '@linode/utilities'; import Search from '@mui/icons-material/Search'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; diff --git a/packages/manager/src/features/IAM/Roles/RolesTable/AssignSelectedRolesDrawer.tsx b/packages/manager/src/features/IAM/Roles/RolesTable/AssignSelectedRolesDrawer.tsx index 0fd71ff1482..0f742afa093 100644 --- a/packages/manager/src/features/IAM/Roles/RolesTable/AssignSelectedRolesDrawer.tsx +++ b/packages/manager/src/features/IAM/Roles/RolesTable/AssignSelectedRolesDrawer.tsx @@ -1,7 +1,7 @@ import { useAccountUsers } from '@linode/queries'; import { ActionsPanel, Autocomplete, Drawer, Typography } from '@linode/ui'; import { useTheme } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React, { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; diff --git a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx index e3d1c66e966..d711b67861d 100644 --- a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx +++ b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx @@ -1,6 +1,6 @@ import { Button, Select, Typography } from '@linode/ui'; import { capitalizeAllWords } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; import { sortRows, diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx index c85f6350ed5..62ae7741f7a 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx @@ -1,6 +1,6 @@ import { Button, CircleProgress, Select, Typography } from '@linode/ui'; import { useTheme } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { useHistory, useParams } from 'react-router-dom'; diff --git a/packages/manager/src/features/IAM/Shared/Permissions/Permissions.style.ts b/packages/manager/src/features/IAM/Shared/Permissions/Permissions.style.ts index 3cb61f3d3fb..cd65e7d75f3 100644 --- a/packages/manager/src/features/IAM/Shared/Permissions/Permissions.style.ts +++ b/packages/manager/src/features/IAM/Shared/Permissions/Permissions.style.ts @@ -1,5 +1,5 @@ import { Box, Typography } from '@linode/ui'; -import { Grid } from '@mui/material'; +import { GridLegacy } from '@mui/material'; import { styled } from '@mui/material/styles'; export const sxTooltipIcon = { @@ -7,7 +7,7 @@ export const sxTooltipIcon = { padding: 0, }; -export const StyledGrid = styled(Grid, { label: 'StyledGrid' })(() => ({ +export const StyledGrid = styled(GridLegacy, { label: 'StyledGrid' })(() => ({ alignItems: 'center', marginBottom: 2, })); diff --git a/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx b/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx index a0432e9d221..3cf9985ad1c 100644 --- a/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx +++ b/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx @@ -1,6 +1,6 @@ import { StyledLinkButton, TooltipIcon, Typography } from '@linode/ui'; import { debounce } from '@mui/material'; -import Grid from '@mui/material/Grid'; +import { Grid } from '@mui/material'; import * as React from 'react'; import { useCalculateHiddenItems } from '../../hooks/useCalculateHiddenItems'; diff --git a/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx b/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx index 234f11ce814..72080851eb5 100644 --- a/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx +++ b/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx @@ -1,5 +1,5 @@ import { Paper, Stack, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; diff --git a/packages/manager/src/features/IAM/Users/UserEntities/AssignedEntitiesTable.tsx b/packages/manager/src/features/IAM/Users/UserEntities/AssignedEntitiesTable.tsx index d05c73af0e3..5cbb7e7aca6 100644 --- a/packages/manager/src/features/IAM/Users/UserEntities/AssignedEntitiesTable.tsx +++ b/packages/manager/src/features/IAM/Users/UserEntities/AssignedEntitiesTable.tsx @@ -1,5 +1,5 @@ import { Select, Typography, useTheme } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { useLocation, useParams } from 'react-router-dom'; diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx index eee51b7b614..6185061e3ea 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx @@ -1,6 +1,6 @@ import { ActionsPanel, Drawer, Typography } from '@linode/ui'; import { useTheme } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React, { useState } from 'react'; import { FormProvider, useFieldArray, useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom'; diff --git a/packages/manager/src/features/Images/ImagesCreate/ImageCreateContainer.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageCreateContainer.tsx index e0543a7cad6..92314e21ad9 100644 --- a/packages/manager/src/features/Images/ImagesCreate/ImageCreateContainer.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageCreateContainer.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { LandingHeader } from 'src/components/LandingHeader'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx index f5d6fdcc288..315dbd44efe 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx @@ -1,6 +1,6 @@ import { useAccount } from '@linode/queries'; import { Stack, Typography } from '@linode/ui'; -import { Grid2, useMediaQuery } from '@mui/material'; +import { Grid, useMediaQuery } from '@mui/material'; import React from 'react'; import { DocsLink } from 'src/components/DocsLink/DocsLink'; @@ -50,7 +50,7 @@ export const ClusterTierPanel = (props: Props) => { - + { } tooltipPlacement={smDownBreakpoint ? 'bottom' : 'right'} /> - + ); }; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index c39def354d4..b2ed26ad5e8 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -8,7 +8,7 @@ import { Typography, } from '@linode/ui'; import { FormLabel, styled } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { ErrorMessage } from 'src/components/ErrorMessage'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index f0804d4082d..41cfb9aabf2 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -16,7 +16,7 @@ import { import { plansNoticesUtils, scrollErrorIntoViewV2 } from '@linode/utilities'; import { createKubeClusterWithRequiredACLSchema } from '@linode/validation'; import { Divider } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { createLazyRoute } from '@tanstack/react-router'; import { pick, remove, update } from 'ramda'; import * as React from 'react'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx index a6fd25d849c..b8988d0b925 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx @@ -1,5 +1,5 @@ import { CircleProgress, ErrorState } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useIsAcceleratedPlansEnabled } from 'src/features/components/PlansPanel/utils'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/APLSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/APLSummaryPanel.tsx index e6e05ba553e..c97ef63f496 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/APLSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/APLSummaryPanel.tsx @@ -1,5 +1,5 @@ import { Paper, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import axios from 'axios'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs.tsx index 77486a71bcb..5730e3342e7 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs.tsx @@ -2,7 +2,7 @@ import { useRegionsQuery } from '@linode/queries'; import { CircleProgress, TooltipIcon, Typography } from '@linode/ui'; import { pluralize } from '@linode/utilities'; import { useMediaQuery } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx index c81d723920b..623f0386f79 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx @@ -1,7 +1,7 @@ import { useProfile } from '@linode/queries'; import { Box, CircleProgress, StyledLinkButton } from '@linode/ui'; import { pluralize } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index 72fecafdd90..7d9e77b6552 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledActionRowGrid = styled(Grid, { diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx index 294be393765..cb611eb2b73 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx @@ -7,7 +7,7 @@ import { Toggle, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx index 78d31a203c2..c03a0df6a0b 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { usePreferences } from '@linode/queries'; import { Box, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx index feeeb15b6b7..376a176027e 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx @@ -1,6 +1,6 @@ import { Notice, Typography } from '@linode/ui'; import { Hidden } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useFlags } from 'src/hooks/useFlags'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx index 03468df571d..91f708c3154 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx @@ -1,7 +1,7 @@ import { Box, Button, Chip } from '@linode/ui'; import { Hidden } from '@linode/ui'; import { convertMegabytesTo } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/AccessTable.tsx b/packages/manager/src/features/Linodes/AccessTable.tsx index 40ad7f09ecf..29bdfff649b 100644 --- a/packages/manager/src/features/Linodes/AccessTable.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { TableBody } from 'src/components/TableBody'; diff --git a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx index c43570848d1..6adff24de39 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx @@ -7,7 +7,7 @@ import { } from '@linode/queries'; import { Box, Notice, Paper, Typography } from '@linode/ui'; import { getQueryParamsFromQueryString } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import { castDraft } from 'immer'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/CloneLanding/Disks.tsx b/packages/manager/src/features/Linodes/CloneLanding/Disks.tsx index 4a33769e7d8..42ba02f7adc 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/Disks.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/Disks.tsx @@ -1,5 +1,5 @@ import { Checkbox } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import Paginate from 'src/components/Paginate'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx index 3701a902690..58aced7e3c8 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx @@ -6,7 +6,7 @@ import { TooltipIcon, Typography, } from '@linode/ui'; -import { Grid2 } from '@mui/material'; +import { Grid } from '@mui/material'; import { useSnackbar } from 'notistack'; import React from 'react'; import { useController, useFormContext } from 'react-hook-form'; @@ -106,7 +106,7 @@ export const InterfaceType = ({ index }: Props) => { aria-labelledby="network-connection-label" sx={{ display: 'block', marginBottom: '0px !important' }} > - + {interfaceTypes.map((interfaceType) => ( { sxCardBaseIcon={{ svg: { fontSize: '20px' } }} /> ))} - + ); diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.tsx index 9fcd813ba09..8fe3799c95e 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.tsx @@ -1,6 +1,6 @@ import { useLinodeBackupsQuery } from '@linode/queries'; import { Box, Notice, Paper, Stack, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { useController, useWatch } from 'react-hook-form'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppSection.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppSection.tsx index 1f0ee2e0d4c..b3a6644f828 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppSection.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppSection.tsx @@ -1,5 +1,5 @@ import { Divider, Stack, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { AppSelectionCard } from './AppSelectionCard'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppsList.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppsList.tsx index 4af15272f10..da3fc000bfe 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppsList.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Marketplace/AppsList.tsx @@ -1,5 +1,5 @@ import { Box, CircleProgress, ErrorState, Stack } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; import { useController, useFormContext } from 'react-hook-form'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx index 0c51ee9e7ae..044d45e24ef 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx @@ -1,7 +1,7 @@ import { useLinodesQuery } from '@linode/queries'; import { getAPIFilterFromQuery } from '@linode/search'; import { Box, Notice, Stack, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useQueryClient } from '@tanstack/react-query'; import React, { useState } from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx index 9d3cae95bce..c13d4bb063b 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx @@ -5,7 +5,7 @@ import { formatStorageUnits, isNotNullOrUndefined, } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts b/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts index 65b4ccf511b..b3104cc72ec 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts @@ -1,7 +1,7 @@ // This component was built assuming an unmodified MUI import { Typography as FontTypography } from '@linode/design-language-system'; import { Box, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import Table from '@mui/material/Table'; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx index 371791dba07..a6ef71338d7 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx @@ -2,7 +2,7 @@ import { usePreferences, useProfile } from '@linode/queries'; import { Box, Chip, Tooltip, TooltipIcon, Typography } from '@linode/ui'; import { pluralize } from '@linode/utilities'; import { useMediaQuery } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { useHistory, useLocation } from 'react-router-dom'; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx index 5068705f3fc..5a217514996 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx @@ -1,5 +1,5 @@ import { useLinodeUpdateMutation, useProfile } from '@linode/queries'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 5b098c45b35..4f778741677 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -34,7 +34,7 @@ import { createStringsFromDevices, scrollErrorIntoViewV2, } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import { useQueryClient } from '@tanstack/react-query'; import { useFormik } from 'formik'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.tsx index 27843aae1f3..67e3eb5d7b5 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.tsx @@ -7,7 +7,7 @@ import { } from '@linode/queries'; import { Autocomplete, ErrorState, Paper, Stack, Typography } from '@linode/ui'; import { formatNumber, formatPercentage, getMetrics } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import { DateTime } from 'luxon'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx index 15e5dc5e7bf..9ffb7e85a00 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx @@ -1,6 +1,6 @@ import { Paper } from '@linode/ui'; import { getMetrics } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx index f2cb11ad22e..8b1b9d0b787 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx @@ -17,7 +17,7 @@ import { Typography, } from '@linode/ui'; import { API_MAX_PAGE_SIZE, areArraysEqual } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx index b47d2c70476..f2c5cd3a4f0 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx @@ -15,7 +15,7 @@ import { Typography, } from '@linode/ui'; import { usePrevious } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/DNSResolvers.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/DNSResolvers.tsx index f600e3f4212..86aea9672f6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/DNSResolvers.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/DNSResolvers.tsx @@ -1,6 +1,6 @@ import { useRegionsQuery } from '@linode/queries'; import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx index be45a2993e5..2b7059d21b6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx @@ -1,7 +1,7 @@ import { useLinodeQuery } from '@linode/queries'; import { useIsGeckoEnabled } from '@linode/shared'; import { Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferContent.tsx index 3a05a077160..4e169011fd3 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferContent.tsx @@ -1,5 +1,5 @@ import { CircleProgress, Notice } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx index 64a4126ff90..3a66fbe25ce 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/AlertSection.tsx @@ -7,7 +7,7 @@ import { Toggle, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index b3189e342d7..52c9582997c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -7,7 +7,7 @@ import { TextField, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx index 8178c71c56f..1f250299659 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeWatchdogPanel.tsx @@ -9,7 +9,7 @@ import { Toggle, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; interface Props { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx index 5f9052309d0..438ece340e3 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx @@ -5,7 +5,7 @@ import { } from '@linode/queries'; import { Box, Button, Paper, Stack, Typography } from '@linode/ui'; import { Hidden } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useParams } from 'react-router-dom'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx index d867f44fb79..40c8c1145ed 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx @@ -1,6 +1,6 @@ import { useLinodeQuery, usePreferences } from '@linode/queries'; import { BetaChip, CircleProgress, ErrorState } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { matchPath, diff --git a/packages/manager/src/features/Linodes/LinodesLanding/CardView.tsx b/packages/manager/src/features/Linodes/LinodesLanding/CardView.tsx index 09d7a0858fd..fd04cbe9864 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/CardView.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/CardView.tsx @@ -1,6 +1,6 @@ import { useProfile } from '@linode/queries'; import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { keyframes, styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx index 0251355ba46..1b4244a0f98 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx @@ -8,7 +8,7 @@ import { Typography, } from '@linode/ui'; import { groupByTags, sortGroups } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import GridView from 'src/assets/icons/grid-view.svg'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx index 61f168f6271..d00fdf9a14c 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx @@ -1,7 +1,7 @@ import { useIsGeckoEnabled } from '@linode/shared'; import { Box, CircleProgress, IconButton, Paper, Tooltip } from '@linode/ui'; import { getQueryParamsFromQueryString } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useLocation } from 'react-router-dom'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.styles.ts b/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.styles.ts index 8ca5e1000cb..8c34adbea3a 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.styles.ts +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.styles.ts @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledWrapperGrid = styled(Grid, { label: 'StyledWrapperGrid' })({ diff --git a/packages/manager/src/features/Linodes/LinodesLanding/TableWrapper.tsx b/packages/manager/src/features/Linodes/LinodesLanding/TableWrapper.tsx index 0e2d82a177e..93d1686f664 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/TableWrapper.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/TableWrapper.tsx @@ -1,5 +1,5 @@ import { usePreferences } from '@linode/queries'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { Table } from 'src/components/Table'; diff --git a/packages/manager/src/features/Linodes/RenderIPs.tsx b/packages/manager/src/features/Linodes/RenderIPs.tsx index 11a77f019d6..62add23b0b2 100644 --- a/packages/manager/src/features/Linodes/RenderIPs.tsx +++ b/packages/manager/src/features/Linodes/RenderIPs.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { Link } from 'src/components/Link'; 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 fccefb80f3e..7dc5619d81d 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ActiveConnections.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ActiveConnections.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; 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 1503a0a11e5..ed8bfd7bed6 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx @@ -1,6 +1,6 @@ import { Box, Notice, Typography } from '@linode/ui'; import { isToday as _isToday } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx index ac82f80e235..9154aa1fea9 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx @@ -1,5 +1,5 @@ import { roundTo } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/CommonStyles.styles.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/CommonStyles.styles.tsx index 8b34bd3a22b..a3120815b10 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/CommonStyles.styles.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/CommonStyles.styles.tsx @@ -1,5 +1,5 @@ import { Paper, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledTypography = styled(Typography, { diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/GaugesSection.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/GaugesSection.tsx index 12ff3431095..624b11101a5 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/GaugesSection.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/GaugesSection.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { CPUGauge } from '../../LongviewLanding/Gauges/CPU'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx index c9243588dd1..fadb9cd3bde 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from '@linode/ui'; import { formatUptime, readableBytes } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import CPUIcon from 'src/assets/icons/longview/cpu-icon.svg'; 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 2c80fda2371..8f7e0526763 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/ListeningServices.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/ListeningServices.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import Paginate from 'src/components/Paginate'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/LongviewDetailOverview.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/LongviewDetailOverview.tsx index 77b2b08e204..4ea7c75ad7d 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/LongviewDetailOverview.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/LongviewDetailOverview.tsx @@ -1,5 +1,5 @@ import { Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLGraphs.tsx index 118d072bb01..c7554b8cf3d 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLGraphs.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; 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 cf45330be89..cfc4b4c266f 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx @@ -1,6 +1,6 @@ import { Box, Notice, Typography } from '@linode/ui'; import { isToday as _isToday } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; 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 8b637126438..ff168c81e2b 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx @@ -1,6 +1,6 @@ import { Box, Notice, Typography } from '@linode/ui'; import { isToday as _isToday } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINXGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINXGraphs.tsx index 0448c525d91..a7b16d50ceb 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINXGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINXGraphs.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx index 58a926f7d38..dc81887acfb 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx @@ -1,5 +1,5 @@ import { isToday as _isToday } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx index 0ec131b8fde..87b90d90172 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx @@ -1,6 +1,6 @@ import { Paper } from '@linode/ui'; import { isToday as _isToday } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ProcessGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ProcessGraphs.tsx index 86a763f3f75..0c586928faa 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ProcessGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ProcessGraphs.tsx @@ -1,5 +1,5 @@ import { convertBytesToTarget, readableBytes } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx index b318ede448b..0b224ee99a4 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx @@ -1,7 +1,7 @@ import { TextField } from '@linode/ui'; import { isToday as _isToday } from '@linode/utilities'; import { escapeRegExp } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx index 372daf05d03..81a4f93ff23 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/TopProcesses.tsx @@ -1,6 +1,6 @@ import { Box, Typography } from '@linode/ui'; import { readableBytes } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { Table } from 'src/components/Table'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.styles.ts b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.styles.ts index 31a4f939257..67f98c0dc9b 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.styles.ts +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.styles.ts @@ -1,5 +1,5 @@ import { Button } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledButton = styled(Button, { label: 'StyledButton' })({ diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx index 2a60e5b9c9b..e0267417bce 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx @@ -1,7 +1,7 @@ import { useProfile } from '@linode/queries'; import { Typography } from '@linode/ui'; import { formatUptime } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { compose } from 'recompose'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientInstructions.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientInstructions.tsx index f1e51e9b333..9c8d6e0e09a 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientInstructions.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientInstructions.tsx @@ -1,5 +1,5 @@ import { Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx index 55916cdc777..e69a8ae61e6 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx @@ -1,6 +1,6 @@ import { useGrants } from '@linode/queries'; import { Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { compose } from 'recompose'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.styles.ts b/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.styles.ts index 49b6dde5137..4c73957710e 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.styles.ts +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.styles.ts @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledCTAGrid = styled(Grid, { label: 'StyledCTAGrid' })( diff --git a/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts b/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts index 689a0de5e95..4a8de82114e 100644 --- a/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts +++ b/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledInstructionGrid = styled(Grid, { diff --git a/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx b/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx index c84f980b4c1..06b7e5f7f42 100644 --- a/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx +++ b/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/Managed/Contacts/Contacts.styles.tsx b/packages/manager/src/features/Managed/Contacts/Contacts.styles.tsx index f43dad5d098..c47a70ceddd 100644 --- a/packages/manager/src/features/Managed/Contacts/Contacts.styles.tsx +++ b/packages/manager/src/features/Managed/Contacts/Contacts.styles.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledWrapperGrid = styled(Grid, { label: 'StyledWrapperGrid' })( diff --git a/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx b/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx index 0818ffc2efc..45dba3fd03c 100644 --- a/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx +++ b/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx @@ -1,6 +1,6 @@ import { ActionsPanel, Drawer, Notice, Select, TextField } from '@linode/ui'; import { createContactSchema } from '@linode/validation/lib/managed.schema'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useNavigate } from '@tanstack/react-router'; import { useMatch } from '@tanstack/react-router'; import { Formik } from 'formik'; diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.styles.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.styles.tsx index d75fefec817..6396b984005 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.styles.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.styles.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledRootGrid = styled(Grid, { label: 'StyledRootGrid' })( diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.tsx index 2ee2e440df1..03b1180aa9e 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/DashboardCard.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.styles.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.styles.tsx index 8feb94065c6..aac50be3668 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.styles.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.styles.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import DashboardCard from './DashboardCard'; diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.tsx index 3d2c1444baa..6486c966caa 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedDashboardCard.tsx @@ -1,5 +1,5 @@ import { CircleProgress, ErrorState } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.styles.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.styles.tsx index ac529066ad7..24b4832d17c 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.styles.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.styles.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledTypography = styled(Typography, { diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.tsx index 5e1e7f9d7fd..9c539b2c10b 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorStatus.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import MonitorFailed from 'src/assets/icons/monitor-failed.svg'; diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.styles.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.styles.tsx index a144c516581..881622f05bb 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.styles.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.styles.tsx @@ -1,5 +1,5 @@ import { Button } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledButton = styled(Button, { label: 'StyledButton' })( diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx index ac55d9ae014..fd947dbe634 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; // eslint-disable-next-line no-restricted-imports import { useHistory } from 'react-router-dom'; diff --git a/packages/manager/src/features/Managed/MonitorDrawer.tsx b/packages/manager/src/features/Managed/MonitorDrawer.tsx index b8c9b26cde8..92635aea7a1 100644 --- a/packages/manager/src/features/Managed/MonitorDrawer.tsx +++ b/packages/manager/src/features/Managed/MonitorDrawer.tsx @@ -8,7 +8,7 @@ import { TextField, } from '@linode/ui'; import { createServiceMonitorSchema } from '@linode/validation/lib/managed.schema'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useMatch, useNavigate, useParams } from '@tanstack/react-router'; import { Formik } from 'formik'; import * as React from 'react'; diff --git a/packages/manager/src/features/Managed/Monitors/IssueDay.styles.tsx b/packages/manager/src/features/Managed/Monitors/IssueDay.styles.tsx index 9e4abbe7f1b..cb1b2b54c28 100644 --- a/packages/manager/src/features/Managed/Monitors/IssueDay.styles.tsx +++ b/packages/manager/src/features/Managed/Monitors/IssueDay.styles.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; diff --git a/packages/manager/src/features/Managed/Monitors/IssueDay.tsx b/packages/manager/src/features/Managed/Monitors/IssueDay.tsx index 2a2d7af15e5..ce34e0b23d2 100644 --- a/packages/manager/src/features/Managed/Monitors/IssueDay.tsx +++ b/packages/manager/src/features/Managed/Monitors/IssueDay.tsx @@ -1,5 +1,5 @@ import { Tooltip } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import Bad from 'src/assets/icons/monitor-failed.svg'; diff --git a/packages/manager/src/features/Managed/Monitors/MonitorRow.styles.tsx b/packages/manager/src/features/Managed/Monitors/MonitorRow.styles.tsx index fd372426b08..4d2f08a9424 100644 --- a/packages/manager/src/features/Managed/Monitors/MonitorRow.styles.tsx +++ b/packages/manager/src/features/Managed/Monitors/MonitorRow.styles.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx b/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx index ecb7ef9334b..0522d1f27ce 100644 --- a/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx +++ b/packages/manager/src/features/Managed/Monitors/MonitorRow.tsx @@ -1,5 +1,5 @@ import { Tooltip, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import TicketIcon from 'src/assets/icons/ticket.svg'; diff --git a/packages/manager/src/features/Managed/Monitors/MonitorTable.styles.tsx b/packages/manager/src/features/Managed/Monitors/MonitorTable.styles.tsx index 20d3366ba45..97d9faeaeff 100644 --- a/packages/manager/src/features/Managed/Monitors/MonitorTable.styles.tsx +++ b/packages/manager/src/features/Managed/Monitors/MonitorTable.styles.tsx @@ -1,4 +1,4 @@ -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledGrid = styled(Grid, { diff --git a/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx b/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx index 9a250b38341..ab573f5296a 100644 --- a/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx +++ b/packages/manager/src/features/Managed/Monitors/MonitorTable.tsx @@ -1,5 +1,5 @@ import { Button, Notice, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useMatch, useNavigate, useParams } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx index 2b99a4de8c4..eae1012dc76 100644 --- a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx @@ -7,7 +7,7 @@ import { Toggle, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useNavigate } from '@tanstack/react-router'; import { Formik } from 'formik'; import * as React from 'react'; diff --git a/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.styles.tsx b/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.styles.tsx index 4a18debd516..5b9b6480b34 100644 --- a/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.styles.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.styles.tsx @@ -1,5 +1,5 @@ import { CircleProgress, Paper, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import SSHKeyIcon from 'src/assets/icons/ssh-key.svg'; diff --git a/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.tsx b/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.tsx index 5428aa29c0b..2215e2eb4e9 100644 --- a/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/LinodePubKey.tsx @@ -1,7 +1,7 @@ import { usePreferences } from '@linode/queries'; import { Button, ErrorState, Stack, Typography } from '@linode/ui'; import { useMediaQuery } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import copy from 'copy-to-clipboard'; import * as React from 'react'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerActiveCheck.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerActiveCheck.tsx index 6dcd084b64f..bc129d6d475 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerActiveCheck.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerActiveCheck.tsx @@ -11,7 +11,7 @@ import { CHECK_INTERVAL, CHECK_TIMEOUT, } from '@linode/validation'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useFlags } from 'src/hooks/useFlags'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx index a714eccf44a..9deb4ea7a12 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx @@ -8,7 +8,7 @@ import { TextField, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx index 88fb929ae4e..82f69f80dc3 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx @@ -11,7 +11,7 @@ import { TextField, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx index e5efa06488c..57af68da21b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx @@ -1,5 +1,5 @@ import { useNodeBalancerQuery } from '@linode/queries'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useParams } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.tsx index 6f18819f921..6656d36eaf9 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.tsx @@ -4,7 +4,7 @@ import { Toggle, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import type { NodeBalancerConfigPanelProps } from './types'; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx index 63a203ce68f..a5b7c09b7a1 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketSSL.tsx @@ -9,7 +9,7 @@ import { TextField, Typography, } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx index fd9a9c8b2f2..c156e07226a 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/FolderTableRow.tsx @@ -1,5 +1,5 @@ import { Hidden } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx index d03a279d1a0..1e1c80a767e 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectTableRow.tsx @@ -1,7 +1,7 @@ import { Box, StyledLinkButton, Typography } from '@linode/ui'; import { Hidden } from '@linode/ui'; import { readableBytes } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import ObjectIcon from 'src/assets/icons/objectStorage/object.svg'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx index 04549f82396..3bfbb0c81ae 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx @@ -1,7 +1,7 @@ import { useProfile, useRegionsQuery } from '@linode/queries'; import { CircleProgress, ErrorState, Notice, Typography } from '@linode/ui'; import { readableBytes, useOpenClose } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx index 55d1a69e042..cc0d8c1dd48 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx @@ -1,7 +1,7 @@ import { useProfile } from '@linode/queries'; import { CircleProgress, ErrorState, Notice, Typography } from '@linode/ui'; import { readableBytes, useOpenClose } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx index 067ceb64f88..2679fe4dedf 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx @@ -1,6 +1,6 @@ import { useAllLinodesQuery, useLinodeQuery } from '@linode/queries'; import { Button, ErrorState, Stack } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx index 8f211d215f2..0a5614a4fc2 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx @@ -1,5 +1,5 @@ import { Box, Divider, Notice, Paper, Stack, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import EnabledIcon from 'src/assets/icons/checkmark-enabled.svg'; diff --git a/packages/manager/src/features/Profile/Referrals/Referrals.styles.ts b/packages/manager/src/features/Profile/Referrals/Referrals.styles.ts index aba4d27b317..dc62bd95228 100644 --- a/packages/manager/src/features/Profile/Referrals/Referrals.styles.ts +++ b/packages/manager/src/features/Profile/Referrals/Referrals.styles.ts @@ -1,5 +1,5 @@ import { Notice, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledResultsWrapper = styled('div', { diff --git a/packages/manager/src/features/Profile/Referrals/Referrals.tsx b/packages/manager/src/features/Profile/Referrals/Referrals.tsx index c8d63e65852..df42cc6e1e5 100644 --- a/packages/manager/src/features/Profile/Referrals/Referrals.tsx +++ b/packages/manager/src/features/Profile/Referrals/Referrals.tsx @@ -1,6 +1,6 @@ import { useProfile } from '@linode/queries'; import { CircleProgress, Notice, Paper, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; diff --git a/packages/manager/src/features/Search/ResultGroup.tsx b/packages/manager/src/features/Search/ResultGroup.tsx index b0e649e1fcd..79864d159c2 100644 --- a/packages/manager/src/features/Search/ResultGroup.tsx +++ b/packages/manager/src/features/Search/ResultGroup.tsx @@ -1,6 +1,6 @@ import { Hidden } from '@linode/ui'; import { capitalize, splitAt } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { isEmpty } from 'ramda'; import * as React from 'react'; diff --git a/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts b/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts index 018d8d69f7e..b9e57065455 100644 --- a/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts +++ b/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts @@ -1,5 +1,5 @@ import { Button, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { TableCell } from 'src/components/TableCell'; diff --git a/packages/manager/src/features/Support/AttachFileListItem.tsx b/packages/manager/src/features/Support/AttachFileListItem.tsx index 7aa4181750b..07251e33bec 100644 --- a/packages/manager/src/features/Support/AttachFileListItem.tsx +++ b/packages/manager/src/features/Support/AttachFileListItem.tsx @@ -1,6 +1,6 @@ import { CloseIcon, InputAdornment, TextField } from '@linode/ui'; import CloudUpload from '@mui/icons-material/CloudUpload'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Support/ExpandableTicketPanel.tsx b/packages/manager/src/features/Support/ExpandableTicketPanel.tsx index 40e4ec90926..9f076e5ee94 100644 --- a/packages/manager/src/features/Support/ExpandableTicketPanel.tsx +++ b/packages/manager/src/features/Support/ExpandableTicketPanel.tsx @@ -1,6 +1,6 @@ import { useProfile } from '@linode/queries'; import { Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx index 3add1c09e2a..c625f4c2b91 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx @@ -4,7 +4,7 @@ import { useSupportTicketQuery, } from '@linode/queries'; import { CircleProgress, ErrorState, Stack } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import { createLazyRoute } from '@tanstack/react-router'; import { isEmpty } from 'ramda'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx b/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx index 249dfcabf31..9643d03b27b 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx @@ -1,7 +1,7 @@ import { uploadAttachment } from '@linode/api-v4'; import { useSupportTicketReplyMutation } from '@linode/queries'; import { Accordion, Notice } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { lensPath, set } from 'ramda'; import * as React from 'react'; import { debounce } from 'throttle-debounce'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/TicketStatus.tsx b/packages/manager/src/features/Support/SupportTicketDetail/TicketStatus.tsx index 20d126a9373..2fa9a918cca 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/TicketStatus.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/TicketStatus.tsx @@ -2,7 +2,7 @@ import { useProfile } from '@linode/queries'; import { Paper, Stack, Typography } from '@linode/ui'; import { Hidden } from '@linode/ui'; import { capitalize } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import React from 'react'; diff --git a/packages/manager/src/features/Support/TicketAttachmentList.tsx b/packages/manager/src/features/Support/TicketAttachmentList.tsx index 6fbcdc8a1c1..0ad1c0cfff9 100644 --- a/packages/manager/src/features/Support/TicketAttachmentList.tsx +++ b/packages/manager/src/features/Support/TicketAttachmentList.tsx @@ -1,7 +1,7 @@ import { Typography } from '@linode/ui'; import InsertDriveFile from '@mui/icons-material/InsertDriveFile'; import InsertPhoto from '@mui/icons-material/InsertPhoto'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { isEmpty, slice } from 'ramda'; import * as React from 'react'; diff --git a/packages/manager/src/features/Support/TicketDetailText.tsx b/packages/manager/src/features/Support/TicketDetailText.tsx index e92058fd13e..913c468c065 100644 --- a/packages/manager/src/features/Support/TicketDetailText.tsx +++ b/packages/manager/src/features/Support/TicketDetailText.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@linode/ui'; import { truncate } from '@linode/utilities'; import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx index 74ae06e3558..8d2d19daa1e 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx @@ -1,7 +1,7 @@ import { useProfile } from '@linode/queries'; import { Box, Divider, Stack, Typography } from '@linode/ui'; import { styled } from '@mui/material'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import Popover from '@mui/material/Popover'; import { useTheme } from '@mui/material/styles'; import React from 'react'; diff --git a/packages/manager/src/features/Users/UserPermissions.styles.ts b/packages/manager/src/features/Users/UserPermissions.styles.ts index 2df58b8f27f..e92c31d68d3 100644 --- a/packages/manager/src/features/Users/UserPermissions.styles.ts +++ b/packages/manager/src/features/Users/UserPermissions.styles.ts @@ -1,5 +1,5 @@ import { CircleProgress, Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; export const StyledDivWrapper = styled('div', { diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index f96db5d4ec4..6ec88b599cc 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -17,7 +17,7 @@ import { Typography, } from '@linode/ui'; import { scrollErrorIntoViewV2 } from '@linode/utilities'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { enqueueSnackbar } from 'notistack'; import { compose, flatten, lensPath, omit, set } from 'ramda'; import * as React from 'react'; diff --git a/packages/manager/src/features/Users/UserProfile/UserDetailsPanel.tsx b/packages/manager/src/features/Users/UserProfile/UserDetailsPanel.tsx index d38a47da35d..15fbadb29db 100644 --- a/packages/manager/src/features/Users/UserProfile/UserDetailsPanel.tsx +++ b/packages/manager/src/features/Users/UserProfile/UserDetailsPanel.tsx @@ -1,5 +1,5 @@ import { Paper, Stack, Typography } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import React from 'react'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; diff --git a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx index f3bc5a375c7..959a6ed681b 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx @@ -1,5 +1,5 @@ import { Button, Divider } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; diff --git a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx index 8ea29742cfc..0827379d6fc 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx @@ -1,5 +1,5 @@ import { Button, CloseIcon, TextField } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; diff --git a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx index 8a3472cee82..94817536a4d 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx @@ -1,5 +1,5 @@ import { ActionsPanel, Notice, Paper } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { FormProvider } from 'react-hook-form'; diff --git a/packages/manager/src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer.tsx b/packages/manager/src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer.tsx index 82d07244040..65e7f29854b 100644 --- a/packages/manager/src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer.tsx @@ -1,5 +1,5 @@ import { ActionsPanel, Box, Drawer, Notice } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { FormProvider } from 'react-hook-form'; diff --git a/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx b/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx index 56c8b4555ee..a6be8949f4c 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx @@ -1,6 +1,6 @@ import { Notice, Typography } from '@linode/ui'; import { Hidden } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { useLocation } from 'react-router-dom'; diff --git a/packages/ui/package.json b/packages/ui/package.json index 03e44aa04de..d21d19cd4f1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -19,9 +19,9 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@linode/design-language-system": "^4.0.0", - "@mui/icons-material": "^6.4.5", - "@mui/material": "^6.4.5", - "@mui/utils": "^6.4.3", + "@mui/icons-material": "^7.1.0", + "@mui/material": "^7.1.0", + "@mui/utils": "^7.1.0", "@mui/x-date-pickers": "^7.27.0", "luxon": "3.4.4", "react": "^18.2.0", diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx index 8f91557de02..c6ae73b3549 100644 --- a/packages/ui/src/components/Accordion/Accordion.tsx +++ b/packages/ui/src/components/Accordion/Accordion.tsx @@ -1,7 +1,7 @@ import { default as _Accordion } from '@mui/material/Accordion'; import AccordionDetails from '@mui/material/AccordionDetails'; import AccordionSummary from '@mui/material/AccordionSummary'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/ui/src/components/Drawer/Drawer.tsx b/packages/ui/src/components/Drawer/Drawer.tsx index d58070a59a6..70f1ac85a06 100644 --- a/packages/ui/src/components/Drawer/Drawer.tsx +++ b/packages/ui/src/components/Drawer/Drawer.tsx @@ -1,6 +1,6 @@ import { CloseIcon } from '@linode/ui'; import _Drawer from '@mui/material/Drawer'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/ui/src/components/ErrorState/ErrorState.tsx b/packages/ui/src/components/ErrorState/ErrorState.tsx index 84a960663a8..a18f1730f39 100644 --- a/packages/ui/src/components/ErrorState/ErrorState.tsx +++ b/packages/ui/src/components/ErrorState/ErrorState.tsx @@ -1,5 +1,5 @@ import ErrorOutline from '@mui/icons-material/ErrorOutline'; -import Grid from '@mui/material/Grid2'; +import Grid from '@mui/material/Grid'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; diff --git a/packages/ui/src/foundations/themes/index.ts b/packages/ui/src/foundations/themes/index.ts index 82f52537661..77b86709092 100644 --- a/packages/ui/src/foundations/themes/index.ts +++ b/packages/ui/src/foundations/themes/index.ts @@ -78,7 +78,7 @@ type ComponentTypes = MergeTypes; * This allows us to add custom fields to the theme. * Avoid doing this unless you have a good reason. */ -declare module '@mui/material/styles/createTheme' { +declare module '@mui/material/styles' { export interface Theme { addCircleHoverEffect?: any; animateCircleIcon?: any; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4825b5d186..d3d23474740 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,17 +165,17 @@ importers: specifier: ^1.3.4 version: 1.3.4(@tanstack/query-core@5.51.24)(@tanstack/react-query@5.51.24(react@18.3.1)) '@mui/icons-material': - specifier: ^6.4.5 - version: 6.4.5(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + specifier: ^7.1.0 + version: 7.1.0(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/material': - specifier: ^6.4.5 - version: 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.1.0 + version: 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/utils': - specifier: ^6.4.3 - version: 6.4.3(@types/react@18.3.12)(react@18.3.1) + specifier: ^7.1.0 + version: 7.1.0(@types/react@18.3.12)(react@18.3.1) '@mui/x-date-pickers': specifier: ^7.27.0 - version: 7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@paypal/react-paypal-js': specifier: ^8.8.3 version: 8.8.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -334,7 +334,7 @@ importers: version: 2.3.0 tss-react: specifier: ^4.8.2 - version: 4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) typescript-fsa: specifier: ^3.0.0 version: 3.0.0 @@ -681,17 +681,17 @@ importers: specifier: ^4.0.0 version: 4.0.0 '@mui/icons-material': - specifier: ^6.4.5 - version: 6.4.5(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + specifier: ^7.1.0 + version: 7.1.0(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/material': - specifier: ^6.4.5 - version: 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.1.0 + version: 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/utils': - specifier: ^6.4.3 - version: 6.4.3(@types/react@18.3.12)(react@18.3.1) + specifier: ^7.1.0 + version: 7.1.0(@types/react@18.3.12)(react@18.3.1) '@mui/x-date-pickers': specifier: ^7.27.0 - version: 7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) luxon: specifier: 3.4.4 version: 3.4.4 @@ -703,7 +703,7 @@ importers: version: 18.3.1(react@18.3.1) tss-react: specifier: ^4.8.2 - version: 4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) devDependencies: '@storybook/addon-actions': specifier: ^8.6.7 @@ -950,6 +950,10 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.27.1': + resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -1645,27 +1649,27 @@ packages: resolution: {integrity: sha512-SvE+tSpcX884RJrPCskXxoS965Ky/pYABDEhWW6oeSRhpUDLrS5nTvT5n1LLSDVDYvty4imVmXsy+3/ROVuknA==} engines: {node: '>=18'} - '@mui/core-downloads-tracker@6.4.5': - resolution: {integrity: sha512-zoXvHU1YuoodgMlPS+epP084Pqv9V+Vg+5IGv9n/7IIFVQ2nkTngYHYxElCq8pdTTbDcgji+nNh0lxri2abWgA==} + '@mui/core-downloads-tracker@7.1.0': + resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==} - '@mui/icons-material@6.4.5': - resolution: {integrity: sha512-4A//t8Nrc+4u4pbVhGarIFU98zpuB5AV9hTNzgXx1ySZJ1tWtx+i/1SbQ8PtGJxWeXlljhwimZJNPQ3x0CiIFw==} + '@mui/icons-material@7.1.0': + resolution: {integrity: sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^6.4.5 + '@mui/material': ^7.1.0 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/material@6.4.5': - resolution: {integrity: sha512-5eyEgSXocIeV1JkXs8mYyJXU0aFyXZIWI5kq2g/mCnIgJe594lkOBNAKnCIaGVfQTu2T6TTEHF8/hHIqpiIRGA==} + '@mui/material@7.1.0': + resolution: {integrity: sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material-pigment-css': ^6.4.3 + '@mui/material-pigment-css': ^7.1.0 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1679,8 +1683,8 @@ packages: '@types/react': optional: true - '@mui/private-theming@6.4.3': - resolution: {integrity: sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ==} + '@mui/private-theming@7.1.0': + resolution: {integrity: sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1689,8 +1693,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@6.4.3': - resolution: {integrity: sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ==} + '@mui/styled-engine@7.1.0': + resolution: {integrity: sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1702,8 +1706,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@6.4.3': - resolution: {integrity: sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg==} + '@mui/system@7.1.0': + resolution: {integrity: sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1726,6 +1730,14 @@ packages: '@types/react': optional: true + '@mui/types@7.4.2': + resolution: {integrity: sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/utils@6.4.3': resolution: {integrity: sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A==} engines: {node: '>=14.0.0'} @@ -1736,6 +1748,16 @@ packages: '@types/react': optional: true + '@mui/utils@7.1.0': + resolution: {integrity: sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/x-date-pickers@7.27.0': resolution: {integrity: sha512-wSx8JGk4WQ2hTObfQITc+zlmUKNleQYoH1hGocaQlpWpo1HhauDtcQfX6sDN0J0dPT2eeyxDWGj4uJmiSfQKcw==} engines: {node: '>=14.0.0'} @@ -5650,6 +5672,9 @@ packages: react-is@19.0.0: resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==} + react-is@19.1.0: + resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} @@ -7036,6 +7061,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.27.1': {} + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -7676,23 +7703,23 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@mui/core-downloads-tracker@6.4.5': {} + '@mui/core-downloads-tracker@7.1.0': {} - '@mui/icons-material@6.4.5(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': + '@mui/icons-material@7.1.0(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/material': 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@babel/runtime': 7.27.1 + '@mui/material': 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 optionalDependencies: '@types/react': 18.3.12 - '@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/core-downloads-tracker': 6.4.5 - '@mui/system': 6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) - '@mui/types': 7.2.21(@types/react@18.3.12) - '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) + '@babel/runtime': 7.27.1 + '@mui/core-downloads-tracker': 7.1.0 + '@mui/system': 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@mui/types': 7.4.2(@types/react@18.3.12) + '@mui/utils': 7.1.0(@types/react@18.3.12)(react@18.3.1) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.12(@types/react@18.3.12) clsx: 2.1.1 @@ -7700,25 +7727,25 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-is: 19.0.0 + react-is: 19.1.0 react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: '@emotion/react': 11.13.5(@types/react@18.3.12)(react@18.3.1) '@emotion/styled': 11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@types/react': 18.3.12 - '@mui/private-theming@6.4.3(@types/react@18.3.12)(react@18.3.1)': + '@mui/private-theming@7.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) + '@babel/runtime': 7.27.1 + '@mui/utils': 7.1.0(@types/react@18.3.12)(react@18.3.1) prop-types: 15.8.1 react: 18.3.1 optionalDependencies: '@types/react': 18.3.12 - '@mui/styled-engine@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)': + '@mui/styled-engine@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.27.1 '@emotion/cache': 11.13.5 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 @@ -7729,13 +7756,13 @@ snapshots: '@emotion/react': 11.13.5(@types/react@18.3.12)(react@18.3.1) '@emotion/styled': 11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) - '@mui/system@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': + '@mui/system@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/private-theming': 6.4.3(@types/react@18.3.12)(react@18.3.1) - '@mui/styled-engine': 6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.21(@types/react@18.3.12) - '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) + '@babel/runtime': 7.27.1 + '@mui/private-theming': 7.1.0(@types/react@18.3.12)(react@18.3.1) + '@mui/styled-engine': 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) + '@mui/types': 7.4.2(@types/react@18.3.12) + '@mui/utils': 7.1.0(@types/react@18.3.12)(react@18.3.1) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -7749,6 +7776,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@mui/types@7.4.2(@types/react@18.3.12)': + dependencies: + '@babel/runtime': 7.27.1 + optionalDependencies: + '@types/react': 18.3.12 + '@mui/utils@6.4.3(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 @@ -7761,11 +7794,23 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@mui/x-date-pickers@7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/utils@7.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/types': 7.4.2(@types/react@18.3.12) + '@types/prop-types': 15.7.14 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 19.1.0 + optionalDependencies: + '@types/react': 18.3.12 + + '@mui/x-date-pickers@7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 - '@mui/material': 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/system': 6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@mui/material': 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/system': 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) '@mui/x-internals': 7.26.0(@types/react@18.3.12)(react@18.3.1) '@types/react-transition-group': 4.4.11 @@ -9825,7 +9870,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.27.1 csstype: 3.1.3 dompurify@3.2.4: @@ -12265,6 +12310,8 @@ snapshots: react-is@19.0.0: {} + react-is@19.1.0: {} + react-lifecycles-compat@3.0.4: {} react-number-format@3.6.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -12327,7 +12374,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.27.1 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -13153,7 +13200,7 @@ snapshots: tslib@2.8.1: {} - tss-react@4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + tss-react@4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@emotion/cache': 11.13.5 '@emotion/react': 11.13.5(@types/react@18.3.12)(react@18.3.1) @@ -13161,7 +13208,7 @@ snapshots: '@emotion/utils': 1.4.2 react: 18.3.1 optionalDependencies: - '@mui/material': 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/material': 7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tsup@8.4.0(@swc/core@1.10.11)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.6.1): dependencies: From 6a383b778fadaf56999e102036ccbe31e88ed36c Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Thu, 15 May 2025 12:02:50 -0400 Subject: [PATCH 04/59] test: [M3-9619] - Un-skip Cypress Firewall E2E tests (#12218) * Unskip Firewall E2E tests, account for Linode Interfaces changes * Added changeset: Unskip Cypress Firewall end-to-end tests --- .../.changeset/pr-12218-tests-1747241849916.md | 5 +++++ .../e2e/core/firewalls/create-firewall.spec.ts | 7 +++---- .../e2e/core/firewalls/delete-firewall.spec.ts | 13 ++++++------- .../firewalls/migrate-linode-with-firewall.spec.ts | 4 ++-- .../e2e/core/firewalls/update-firewall.spec.ts | 10 +++++----- packages/manager/cypress/support/util/linodes.ts | 1 + 6 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 packages/manager/.changeset/pr-12218-tests-1747241849916.md diff --git a/packages/manager/.changeset/pr-12218-tests-1747241849916.md b/packages/manager/.changeset/pr-12218-tests-1747241849916.md new file mode 100644 index 00000000000..08ed1fb0b41 --- /dev/null +++ b/packages/manager/.changeset/pr-12218-tests-1747241849916.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Unskip Cypress Firewall end-to-end tests ([#12218](https://github.com/linode/manager/pull/12218)) diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index 43822b48fa3..593d2007d86 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -7,9 +7,8 @@ import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; authenticate(); -// Firewall GET API request performance issues need to be addressed in order to unskip this test -// See M3-9619 -describe.skip('create firewall', () => { + +describe('create firewall', () => { before(() => { cleanUp(['lke-clusters', 'linodes', 'firewalls']); }); @@ -101,7 +100,7 @@ describe.skip('create firewall', () => { .should('be.visible') .click(); - cy.findByLabelText('Linodes').should('be.visible').click(); + cy.focused().type('{esc}'); ui.buttonGroup .findButtonByTitle('Create Firewall') diff --git a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts index f0037b28ed8..a99b70fc142 100644 --- a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts @@ -7,6 +7,7 @@ import { mockGetFirewallSettings, } from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; +import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; import { accountFactory, firewallFactory } from 'src/factories'; @@ -15,13 +16,11 @@ import { DEFAULT_FIREWALL_TOOLTIP_TEXT } from 'src/features/Firewalls/FirewallLa import type { Firewall } from '@linode/api-v4'; authenticate(); -// Firewall GET API request performance issues need to be addressed in order to unskip this test -// See M3-9619 describe('delete firewall', () => { - // TODO Restore clean-up when `deletes a firewall` test is unskipped. - // before(() => { - // cleanUp('firewalls'); - // }); + before(() => { + cleanUp('firewalls'); + }); + beforeEach(() => { cy.tag('method:e2e'); }); @@ -32,7 +31,7 @@ describe('delete firewall', () => { * - Confirms that firewall is still in landing page list after canceled operation. * - Confirms that firewall is removed from landing page list after confirmed operation. */ - it.skip('deletes a firewall', () => { + it('deletes a firewall', () => { const firewallRequest = firewallFactory.build({ label: randomLabel(), }); diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index b7d797faaf2..0f174987390 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -189,8 +189,8 @@ describe('Migrate Linode With Firewall', () => { .should('be.visible') .click(); - // Click on the Select again to dismiss the autocomplete popper. - cy.findByLabelText('Linodes').should('be.visible').click(); + // Dismiss the autocomplete popper. + cy.focused().type('{esc}'); ui.buttonGroup .findButtonByTitle('Create Firewall') diff --git a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts index e90d64d3c02..a7b47ce4906 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -1,4 +1,4 @@ -import { createFirewall, createLinode } from '@linode/api-v4'; +import { createFirewall } from '@linode/api-v4'; import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { @@ -7,6 +7,7 @@ import { } from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { createTestLinode } from 'support/util/linodes'; import { randomItem, randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; @@ -149,7 +150,7 @@ const addLinodesToFirewall = (firewall: Firewall, linode: Linode) => { .should('be.visible') .click(); - cy.findByLabelText('Linodes').should('be.visible').click(); + cy.focused().type('{esc}'); ui.button.findByTitle('Add').should('be.visible').click(); }); @@ -160,8 +161,7 @@ const createLinodeAndFirewall = async ( firewallRequestPayload: CreateFirewallPayload ) => { return Promise.all([ - // eslint-disable-next-line @linode/cloud-manager/no-createLinode - createLinode(linodeRequestPayload), + createTestLinode(linodeRequestPayload, { securityMethod: 'powered_off' }), createFirewall(firewallRequestPayload), ]); }; @@ -169,7 +169,7 @@ const createLinodeAndFirewall = async ( authenticate(); // Firewall GET API request performance issues need to be addressed in order to unskip this test // See M3-9619 -describe.skip('update firewall', () => { +describe('update firewall', () => { before(() => { cleanUp('firewalls'); }); diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index 6c3d5407f61..3ebe1b4eb1d 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -121,6 +121,7 @@ export const createTestLinode = async ( const resolvedCreatePayload = { ...createLinodeRequestFactory.build({ interface_generation: 'legacy_config', + firewall_id: null, booted: false, image: 'linode/ubuntu24.04', label: randomLabel(), From 816873c2f4b481b89ce9a42f726eede9090988ad Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Thu, 15 May 2025 12:36:00 -0400 Subject: [PATCH 05/59] test: [M3-9985] - Exclude distributed regions from `chooseRegion` output by default (#12226) * Exclude distributed regions from `chooseRegion` output by default * Added changeset: Exclude distributed regions when selecting regions for API operations --- .../pr-12226-tests-1747314916451.md | 5 +++++ .../manager/cypress/support/util/regions.ts | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 packages/manager/.changeset/pr-12226-tests-1747314916451.md diff --git a/packages/manager/.changeset/pr-12226-tests-1747314916451.md b/packages/manager/.changeset/pr-12226-tests-1747314916451.md new file mode 100644 index 00000000000..50aaf97105d --- /dev/null +++ b/packages/manager/.changeset/pr-12226-tests-1747314916451.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Exclude distributed regions when selecting regions for API operations ([#12226](https://github.com/linode/manager/pull/12226)) diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index 9dab6756dbb..506577723dd 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -228,7 +228,7 @@ export const getRegionByLabel = (label: string, searchRegions?: Region[]) => { interface ChooseRegionOptions { /** * If specified, the region returned will support the defined capabilities - * @example 'Managed Databases' + * @example ['Managed Databases'] */ capabilities?: Capabilities[]; @@ -237,6 +237,11 @@ interface ChooseRegionOptions { */ exclude?: string[]; + /** + * Whether or not to include distributed regions in potential output. + */ + includeDistributed?: boolean; + /** * Regions from which to choose. If unspecified, Regions exposed by the API will be used. */ @@ -323,7 +328,17 @@ const resolveSearchRegions = ( const capableRegions = regionsWithCapabilities( options?.regions ?? regions, requiredCapabilities - ).filter((region: Region) => !allDisallowedRegionIds.includes(region.id)); + ).filter((region: Region) => { + const isDisallowed = !allDisallowedRegionIds.includes(region.id); + const isDistributed = region.site_type === 'distributed'; + + // Exclude distributed regions in output if `options.distributed` is not true. + if (isDistributed && options?.includeDistributed !== true) { + return false; + } + + return isDisallowed; + }); if (!capableRegions.length) { throw new Error( From 0de6825c21097b53de77044083a1b679e1f20588 Mon Sep 17 00:00:00 2001 From: Hana Xu <115299789+hana-akamai@users.noreply.github.com> Date: Thu, 15 May 2025 13:50:31 -0400 Subject: [PATCH 06/59] fix: [M3-9920] - Do not set ACL Revision ID to empty string on LKE clusters (#12210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 API should be generating the Revision ID field for LKE/LKE-E clusters but it's currently not since we're hardcoding the revision string to an empty string so the API is treating that as deliberate user-action ## Changes 🔄 - Do not explicitly set revision id to an empty string for LKE/LKE-E clusters unless ACL is disabled ## How to test 🧪 ### Prerequisites (How to setup test environment) - Ensure your account as lke-e customer tags (see project tracker) ### Reproduction steps (How to reproduce the issue, if applicable) - [ ] Create a LKE/LKE-E cluster with ACL enabled. Check the network payload, you should see `revision id` explicitly being set to `''` - [ ] The Revision ID input in the Control Plane ACL drawer is an empty string ### Verification steps (How to verify changes) - [ ] Checkout this branch or Preview link and create a LKE/LKE-E cluster with ACL enabled. Check the network payload and you should not see `revision id` explictly set to `''` - [ ] The Revision ID input in the Control Plane ACL drawer is not empty ``` pnpm cy:run -s "cypress/e2e/core/kubernetes/lke-create.spec.ts" ``` --- .../pr-12210-fixed-1747239552927.md | 5 ++++ .../e2e/core/kubernetes/lke-create.spec.ts | 27 +++++++++++++------ .../e2e/core/kubernetes/lke-update.spec.ts | 12 +++++++-- .../CreateCluster/CreateCluster.tsx | 1 - .../KubeControlPaneACLDrawer.tsx | 9 +++++-- 5 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 packages/manager/.changeset/pr-12210-fixed-1747239552927.md diff --git a/packages/manager/.changeset/pr-12210-fixed-1747239552927.md b/packages/manager/.changeset/pr-12210-fixed-1747239552927.md new file mode 100644 index 00000000000..4609dea254a --- /dev/null +++ b/packages/manager/.changeset/pr-12210-fixed-1747239552927.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Do not set ACL Revision ID to empty string on LKE clusters ([#12210](https://github.com/linode/manager/pull/12210)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 648c33e3581..6956c4a386d 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -723,7 +723,6 @@ describe('LKE Cluster Creation with ACL', () => { const mockACL = kubernetesControlPlaneACLFactory.build({ acl: { enabled: false, - 'revision-id': '', }, }); const mockCluster = kubernetesClusterFactory.build({ @@ -822,9 +821,7 @@ describe('LKE Cluster Creation with ACL', () => { * - Confirms LKE summary page shows that ACL is enabled */ it('creates an LKE cluster with ACL enabled', () => { - const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - 'revision-id': '', - }); + const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build(); const mockACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, @@ -942,7 +939,15 @@ describe('LKE Cluster Creation with ACL', () => { ui.button .findByTitle('Enabled (3 IP Addresses)') .should('be.visible') - .should('be.enabled'); + .should('be.enabled') + .click(); + + // Confirm that the Revision ID has been auto-generated by the API + cy.get('[data-qa-drawer="true"]').within(() => { + cy.findByLabelText('Revision ID').within(() => { + cy.get('input').should('not.be.empty'); + }); + }); }); /** @@ -966,7 +971,6 @@ describe('LKE Cluster Creation with ACL', () => { ipv6: [], }, enabled: true, - 'revision-id': '', }, }); mockGetControlPlaneACL(mockedEnterpriseCluster.id, mockACL).as( @@ -1160,7 +1164,15 @@ describe('LKE Cluster Creation with ACL', () => { ui.button .findByTitle('Enabled (0 IP Addresses)') .should('be.visible') - .should('be.enabled'); + .should('be.enabled') + .click(); + + // Confirm that the Revision ID has been auto-generated by the API + cy.get('[data-qa-drawer="true"]').within(() => { + cy.findByLabelText('Revision ID').within(() => { + cy.get('input').should('not.be.empty'); + }); + }); }); /** @@ -1344,7 +1356,6 @@ describe('LKE Cluster Creation with LKE-E', () => { ipv6: [], }, enabled: true, - 'revision-id': '', }, }); diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index bf83aaccb33..095fcfe241f 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -2728,7 +2728,7 @@ describe('LKE ACL updates', () => { acl: mockUpdatedACLOptions2, }); mockUpdateControlPlaneACL(mockCluster.id, mockUpdatedControlPlaneACL2).as( - 'updateControlPlaneACL' + 'updateControlPlaneACL2' ); // confirm data within drawer is updated and edit IPs again @@ -2755,6 +2755,9 @@ describe('LKE ACL updates', () => { mockRevisionId ); + // clear Revision ID + cy.findByLabelText('Revision ID').clear(); + // update IPv6 addresses cy.findByDisplayValue('10.0.0.0/24').should('be.visible'); cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') @@ -2779,7 +2782,7 @@ describe('LKE ACL updates', () => { .click(); }); - cy.wait(['@updateControlPlaneACL']); + cy.wait(['@updateControlPlaneACL2']); // confirm summary panel updates cy.contains('Control Plane ACL').should('be.visible'); @@ -2795,6 +2798,11 @@ describe('LKE ACL updates', () => { .findByTitle(`Control Plane ACL for ${mockCluster.label}`) .should('be.visible') .within(() => { + // confirm Revision ID was "regenerated" after being cleared instead of displaying an empty string + cy.findByLabelText('Revision ID').should( + 'have.value', + mockRevisionId + ); // confirm updated IPv6 addresses display cy.findByDisplayValue( '8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e' diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 41cfb9aabf2..298050808b0 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -255,7 +255,6 @@ export const CreateCluster = () => { control_plane: { acl: { enabled: controlPlaneACL, - 'revision-id': '', ...(controlPlaneACL && // only send the IPs if we are enabling IPACL (_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index f3a4e48e170..8bf91b04124 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -103,7 +103,7 @@ export const KubeControlPlaneACLDrawer = ( : [''], }, enabled: aclPayload?.enabled ?? false, - 'revision-id': aclPayload?.['revision-id'] ?? '', + 'revision-id': aclPayload?.['revision-id'], }, }, }); @@ -149,7 +149,12 @@ export const KubeControlPlaneACLDrawer = ( const payload: KubernetesControlPlaneACLPayload = { acl: { enabled: acl.enabled, - 'revision-id': acl['revision-id'], + /** + * If revision-id is an empty string, we want to remove revision-id from the payload + * to let the API know to generate a new one + */ + 'revision-id': + acl['revision-id'] === '' ? undefined : acl['revision-id'], ...{ addresses: { ipv4, From 7c7ff7e9054ab055c70a977e19af46c3f1b9af5d Mon Sep 17 00:00:00 2001 From: cpathipa <119517080+cpathipa@users.noreply.github.com> Date: Thu, 15 May 2025 15:37:13 -0500 Subject: [PATCH 07/59] refactor: [M3-9375] - Move Images queries (#12205) * Move Images queries to package * Move images queries * Update paths * Added changeset: Moved images related queries and dependencies to shared `queries` package * Added changeset: Created `images/` directory and migrated relevant query keys and hooks --- .../pr-12205-tech-stories-1747256053421.md | 5 +++++ .../src/components/ImageSelect/ImageSelect.tsx | 2 +- .../src/components/StackScript/StackScript.tsx | 3 +-- .../Images/ImagesCreate/CreateImageTab.tsx | 2 +- .../features/Images/ImagesCreate/ImageUpload.tsx | 2 +- .../Images/ImagesLanding/EditImageDrawer.tsx | 2 +- .../ImageRegions/ManageImageRegionsForm.tsx | 6 ++++-- .../Images/ImagesLanding/ImagesLanding.tsx | 12 ++++++------ .../src/features/Linodes/LinodeCreate/Region.tsx | 3 +-- .../Linodes/LinodeCreate/Summary/Summary.tsx | 3 +-- .../Linodes/LinodeCreate/Tabs/Images.tsx | 3 +-- .../Linodes/LinodeCreate/UserData/UserData.tsx | 3 +-- .../LinodeCreate/shared/LinodeSelectTableRow.tsx | 3 +-- .../LinodeCreate/shared/SelectLinodeCard.tsx | 3 +-- .../features/Linodes/LinodeCreate/utilities.ts | 2 +- .../src/features/Linodes/LinodeEntityDetail.tsx | 2 +- .../LinodesDetail/LinodeRebuild/UserData.tsx | 3 +-- .../Linodes/MigrateLinode/MigrateLinode.tsx | 2 +- .../manager/src/features/Search/useAPISearch.ts | 2 +- .../src/features/Search/useClientSideSearch.ts | 2 +- packages/manager/src/hooks/useEventHandlers.ts | 2 +- .../.changeset/pr-12205-added-1747256092908.md | 5 +++++ .../src/queries => queries/src/images}/images.ts | 16 ++++++++-------- packages/queries/src/images/index.ts | 1 + packages/queries/src/index.ts | 1 + 25 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md create mode 100644 packages/queries/.changeset/pr-12205-added-1747256092908.md rename packages/{manager/src/queries => queries/src/images}/images.ts (97%) create mode 100644 packages/queries/src/images/index.ts diff --git a/packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md b/packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md new file mode 100644 index 00000000000..bfac9d35693 --- /dev/null +++ b/packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Moved images related queries and dependencies to shared `queries` package ([#12205](https://github.com/linode/manager/pull/12205)) diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index 650543f1a39..e53527c5648 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -1,3 +1,4 @@ +import { useAllImagesQuery } from '@linode/queries'; import { Autocomplete, Box, @@ -10,7 +11,6 @@ import { DateTime } from 'luxon'; import React, { useMemo } from 'react'; import { imageFactory } from 'src/factories/images'; -import { useAllImagesQuery } from 'src/queries/images'; import { formatDate } from 'src/utilities/formatDate'; import { OSIcon } from '../OSIcon'; diff --git a/packages/manager/src/components/StackScript/StackScript.tsx b/packages/manager/src/components/StackScript/StackScript.tsx index 874c40f30f3..cfaed1bfb56 100644 --- a/packages/manager/src/components/StackScript/StackScript.tsx +++ b/packages/manager/src/components/StackScript/StackScript.tsx @@ -1,4 +1,4 @@ -import { listToItemsByID } from '@linode/queries'; +import { listToItemsByID, useAllImagesQuery } from '@linode/queries'; import { Box, Button, @@ -17,7 +17,6 @@ import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Link } from 'src/components/Link'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; -import { useAllImagesQuery } from 'src/queries/images'; import { CodeBlock } from '../CodeBlock/CodeBlock'; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index b8d7014f93f..f1da3b03a58 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -1,6 +1,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useAllLinodeDisksQuery, + useCreateImageMutation, useGrants, useLinodeQuery, useRegionsQuery, @@ -30,7 +31,6 @@ import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useEventsPollingActions } from 'src/queries/events/events'; -import { useCreateImageMutation } from 'src/queries/images'; import type { CreateImagePayload } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx index dc5d0cc1915..566e1ee3578 100644 --- a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx @@ -4,6 +4,7 @@ import { useMutateAccountAgreements, useProfile, useRegionsQuery, + useUploadImageMutation, } from '@linode/queries'; import { useIsGeckoEnabled } from '@linode/shared'; import { @@ -36,7 +37,6 @@ import { MAX_FILE_SIZE_IN_BYTES } from 'src/components/Uploaders/reducer'; import { useFlags } from 'src/hooks/useFlags'; import { usePendingUpload } from 'src/hooks/usePendingUpload'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useUploadImageMutation } from 'src/queries/images'; import { setPendingUpload } from 'src/store/pendingUpload'; import { getGDPRDetails } from 'src/utilities/formatRegion'; import { reportAgreementSigningError } from 'src/utilities/reportAgreementSigningError'; diff --git a/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx index fc3aad177c8..6299f3ed1e9 100644 --- a/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx @@ -1,4 +1,5 @@ import { yupResolver } from '@hookform/resolvers/yup'; +import { useUpdateImageMutation } from '@linode/queries'; import { ActionsPanel, Drawer, Notice, TextField } from '@linode/ui'; import { Stack, Typography } from '@linode/ui'; import { updateImageSchema } from '@linode/validation'; @@ -7,7 +8,6 @@ import { Controller, useForm } from 'react-hook-form'; import Lock from 'src/assets/icons/lock.svg'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; -import { useUpdateImageMutation } from 'src/queries/images'; import { useImageAndLinodeGrantCheck } from '../utils'; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx index 53d907d0245..25e58dfb609 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx @@ -1,4 +1,7 @@ -import { useRegionsQuery } from '@linode/queries'; +import { + useRegionsQuery, + useUpdateImageRegionsMutation, +} from '@linode/queries'; import { useIsGeckoEnabled } from '@linode/shared'; import { ActionsPanel, Notice, Paper, Stack, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; @@ -9,7 +12,6 @@ import type { Resolver } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { RegionMultiSelect } from 'src/components/RegionSelect/RegionMultiSelect'; import { useFlags } from 'src/hooks/useFlags'; -import { useUpdateImageRegionsMutation } from 'src/queries/images'; import { ImageRegionRow } from './ImageRegionRow'; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index a293af0bbd0..87a3af83312 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -1,3 +1,9 @@ +import { + imageQueries, + useDeleteImageMutation, + useImageQuery, + useImagesQuery, +} from '@linode/queries'; import { getAPIFilterFromQuery } from '@linode/search'; import { ActionsPanel, @@ -44,12 +50,6 @@ import { isEventInProgressDiskImagize, } from 'src/queries/events/event.helpers'; import { useEventsInfiniteQuery } from 'src/queries/events/events'; -import { - imageQueries, - useDeleteImageMutation, - useImageQuery, - useImagesQuery, -} from 'src/queries/images'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx index 90589c74af8..6999a054c73 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx @@ -1,4 +1,4 @@ -import { useRegionsQuery } from '@linode/queries'; +import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { useIsGeckoEnabled } from '@linode/shared'; import { Box, Notice, Paper, Typography } from '@linode/ui'; import { getIsLegacyInterfaceArray } from '@linode/utilities'; @@ -14,7 +14,6 @@ import { isDistributedRegionSupported } from 'src/components/RegionSelect/Region import { RegionHelperText } from 'src/components/SelectRegionPanel/RegionHelperText'; import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useImageQuery } from 'src/queries/images'; import { useTypeQuery } from 'src/queries/types'; import { sendLinodeCreateFormInputEvent, diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx index 1f9b44c3544..29e54cbb041 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx @@ -1,4 +1,4 @@ -import { useRegionsQuery } from '@linode/queries'; +import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { Divider, Paper, Stack, Typography } from '@linode/ui'; import { formatStorageUnits } from '@linode/utilities'; import { useTheme } from '@mui/material'; @@ -6,7 +6,6 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import React from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; -import { useImageQuery } from 'src/queries/images'; import { useTypeQuery } from 'src/queries/types'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Images.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Images.tsx index e6d9ef1f36b..d9cc78300dc 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Images.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Images.tsx @@ -1,4 +1,4 @@ -import { useRegionsQuery } from '@linode/queries'; +import { useAllImagesQuery, useRegionsQuery } from '@linode/queries'; import { Box, Paper, Stack, Typography } from '@linode/ui'; import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; @@ -10,7 +10,6 @@ import { getAPIFilterForImageSelect } from 'src/components/ImageSelect/utilities import { Link } from 'src/components/Link'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useAllImagesQuery } from 'src/queries/images'; import { Region } from '../Region'; import { getGeneratedLinodeLabel } from '../utilities'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.tsx b/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.tsx index 57c45c05288..119039f4156 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.tsx @@ -1,11 +1,10 @@ -import { useRegionsQuery } from '@linode/queries'; +import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { Accordion, Notice, TextField, Typography } from '@linode/ui'; import React, { useMemo } from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useImageQuery } from 'src/queries/images'; import { UserDataHeading } from './UserDataHeading'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx index b23d0f7ecaf..81a847bcfe7 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx @@ -1,4 +1,4 @@ -import { useRegionsQuery } from '@linode/queries'; +import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { FormControlLabel, Radio } from '@linode/ui'; import { capitalize, formatStorageUnits } from '@linode/utilities'; import React from 'react'; @@ -8,7 +8,6 @@ import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { getLinodeIconStatus } from 'src/features/Linodes/LinodesLanding/utils'; -import { useImageQuery } from 'src/queries/images'; import { useTypeQuery } from 'src/queries/types'; import type { Linode } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx index c13d4bb063b..ec7f6d8bf5f 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx @@ -1,4 +1,4 @@ -import { useRegionsQuery } from '@linode/queries'; +import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { Button, Stack } from '@linode/ui'; import { capitalizeAllWords, @@ -11,7 +11,6 @@ import React from 'react'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; -import { useImageQuery } from 'src/queries/images'; import { useTypeQuery } from 'src/queries/types'; import { getLinodeIconStatus } from '../../LinodesLanding/utils'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts index efc5e0cbcc3..11779728733 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts @@ -1,6 +1,7 @@ import { accountQueries, firewallQueries, + imageQueries, linodeQueries, stackscriptQueries, } from '@linode/queries'; @@ -15,7 +16,6 @@ import { useCallback } from 'react'; import type { FieldErrors } from 'react-hook-form'; import { useHistory } from 'react-router-dom'; -import { imageQueries } from 'src/queries/images'; import { sendCreateLinodeEvent } from 'src/utilities/analytics/customEventAnalytics'; import { sendLinodeCreateFormErrorEvent } from 'src/utilities/analytics/formEventAnalytics'; import { isPrivateIP } from 'src/utilities/ipUtils'; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index cc6badeb0b0..10d6a7e9a48 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -1,4 +1,5 @@ import { + useAllImagesQuery, useLinodeFirewallsQuery, useLinodeVolumesQuery, useRegionsQuery, @@ -14,7 +15,6 @@ import { notificationCenterContext as _notificationContext } from 'src/features/ import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useVPCInterface } from 'src/hooks/useVPCInterface'; import { useInProgressEvents } from 'src/queries/events/events'; -import { useAllImagesQuery } from 'src/queries/images'; import { useTypeQuery } from 'src/queries/types'; import { LinodeEntityDetailBody } from './LinodeEntityDetailBody'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/UserData.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/UserData.tsx index b6a0e89ce3d..7d3f346af5c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/UserData.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/UserData.tsx @@ -1,10 +1,9 @@ -import { useLinodeQuery, useRegionQuery } from '@linode/queries'; +import { useImageQuery, useLinodeQuery, useRegionQuery } from '@linode/queries'; import { Accordion, Checkbox, Notice, TextField, Typography } from '@linode/ui'; import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { Link } from 'src/components/Link'; -import { useImageQuery } from 'src/queries/images'; import type { RebuildLinodeFormValues } from './utils'; diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index 89119f0642a..26a909ea2f3 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -1,6 +1,7 @@ import { useAccountAgreements, useAllLinodeDisksQuery, + useImageQuery, useLinodeMigrateMutation, useLinodeQuery, useMutateAccountAgreements, @@ -35,7 +36,6 @@ import { useEventsPollingActions, useInProgressEvents, } from 'src/queries/events/events'; -import { useImageQuery } from 'src/queries/images'; import { useTypeQuery } from 'src/queries/types'; import { sendMigrationInitiatedEvent } from 'src/utilities/analytics/customEventAnalytics'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/src/features/Search/useAPISearch.ts b/packages/manager/src/features/Search/useAPISearch.ts index 4b7637ff80b..c7a71ef6aac 100644 --- a/packages/manager/src/features/Search/useAPISearch.ts +++ b/packages/manager/src/features/Search/useAPISearch.ts @@ -1,6 +1,7 @@ import { useStackScriptsInfiniteQuery } from '@linode/queries'; import { useInfiniteVolumesQuery } from '@linode/queries'; import { useFirewallsInfiniteQuery } from '@linode/queries'; +import { useImagesInfiniteQuery } from '@linode/queries'; import { useInfiniteNodebalancersQuery } from '@linode/queries'; import { useInfiniteLinodesQuery } from '@linode/queries'; import { getAPIFilterFromQuery } from '@linode/search'; @@ -8,7 +9,6 @@ import { useDebouncedValue } from '@linode/utilities'; import { useDatabasesInfiniteQuery } from 'src/queries/databases/databases'; import { useDomainsInfiniteQuery } from 'src/queries/domains'; -import { useImagesInfiniteQuery } from 'src/queries/images'; import { useKubernetesClustersInfiniteQuery } from 'src/queries/kubernetes'; import { databaseToSearchableItem, diff --git a/packages/manager/src/features/Search/useClientSideSearch.ts b/packages/manager/src/features/Search/useClientSideSearch.ts index 74659bae398..db164db1d97 100644 --- a/packages/manager/src/features/Search/useClientSideSearch.ts +++ b/packages/manager/src/features/Search/useClientSideSearch.ts @@ -3,11 +3,11 @@ import { useAllFirewallsQuery } from '@linode/queries'; import { useAllVolumesQuery } from '@linode/queries'; import { useAllNodeBalancersQuery } from '@linode/queries'; import { useAllAccountStackScriptsQuery } from '@linode/queries'; +import { useAllImagesQuery } from '@linode/queries'; import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { useAllDatabasesQuery } from 'src/queries/databases/databases'; import { useAllDomainsQuery } from 'src/queries/domains'; -import { useAllImagesQuery } from 'src/queries/images'; import { useAllKubernetesClustersQuery } from 'src/queries/kubernetes'; import { useObjectStorageBuckets } from 'src/queries/object-storage/queries'; import { diff --git a/packages/manager/src/hooks/useEventHandlers.ts b/packages/manager/src/hooks/useEventHandlers.ts index df799c14c09..4d6e38144ab 100644 --- a/packages/manager/src/hooks/useEventHandlers.ts +++ b/packages/manager/src/hooks/useEventHandlers.ts @@ -1,5 +1,6 @@ import { firewallEventsHandler, + imageEventsHandler, nodebalancerEventHandler, oauthClientsEventHandler, placementGroupEventHandler, @@ -11,7 +12,6 @@ import { useQueryClient } from '@tanstack/react-query'; import { databaseEventsHandler } from 'src/queries/databases/events'; import { domainEventsHandler } from 'src/queries/domains'; -import { imageEventsHandler } from 'src/queries/images'; import { stackScriptEventHandler } from 'src/queries/stackscripts'; import { supportTicketEventHandler } from 'src/queries/support'; import { volumeEventsHandler } from 'src/queries/volumes/events'; diff --git a/packages/queries/.changeset/pr-12205-added-1747256092908.md b/packages/queries/.changeset/pr-12205-added-1747256092908.md new file mode 100644 index 00000000000..b08e36a1bbd --- /dev/null +++ b/packages/queries/.changeset/pr-12205-added-1747256092908.md @@ -0,0 +1,5 @@ +--- +"@linode/queries": Added +--- + +Created `images/` directory and migrated relevant query keys and hooks ([#12205](https://github.com/linode/manager/pull/12205)) diff --git a/packages/manager/src/queries/images.ts b/packages/queries/src/images/images.ts similarity index 97% rename from packages/manager/src/queries/images.ts rename to packages/queries/src/images/images.ts index 19611911847..0664ebc5d3c 100644 --- a/packages/manager/src/queries/images.ts +++ b/packages/queries/src/images/images.ts @@ -34,10 +34,10 @@ import type { UseQueryOptions } from '@tanstack/react-query'; export const getAllImages = ( passedParams: Params = {}, - passedFilter: Filter = {} + passedFilter: Filter = {}, ) => getAll((params, filter) => - getImages({ ...params, ...passedParams }, { ...filter, ...passedFilter }) + getImages({ ...params, ...passedParams }, { ...filter, ...passedFilter }), )().then((data) => data.data); export const imageQueries = createQueryKeys('images', { @@ -63,7 +63,7 @@ export const imageQueries = createQueryKeys('images', { export const useImagesQuery = ( params: Params, filters: Filter, - options?: Partial, APIError[]>> + options?: Partial, APIError[]>>, ) => useQuery, APIError[]>({ ...imageQueries.paginated(params, filters), @@ -102,7 +102,7 @@ export const useCreateImageMutation = () => { }); queryClient.setQueryData( imageQueries.image(image.id).queryKey, - image + image, ); // If a restricted user creates an entity, we must make sure grants are up to date. queryClient.invalidateQueries({ @@ -127,7 +127,7 @@ export const useUpdateImageMutation = () => { }); queryClient.setQueryData( imageQueries.image(image.id).queryKey, - image + image, ); }, }); @@ -151,7 +151,7 @@ export const useDeleteImageMutation = () => { export const useAllImagesQuery = ( params: Params = {}, filters: Filter = {}, - enabled = true + enabled = true, ) => useQuery({ ...imageQueries.all(params, filters), @@ -171,7 +171,7 @@ export const useUploadImageMutation = () => { }); queryClient.setQueryData( imageQueries.image(data.image.id).queryKey, - data.image + data.image, ); }, }); @@ -190,7 +190,7 @@ export const useUpdateImageRegionsMutation = (imageId: string) => { }); queryClient.setQueryData( imageQueries.image(image.id).queryKey, - image + image, ); }, }); diff --git a/packages/queries/src/images/index.ts b/packages/queries/src/images/index.ts new file mode 100644 index 00000000000..67e4bd303d9 --- /dev/null +++ b/packages/queries/src/images/index.ts @@ -0,0 +1 @@ +export * from './images'; diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index 7bf48ab2284..843314c2069 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -2,6 +2,7 @@ export * from './account'; export * from './base'; export * from './eventHandlers'; export * from './firewalls'; +export * from './images'; export * from './linodes'; export * from './networking'; export * from './nodebalancers'; From b23ebe60b737aed8bffd6c98904137d461dbbd59 Mon Sep 17 00:00:00 2001 From: hasyed-akamai Date: Fri, 16 May 2025 11:17:22 +0530 Subject: [PATCH 08/59] fix: [M3-9668, M3-9538] - Fixed Formatting of the `volume status` and broken spacing in `MultipleIPInput` component (#12207) * fix: [M3-9668, M3-9538, M3-9737] - Bug Fix * Added changeset: Formatting of the volume status and broken spacing in MultipleIPInput component and Nodebalancer regression * created util for formatted Status and use Stack spacing instead of margin * fixed e2e tests * revert back the changes for Nodebalancer regression * Update changeset description * fix unit tests --- .../pr-12207-fixed-1747120917933.md | 5 + .../e2e/core/volumes/clone-volume.spec.ts | 2 +- .../e2e/core/volumes/resize-volume.spec.ts | 2 +- .../e2e/core/volumes/update-volume.spec.ts | 4 +- .../e2e/core/volumes/upgrade-volume.spec.ts | 12 +-- .../MultipleIPInput/MultipleIPInput.tsx | 100 ++++++++++-------- .../Maintenance/MaintenanceTableRow.tsx | 4 +- .../Images/ImagesLanding/ImageStatus.tsx | 4 +- .../shared/LinodeSelectTableRow.tsx | 4 +- .../LinodeCreate/shared/SelectLinodeCard.tsx | 4 +- .../LinodesLanding/LinodeRow/LinodeRow.tsx | 4 +- .../src/features/Linodes/transitions.ts | 4 +- .../PlacementGroupsLinodesTableRow.tsx | 4 +- .../VPCs/VPCDetail/SubnetLinodeRow.tsx | 4 +- .../src/features/Volumes/VolumeTableRow.tsx | 4 +- packages/manager/src/mocks/serverHandlers.ts | 1 + .../src/helpers/formatStatus.test.ts | 27 +++++ .../utilities/src/helpers/formatStatus.ts | 5 + packages/utilities/src/helpers/index.ts | 1 + 19 files changed, 121 insertions(+), 74 deletions(-) create mode 100644 packages/manager/.changeset/pr-12207-fixed-1747120917933.md create mode 100644 packages/utilities/src/helpers/formatStatus.test.ts create mode 100644 packages/utilities/src/helpers/formatStatus.ts diff --git a/packages/manager/.changeset/pr-12207-fixed-1747120917933.md b/packages/manager/.changeset/pr-12207-fixed-1747120917933.md new file mode 100644 index 00000000000..498774f077b --- /dev/null +++ b/packages/manager/.changeset/pr-12207-fixed-1747120917933.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Formatting of the volume status and broken spacing in MultipleIPInput component ([#12207](https://github.com/linode/manager/pull/12207)) diff --git a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts index 8288734266a..37f33e292f3 100644 --- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts @@ -49,7 +49,7 @@ describe('volume clone flow', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); cy.findByLabelText( `Action menu for Volume ${volume.label}` ).click(); diff --git a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts index a34ca3cc7ec..a197a9f376b 100644 --- a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts @@ -68,7 +68,7 @@ describe('volume resize flow', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); cy.findByText(`${oldSize} GB`).should('be.visible'); cy.findByLabelText( `Action menu for Volume ${volume.label}` diff --git a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts index 9b1b021897c..78ffb0dbdc7 100644 --- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts @@ -45,7 +45,7 @@ describe('volume update flow', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); }); ui.actionMenu .findByTitle(`Action menu for Volume ${volume.label}`) @@ -106,7 +106,7 @@ describe('volume update flow', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); }); ui.actionMenu diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts index f6ff9e71a2d..5e3cc477dd7 100644 --- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -78,7 +78,7 @@ describe('volume upgrade/migration', () => { cy.wait('@getEvents'); - cy.findByText(`migrating (${percentage}%)`).should('be.visible'); + cy.findByText(`Migrating (${percentage}%)`).should('be.visible'); } const mockFinishedMigrationEvent = eventFactory.build({ @@ -94,7 +94,7 @@ describe('volume upgrade/migration', () => { mockGetEvents([]); - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`); }); @@ -173,7 +173,7 @@ describe('volume upgrade/migration', () => { cy.wait('@getEvents'); - cy.findByText(`migrating (${percentage}%)`).should('be.visible'); + cy.findByText(`Migrating (${percentage}%)`).should('be.visible'); } const mockFinishedMigrationEvent = eventFactory.build({ @@ -189,7 +189,7 @@ describe('volume upgrade/migration', () => { mockGetEvents([]); - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`); }); @@ -264,7 +264,7 @@ describe('volume upgrade/migration', () => { cy.wait('@getEvents'); - cy.findByText(`migrating (${percentage}%)`).should('be.visible'); + cy.findByText(`Migrating (${percentage}%)`).should('be.visible'); } const mockFinishedMigrationEvent = eventFactory.build({ @@ -280,7 +280,7 @@ describe('volume upgrade/migration', () => { mockGetEvents([]); - cy.findByText('active').should('be.visible'); + cy.findByText('Active').should('be.visible'); ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`); }); diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 08a5b00bf1b..c6a66c6a3aa 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -1,8 +1,10 @@ import { Button, CloseIcon, + IconButton, InputLabel, Notice, + Stack, TextField, TooltipIcon, Typography, @@ -245,54 +247,58 @@ export const MultipleIPInput = React.memo((props: MultipeIPInputProps) => { {helperText} )} {error && } - {ips.map((thisIP, idx) => ( - - - handleBlur(e, idx)} - onChange={(e: React.ChangeEvent) => - handleChange(e, idx) - } - placeholder={placeholder} - value={thisIP.address} - /> - - {/** Don't show the button for the first input since it won't do anything, unless this component is - * used in DBaaS or for Linode VPC interfaces - */} - - {(idx > 0 || forDatabaseAccessControls || forVPCIPv4Ranges) && ( - - )} + + {ips.map((thisIP, idx) => ( + + + handleBlur(e, idx)} + onChange={(e: React.ChangeEvent) => + handleChange(e, idx) + } + placeholder={placeholder} + value={thisIP.address} + /> + + {/** Don't show the button for the first input since it won't do anything, unless this component is + * used in DBaaS or for Linode VPC interfaces + */} + + {(idx > 0 || forDatabaseAccessControls || forVPCIPv4Ranges) && ( + removeInput(idx)} + > + + + )} + - - ))} + ))} + {addIPButton} ); diff --git a/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx b/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx index f3ae90e98fa..86d7b7c05b6 100644 --- a/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx +++ b/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx @@ -1,7 +1,7 @@ import { useProfile } from '@linode/queries'; import { Tooltip } from '@linode/ui'; import { Hidden } from '@linode/ui'; -import { capitalize, truncate } from '@linode/utilities'; +import { capitalize, getFormattedStatus, truncate } from '@linode/utilities'; import * as React from 'react'; import { Link } from 'src/components/Link'; @@ -70,7 +70,7 @@ export const MaintenanceTableRow = (props: AccountMaintenance) => { - {capitalize(type.replace('_', ' '))} + {getFormattedStatus(type)} diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx index 17870d4ce05..a3abe9d1581 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx @@ -1,5 +1,5 @@ import { Stack, TooltipIcon } from '@linode/ui'; -import { capitalizeAllWords } from '@linode/utilities'; +import { capitalizeAllWords, getFormattedStatus } from '@linode/utilities'; import React from 'react'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; @@ -65,7 +65,7 @@ export const ImageStatus = (props: Props) => { return ( - {capitalizeAllWords(image.status.replace('_', ' '))} + {getFormattedStatus(image.status)} {showEventProgress && ` (${event.percent_complete}%)`} ); diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx index 81a847bcfe7..c2e3fbb6b99 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx @@ -1,6 +1,6 @@ import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { FormControlLabel, Radio } from '@linode/ui'; -import { capitalize, formatStorageUnits } from '@linode/utilities'; +import { formatStorageUnits, getFormattedStatus } from '@linode/utilities'; import React from 'react'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; @@ -46,7 +46,7 @@ export const LinodeSelectTableRow = (props: Props) => { - {capitalize(linode.status.replace('_', ' '))} + {getFormattedStatus(linode.status)} {image?.label ?? linode.image} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx index ec7f6d8bf5f..cadf4187b20 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx @@ -1,8 +1,8 @@ import { useImageQuery, useRegionsQuery } from '@linode/queries'; import { Button, Stack } from '@linode/ui'; import { - capitalizeAllWords, formatStorageUnits, + getFormattedStatus, isNotNullOrUndefined, } from '@linode/utilities'; import Grid from '@mui/material/Grid'; @@ -75,7 +75,7 @@ export const SelectLinodeCard = ({ aria-label={`Linode status ${linode?.status ?? iconStatus}`} status={iconStatus} /> - {capitalizeAllWords(linode.status.replace('_', ' '))} + {getFormattedStatus(linode.status)} {shouldShowPowerButton && ( + + )} + + {isEnabled && isEditing && ( + setIsEditing(false)} + open={isEditing} + title={`Edit ${preset.label}`} + > +

+ {formData.length} custom item{formData.length !== 1 && 's'} +

+ + +
+ + + + +
+ + +
+ String(index))} // we have to use a string bc Sortable doesn't like id=0 + strategy={verticalListSortingStrategy} + > + {formData.map((item, index) => + renderItemBox({ + item, + index, + onDelete: () => + handleInputChange( + formData.filter((_, i) => i !== index) + ), + onEdit: (updater) => + handleInputChange( + formData.map((item, i) => + i === index ? updater(item) : item + ) + ), + }) + )} + +
+
+
+ +
+
+
+ )} + + ); +}; + +const FieldWrapper = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +interface ItemBoxProps { + editItem: (updater: (prev: T) => T) => void; + formFields: ( + onChange: ( + e: React.ChangeEvent< + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + > + ) => void + ) => JSX.Element[]; + id: string; + item: T; + onDelete: () => void; + summary: JSX.Element; +} + +export const ItemBox = ({ + onDelete, + editItem, + id, + summary, + formFields, +}: ItemBoxProps) => { + const [isCollapsed, setIsCollapsed] = React.useState(true); + + const { + active, + attributes, + isDragging, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id }); + + const isActive = Boolean(active); + + // dnd-kit styles + const dndStyles = { + position: 'relative', + transform: CSS.Translate.toString(transform), + transition: isActive ? transition : 'none', + zIndex: isDragging ? 9999 : 0, + } as const; + + const handleInputChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + > + ) => { + const { name, value } = e.target; + + const checkboxValue: boolean | undefined = + e.target.type === 'checkbox' && 'checked' in e.target + ? e.target.checked + : undefined; + + editItem((prev) => ({ + ...prev, + [name]: checkboxValue ?? (value || null), + })); + }; + + return ( +
+
+
+ +
+ +
+ {summary} +
+
+ +
+
+ {!isCollapsed && ( + + {formFields(handleInputChange).map((field, index) => ( + {field} + ))} + + )} +
+ ); +}; diff --git a/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx b/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx new file mode 100644 index 00000000000..7efbd9c374c --- /dev/null +++ b/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx @@ -0,0 +1,164 @@ +import { Typography } from '@linode/ui'; +import * as React from 'react'; + +import { accountMaintenanceFactory } from 'src/factories'; +import { extraMockPresets } from 'src/mocks/presets'; +import { setCustomMaintenanceData } from 'src/mocks/presets/extra/account/customMaintenance'; + +import { saveCustomMaintenanceData } from '../utils'; +import { ExtraPresetList, ItemBox } from './ExtraPresetList'; +import { JsonTextArea } from './JsonTextArea'; + +import type { AccountMaintenance } from '@linode/api-v4'; + +const MAINTENANCE_PRESET_ID = 'maintenance:custom' as const; + +const maintenancePreset = extraMockPresets.find( + (p) => p.id === MAINTENANCE_PRESET_ID +); + +interface ExtraPresetMaintenanceProps { + customMaintenanceData: AccountMaintenance[] | null | undefined; + handlers: string[]; + onFormChange?: (data: AccountMaintenance[] | null | undefined) => void; + onTogglePreset: ( + e: React.ChangeEvent, + presetId: string + ) => void; +} + +export const ExtraPresetMaintenance = ({ + customMaintenanceData, + handlers, + onFormChange, + onTogglePreset, +}: ExtraPresetMaintenanceProps) => { + if (!maintenancePreset) return null; + + const isEnabled = handlers.includes(MAINTENANCE_PRESET_ID); + + return ( + ( + + )} + saveDataToLocalStorage={saveCustomMaintenanceData} + setMSWData={setCustomMaintenanceData} + /> + ); +}; + +interface MaintenanceBoxProps { + editMaintenance: ( + updater: (prev: AccountMaintenance) => AccountMaintenance + ) => void; + id: string; + maintenance: AccountMaintenance; + onDelete: () => void; +} + +const MaintenanceBox = (props: MaintenanceBoxProps) => { + const { maintenance, onDelete, editMaintenance, id } = props; + + return ( + renderMaintenanceFields(maintenance, onChange)} + id={id} + item={maintenance} + onDelete={onDelete} + summary={ + + Entity {maintenance.entity?.label} |{' '} + Type {maintenance.type} | Status{' '} + {maintenance.status} | Reason {maintenance.reason} + + } + /> + ); +}; + +const renderMaintenanceFields = ( + maintenance: AccountMaintenance, + onChange: ( + e: React.ChangeEvent< + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + > + ) => void +) => [ + , + + , + + , + + , + + , +]; + +const maintenanceTemplates = { + Default: () => accountMaintenanceFactory.build(), + Completed: () => accountMaintenanceFactory.build({ status: 'completed' }), + Pending: () => accountMaintenanceFactory.build({ status: 'pending' }), + Started: () => accountMaintenanceFactory.build({ status: 'started' }), +} as const; diff --git a/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx b/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx new file mode 100644 index 00000000000..e835102588e --- /dev/null +++ b/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx @@ -0,0 +1,255 @@ +import { Typography } from '@linode/ui'; +import * as React from 'react'; + +import { notificationFactory } from 'src/factories'; +import { extraMockPresets } from 'src/mocks/presets'; +import { setCustomNotificationsData } from 'src/mocks/presets/extra/account/customNotifications'; + +import { saveCustomNotificationsData } from '../utils'; +import { ExtraPresetList, ItemBox } from './ExtraPresetList'; +import { JsonTextArea } from './JsonTextArea'; + +import type { Notification } from '@linode/api-v4'; + +const NOTIFICATIONS_PRESET_ID = 'notifications:custom' as const; + +const notificationsPreset = extraMockPresets.find( + (p) => p.id === NOTIFICATIONS_PRESET_ID +); + +interface ExtraPresetNotificationsProps { + customNotificationsData: Notification[] | null | undefined; + handlers: string[]; + onFormChange?: (data: Notification[] | null | undefined) => void; + onTogglePreset: ( + e: React.ChangeEvent, + presetId: string + ) => void; +} + +export const ExtraPresetNotifications = ({ + customNotificationsData, + handlers, + onFormChange, + onTogglePreset, +}: ExtraPresetNotificationsProps) => { + if (!notificationsPreset) return null; + + const isEnabled = handlers.includes(NOTIFICATIONS_PRESET_ID); + + return ( + ( + + )} + saveDataToLocalStorage={saveCustomNotificationsData} + setMSWData={setCustomNotificationsData} + /> + ); +}; + +interface NotificationBoxProps { + editNotification: (updater: (prev: Notification) => Notification) => void; + id: string; + notification: Notification; + onDelete: () => void; +} + +const NotificationBox = (props: NotificationBoxProps) => { + const { notification, onDelete, editNotification, id } = props; + + return ( + + renderNotificationFields(notification, onChange) + } + id={id} + item={notification} + onDelete={onDelete} + summary={ + + Entity {notification.entity?.label} |{' '} + Label {notification.label} | Type{' '} + {notification.type} + + } + /> + ); +}; + +const renderNotificationFields = ( + notification: Notification, + onChange: ( + e: React.ChangeEvent< + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + > + ) => void +) => [ + , + + , + + , + + , + + , + + , + + , + + , +]; + +const notificationTemplates = { + Default: () => notificationFactory.build(), + 'Migration Notification': () => + notificationFactory.build({ + entity: { id: 0, label: 'linode-0', type: 'linode' }, + label: 'You have a migration pending!', + message: + 'You have a migration pending! Your Linode must be offline before starting the migration.', + severity: 'major', + type: 'migration_pending', + }), + 'Minor Severity Notification': () => + notificationFactory.build({ + message: 'Testing for minor notification', + severity: 'minor', + type: 'notice', + }), + 'Critical Severity Notification': () => + notificationFactory.build({ + message: 'Testing for critical notification', + severity: 'critical', + type: 'notice', + }), + 'Balance Notification': () => + notificationFactory.build({ + message: 'You have an overdue balance!', + severity: 'major', + type: 'payment_due', + }), + 'Block Storage Migration Scheduled Notification': () => + notificationFactory.build({ + body: 'Your volumes in us-east will be upgraded to NVMe.', + entity: { + id: 20, + label: 'eligibleNow', + type: 'volume', + url: '/volumes/20', + }, + label: 'You have a scheduled Block Storage volume upgrade pending!', + message: + 'The Linode that the volume is attached to will shut down in order to complete the upgrade and reboot once it is complete. Any other volumes attached to the same Linode will also be upgraded.', + severity: 'critical', + type: 'volume_migration_scheduled', + until: '2021-10-16T04:00:00', + when: '2021-09-30T04:00:00', + }), +} as const; diff --git a/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx b/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx index 31e788dc0ca..a2d9613f864 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetOptions.tsx @@ -4,17 +4,36 @@ import { getMockPresetGroups } from 'src/mocks/mockPreset'; import { extraMockPresets } from 'src/mocks/presets'; import { ExtraPresetAccount } from './ExtraPresetAccount'; +import { ExtraPresetEvents } from './ExtraPresetEvents'; +import { ExtraPresetMaintenance } from './ExtraPresetMaintenance'; +import { ExtraPresetNotifications } from './ExtraPresetNotifications'; import { ExtraPresetOptionCheckbox } from './ExtraPresetOptionCheckbox'; import { ExtraPresetOptionSelect } from './ExtraPresetOptionSelect'; import { ExtraPresetProfile } from './ExtraPresetProfile'; -import type { Account, Profile } from '@linode/api-v4'; +import type { + Account, + AccountMaintenance, + Event, + Notification, + Profile, +} from '@linode/api-v4'; export interface ExtraPresetOptionsProps { customAccountData?: Account | null; + customEventsData?: Event[] | null; + customMaintenanceData?: AccountMaintenance[] | null; + customNotificationsData?: Notification[] | null; customProfileData?: null | Profile; handlers: string[]; onCustomAccountChange?: (data: Account | null | undefined) => void; + onCustomEventsChange?: (data: Event[] | null | undefined) => void; + onCustomMaintenanceChange?: ( + data: AccountMaintenance[] | null | undefined + ) => void; + onCustomNotificationsChange?: ( + data: Notification[] | null | undefined + ) => void; onCustomProfileChange?: (data: null | Profile | undefined) => void; onPresetCountChange: (e: React.ChangeEvent, presetId: string) => void; onSelectChange: (e: React.ChangeEvent, presetId: string) => void; @@ -28,9 +47,15 @@ export interface ExtraPresetOptionsProps { export const ExtraPresetOptions = ({ customAccountData, customProfileData, + customEventsData, + customMaintenanceData, + customNotificationsData, handlers, onCustomAccountChange, onCustomProfileChange, + onCustomEventsChange, + onCustomMaintenanceChange, + onCustomNotificationsChange, onPresetCountChange, onSelectChange, onTogglePreset, @@ -88,6 +113,30 @@ export const ExtraPresetOptions = ({ onTogglePreset={onTogglePreset} /> )} + {currentGroupType === 'events' && ( + + )} + {currentGroupType === 'maintenance' && ( + + )} + {currentGroupType === 'notifications' && ( + + )} ); })} diff --git a/packages/manager/src/dev-tools/constants.ts b/packages/manager/src/dev-tools/constants.ts index d2b7a66d212..b1159189bed 100644 --- a/packages/manager/src/dev-tools/constants.ts +++ b/packages/manager/src/dev-tools/constants.ts @@ -13,3 +13,11 @@ export const LOCAL_STORAGE_PRESETS_MAP_KEY = 'msw-preset-count-map'; export const LOCAL_STORAGE_ACCOUNT_FORM_DATA_KEY = 'msw-account-form-data'; export const LOCAL_STORAGE_PROFILE_FORM_DATA_KEY = 'msw-profile-form-data'; + +export const LOCAL_STORAGE_EVENTS_FORM_DATA_KEY = 'msw-events-form-data'; + +export const LOCAL_STORAGE_MAINTENANCE_FORM_DATA_KEY = + 'msw-maintenance-form-data'; + +export const LOCAL_STORAGE_NOTIFICATIONS_FORM_DATA_KEY = + 'msw-notifications-form-data'; diff --git a/packages/manager/src/dev-tools/dev-tools.css b/packages/manager/src/dev-tools/dev-tools.css index 0cd87da2296..feec0feee2a 100644 --- a/packages/manager/src/dev-tools/dev-tools.css +++ b/packages/manager/src/dev-tools/dev-tools.css @@ -351,6 +351,10 @@ overflow-y: auto; padding-right: 24px; + .dev-tools__modal-form__no-max-height { + max-height: initial; + } + label { display: flex; flex-direction: column; @@ -378,6 +382,48 @@ font-family: monospace; white-space: pre; } + + input[type="checkbox"] { + align-self: start; + } +} + +.dev-tools__modal__rectangle-group { + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.5); + border-radius: 2px; + padding: 10px; + + .dev-tools__modal__controls { + background-color: white; + top: 0; + position: sticky; + display: flex; + flex-direction: row; + margin: -11px; + margin-bottom: -10px; + padding: 10px; + border-radius: 2px; + border: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: none; + + button { + white-space: nowrap; + } + + > div:first-child { + width: calc(100% - 55px); + } + + > div:last-child { + flex-grow: 1; + text-align: right; + } + } + + form { + margin-top: 10px; + } } /* diff --git a/packages/manager/src/dev-tools/utils.ts b/packages/manager/src/dev-tools/utils.ts index dfc8234a345..9fd14ac0bd1 100644 --- a/packages/manager/src/dev-tools/utils.ts +++ b/packages/manager/src/dev-tools/utils.ts @@ -2,7 +2,10 @@ import { defaultBaselineMockPreset, extraMockPresets } from 'src/mocks/presets'; import { LOCAL_STORAGE_ACCOUNT_FORM_DATA_KEY, + LOCAL_STORAGE_EVENTS_FORM_DATA_KEY, LOCAL_STORAGE_KEY, + LOCAL_STORAGE_MAINTENANCE_FORM_DATA_KEY, + LOCAL_STORAGE_NOTIFICATIONS_FORM_DATA_KEY, LOCAL_STORAGE_PRESET_EXTRAS_KEY, LOCAL_STORAGE_PRESET_KEY, LOCAL_STORAGE_PRESETS_MAP_KEY, @@ -11,7 +14,13 @@ import { LOCAL_STORAGE_SEEDS_COUNT_MAP_KEY, } from './constants'; -import type { Account, Profile } from '@linode/api-v4'; +import type { + Account, + AccountMaintenance, + Event, + Notification, + Profile, +} from '@linode/api-v4'; import type { MockPresetBaselineId, MockPresetExtraId, @@ -189,3 +198,67 @@ export const saveCustomProfileData = (data: null | Profile): void => { ); } }; + +/** + * Retrieves the custom events form data from local storage. + */ +export const getCustomEventsData = (): Event[] | null => { + const data = localStorage.getItem(LOCAL_STORAGE_EVENTS_FORM_DATA_KEY); + return data ? JSON.parse(data) : null; +}; + +/** + * Saves the custom events form data to local storage. + */ +export const saveCustomEventsData = (data: Event[] | null): void => { + if (data) { + localStorage.setItem( + LOCAL_STORAGE_EVENTS_FORM_DATA_KEY, + JSON.stringify(data) + ); + } +}; + +/** + * Retrieves the custom maintenance form data from local storage. + */ +export const getCustomMaintenanceData = (): AccountMaintenance[] | null => { + const data = localStorage.getItem(LOCAL_STORAGE_MAINTENANCE_FORM_DATA_KEY); + return data ? JSON.parse(data) : null; +}; + +/** + * Saves the custom maintenance form data to local storage. + */ +export const saveCustomMaintenanceData = ( + data: AccountMaintenance[] | null +): void => { + if (data) { + localStorage.setItem( + LOCAL_STORAGE_MAINTENANCE_FORM_DATA_KEY, + JSON.stringify(data) + ); + } +}; + +/** + * Retrieves the custom notifications form data from local storage. + */ +export const getCustomNotificationsData = (): Notification[] | null => { + const data = localStorage.getItem(LOCAL_STORAGE_NOTIFICATIONS_FORM_DATA_KEY); + return data ? JSON.parse(data) : null; +}; + +/** + * Saves the custom notifications form data to local storage. + */ +export const saveCustomNotificationsData = ( + data: Notification[] | null +): void => { + if (data) { + localStorage.setItem( + LOCAL_STORAGE_NOTIFICATIONS_FORM_DATA_KEY, + JSON.stringify(data) + ); + } +}; diff --git a/packages/manager/src/factories/notification.ts b/packages/manager/src/factories/notification.ts index 82907bec271..2c2b2eb5e40 100644 --- a/packages/manager/src/factories/notification.ts +++ b/packages/manager/src/factories/notification.ts @@ -21,7 +21,7 @@ export const notificationFactory = Factory.Sync.makeFactory({ severity: 'critical', type: 'maintenance', until: null, - when: DateTime.local().plus({ days: 7 }).toISODate(), + when: DateTime.local().plus({ days: 7 }).toISO({ includeOffset: false }), }); export const abuseTicketNotificationFactory = notificationFactory.extend({ diff --git a/packages/manager/src/mocks/presets/extra/account/customEvents.ts b/packages/manager/src/mocks/presets/extra/account/customEvents.ts new file mode 100644 index 00000000000..f3b17c31c51 --- /dev/null +++ b/packages/manager/src/mocks/presets/extra/account/customEvents.ts @@ -0,0 +1,28 @@ +import { http, HttpResponse } from 'msw'; + +import { makeResourcePage } from 'src/mocks/serverHandlers'; + +import type { Event } from '@linode/api-v4'; +import type { MockPresetExtra } from 'src/mocks/types'; + +let customEventsData: Event[] | null = null; + +export const setCustomEventsData = (data: Event[] | null) => { + customEventsData = data; +}; + +const mockCustomEvents = () => { + return [ + http.get('*/account/events', () => { + return HttpResponse.json(makeResourcePage(customEventsData ?? [])); + }), + ]; +}; + +export const customEventsPreset: MockPresetExtra = { + desc: 'Custom Events', + group: { id: 'Events', type: 'events' }, + handlers: [mockCustomEvents], + id: 'events:custom', + label: 'Custom Events', +}; diff --git a/packages/manager/src/mocks/presets/extra/account/customMaintenance.ts b/packages/manager/src/mocks/presets/extra/account/customMaintenance.ts new file mode 100644 index 00000000000..96f57cc4339 --- /dev/null +++ b/packages/manager/src/mocks/presets/extra/account/customMaintenance.ts @@ -0,0 +1,67 @@ +import { http, HttpResponse } from 'msw'; + +import { makeResourcePage } from 'src/mocks/serverHandlers'; + +import type { AccountMaintenance } from '@linode/api-v4'; +import type { MockPresetExtra } from 'src/mocks/types'; + +let customMaintenanceData: AccountMaintenance[] | null = null; + +export const setCustomMaintenanceData = (data: AccountMaintenance[] | null) => { + customMaintenanceData = data; +}; + +const mockCustomMaintenance = () => { + return [ + http.get('*/account/maintenance', ({ request }) => { + const url = new URL(request.url); + + const page = Number(url.searchParams.get('page') || 1); + const pageSize = Number(url.searchParams.get('page_size') || 25); + const headers = JSON.parse(request.headers.get('x-filter') || '{}'); + + const accountMaintenance = + customMaintenanceData?.filter((maintenance) => + JSON.stringify(headers.status).includes(maintenance.status) + ) ?? []; + + if (request.headers.get('x-filter')) { + accountMaintenance.sort((a, b) => { + const statusA = a[headers['+order_by'] as keyof AccountMaintenance]; + const statusB = b[headers['+order_by'] as keyof AccountMaintenance]; + + if (statusA < statusB) { + return -1; + } + if (statusA > statusB) { + return 1; + } + return 0; + }); + + if (headers['+order'] === 'desc') { + accountMaintenance.reverse(); + } + return HttpResponse.json({ + data: accountMaintenance.slice( + (page - 1) * pageSize, + (page - 1) * pageSize + pageSize + ), + page, + pages: Math.ceil(accountMaintenance.length / pageSize), + results: accountMaintenance.length, + }); + } + + return HttpResponse.json(makeResourcePage(accountMaintenance)); + }), + ]; +}; + +export const customMaintenancePreset: MockPresetExtra = { + desc: 'Custom Maintenance', + group: { id: 'Maintenance', type: 'maintenance' }, + handlers: [mockCustomMaintenance], + id: 'maintenance:custom', + label: 'Custom Maintenance', +}; diff --git a/packages/manager/src/mocks/presets/extra/account/customNotifications.ts b/packages/manager/src/mocks/presets/extra/account/customNotifications.ts new file mode 100644 index 00000000000..2d614da381c --- /dev/null +++ b/packages/manager/src/mocks/presets/extra/account/customNotifications.ts @@ -0,0 +1,29 @@ +import { http } from 'msw'; + +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { makeResponse } from 'src/mocks/utilities/response'; + +import type { Notification } from '@linode/api-v4'; +import type { MockPresetExtra } from 'src/mocks/types'; + +let customNotificationData: Notification[] | null = null; + +export const setCustomNotificationsData = (data: Notification[] | null) => { + customNotificationData = data; +}; + +const mockCustomNotifications = () => { + return [ + http.get('*/v4*/account/notifications', async () => { + return makeResponse(makeResourcePage(customNotificationData ?? [])); + }), + ]; +}; + +export const customNotificationsPreset: MockPresetExtra = { + desc: 'Custom Notifications', + group: { id: 'Notifications', type: 'notifications' }, + handlers: [mockCustomNotifications], + id: 'notifications:custom', + label: 'Custom Notifications', +}; diff --git a/packages/manager/src/mocks/presets/index.ts b/packages/manager/src/mocks/presets/index.ts index 538b739a05c..174ae7e87f8 100644 --- a/packages/manager/src/mocks/presets/index.ts +++ b/packages/manager/src/mocks/presets/index.ts @@ -6,6 +6,9 @@ import { baselineCrudPreset } from './baseline/crud'; import { baselineLegacyPreset } from './baseline/legacy'; import { baselineNoMocksPreset } from './baseline/noMocks'; import { customAccountPreset } from './extra/account/customAccount'; +import { customEventsPreset } from './extra/account/customEvents'; +import { customMaintenancePreset } from './extra/account/customMaintenance'; +import { customNotificationsPreset } from './extra/account/customNotifications'; import { customProfilePreset } from './extra/account/customProfile'; import { managedDisabledPreset } from './extra/account/managedDisabled'; import { managedEnabledPreset } from './extra/account/managedEnabled'; @@ -41,6 +44,9 @@ export const extraMockPresets: MockPresetExtra[] = [ apiResponseTimePreset, customAccountPreset, customProfilePreset, + customEventsPreset, + customMaintenancePreset, + customNotificationsPreset, linodeLimitsPreset, lkeLimitsPreset, managedEnabledPreset, diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index dbd7609297d..ad74a5324c1 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -60,23 +60,41 @@ export interface MockPresetBaseline extends MockPresetBase { * Mock Preset Extra */ export type MockPresetExtraGroup = { - id: - | 'Account' - | 'API' - | 'Capabilities' - | 'Limits' - | 'Managed' - | 'Profile' - | 'Regions'; - type: 'account' | 'checkbox' | 'profile' | 'select'; + id: MockPresetExtraGroupId; + type: MockPresetExtraGroupType; }; + +export type MockPresetExtraGroupId = + | 'Account' + | 'API' + | 'Capabilities' + | 'Events' + | 'Limits' + | 'Maintenance' + | 'Managed' + | 'Notifications' + | 'Profile' + | 'Regions'; + +export type MockPresetExtraGroupType = + | 'account' + | 'checkbox' + | 'events' + | 'maintenance' + | 'notifications' + | 'profile' + | 'select'; + export type MockPresetExtraId = | 'account:custom' | 'account:managed-disabled' | 'account:managed-enabled' | 'api:response-time' + | 'events:custom' | 'limits:linode-limits' | 'limits:lke-limits' + | 'maintenance:custom' + | 'notifications:custom' | 'profile:custom' | 'regions:core-and-distributed' | 'regions:core-only' From 5dc85c0358f96309cefa5f330140926c827fe428 Mon Sep 17 00:00:00 2001 From: Harsh Shankar Rao Date: Wed, 21 May 2025 16:44:06 +0530 Subject: [PATCH 22/59] refactor: [M3-9647] - Reduce api requests made for every keystroke in Volume attach drawer (#12052) * refactor: [M3-9647] - Reduce api requests made for every keystroke in Volume attach drawer * Added changeset: Reduce api requests made for every keystroke in Volume attach drawer * feedback @bnussman-akamai * added useVolumeQuery() for the searched volume * add useDebouncedValue hook --- .../pr-12052-tech-stories-1744889150021.md | 5 +++ .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 5 ++- .../Drawers/VolumeDrawer/VolumeSelect.tsx | 33 ++++++++++++------- 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md diff --git a/packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md b/packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md new file mode 100644 index 00000000000..06a423492e9 --- /dev/null +++ b/packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Reduce api requests made for every keystroke in Volume attach drawer ([#12052](https://github.com/linode/manager/pull/12052)) diff --git a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAttachForm.tsx index d390d02a7ca..91a279929ed 100644 --- a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -97,7 +97,10 @@ export const LinodeVolumeAttachForm = (props: Props) => { validationSchema: AttachVolumeValidationSchema, }); - const { data: volume } = useVolumeQuery(values.volume_id); + const { data: volume } = useVolumeQuery( + values.volume_id, + values.volume_id !== -1 + ); const linodeRequiresClientLibraryUpdate = volume?.encryption === 'enabled' && diff --git a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/VolumeSelect.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/VolumeSelect.tsx index e2c3e2f18d8..9e5717dd77f 100644 --- a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/VolumeSelect.tsx +++ b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/VolumeSelect.tsx @@ -1,5 +1,6 @@ -import { useInfiniteVolumesQuery } from '@linode/queries'; +import { useInfiniteVolumesQuery, useVolumeQuery } from '@linode/queries'; import { Autocomplete } from '@linode/ui'; +import { useDebouncedValue } from '@linode/utilities'; import * as React from 'react'; interface Props { @@ -17,11 +18,13 @@ export const VolumeSelect = (props: Props) => { const [inputValue, setInputValue] = React.useState(''); - const searchFilter = inputValue + const debouncedInputValue = useDebouncedValue(inputValue); + + const searchFilter = debouncedInputValue ? { '+or': [ - { label: { '+contains': inputValue } }, - { tags: { '+contains': inputValue } }, + { label: { '+contains': debouncedInputValue } }, + { tags: { '+contains': debouncedInputValue } }, ], } : {}; @@ -35,9 +38,16 @@ export const VolumeSelect = (props: Props) => { '+order_by': 'label', }); - const options = data?.pages - .flatMap((page) => page.data) - .map(({ id, label }) => ({ id, label })); + const options = data?.pages.flatMap((page) => page.data); + + const { data: volume, isLoading: isLoadingSelected } = useVolumeQuery( + value, + value > 0 + ); + + if (value && volume && !options?.some((item) => item.id === volume.id)) { + options?.push(volume); + } const selectedVolume = options?.find((option) => option.id === value) ?? null; @@ -45,6 +55,7 @@ export const VolumeSelect = (props: Props) => { options} helperText={ region && "Only volumes in this Linode's region are attachable." } @@ -64,13 +75,13 @@ export const VolumeSelect = (props: Props) => { } }, }} - loading={isLoading} + loading={isLoading || isLoadingSelected} onBlur={onBlur} - onChange={(event, value) => { - onChange(value?.id ?? -1); + onChange={(_, value) => { setInputValue(''); + onChange(value?.id ?? -1); }} - onInputChange={(event, value, reason) => { + onInputChange={(_, value, reason) => { if (reason === 'input') { setInputValue(value); } else { From d2214850f637bd5b640c027d393cfda2981d656c Mon Sep 17 00:00:00 2001 From: dmcintyr-akamai Date: Wed, 21 May 2025 08:07:59 -0400 Subject: [PATCH 23/59] test [M3-10000]: Account quotas navigation and permissions (#12250) * quota testing w/ feature flag enabled/disabled * use usermenu shortcut * ui shortcuts, TODO * Added changeset: Account quotas navigation and permissions * move new file to proper directory * improve label search --- .../pr-12250-tests-1747757135754.md | 5 ++ .../cypress/e2e/core/account/quotas.spec.ts | 89 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 packages/manager/.changeset/pr-12250-tests-1747757135754.md create mode 100644 packages/manager/cypress/e2e/core/account/quotas.spec.ts diff --git a/packages/manager/.changeset/pr-12250-tests-1747757135754.md b/packages/manager/.changeset/pr-12250-tests-1747757135754.md new file mode 100644 index 00000000000..fdb60ba9f6f --- /dev/null +++ b/packages/manager/.changeset/pr-12250-tests-1747757135754.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Account quotas navigation and permissions ([#12250](https://github.com/linode/manager/pull/12250)) diff --git a/packages/manager/cypress/e2e/core/account/quotas.spec.ts b/packages/manager/cypress/e2e/core/account/quotas.spec.ts new file mode 100644 index 00000000000..bbfc38d8bb2 --- /dev/null +++ b/packages/manager/cypress/e2e/core/account/quotas.spec.ts @@ -0,0 +1,89 @@ +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { ui } from 'support/ui'; + +describe('Quotas accessible when limitsEvolution feature flag enabled', () => { + beforeEach(() => { + // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. + mockAppendFeatureFlags({ + limitsEvolution: { + enabled: true, + }, + }).as('getFeatureFlags'); + }); + it('can navigate directly to Quotas page', () => { + cy.visitWithLogin('/account/quotas'); + cy.wait('@getFeatureFlags'); + cy.url().should('endWith', '/quotas'); + cy.contains( + 'View your Object Storage quotas by applying the endpoint filter below' + ).should('be.visible'); + }); + + it('can navigate to the Quotas page via the User Menu', () => { + cy.visitWithLogin('/'); + cy.wait('@getFeatureFlags'); + // Open user menu + ui.userMenuButton.find().click(); + ui.userMenu.find().within(() => { + cy.get('[data-testid="menu-item-Quotas"]').should('be.visible').click(); + cy.url().should('endWith', '/quotas'); + }); + }); + + it('Quotas tab is visible from all other tabs in Account tablist', () => { + cy.visitWithLogin('/account/billing'); + cy.wait('@getFeatureFlags'); + ui.tabList.find().within(() => { + cy.get('a').each(($link) => { + cy.wrap($link).click(); + cy.get('[data-testid="Quotas"]').should('be.visible'); + }); + }); + cy.get('[data-testid="Quotas"]').should('be.visible').click(); + cy.url().should('endWith', '/quotas'); + }); +}); + +describe('Quotas inaccessible when limitsEvolution feature flag disabled', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + limitsEvolution: { + enabled: false, + }, + }).as('getFeatureFlags'); + }); + it('Quotas page is inaccessible', () => { + cy.visitWithLogin('/account/quotas'); + cy.wait('@getFeatureFlags'); + cy.url().should('endWith', '/billing'); + }); + + it('cannot navigate to the Quotas tab via the Users & Grants link in the User Menu', () => { + cy.visitWithLogin('/'); + cy.wait('@getFeatureFlags'); + // Open user menu + ui.userMenuButton.find().click(); + ui.userMenu.find().within(() => { + cy.get('[data-testid="menu-item-Quotas"]').should('not.exist'); + cy.get('[data-testid="menu-item-Users & Grants"]') + .should('be.visible') + .click(); + }); + cy.url().should('endWith', '/users'); + cy.get('[data-testid="Quotas"]').should('not.exist'); + }); + + it('cannot navigate to the Quotas tab via the Billing link in the User Menu', () => { + cy.visitWithLogin('/'); + cy.wait('@getFeatureFlags'); + ui.userMenuButton.find().click(); + ui.userMenu.find().within(() => { + cy.get('[data-testid="menu-item-Quotas"]').should('not.exist'); + cy.get('[data-testid="menu-item-Billing & Contact Information"]') + .should('be.visible') + .click(); + }); + cy.url().should('endWith', '/billing'); + cy.get('[data-testid="Quotas"]').should('not.exist'); + }); +}); From 38b825139b7bfee176ffa11b40fe15f509505740 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 21 May 2025 09:26:04 -0400 Subject: [PATCH 24/59] chore: [M3-9992] - Remove `recompose` from Longview (#12239) * remove recompose from longview * Added changeset: Remove recompose from Longview --------- Co-authored-by: Banks Nussman --- .../pr-12239-tech-stories-1747407424807.md | 5 + .../LongviewDetail/LongviewDetail.tsx | 60 +++-- .../LongviewLanding/Gauges/Network.tsx | 8 +- .../LongviewLanding/LongviewClientHeader.tsx | 205 +++++++++--------- .../LongviewLanding/LongviewClientRow.tsx | 27 +-- .../LongviewLanding/LongviewClients.tsx | 7 +- 6 files changed, 145 insertions(+), 167 deletions(-) create mode 100644 packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md diff --git a/packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md b/packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md new file mode 100644 index 00000000000..2f2d224a88d --- /dev/null +++ b/packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Remove recompose from Longview ([#12239](https://github.com/linode/manager/pull/12239)) diff --git a/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx b/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx index 5689e12a496..a3451c35116 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/LongviewDetail.tsx @@ -2,7 +2,6 @@ import { useProfile } from '@linode/queries'; import { CircleProgress, ErrorState, Notice, Paper } from '@linode/ui'; import { NotFound } from '@linode/ui'; import * as React from 'react'; -import { compose } from 'recompose'; import { LandingHeader } from 'src/components/LandingHeader'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; @@ -294,39 +293,38 @@ export const LongviewDetail = (props: CombinedProps) => { ); }; -type LongviewDetailParams = { - id: string; -}; - -const EnhancedLongviewDetail = compose( - React.memo, +interface LongviewDetailParams { + id: number; +} +const EnhancedLongviewDetail = React.memo( withClientStats<{ match: { params: LongviewDetailParams } }>((ownProps) => { - return +(ownProps?.match?.params?.id ?? ''); - }), - withLongviewClients( - ( - own, - { - longviewClientsData, - longviewClientsError, - longviewClientsLastUpdated, - longviewClientsLoading, - } - ) => { - // This is explicitly typed, otherwise `client` would be typed as - // `LongviewClient`, even though there's a chance it could be undefined. - const client: LongviewClient | undefined = - longviewClientsData[own?.match.params.id ?? '']; + return ownProps.match.params.id; + })( + withLongviewClients( + ( + own, + { + longviewClientsData, + longviewClientsError, + longviewClientsLastUpdated, + longviewClientsLoading, + } + ) => { + // This is explicitly typed, otherwise `client` would be typed as + // `LongviewClient`, even though there's a chance it could be undefined. + const client: LongviewClient | undefined = + longviewClientsData[own?.match.params.id ?? '']; - return { - client, - longviewClientsError, - longviewClientsLastUpdated, - longviewClientsLoading, - }; - } + return { + client, + longviewClientsError, + longviewClientsLastUpdated, + longviewClientsLoading, + }; + } + )(LongviewDetail) ) -)(LongviewDetail); +); export default EnhancedLongviewDetail; diff --git a/packages/manager/src/features/Longview/LongviewLanding/Gauges/Network.tsx b/packages/manager/src/features/Longview/LongviewLanding/Gauges/Network.tsx index d05fc9ab0d5..55b190bca7a 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/Gauges/Network.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/Gauges/Network.tsx @@ -1,7 +1,6 @@ import { Typography } from '@linode/ui'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; -import { compose } from 'recompose'; import { GaugePercent } from 'src/components/GaugePercent/GaugePercent'; import withClientStats from 'src/containers/longview.stats.container'; @@ -102,10 +101,9 @@ const Network = (props: NetworkProps) => { ); }; -export const NetworkGauge = compose( - React.memo, - withClientStats((ownProps) => ownProps.clientID) -)(Network); +export const NetworkGauge = React.memo( + withClientStats((ownProps) => ownProps.clientID)(Network) +); /* What's returned from Network is a bit of an unknown, but assuming that diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx index e0267417bce..94aeb00fbc8 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx @@ -3,7 +3,6 @@ import { Typography } from '@linode/ui'; import { formatUptime } from '@linode/utilities'; import Grid from '@mui/material/Grid'; import * as React from 'react'; -import { compose } from 'recompose'; import { EditableEntityLabel } from 'src/components/EditableEntityLabel/EditableEntityLabel'; import { Link } from 'src/components/Link'; @@ -34,115 +33,111 @@ interface Props { userCanModifyClient: boolean; } -interface LongviewClientHeaderProps extends Props, DispatchProps, LVDataProps {} +interface LongviewClientHeaderProps extends Props, LVDataProps {} -const enhanced = compose( - withClientStats((ownProps: Props) => ownProps.clientID) -); +export const LongviewClientHeader = withClientStats( + (ownProps: Props) => ownProps.clientID +)((props: LongviewClientHeaderProps) => { + const { + clientID, + clientLabel, + lastUpdatedError, + longviewClientData, + longviewClientDataLoading, + longviewClientLastUpdated, + openPackageDrawer, + updateLongviewClient, + userCanModifyClient, + } = props; -export const LongviewClientHeader = enhanced( - (props: LongviewClientHeaderProps) => { - const { - clientID, - clientLabel, - lastUpdatedError, - longviewClientData, - longviewClientDataLoading, - longviewClientLastUpdated, - openPackageDrawer, - updateLongviewClient, - userCanModifyClient, - } = props; + const [updating, setUpdating] = React.useState(false); - const [updating, setUpdating] = React.useState(false); + const { data: profile } = useProfile(); - const { data: profile } = useProfile(); + const handleUpdateLabel = (newLabel: string) => { + setUpdating(true); + return updateLongviewClient(clientID, newLabel) + .then((_) => { + setUpdating(false); + }) + .catch((error) => { + setUpdating(false); + return Promise.reject( + getAPIErrorOrDefault(error, 'Error updating label')[0].reason + ); + }); + }; - const handleUpdateLabel = (newLabel: string) => { - setUpdating(true); - return updateLongviewClient(clientID, newLabel) - .then((_) => { - setUpdating(false); - }) - .catch((error) => { - setUpdating(false); - return Promise.reject( - getAPIErrorOrDefault(error, 'Error updating label')[0].reason - ); - }); - }; + const hostname = + longviewClientData.SysInfo?.hostname ?? 'Hostname not available'; + const uptime = longviewClientData?.Uptime ?? null; + const formattedUptime = + uptime !== null ? `Up ${formatUptime(uptime)}` : 'Uptime not available'; + const packages = longviewClientData?.Packages ?? null; + const numPackagesToUpdate = packages ? packages.length : 0; + const packagesToUpdate = getPackageNoticeText(packages); - const hostname = - longviewClientData.SysInfo?.hostname ?? 'Hostname not available'; - const uptime = longviewClientData?.Uptime ?? null; - const formattedUptime = - uptime !== null ? `Up ${formatUptime(uptime)}` : 'Uptime not available'; - const packages = longviewClientData?.Packages ?? null; - const numPackagesToUpdate = packages ? packages.length : 0; - const packagesToUpdate = getPackageNoticeText(packages); + const formattedlastUpdatedTime = + longviewClientLastUpdated !== undefined + ? `Last updated ${formatDate(longviewClientLastUpdated, { + timezone: profile?.timezone, + })}` + : 'Latest update time not available'; - const formattedlastUpdatedTime = - longviewClientLastUpdated !== undefined - ? `Last updated ${formatDate(longviewClientLastUpdated, { - timezone: profile?.timezone, - })}` - : 'Latest update time not available'; + /** + * The pathOrs ahead will default to 'not available' values if + * there's an error, so the only case we need to handle is + * the loading state, which should be displayed only if + * data is loading for the first time and there are no errors. + */ + const loading = + longviewClientDataLoading && + !lastUpdatedError && + longviewClientLastUpdated !== 0; - /** - * The pathOrs ahead will default to 'not available' values if - * there's an error, so the only case we need to handle is - * the loading state, which should be displayed only if - * data is loading for the first time and there are no errors. - */ - const loading = - longviewClientDataLoading && - !lastUpdatedError && - longviewClientLastUpdated !== 0; - - return ( - - - {userCanModifyClient ? ( - - ) : ( - - )} - - - {loading ? ( - Loading... - ) : ( - <> - {formattedUptime} - {numPackagesToUpdate > 0 ? ( - openPackageDrawer()} - title={packagesToUpdate} - > - {packagesToUpdate} - - ) : ( - {packagesToUpdate} - )} - - )} - - - View Details - {!loading && ( - - - {formattedlastUpdatedTime} - - - )} - - - ); - } -); + return ( + + + {userCanModifyClient ? ( + + ) : ( + + )} + + + {loading ? ( + Loading... + ) : ( + <> + {formattedUptime} + {numPackagesToUpdate > 0 ? ( + openPackageDrawer()} + title={packagesToUpdate} + > + {packagesToUpdate} + + ) : ( + {packagesToUpdate} + )} + + )} + + + View Details + {!loading && ( + + + {formattedlastUpdatedTime} + + + )} + + + ); +}); diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx index e69a8ae61e6..a41a1019968 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientRow.tsx @@ -2,7 +2,6 @@ import { useGrants } from '@linode/queries'; import { Paper } from '@linode/ui'; import Grid from '@mui/material/Grid'; import * as React from 'react'; -import { compose } from 'recompose'; import withLongviewClients from 'src/containers/longview.container'; import withClientStats from 'src/containers/longview.stats.container'; @@ -19,7 +18,6 @@ import { LongviewClientHeader } from './LongviewClientHeader'; import { LongviewClientInstructions } from './LongviewClientInstructions'; import type { ActionHandlers } from './LongviewActionMenu'; -import type { Grant } from '@linode/api-v4'; import type { DispatchProps } from 'src/containers/longview.container'; import type { Props as LVDataProps } from 'src/containers/longview.stats.container'; @@ -31,11 +29,7 @@ interface Props extends ActionHandlers { openPackageDrawer: () => void; } -interface LongviewClientRowProps - extends Props, - LVDataProps, - DispatchProps, - GrantProps {} +interface LongviewClientRowProps extends Props, LVDataProps, DispatchProps {} const LongviewClientRow = (props: LongviewClientRowProps) => { const { @@ -58,9 +52,7 @@ const LongviewClientRow = (props: LongviewClientRowProps) => { const longviewPermissions = grants?.longview || []; - const thisPermission = (longviewPermissions as Grant[]).find( - (r) => r.id === clientID - ); + const thisPermission = longviewPermissions.find((r) => r.id === clientID); const userCanModifyClient = thisPermission ? thisPermission.permissions === 'read_write' @@ -213,13 +205,8 @@ const LongviewClientRow = (props: LongviewClientRowProps) => { ); }; -interface GrantProps { - userCanModifyClient: boolean; -} - -export default compose( - React.memo, - withClientStats((ownProps) => ownProps.clientID), - /** We only need the update action here, easier than prop drilling through 4 components */ - withLongviewClients(() => ({})) -)(LongviewClientRow); +export default React.memo( + withClientStats((ownProps) => ownProps.clientID)( + withLongviewClients(() => ({}))(LongviewClientRow) + ) +); diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx index 76d98ba07e9..47fd5ac729e 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx @@ -3,7 +3,6 @@ import { Autocomplete, Typography } from '@linode/ui'; import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; import { connect } from 'react-redux'; -import { compose } from 'recompose'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; @@ -289,11 +288,7 @@ const mapStateToProps: MapState = (state, _ownProps) => { const connected = connect(mapStateToProps); -export default compose( - React.memo, - connected, - withLongviewClients() -)(LongviewClients); +export default React.memo(connected(withLongviewClients()(LongviewClients))); /** * Helper function for sortClientsBy, From 58ca5da64617d95b11d26fd72015009e441958d2 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Wed, 21 May 2025 11:14:42 -0400 Subject: [PATCH 25/59] fix: [M3-9900] - Simplify `linode_resize` started event (#12252) * simplify event * Added changeset: Make `linode_resize` started event generic * dupe changeset --- .../pr-12252-fixed-1747765254237.md | 5 +++ .../src/features/Events/factories/linode.tsx | 34 ++++--------------- 2 files changed, 11 insertions(+), 28 deletions(-) create mode 100644 packages/manager/.changeset/pr-12252-fixed-1747765254237.md diff --git a/packages/manager/.changeset/pr-12252-fixed-1747765254237.md b/packages/manager/.changeset/pr-12252-fixed-1747765254237.md new file mode 100644 index 00000000000..37e28d21234 --- /dev/null +++ b/packages/manager/.changeset/pr-12252-fixed-1747765254237.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Make `linode_resize` started event generic ([#12252](https://github.com/linode/manager/pull/12252)) diff --git a/packages/manager/src/features/Events/factories/linode.tsx b/packages/manager/src/features/Events/factories/linode.tsx index a423b98a276..2d80ed84d26 100644 --- a/packages/manager/src/features/Events/factories/linode.tsx +++ b/packages/manager/src/features/Events/factories/linode.tsx @@ -1,14 +1,10 @@ -import { useLinodeQuery } from '@linode/queries'; -import { formatStorageUnits } from '@linode/utilities'; import * as React from 'react'; import { Link } from 'src/components/Link'; -import { useTypeQuery } from 'src/queries/types'; import { EventLink } from '../EventLink'; import type { PartialEventMap } from '../types'; -import type { Event } from '@linode/api-v4'; export const linode: PartialEventMap<'linode'> = { linode_addip: { @@ -488,7 +484,12 @@ export const linode: PartialEventMap<'linode'> = { resizing. ), - started: (e) => , + started: (e) => ( + <> + Linode is resizing{' '} + to the selected plan. + + ), }, linode_resize_create: { notification: (e) => ( @@ -571,26 +572,3 @@ export const linode: PartialEventMap<'linode'> = { ), }, }; - -const LinodeResizeStartedMessage = ({ event }: { event: Event }) => { - const { data: linode } = useLinodeQuery(event.entity?.id ?? -1); - const type = useTypeQuery(linode?.type ?? ''); - - return ( - <> - Linode is{' '} - resizing - {type && ( - <> - {' '} - to the{' '} - {type.data?.label && ( - {formatStorageUnits(type.data.label)} - )}{' '} - Plan - - )} - . - - ); -}; From ae275e17cd470ce7971daeb60a4246c6fab83a6c Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 21 May 2025 12:14:28 -0400 Subject: [PATCH 26/59] upcoming: [M3-9999] - Event message tweaks for Linode Interfaces (#12243) * update events and add dev warning * do some event message factory clean up * Added changeset: Event message tweaks for Linode Interfaces * add a basic event handler --------- Co-authored-by: Banks Nussman --- ...r-12243-upcoming-features-1747670800164.md | 5 ++ .../Snackbar/ToastNotifications.stories.tsx | 27 +++++---- .../features/Events/factories/interface.tsx | 59 ++----------------- .../src/features/Events/utils.test.tsx | 20 ++++--- .../manager/src/features/Events/utils.tsx | 49 +++++++-------- .../manager/src/hooks/useEventHandlers.ts | 5 ++ .../manager/src/queries/linodes/events.ts | 14 +++++ 7 files changed, 79 insertions(+), 100 deletions(-) create mode 100644 packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md diff --git a/packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md b/packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md new file mode 100644 index 00000000000..f650f6fef4b --- /dev/null +++ b/packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Event message tweaks for Linode Interfaces ([#12243](https://github.com/linode/manager/pull/12243)) diff --git a/packages/manager/src/components/Snackbar/ToastNotifications.stories.tsx b/packages/manager/src/components/Snackbar/ToastNotifications.stories.tsx index ca429c7a94a..b6c97cc3ba5 100644 --- a/packages/manager/src/components/Snackbar/ToastNotifications.stories.tsx +++ b/packages/manager/src/components/Snackbar/ToastNotifications.stories.tsx @@ -3,6 +3,7 @@ import { useSnackbar } from 'notistack'; import React from 'react'; import { Snackbar } from 'src/components/Snackbar/Snackbar'; +import { eventFactory } from 'src/factories'; import { getEventMessage } from 'src/features/Events/utils'; import type { Meta, StoryObj } from '@storybook/react'; @@ -97,18 +98,20 @@ export const WithEventMessage: Story = { render: (args) => { const WithEventMessage = () => { const { enqueueSnackbar } = useSnackbar(); - const message = getEventMessage({ - action: 'placement_group_assign', - entity: { - label: 'Entity', - url: 'https://google.com', - }, - secondary_entity: { - label: 'Secondary Entity', - url: 'https://google.com', - }, - status: 'notification', - }); + const message = getEventMessage( + eventFactory.build({ + action: 'placement_group_assign', + entity: { + label: 'Entity', + url: 'https://google.com', + }, + secondary_entity: { + label: 'Secondary Entity', + url: 'https://google.com', + }, + status: 'notification', + }) + ); return ( From 017bb889291ff468663997fa0e550bcf3019d8e1 Mon Sep 17 00:00:00 2001 From: carrillo-erik <119514965+carrillo-erik@users.noreply.github.com> Date: Wed, 21 May 2025 14:31:01 -0700 Subject: [PATCH 31/59] refactor: [M3-9656] - [Akamai Design System] Label Component (#12224) * refactor: [M3-9652] - [Akamai Design System] Label Component * Add changeset * PR feedback to use icon tokens for various states --- .../pr-12224-changed-1747294477261.md | 5 ++ .../TextField/TextField.stories.tsx | 30 ++++++++++ .../ui/src/components/TextField/TextField.tsx | 57 ++++++++++++++++--- .../TooltipIcon/TooltipIcon.stories.tsx | 18 ++++++ .../components/TooltipIcon/TooltipIcon.tsx | 21 ++++--- packages/ui/src/foundations/themes/dark.ts | 8 +-- packages/ui/src/foundations/themes/light.ts | 8 +-- 7 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 packages/ui/.changeset/pr-12224-changed-1747294477261.md diff --git a/packages/ui/.changeset/pr-12224-changed-1747294477261.md b/packages/ui/.changeset/pr-12224-changed-1747294477261.md new file mode 100644 index 00000000000..caa83585785 --- /dev/null +++ b/packages/ui/.changeset/pr-12224-changed-1747294477261.md @@ -0,0 +1,5 @@ +--- +"@linode/ui": Changed +--- + +Akamai Design System - Label component ([#12224](https://github.com/linode/manager/pull/12224)) diff --git a/packages/ui/src/components/TextField/TextField.stories.tsx b/packages/ui/src/components/TextField/TextField.stories.tsx index eae1adca573..ae3a9f00ffa 100644 --- a/packages/ui/src/components/TextField/TextField.stories.tsx +++ b/packages/ui/src/components/TextField/TextField.stories.tsx @@ -98,6 +98,36 @@ export const WithTooltip: Story = { }, }; +export const WithTooltipIconLeft: Story = { + args: { + label: 'Label', + labelTooltipText: 'Tooltip Text', + noMarginTop: true, + placeholder: 'Placeholder', + labelTooltipIconPosition: 'left', + }, +}; + +export const WithTooltipSmall: Story = { + args: { + label: 'Label', + labelTooltipText: 'Tooltip Text', + noMarginTop: true, + placeholder: 'Placeholder', + labelTooltipIconSize: 'small', + }, +}; + +export const WithTooltipLarge: Story = { + args: { + label: 'Label', + labelTooltipText: 'Tooltip Text', + noMarginTop: true, + placeholder: 'Placeholder', + labelTooltipIconSize: 'large', + }, +}; + export const WithAdornment: Story = { args: { InputProps: { diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index c9ee4281a3a..46b8999bea5 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -103,6 +103,16 @@ interface BaseProps { type Value = null | number | string | undefined; interface LabelToolTipProps { + /** + * Position of the tooltip icon + * @default right + */ + labelTooltipIconPosition?: 'left' | 'right'; + /** + * Size of the tooltip icon + * @default small + */ + labelTooltipIconSize?: 'large' | 'small'; labelTooltipText?: JSX.Element | string; } @@ -147,6 +157,8 @@ export const TextField = (props: TextFieldProps) => { inputProps, label, labelTooltipText, + labelTooltipIconPosition = 'right', + labelTooltipIconSize = 'small', loading, max, min, @@ -170,6 +182,26 @@ export const TextField = (props: TextFieldProps) => { const [_value, setValue] = React.useState(value ?? ''); const theme = useTheme(); + const sxTooltipIconLeft = { + marginRight: `${theme.spacingFunction(4)}`, + padding: `${theme.spacingFunction(4)} ${theme.spacingFunction(4)} ${theme.spacingFunction(4)} ${theme.spacingFunction(2)}`, + '&& svg': { + fill: theme.tokens.component.Label.Icon, + stroke: theme.tokens.component.Label.Icon, + strokeWidth: 0, + ':hover': { + color: theme.tokens.alias.Content.Icon.Primary.Hover, + fill: theme.tokens.alias.Content.Icon.Primary.Hover, + stroke: theme.tokens.alias.Content.Icon.Primary.Hover, + }, + }, + }; + + const sxTooltipIconRight = { + marginLeft: `${theme.spacingFunction(4)}`, + padding: `${theme.spacingFunction(4)}`, + }; + const { errorScrollClassName, errorTextId, helperTextId, validInputId } = useFieldIds({ errorGroup, hasError: Boolean(errorText), inputId, label }); @@ -254,9 +286,7 @@ export const TextField = (props: TextFieldProps) => { return ( { ...(!noMarginTop && { marginTop: theme.spacing(2) }), }} > + {labelTooltipText && labelTooltipIconPosition === 'left' && ( + + )} @@ -293,13 +336,11 @@ export const TextField = (props: TextFieldProps) => { )} - {labelTooltipText && ( + {labelTooltipText && labelTooltipIconPosition === 'right' && ( diff --git a/packages/ui/src/components/TooltipIcon/TooltipIcon.stories.tsx b/packages/ui/src/components/TooltipIcon/TooltipIcon.stories.tsx index 211c9308c76..4c5ce121d2f 100644 --- a/packages/ui/src/components/TooltipIcon/TooltipIcon.stories.tsx +++ b/packages/ui/src/components/TooltipIcon/TooltipIcon.stories.tsx @@ -28,4 +28,22 @@ export const VariableWidth: Story = { render: (args) => , }; +export const SmallTooltipIcon: Story = { + args: { + status: 'help', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + labelTooltipIconSize: 'small', + }, + render: (args) => , +}; + +export const LargeTooltipIcon: Story = { + args: { + status: 'help', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + labelTooltipIconSize: 'large', + }, + render: (args) => , +}; + export default meta; diff --git a/packages/ui/src/components/TooltipIcon/TooltipIcon.tsx b/packages/ui/src/components/TooltipIcon/TooltipIcon.tsx index c757d379c65..23317c2d2c7 100644 --- a/packages/ui/src/components/TooltipIcon/TooltipIcon.tsx +++ b/packages/ui/src/components/TooltipIcon/TooltipIcon.tsx @@ -40,6 +40,11 @@ export interface TooltipIconProps * @todo this seems like a flaw... passing an icon should not require `status` to be `other` */ icon?: JSX.Element; + /** + * Size of the tooltip icon + * @default small + */ + labelTooltipIconSize?: 'large' | 'small'; /** * Enables a leaveDelay of 3000ms * @default false @@ -100,6 +105,7 @@ export const TooltipIcon = (props: TooltipIconProps) => { tooltipAnalyticsEvent, tooltipPosition, width, + labelTooltipIconSize, } = props; const handleOpenTooltip = () => { @@ -112,18 +118,17 @@ export const TooltipIcon = (props: TooltipIconProps) => { const sxRootStyle = { '&&': { - fill: theme.tokens.color.Neutrals[50], - stroke: theme.tokens.color.Neutrals[50], + fill: theme.tokens.component.Label.InfoIcon, + stroke: theme.tokens.component.Label.InfoIcon, strokeWidth: 0, }, '&:hover': { - color: theme.palette.primary.main, - fill: theme.palette.primary.main, - stroke: theme.palette.primary.main, + color: theme.tokens.alias.Content.Icon.Primary.Hover, + fill: theme.tokens.alias.Content.Icon.Primary.Hover, + stroke: theme.tokens.alias.Content.Icon.Primary.Hover, }, - color: theme.tokens.color.Neutrals[50], - height: 20, - width: 20, + height: labelTooltipIconSize === 'small' ? 16 : 20, + width: labelTooltipIconSize === 'small' ? 16 : 20, }; switch (status) { diff --git a/packages/ui/src/foundations/themes/dark.ts b/packages/ui/src/foundations/themes/dark.ts index f9f95453dcf..38bce07501a 100644 --- a/packages/ui/src/foundations/themes/dark.ts +++ b/packages/ui/src/foundations/themes/dark.ts @@ -553,15 +553,15 @@ export const darkTheme: ThemeOptions = { styleOverrides: { root: { '&$disabled': { - color: Color.Neutrals[40], + color: Component.Label.Text, }, '&$error': { - color: Color.Neutrals[40], + color: Component.Label.Text, }, '&.Mui-focused': { - color: Color.Neutrals[40], + color: Component.Label.Text, }, - color: Color.Neutrals[40], + color: Component.Label.Text, }, }, }, diff --git a/packages/ui/src/foundations/themes/light.ts b/packages/ui/src/foundations/themes/light.ts index 47f727cd72e..e7b2e717c56 100644 --- a/packages/ui/src/foundations/themes/light.ts +++ b/packages/ui/src/foundations/themes/light.ts @@ -877,16 +877,16 @@ export const lightTheme: ThemeOptions = { styleOverrides: { root: { '&$disabled': { - color: Color.Neutrals[70], + color: Component.Label.Text, opacity: 0.5, }, '&$error': { - color: Color.Neutrals[70], + color: Component.Label.Text, }, '&.Mui-focused': { - color: Color.Neutrals[70], + color: Component.Label.Text, }, - color: Color.Neutrals[70], + color: Component.Label.Text, font: Typography.Body.Bold, marginBottom: 8, }, From 2163ffef0b4d9c6b7e9a872dd1290772066a72f5 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Thu, 22 May 2025 09:12:24 -0400 Subject: [PATCH 32/59] tech-story: [M3-10017] - Reroute /search (#12258) * Reroute /search * Add missing comment * Added changeset: Reroute search feature --- .../pr-12258-tech-stories-1747847887739.md | 5 +++++ packages/manager/eslint.config.js | 3 +++ packages/manager/src/MainContent.tsx | 7 ------- packages/manager/src/components/Tag/Tag.tsx | 1 + .../src/features/Search/SearchLanding.tsx | 21 +++++++------------ .../features/TopMenu/SearchBar/SearchBar.tsx | 1 + .../TopMenu/SearchBar/SearchSuggestion.tsx | 1 + packages/manager/src/routes/index.tsx | 1 + packages/manager/src/routes/search/index.ts | 9 +++++--- .../src/routes/search/searchLazyRoutes.ts | 7 +++++++ 10 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md create mode 100644 packages/manager/src/routes/search/searchLazyRoutes.ts diff --git a/packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md b/packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md new file mode 100644 index 00000000000..40a3fb0767d --- /dev/null +++ b/packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Reroute search feature ([#12258](https://github.com/linode/manager/pull/12258)) diff --git a/packages/manager/eslint.config.js b/packages/manager/eslint.config.js index e1a650d46a9..a2b7651e04b 100644 --- a/packages/manager/eslint.config.js +++ b/packages/manager/eslint.config.js @@ -411,6 +411,9 @@ export const baseConfig = [ 'src/features/NodeBalancers/**/*', 'src/features/ObjectStorage/**/*', 'src/features/PlacementGroups/**/*', + 'src/features/Search/**/*', + 'src/features/TopMenu/SearchBar/**/*', + 'src/components/Tag/**/*', 'src/features/StackScripts/**/*', 'src/features/Volumes/**/*', 'src/features/VPCs/**/*', diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index a3d0a793e1b..768d7599f7b 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -144,9 +144,6 @@ const Help = React.lazy(() => default: module.HelpAndSupport, })) ); -const SearchLanding = React.lazy( - () => import('src/features/Search/SearchLanding') -); const EventsLanding = React.lazy(() => import('src/features/Events/EventsLanding').then((module) => ({ default: module.EventsLanding, @@ -375,10 +372,6 @@ export const MainContent = () => { - { - const location = useLocation(); - const query = getQueryParamFromQueryString(location.search, 'query'); +export const SearchLanding = () => { + const { query } = useSearch({ + from: '/search', + }); const { combinedResults, entityErrors, isLoading, searchResultsByEntity } = - useSearch({ + useCMSearch({ query, }); @@ -62,9 +61,3 @@ const SearchLanding = () => { ); }; - -export const searchLandingLazyRoute = createLazyRoute('/search')({ - component: SearchLanding, -}); - -export default SearchLanding; diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx index 9054d716b44..d65bb51fb97 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx @@ -9,6 +9,7 @@ import { import { getQueryParamsFromQueryString } from '@linode/utilities'; import { useMediaQuery, useTheme } from '@mui/material'; import React from 'react'; +// eslint-disable-next-line no-restricted-imports import { useHistory } from 'react-router-dom'; import Search from 'src/assets/icons/search.svg'; diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchSuggestion.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchSuggestion.tsx index b9b1ac07a20..ed03be68b4a 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchSuggestion.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchSuggestion.tsx @@ -1,5 +1,6 @@ import { Box, Chip, SvgIcon } from '@linode/ui'; import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports import { useHistory } from 'react-router-dom'; import { searchableEntityIconMap } from 'src/features/Search/utils'; diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index cf725715d52..5cf6170bd24 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -100,6 +100,7 @@ export const migrationRouteTree = migrationRootRoute.addChildren([ nodeBalancersRouteTree, objectStorageRouteTree, placementGroupsRouteTree, + searchRouteTree, stackScriptsRouteTree, volumesRouteTree, vpcsRouteTree, diff --git a/packages/manager/src/routes/search/index.ts b/packages/manager/src/routes/search/index.ts index 5e2119ab625..949ee8b007e 100644 --- a/packages/manager/src/routes/search/index.ts +++ b/packages/manager/src/routes/search/index.ts @@ -3,19 +3,22 @@ import { createRoute } from '@tanstack/react-router'; import { rootRoute } from '../root'; import { SearchRoute } from './SearchRoute'; +type SearchSearchParams = { + query: string; +}; + const searchRoute = createRoute({ component: SearchRoute, getParentRoute: () => rootRoute, path: 'search', + validateSearch: (search: SearchSearchParams) => search, }); const searchLandingRoute = createRoute({ getParentRoute: () => searchRoute, path: '/', }).lazy(() => - import('src/features/Search/SearchLanding').then( - (m) => m.searchLandingLazyRoute - ) + import('./searchLazyRoutes').then((m) => m.searchLandingLazyRoute) ); export const searchRouteTree = searchRoute.addChildren([searchLandingRoute]); diff --git a/packages/manager/src/routes/search/searchLazyRoutes.ts b/packages/manager/src/routes/search/searchLazyRoutes.ts new file mode 100644 index 00000000000..98e89c9024d --- /dev/null +++ b/packages/manager/src/routes/search/searchLazyRoutes.ts @@ -0,0 +1,7 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import { SearchLanding } from 'src/features/Search/SearchLanding'; + +export const searchLandingLazyRoute = createLazyRoute('/search')({ + component: SearchLanding, +}); From 00ef4203aa840fd399565b7bd5a5cb0216342bdb Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 22 May 2025 08:32:12 -0700 Subject: [PATCH 33/59] change: [M3-9997] - Update estimated time for LKE-E node pool pending creation message (#12251) * Update copy and util to increase the time to 20 minutes * Update test * Update the actual interval to 20 minutes too, whoops * Added changeset: Update estimated time for LKE-E node pool pending creation message --- .../pr-12251-upcoming-features-1747763494658.md | 5 +++++ .../NodePoolsDisplay/NodeTable.test.tsx | 4 ++-- .../NodePoolsDisplay/NodeTable.tsx | 12 ++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md diff --git a/packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md b/packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md new file mode 100644 index 00000000000..c03060d6768 --- /dev/null +++ b/packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update estimated time for LKE-E node pool pending creation message ([#12251](https://github.com/linode/manager/pull/12251)) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx index 31e5eb46228..b961ff4e4d5 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx @@ -91,7 +91,7 @@ describe('NodeTable', () => { getByText('Pool ID 1'); }); - it('displays a provisioning message if the cluster was created within the first 10 mins and there are no nodes yet', async () => { + it('displays a provisioning message if the cluster was created within the first 20 mins and there are no nodes yet', async () => { const clusterProps = { ...props, clusterCreated: DateTime.local().toISO(), @@ -108,7 +108,7 @@ describe('NodeTable', () => { ).toBeVisible(); expect( - await findByText('Provisioning can take up to 10 minutes.') + await findByText('Provisioning can take up to ~20 minutes.') ).toBeVisible(); }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx index d9e6923fa2b..c162c6ed318 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx @@ -110,9 +110,9 @@ export const NodeTable = React.memo((props: Props) => { }) : null; - // It takes ~5 minutes for LKE-E cluster nodes to be provisioned and we want to explain this to the user + // It takes anywhere between 5-20+ minutes for LKE-E cluster nodes to be provisioned and we want to explain this to the user // since nodes are not returned right away unlike standard LKE - const isEnterpriseClusterWithin10MinsOfCreation = () => { + const isEnterpriseClusterWithin20MinsOfCreation = () => { if (clusterTier !== 'enterprise') { return false; } @@ -121,7 +121,7 @@ export const NodeTable = React.memo((props: Props) => { const interval = Interval.fromDateTimes( createdTime, - createdTime.plus({ minutes: 10 }) + createdTime.plus({ minutes: 20 }) ); const currentTime = DateTime.fromISO(DateTime.now().toISO(), { @@ -188,7 +188,7 @@ export const NodeTable = React.memo((props: Props) => { {rowData.length === 0 && - isEnterpriseClusterWithin10MinsOfCreation() && ( + isEnterpriseClusterWithin20MinsOfCreation() && ( { provisioning is complete. - Provisioning can take up to 10 minutes. + Provisioning can take up to ~20 minutes. } @@ -214,7 +214,7 @@ export const NodeTable = React.memo((props: Props) => { )} {(rowData.length > 0 || - !isEnterpriseClusterWithin10MinsOfCreation()) && ( + !isEnterpriseClusterWithin20MinsOfCreation()) && ( Date: Thu, 22 May 2025 08:32:45 -0700 Subject: [PATCH 34/59] fix: [M3-9940] - Fix newest LKE-E kubernetes version not being selected by default in create flow (#12246) * Update mocks to try to repro * Create sortByTieredVersion util to handle lke suffix correctly * Add sortByTieredVersion test coverage * Switch to using just sortByTieredVersion in getLatestVersion util * Clean up mocks * Clean up old param in JSDocs comment * Improve test coverage * Update the MSW version handlers * Add changeset * Address feedback: improve JSDoc comment * Address feedback: rename function and move it to kubeUtils --- ...r-12246-upcoming-features-1747750319787.md | 5 + .../src/factories/kubernetesCluster.ts | 4 +- .../src/features/Kubernetes/kubeUtils.test.ts | 146 ++++++++++++++++-- .../src/features/Kubernetes/kubeUtils.ts | 75 ++++++++- .../mocks/presets/crud/handlers/kubernetes.ts | 25 ++- packages/utilities/src/helpers/sort-by.ts | 2 +- 6 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md diff --git a/packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md b/packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md new file mode 100644 index 00000000000..923f6501d3a --- /dev/null +++ b/packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix newest LKE-E kubernetes version not being selected by default in create flow ([#12246](https://github.com/linode/manager/pull/12246)) diff --git a/packages/manager/src/factories/kubernetesCluster.ts b/packages/manager/src/factories/kubernetesCluster.ts index 5e81fae6533..f6aa63c4586 100644 --- a/packages/manager/src/factories/kubernetesCluster.ts +++ b/packages/manager/src/factories/kubernetesCluster.ts @@ -110,13 +110,13 @@ export const kubernetesVersionFactory = export const kubernetesStandardTierVersionFactory = Factory.Sync.makeFactory({ - id: Factory.each((id) => `v1.3${id}`), + id: Factory.each((id) => `1.3${id}`), tier: 'standard', }); export const kubernetesEnterpriseTierVersionFactory = Factory.Sync.makeFactory({ - id: Factory.each((id) => `v1.31.${id}+lke1`), + id: Factory.each((id) => `v1.31.${id}+lke${id}`), tier: 'enterprise', }); diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts index e110b4b1f6e..3f5206b23f3 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts @@ -10,6 +10,7 @@ import { import { extendType } from 'src/utilities/extendType'; import { + compareByKubernetesVersion, getLatestVersion, getNextVersion, getTotalClusterMemoryCPUAndStorage, @@ -71,6 +72,106 @@ describe('helper functions', () => { type: 'not-a-real-type', }); + describe('compareByKubernetesVersion', () => { + it('should identify the later standard tier major version as greater', () => { + const result = compareByKubernetesVersion('2.0.0', '1.0.0', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later standard tier minor version as greater', () => { + const result = compareByKubernetesVersion('1.2.0', '1.1.0', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later standard tier patch version as greater', () => { + const result = compareByKubernetesVersion('1.1.2', '1.1.1', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later enterprise tier major version as greater', () => { + const result = compareByKubernetesVersion( + 'v2.0.0+lke1', + 'v1.0.0+lke2', + 'asc' + ); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later enterprise tier minor version as greater', () => { + const result = compareByKubernetesVersion( + 'v1.2.0+lke1', + 'v1.1.0+lke2', + 'asc' + ); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later enterprise tier patch version as greater', () => { + const result = compareByKubernetesVersion( + 'v1.1.2+lke1', + 'v1.1.1+lke1', + 'asc' + ); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the enterprise tier patch version with the later enterprise release version as greater', () => { + const result = compareByKubernetesVersion( + 'v1.1.1+lke2', + 'v1.1.1+lke1', + 'asc' + ); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later standard tier minor version with differing number of digits', () => { + const result = compareByKubernetesVersion('1.30', '1.3', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should return negative when the first version is earlier in ascending order with standard tier versions', () => { + const result = compareByKubernetesVersion('1.0.0', '2.0.0', 'asc'); + expect(result).toBeLessThan(0); + }); + + it('should return positive when the first version is earlier in descending order with standard tier versions', () => { + const result = compareByKubernetesVersion('1.0.0', '2.0.0', 'desc'); + expect(result).toBeGreaterThan(0); + }); + + it('should return negative when the first version is earlier in ascending order with enterprise tier versions', () => { + const result = compareByKubernetesVersion( + 'v1.0.0+lke1', + 'v2.0.0+lke1', + 'asc' + ); + expect(result).toBeLessThan(0); + }); + + it('should return positive when the first version is earlier in descending order with enterprise tier versions', () => { + const result = compareByKubernetesVersion( + 'v1.0.0+lke1', + 'v2.0.0+lke1', + 'desc' + ); + expect(result).toBeGreaterThan(0); + }); + + it('should return zero when standard tier versions are equal', () => { + const result = compareByKubernetesVersion('1.2.3', '1.2.3', 'asc'); + expect(result).toEqual(0); + }); + + it('should return zero when enterprise tier versions are equal', () => { + const result = compareByKubernetesVersion( + 'v1.2.3+lke1', + 'v1.2.3+lke1', + 'asc' + ); + expect(result).toEqual(0); + }); + }); + describe('Get total cluster memory/CPUs', () => { const pools = nodePoolFactory.buildList(3, { nodes: kubeLinodeFactory.buildList(3), @@ -144,7 +245,7 @@ describe('helper functions', () => { }); describe('getLatestVersion', () => { - it('should return the correct latest version from a list of versions', () => { + it('should return the correct latest version from a list of versions in asc order', () => { const versions = [ { label: '1.00', value: '1.00' }, { label: '1.10', value: '1.10' }, @@ -154,14 +255,36 @@ describe('helper functions', () => { expect(result).toEqual({ label: '2.00', value: '2.00' }); }); - it('should return the correct latest version from a list of enterprise versions', () => { + it('should return the correct latest version from a list of versions in desc order', () => { + const versions = [ + { label: '2.00', value: '2.00' }, + { label: '1.10', value: '1.10' }, + { label: '1.00', value: '1.00' }, + ]; + const result = getLatestVersion(versions); + expect(result).toEqual({ label: '2.00', value: '2.00' }); + }); + + it('should return the correct latest version from a list of enterprise versions in asc order', () => { + const enterpriseVersions = [ + { label: 'v1.31.1+lke4', value: 'v1.31.1+lke4' }, + { label: 'v1.31.6+lke2', value: 'v1.31.6+lke2' }, + { label: 'v1.31.6+lke3', value: 'v1.31.6+lke3' }, + { label: 'v1.31.8+lke1', value: 'v1.31.8+lke1' }, + ]; + const result = getLatestVersion(enterpriseVersions); + expect(result).toEqual({ label: 'v1.31.8+lke1', value: 'v1.31.8+lke1' }); + }); + + it('should return the correct latest version from a list of enterprise versions in desc order', () => { const enterpriseVersions = [ - { label: '1.31.1+lke1', value: '1.31.1+lke1' }, - { label: '1.31.1+lke2', value: '1.31.1+lke2' }, - { label: '1.32.1+lke1', value: '1.32.1+lke1' }, + { label: 'v1.31.8+lke1', value: 'v1.31.8+lke1' }, + { label: 'v1.31.6+lke3', value: 'v1.31.6+lke3' }, + { label: 'v1.31.6+lke2', value: 'v1.31.6+lke2' }, + { label: 'v1.31.1+lke4', value: 'v1.31.1+lke4' }, ]; const result = getLatestVersion(enterpriseVersions); - expect(result).toEqual({ label: '1.32.1+lke1', value: '1.32.1+lke1' }); + expect(result).toEqual({ label: 'v1.31.8+lke1', value: 'v1.31.8+lke1' }); }); it('should handle latest version minor version correctly', () => { @@ -207,14 +330,15 @@ describe('helper functions', () => { it('should get the next version when given a current enterprise version', () => { const versions: KubernetesTieredVersion[] = [ - { id: '1.31.1+lke1', tier: 'enterprise' }, - { id: '1.31.1+lke2', tier: 'enterprise' }, - { id: '1.32.1+lke1', tier: 'enterprise' }, + { id: 'v1.31.1+lke4', tier: 'enterprise' }, + { id: 'v1.31.6+lke2', tier: 'enterprise' }, + { id: 'v1.31.6+lke3', tier: 'enterprise' }, + { id: 'v1.31.8+lke1', tier: 'enterprise' }, ]; - const currentVersion = '1.31.1+lke2'; + const currentVersion = 'v1.31.6+lke2'; const result = getNextVersion(currentVersion, versions); - expect(result).toEqual('1.32.1+lke1'); + expect(result).toEqual('v1.31.6+lke3'); }); it('should get the next version when given an obsolete current version', () => { diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.ts b/packages/manager/src/features/Kubernetes/kubeUtils.ts index 73f57188ad0..953bb118938 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.ts @@ -1,9 +1,5 @@ import { useAccount, useAccountBetaQuery } from '@linode/queries'; -import { - getBetaStatus, - isFeatureEnabledV2, - sortByVersion, -} from '@linode/utilities'; +import { getBetaStatus, isFeatureEnabledV2 } from '@linode/utilities'; import { useFlags } from 'src/hooks/useFlags'; import { @@ -21,12 +17,79 @@ import type { } from '@linode/api-v4/lib/kubernetes'; import type { ExtendedType } from 'src/utilities/extendType'; +type SortOrder = 'asc' | 'desc'; interface ClusterData { CPU: number; RAM: number; Storage: number; } +/** + * Compares two semantic version strings based on the specified order, including with special handling of LKE-Enterprise tier versions. + * + * This function splits each version string into its constituent parts (major, minor, patch), + * compares them numerically, and returns a positive number, zero, or a negative number + * based on the specified sorting order. If components are missing in either version, + * they are treated as zero. + * + * @param {string} a - The first version string to compare. + * @param {string} b - The second version string to compare. + * @param {SortOrder} order - The intended sort direction of the output; 'asc' means lower versions come first, 'desc' means higher versions come first. + * @returns {number} Returns a positive number if version `a` is greater than `b` according to the sort order, + * zero if they are equal, and a negative number if `b` is greater than `a`. + * * @example + * // returns a positive number + * sortByVersion('1.2.3', '1.2.2', 'asc'); + * sortByVersion('v1.2.3+lke1', 'v1.2.2+lke2', 'asc'); + * + * @example + * // returns zero + * sortByVersion('1.2.3', '1.2.3', 'asc'); + * sortByVersion('v1.2.3+lke1', 'v1.2.3+lke1', 'asc'); + * + * @example + * // returns a negative number + * sortByVersion('1.2.3', '1.2.4', 'asc'); + * sortByVersion('v1.2.3+lke1', 'v1.2.4+lke1', 'asc'); + */ +export const compareByKubernetesVersion = ( + a: string, + b: string, + order: SortOrder +): number => { + // For LKE-E versions, remove the 'v' prefix and split the core version (X.X.X) from the enterprise release version (+lkeX). + const aStrippedVersion = a.replace('v', ''); + const bStrippedVersion = b.replace('v', ''); + const [aCoreVersion, aEnterpriseVersion] = aStrippedVersion.split('+'); + const [bCoreVersion, bEnterpriseVersion] = bStrippedVersion.split('+'); + + const aParts = aCoreVersion.split('.'); + const bParts = bCoreVersion.split('.'); + // For LKE-E versions, extract the number from the +lke suffix. + const aEnterpriseVersionNum = + Number(aEnterpriseVersion?.replace(/\D+/g, '')) || 0; + const bEnterpriseVersionNum = + Number(bEnterpriseVersion?.replace(/\D+/g, '')) || 0; + + const result = (() => { + for (let i = 0; i < Math.max(aParts.length, bParts.length); i += 1) { + // If one version has a part and another doesn't (e.g. 3.1 vs 3.1.1), + // treat the missing part as 0. + const aNumber = Number(aParts[i]) || 0; + const bNumber = Number(bParts[i]) || 0; + const diff = aNumber - bNumber; + + if (diff !== 0) { + return diff; + } + } + // If diff is 0, the core versions are the same, so compare the enterprise release version numbers. + return aEnterpriseVersionNum - bEnterpriseVersionNum; + })(); + + return order === 'asc' ? result : -result; +}; + export const getTotalClusterMemoryCPUAndStorage = ( pools: KubeNodePoolResponse[], types: ExtendedType[] @@ -179,7 +242,7 @@ export const getLatestVersion = ( versions: { label: string; value: string }[] ): { label: string; value: string } => { const sortedVersions = versions.sort((a, b) => { - return sortByVersion(a.value, b.value, 'asc'); + return compareByKubernetesVersion(a.value, b.value, 'asc'); }); const latestVersion = sortedVersions.pop(); diff --git a/packages/manager/src/mocks/presets/crud/handlers/kubernetes.ts b/packages/manager/src/mocks/presets/crud/handlers/kubernetes.ts index c1a7d847b49..1d9f0d29fa4 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/kubernetes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/kubernetes.ts @@ -388,6 +388,10 @@ export const getKubernetesVersions = () => [ APIErrorResponse | APIPaginatedResponse > => { const versions = kubernetesVersionFactory.buildList(3); + + // Send the data in this explicit order to match the API. + request.headers.set('X-Filter', JSON.stringify({ '+order': 'desc' })); + return makePaginatedResponse({ data: versions, request, @@ -402,6 +406,9 @@ export const getKubernetesVersions = () => [ }): StrictResponse< APIErrorResponse | APIPaginatedResponse > => { + // Send the data in this explicit order to match the API. + request.headers.set('X-Filter', JSON.stringify({ '+order': 'desc' })); + const versions = kubernetesStandardTierVersionFactory.buildList(3); return makePaginatedResponse({ data: versions, @@ -417,7 +424,23 @@ export const getKubernetesVersions = () => [ }): StrictResponse< APIErrorResponse | APIPaginatedResponse > => { - const versions = kubernetesEnterpriseTierVersionFactory.buildList(3); + const kubeVersion1 = kubernetesEnterpriseTierVersionFactory.build({ + id: 'v1.31.8+lke1', + }); + const kubeVersion2 = kubernetesEnterpriseTierVersionFactory.build({ + id: 'v1.31.6+lke3', + }); + const kubeVersion3 = kubernetesEnterpriseTierVersionFactory.build({ + id: 'v1.31.6+lke2', + }); + const kubeVersion4 = kubernetesEnterpriseTierVersionFactory.build({ + id: 'v1.31.1+lke4', + }); + const versions = [kubeVersion1, kubeVersion2, kubeVersion3, kubeVersion4]; + + // Send the data in this explicit order to match the API. + request.headers.set('X-Filter', JSON.stringify({ '+order': 'desc' })); + return makePaginatedResponse({ data: versions, request, diff --git a/packages/utilities/src/helpers/sort-by.ts b/packages/utilities/src/helpers/sort-by.ts index 410a1b872d5..a37c33f683d 100644 --- a/packages/utilities/src/helpers/sort-by.ts +++ b/packages/utilities/src/helpers/sort-by.ts @@ -47,7 +47,7 @@ export const sortByArrayLength = (a: any[], b: any[], order: SortOrder) => { * * @param {string} a - The first version string to compare. * @param {string} b - The second version string to compare. - * @param {SortOrder} order - The order to sort by, can be 'asc' for ascending or 'desc' for descending. + * @param {SortOrder} order - The intended sort direction of the output; 'asc' means lower versions come first, 'desc' means higher versions come first. * @returns {number} Returns a positive number if version `a` is greater than `b` according to the sort order, * zero if they are equal, and a negative number if `b` is greater than `a`. * From e74a3f11be8a2dbe1fcdb2aa4b1599453b9645d3 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 22 May 2025 11:18:28 -0700 Subject: [PATCH 35/59] fix: [M3-10008] - Fix erroneous Sentry error condition in Adobe Analytics hook (#12265) * Remove check for certain script count * Update comment * Added changeset: Fix erroneous Sentry error in useAdobeAnalytics hook --- .../pr-12265-tech-stories-1747931205927.md | 5 +++++ packages/manager/src/constants.ts | 1 - packages/manager/src/hooks/useAdobeAnalytics.ts | 13 +++---------- 3 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md diff --git a/packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md b/packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md new file mode 100644 index 00000000000..addb44a5fcd --- /dev/null +++ b/packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Fix erroneous Sentry error in useAdobeAnalytics hook ([#12265](https://github.com/linode/manager/pull/12265)) diff --git a/packages/manager/src/constants.ts b/packages/manager/src/constants.ts index 4bcd1d0d871..3f3ff81983d 100644 --- a/packages/manager/src/constants.ts +++ b/packages/manager/src/constants.ts @@ -76,7 +76,6 @@ export const OAUTH_TOKEN_REFRESH_TIMEOUT = LOGIN_SESSION_LIFETIME_MS / 2; /** Adobe Analytics */ export const ADOBE_ANALYTICS_URL = import.meta.env .REACT_APP_ADOBE_ANALYTICS_URL; -export const NUM_ADOBE_SCRIPTS = 3; /** Pendo */ export const PENDO_API_KEY = import.meta.env.REACT_APP_PENDO_API_KEY; diff --git a/packages/manager/src/hooks/useAdobeAnalytics.ts b/packages/manager/src/hooks/useAdobeAnalytics.ts index 666e1483b72..607ba914feb 100644 --- a/packages/manager/src/hooks/useAdobeAnalytics.ts +++ b/packages/manager/src/hooks/useAdobeAnalytics.ts @@ -2,7 +2,7 @@ import { loadScript } from '@linode/utilities'; // `loadScript` from `useScript` import React from 'react'; import { useHistory } from 'react-router-dom'; -import { ADOBE_ANALYTICS_URL, NUM_ADOBE_SCRIPTS } from 'src/constants'; +import { ADOBE_ANALYTICS_URL } from 'src/constants'; import { reportException } from 'src/exceptionReporting'; /** @@ -16,15 +16,8 @@ export const useAdobeAnalytics = () => { if (ADOBE_ANALYTICS_URL) { loadScript(ADOBE_ANALYTICS_URL, { location: 'head' }) .then((data) => { - const adobeScriptTags = document.querySelectorAll( - 'script[src^="https://assets.adobedtm.com/"]' - ); - // Log an error; if the promise resolved, the _satellite object and 3 Adobe scripts should be present in the DOM. - if ( - data.status !== 'ready' || - !window._satellite || - adobeScriptTags.length !== NUM_ADOBE_SCRIPTS - ) { + // Log a Sentry error if the Launch script isn't ready or the _satellite object isn't present in the DOM. + if (data.status !== 'ready' || !window._satellite) { reportException( 'Adobe Analytics error: Not all Adobe Launch scripts and extensions were loaded correctly; analytics cannot be sent.' ); From 9de38986fba5515c8ce289b2aa25e34f029bf36b Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 22 May 2025 13:33:21 -0700 Subject: [PATCH 36/59] fix: [M3-10011] - Disable the Kubernetes Dashboard request for LKE-E clusters (#12266) * Disable the dashboard request for enterprise tier clusters * Add comment * Added changeset: Disable the Kubernetes Dashboard request for LKE-E clusters * Fix test failures by removing mocked dashboard call for LKE-E clusters --- .../pr-12266-upcoming-features-1747938663753.md | 5 +++++ .../cypress/e2e/core/kubernetes/lke-create.spec.ts | 6 +----- .../KubernetesClusterDetail/KubeSummaryPanel.tsx | 3 ++- packages/manager/src/queries/kubernetes.ts | 12 ++++++++---- 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md diff --git a/packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md b/packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md new file mode 100644 index 00000000000..60825e38963 --- /dev/null +++ b/packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Disable the Kubernetes Dashboard request for LKE-E clusters ([#12266](https://github.com/linode/manager/pull/12266)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 9f165ac78a1..3275b515699 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -958,7 +958,7 @@ describe('LKE Cluster Creation with ACL', () => { * - Confirms at least one IP must be provided for ACL unless acknowledgement is checked * - Confirms the cluster details page shows ACL is enabled */ - it('creates an LKE cluster with ACL enabled by default and handles IP address validation', () => { + it('creates an LKE-E cluster with ACL enabled by default and handles IP address validation', () => { const clusterLabel = randomLabel(); const mockedEnterpriseCluster = kubernetesClusterFactory.build({ k8s_version: latestEnterpriseTierKubernetesVersion.id, @@ -1012,7 +1012,6 @@ describe('LKE Cluster Creation with ACL', () => { mockedEnterpriseCluster.id, mockedEnterpriseClusterPools ).as('getClusterPools'); - mockGetDashboardUrl(mockedEnterpriseCluster.id).as('getDashboardUrl'); mockGetApiEndpoints(mockedEnterpriseCluster.id).as('getApiEndpoints'); cy.visitWithLogin('/kubernetes/clusters'); @@ -1152,7 +1151,6 @@ describe('LKE Cluster Creation with ACL', () => { '@createCluster', '@getLKEEnterpriseClusterTypes', '@getLinodeTypes', - '@getDashboardUrl', '@getApiEndpoints', '@getControlPlaneACL', ]); @@ -1409,7 +1407,6 @@ describe('LKE Cluster Creation with LKE-E', () => { mockedEnterpriseCluster.id, mockedEnterpriseClusterPools ).as('getClusterPools'); - mockGetDashboardUrl(mockedEnterpriseCluster.id).as('getDashboardUrl'); mockGetApiEndpoints(mockedEnterpriseCluster.id).as('getApiEndpoints'); cy.visitWithLogin('/kubernetes/clusters'); @@ -1594,7 +1591,6 @@ describe('LKE Cluster Creation with LKE-E', () => { '@createCluster', '@getLKEEnterpriseClusterTypes', '@getLinodeTypes', - '@getDashboardUrl', '@getApiEndpoints', '@getControlPlaneACL', ]); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index aff825d5453..0981cbe86f8 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -54,8 +54,9 @@ export const KubeSummaryPanel = React.memo((props: Props) => { React.useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); + // Access to the Kubernetes Dashboard is not supported for LKE-E clusters. const { data: dashboard, error: dashboardError } = - useKubernetesDashboardQuery(cluster.id); + useKubernetesDashboardQuery(cluster.id, cluster.tier !== 'enterprise'); const { error: resetKubeConfigError, diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index fd0c71d44ca..312e596c91a 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -506,10 +506,14 @@ export const useAllKubernetesNodePoolQuery = ( }); }; -export const useKubernetesDashboardQuery = (clusterId: number) => { - return useQuery( - kubernetesQueries.cluster(clusterId)._ctx.dashboard - ); +export const useKubernetesDashboardQuery = ( + clusterId: number, + enabled: boolean = true +) => { + return useQuery({ + ...kubernetesQueries.cluster(clusterId)._ctx.dashboard, + enabled, + }); }; export const useKubernetesVersionQuery = () => From 74d85ad157189c0ba31fb1dff126b73170ba9a95 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Fri, 23 May 2025 10:24:49 -0400 Subject: [PATCH 37/59] chore: [M3-10036] - Re-add `eslint-plugin-react-refresh` eslint plugin (#12267) * re-add plugin and test in a few places * Added changeset: Re-add `eslint-plugin-react-refresh` eslint plugin --------- Co-authored-by: Banks Nussman --- package.json | 1 + .../pr-12267-tech-stories-1747942707347.md | 5 +++++ packages/manager/eslint.config.js | 6 ++++-- packages/manager/src/components/Avatar/Avatar.tsx | 2 +- .../LinodesDetail/LinodeStorage/LinodeDisks.tsx | 5 +---- .../LinodesDetailHeader/MutationNotification.tsx | 2 +- .../src/features/Linodes/LinodesDetail/utilities.ts | 6 ++++++ .../features/Linodes/MigrateLinode/MigrateLinode.tsx | 2 +- pnpm-lock.yaml | 12 ++++++++++++ 9 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md diff --git a/package.json b/package.json index b60be5abe3c..3c6edaaa5ef 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "eslint-plugin-prettier": "~5.2.6", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "0.4.20", "eslint-plugin-sonarjs": "^3.0.2", "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-xss": "^0.1.12", diff --git a/packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md b/packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md new file mode 100644 index 00000000000..eac0ae4b063 --- /dev/null +++ b/packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Re-add `eslint-plugin-react-refresh` eslint plugin ([#12267](https://github.com/linode/manager/pull/12267)) diff --git a/packages/manager/eslint.config.js b/packages/manager/eslint.config.js index a2b7651e04b..54f95455e54 100644 --- a/packages/manager/eslint.config.js +++ b/packages/manager/eslint.config.js @@ -9,6 +9,7 @@ import perfectionist from 'eslint-plugin-perfectionist'; import prettier from 'eslint-plugin-prettier'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; import sonarjs from 'eslint-plugin-sonarjs'; import testingLibrary from 'eslint-plugin-testing-library'; import xss from 'eslint-plugin-xss'; @@ -18,7 +19,6 @@ import tseslint from 'typescript-eslint'; // Shared import restrictions between different rule contexts const restrictedImportPaths = [ - 'rxjs', '@mui/core', '@mui/system', '@mui/icons-material', @@ -136,11 +136,12 @@ export const baseConfig = [ }, }, - // 5. React and React Hooks + // 5. React, React Hooks, and React Refresh { files: ['**/*.{ts,tsx}'], plugins: { react, + 'react-refresh': reactRefresh, }, rules: { 'react-hooks/exhaustive-deps': 'warn', @@ -152,6 +153,7 @@ export const baseConfig = [ 'react/no-unescaped-entities': 'warn', 'react/prop-types': 'off', 'react/self-closing-comp': 'warn', + 'react-refresh/only-export-components': 'warn', // @todo make this error once we fix all occurrences }, }, diff --git a/packages/manager/src/components/Avatar/Avatar.tsx b/packages/manager/src/components/Avatar/Avatar.tsx index e4faf58a24f..e047b6096d0 100644 --- a/packages/manager/src/components/Avatar/Avatar.tsx +++ b/packages/manager/src/components/Avatar/Avatar.tsx @@ -8,7 +8,7 @@ import AkamaiWave from 'src/assets/logo/akamai-wave.svg'; import type { SxProps, Theme } from '@mui/material'; -export const DEFAULT_AVATAR_SIZE = 28; +const DEFAULT_AVATAR_SIZE = 28; export interface AvatarProps { /** diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx index 438ece340e3..5bf2f420c30 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.tsx @@ -23,6 +23,7 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading' import { TableSortCell } from 'src/components/TableSortCell'; import { sendEvent } from 'src/utilities/analytics/utils'; +import { addUsedDiskSpace } from '../utilities'; import { CreateDiskDrawer } from './CreateDiskDrawer'; import { DeleteDiskDialog } from './DeleteDiskDialog'; import { LinodeDiskRow } from './LinodeDiskRow'; @@ -238,7 +239,3 @@ export const LinodeDisks = () => { ); }; - -export const addUsedDiskSpace = (disks: Disk[]) => { - return disks.reduce((accum, eachDisk) => eachDisk.size + accum, 0); -}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx index 5542367bc4c..5c2bdd45322 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx @@ -12,8 +12,8 @@ import { MBpsIntraDC } from 'src/constants'; import { useEventsPollingActions } from 'src/queries/events/events'; import { useTypeQuery } from 'src/queries/types'; -import { addUsedDiskSpace } from '../LinodeStorage/LinodeDisks'; import { MutateDrawer } from '../MutateDrawer/MutateDrawer'; +import { addUsedDiskSpace } from '../utilities'; interface Props { linodeId: number; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/utilities.ts b/packages/manager/src/features/Linodes/LinodesDetail/utilities.ts index 82d11628385..d032e2c4935 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodesDetail/utilities.ts @@ -1,3 +1,5 @@ +import type { Disk } from '@linode/api-v4'; + export const sshLink = (ipv4: string) => { return `ssh root@${ipv4}`; }; @@ -19,3 +21,7 @@ export const getSelectedDeviceOption = ( } return optionList.find((option) => option.value === selectedValue) || null; }; + +export const addUsedDiskSpace = (disks: Disk[]) => { + return disks.reduce((accum, eachDisk) => eachDisk.size + accum, 0); +}; diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index 26a909ea2f3..9b9cc62a741 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -43,7 +43,7 @@ import { getGDPRDetails } from 'src/utilities/formatRegion'; import { getLinodeDescription } from 'src/utilities/getLinodeDescription'; import { reportAgreementSigningError } from 'src/utilities/reportAgreementSigningError'; -import { addUsedDiskSpace } from '../LinodesDetail/LinodeStorage/LinodeDisks'; +import { addUsedDiskSpace } from '../LinodesDetail/utilities'; import { CautionNotice } from './CautionNotice'; import { ConfigureForm } from './ConfigureForm'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1eefb2c2f7d..103fb1a4a0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: eslint-plugin-react-hooks: specifier: ^5.2.0 version: 5.2.0(eslint@9.23.0(jiti@2.4.2)) + eslint-plugin-react-refresh: + specifier: 0.4.20 + version: 0.4.20(eslint@9.23.0(jiti@2.4.2)) eslint-plugin-sonarjs: specifier: ^3.0.2 version: 3.0.2(eslint@9.23.0(jiti@2.4.2)) @@ -3877,6 +3880,11 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + eslint-plugin-react@7.37.4: resolution: {integrity: sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==} engines: {node: '>=4'} @@ -10165,6 +10173,10 @@ snapshots: dependencies: eslint: 9.23.0(jiti@2.4.2) + eslint-plugin-react-refresh@0.4.20(eslint@9.23.0(jiti@2.4.2)): + dependencies: + eslint: 9.23.0(jiti@2.4.2) + eslint-plugin-react@7.37.4(eslint@9.23.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 From 91169a2634cfd84d1f39140caffe948620108bfb Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Fri, 23 May 2025 11:07:56 -0400 Subject: [PATCH 38/59] fix: [M3-100041] - Make `quota_id` type a string (#12272) * change API type to string and update codebase * changesets --- packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md | 5 +++++ packages/api-v4/src/quotas/quotas.ts | 4 ++-- packages/api-v4/src/quotas/types.ts | 2 +- packages/manager/.changeset/pr-12272-fixed-1748009662401.md | 5 +++++ packages/manager/src/factories/quotas.ts | 2 +- packages/manager/src/mocks/presets/crud/handlers/quotas.ts | 4 ++-- packages/queries/src/quotas/keys.ts | 2 +- packages/queries/src/quotas/quotas.ts | 2 +- 8 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md create mode 100644 packages/manager/.changeset/pr-12272-fixed-1748009662401.md diff --git a/packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md b/packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md new file mode 100644 index 00000000000..f8ae3ecb7ed --- /dev/null +++ b/packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Fixed +--- + +Make quota_id a string ([#12272](https://github.com/linode/manager/pull/12272)) diff --git a/packages/api-v4/src/quotas/quotas.ts b/packages/api-v4/src/quotas/quotas.ts index 2f7a7303f2b..25f1ee8f34d 100644 --- a/packages/api-v4/src/quotas/quotas.ts +++ b/packages/api-v4/src/quotas/quotas.ts @@ -45,9 +45,9 @@ export const getQuotas = ( * Returns the usage for a single quota within a particular service specified by `type`. * * @param type { QuotaType } retrieve a quota within this service type. - * @param id { number } the quota ID to look up. + * @param id { string } the quota ID to look up. */ -export const getQuotaUsage = (type: QuotaType, id: number) => +export const getQuotaUsage = (type: QuotaType, id: string) => Request( setURL(`${BETA_API_ROOT}/${type}/quotas/${id}/usage`), setMethod('GET'), diff --git a/packages/api-v4/src/quotas/types.ts b/packages/api-v4/src/quotas/types.ts index 93719ac53c3..f7f9f37d773 100644 --- a/packages/api-v4/src/quotas/types.ts +++ b/packages/api-v4/src/quotas/types.ts @@ -20,7 +20,7 @@ export interface Quota { /** * A unique identifier for the quota. */ - quota_id: number; + quota_id: string; /** * The account-wide limit for this service, measured in units diff --git a/packages/manager/.changeset/pr-12272-fixed-1748009662401.md b/packages/manager/.changeset/pr-12272-fixed-1748009662401.md new file mode 100644 index 00000000000..d15f3149018 --- /dev/null +++ b/packages/manager/.changeset/pr-12272-fixed-1748009662401.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Make quota_id a string ([#12272](https://github.com/linode/manager/pull/12272)) diff --git a/packages/manager/src/factories/quotas.ts b/packages/manager/src/factories/quotas.ts index 3b47cbf82b8..9fdbde146be 100644 --- a/packages/manager/src/factories/quotas.ts +++ b/packages/manager/src/factories/quotas.ts @@ -4,7 +4,7 @@ import type { Quota, QuotaUsage } from '@linode/api-v4/lib/quotas/types'; export const quotaFactory = Factory.Sync.makeFactory({ description: 'Maximimum number of vCPUs allowed', - quota_id: Factory.each((id) => id), + quota_id: Factory.each((id) => id.toString()), quota_limit: 50, quota_name: 'Linode Dedicated vCPUs', region_applied: 'us-east', diff --git a/packages/manager/src/mocks/presets/crud/handlers/quotas.ts b/packages/manager/src/mocks/presets/crud/handlers/quotas.ts index 95366a9c727..7bbcd0ce3b8 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/quotas.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/quotas.ts @@ -169,7 +169,7 @@ export const getQuotas = () => [ '*/v4*/:service/quotas/:id', async ({ params }): Promise> => { const quota = mockQuotas[params.service as QuotaType].find( - ({ quota_id }) => quota_id === +params.id + ({ quota_id }) => quota_id === params.id ); if (!quota) { @@ -187,7 +187,7 @@ export const getQuotas = () => [ }): Promise> => { const service = params.service as QuotaType; const quota = mockQuotas[service].find( - ({ quota_id }) => quota_id === +params.id + ({ quota_id }) => quota_id === params.id ); if (!quota) { diff --git a/packages/queries/src/quotas/keys.ts b/packages/queries/src/quotas/keys.ts index bc77e8e9b6f..bc483878472 100644 --- a/packages/queries/src/quotas/keys.ts +++ b/packages/queries/src/quotas/keys.ts @@ -20,7 +20,7 @@ export const quotaQueries = createQueryKeys('quotas', { queryFn: () => getQuota(type, id), queryKey: [id], }), - usage: (id: number) => ({ + usage: (id: string) => ({ queryFn: () => getQuotaUsage(type, id), queryKey: [id], }), diff --git a/packages/queries/src/quotas/quotas.ts b/packages/queries/src/quotas/quotas.ts index 8571cf341d2..abb92b6e947 100644 --- a/packages/queries/src/quotas/quotas.ts +++ b/packages/queries/src/quotas/quotas.ts @@ -43,7 +43,7 @@ export const useAllQuotasQuery = ( export const useQuotaUsageQuery = ( service: QuotaType, - id: number, + id: string, enabled = true, ) => useQuery({ From 4f3672294054fd2014a67a3582615d70e9091821 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Fri, 23 May 2025 11:53:22 -0400 Subject: [PATCH 39/59] chore: [M3-10007] - Shared TypeScript configuration for packages (#12254) * initial work * do some small clean up * remove something that is enabled by default * make emitting types opt-in rather than the default * clean up command * try a different style comment * comment each tsconfig * fix typo --------- Co-authored-by: Banks Nussman --- packages/api-v4/package.json | 1 + packages/api-v4/tsconfig.json | 12 +---------- packages/manager/package.json | 2 +- packages/manager/tsconfig.json | 1 - packages/queries/package.json | 1 + packages/queries/tsconfig.json | 21 +++++-------------- packages/search/package.json | 6 +++++- packages/search/tsconfig.json | 12 +---------- packages/shared/package.json | 2 +- packages/shared/tsconfig.json | 19 +++++------------ packages/tsconfig/package.json | 12 +++++++++++ packages/tsconfig/tsconfig.emit-types.json | 10 +++++++++ packages/tsconfig/tsconfig.non-strict.json | 14 +++++++++++++ packages/tsconfig/tsconfig.package.json | 16 +++++++++++++++ packages/tsconfig/tsconfig.react.json | 6 ++++++ packages/ui/package.json | 2 +- packages/ui/tsconfig.json | 16 ++++----------- packages/utilities/package.json | 2 +- packages/utilities/tsconfig.json | 16 ++++----------- packages/validation/package.json | 1 + packages/validation/tsconfig.json | 12 +---------- pnpm-lock.yaml | 24 ++++++++++++++++++++++ 22 files changed, 115 insertions(+), 93 deletions(-) create mode 100644 packages/tsconfig/package.json create mode 100644 packages/tsconfig/tsconfig.emit-types.json create mode 100644 packages/tsconfig/tsconfig.non-strict.json create mode 100644 packages/tsconfig/tsconfig.package.json create mode 100644 packages/tsconfig/tsconfig.react.json diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 0a96986b3ae..a8bfff35ce3 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -56,6 +56,7 @@ "lib" ], "devDependencies": { + "@linode/tsconfig": "workspace:*", "axios-mock-adapter": "^1.22.0", "concurrently": "^9.0.1", "tsup": "^8.4.0" diff --git a/packages/api-v4/tsconfig.json b/packages/api-v4/tsconfig.json index 58df0f1dff7..22d69fd4250 100644 --- a/packages/api-v4/tsconfig.json +++ b/packages/api-v4/tsconfig.json @@ -1,18 +1,8 @@ { + "extends": ["@linode/tsconfig/package", "@linode/tsconfig/emit-types"], "compilerOptions": { - "target": "esnext", - "module": "esnext", - "emitDeclarationOnly": true, - "declaration": true, "outDir": "./lib", - "esModuleInterop": true, - "moduleResolution": "bundler", - "skipLibCheck": true, - "strict": true, "baseUrl": ".", - "noUnusedLocals": true, - "declarationMap": true, - "incremental": true }, "include": [ "src" diff --git a/packages/manager/package.json b/packages/manager/package.json index d3031f474d9..e9640c0f39d 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -112,7 +112,7 @@ "cy:component": "cypress open --component", "cy:component:run": "cypress run --component --headless -b chrome", "cy:rec-snap": "cypress run --headless -b chrome --env visualRegMode=record --spec ./cypress/integration/**/*visual*.spec.ts", - "typecheck": "tsc --noEmit && tsc -p cypress --noEmit", + "typecheck": "tsc && tsc -p cypress", "coverage": "vitest run --coverage && open coverage/index.html", "coverage:summary": "vitest run --coverage.enabled --reporter=junit --coverage.reporter=json-summary" }, diff --git a/packages/manager/tsconfig.json b/packages/manager/tsconfig.json index 57e2d52cdbd..2796b3ef2ed 100644 --- a/packages/manager/tsconfig.json +++ b/packages/manager/tsconfig.json @@ -23,7 +23,6 @@ /* Interop Constraints */ "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, /* Type Checking */ "allowUnreachableCode": false, diff --git a/packages/queries/package.json b/packages/queries/package.json index a396c566990..7db5871f40e 100644 --- a/packages/queries/package.json +++ b/packages/queries/package.json @@ -36,6 +36,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@linode/tsconfig": "workspace:*", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "~6.4.2", "@testing-library/react": "~16.0.0", diff --git a/packages/queries/tsconfig.json b/packages/queries/tsconfig.json index f335a47531f..02145053904 100644 --- a/packages/queries/tsconfig.json +++ b/packages/queries/tsconfig.json @@ -1,19 +1,8 @@ { - "compilerOptions": { - "jsx": "react", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "noEmit": true, - "allowUnreachableCode": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUnusedLocals": true, - "strictNullChecks": true, - "forceConsistentCasingInFileNames": true, - "incremental": true - }, + "extends": [ + "@linode/tsconfig/package", + "@linode/tsconfig/react", + "@linode/tsconfig/non-strict" + ], "include": ["src"] } diff --git a/packages/search/package.json b/packages/search/package.json index 46b074abb63..60a62446bc7 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -9,12 +9,16 @@ "license": "Apache-2.0", "scripts": { "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "typecheck": "tsc" }, "dependencies": { "peggy": "^4.0.3", "@linode/api-v4": "workspace:*" }, + "devDependencies": { + "@linode/tsconfig": "workspace:*" + }, "peerDependencies": { "vite": "^6.3.4" } diff --git a/packages/search/tsconfig.json b/packages/search/tsconfig.json index a175d2d5de6..ae1c86a6567 100644 --- a/packages/search/tsconfig.json +++ b/packages/search/tsconfig.json @@ -1,14 +1,4 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "noEmit": true, - "strict": true, - "noUnusedLocals": true, - "forceConsistentCasingInFileNames": true, - "incremental": true - }, + "extends": ["@linode/tsconfig/package"], "include": ["src"] } diff --git a/packages/shared/package.json b/packages/shared/package.json index 6dd969ce11c..3a94fb74f8b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -21,7 +21,6 @@ "typecheck": "tsc", "test": "vitest run", "test:watch": "vitest", - "test:debug": "node --inspect-brk scripts/test.js --runInBand", "precommit": "lint-staged" }, "lint-staged": { @@ -39,6 +38,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@linode/tsconfig": "workspace:*", "@storybook/addon-actions": "^8.6.7", "@storybook/react": "^8.6.7", "@testing-library/dom": "^10.1.0", diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 160560a852f..92b9f1a2b39 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -1,19 +1,10 @@ { + "extends": [ + "@linode/tsconfig/package", + "@linode/tsconfig/react", + "@linode/tsconfig/non-strict" + ], "compilerOptions": { - "jsx": "react", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "noEmit": true, - "allowUnreachableCode": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUnusedLocals": true, - "strictNullChecks": true, - "forceConsistentCasingInFileNames": true, - "incremental": true, "types": ["@testing-library/jest-dom"] }, "include": ["src"] diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json new file mode 100644 index 00000000000..c0157564d08 --- /dev/null +++ b/packages/tsconfig/package.json @@ -0,0 +1,12 @@ +{ + "name": "@linode/tsconfig", + "version": "0.0.0", + "description": "Shared TypeScript configuration for Linode projects", + "keywords": ["TypeScript", "tsconfig", "tsc", "Linode", "Akamai"], + "exports": { + "./package": "./tsconfig.package.json", + "./react": "./tsconfig.react.json", + "./non-strict": "./tsconfig.non-strict.json", + "./emit-types": "./tsconfig.emit-types.json" + } +} diff --git a/packages/tsconfig/tsconfig.emit-types.json b/packages/tsconfig/tsconfig.emit-types.json new file mode 100644 index 00000000000..818c04598aa --- /dev/null +++ b/packages/tsconfig/tsconfig.emit-types.json @@ -0,0 +1,10 @@ +{ + "//": "This tsconfig makes tsc emit type declaration files.", + "//": "Currently, only api-v4 and validation emit types. All other packages don't have a build step.", + "//": "Note: When using this config, specify outDir in the comsuming tsconfig", + "compilerOptions": { + "declarationMap": true, + "declaration": true, + "emitDeclarationOnly": true + } +} diff --git a/packages/tsconfig/tsconfig.non-strict.json b/packages/tsconfig/tsconfig.non-strict.json new file mode 100644 index 00000000000..8415631881a --- /dev/null +++ b/packages/tsconfig/tsconfig.non-strict.json @@ -0,0 +1,14 @@ +{ + "//": "This tsconfig disables strict mode but enables some nice strict-like options.", + "//": "Note: We want to work towards making all of our packages use strict: true", + "//": "The current blocker is reated to type inference with our React Query query key factory.", + "compilerOptions": { + "strict": false, + "allowUnreachableCode": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "strictNullChecks": true, + } +} \ No newline at end of file diff --git a/packages/tsconfig/tsconfig.package.json b/packages/tsconfig/tsconfig.package.json new file mode 100644 index 00000000000..6501cc53a23 --- /dev/null +++ b/packages/tsconfig/tsconfig.package.json @@ -0,0 +1,16 @@ +{ + "//": "This tsconfig is our base config for packages.", + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "bundler", + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "incremental": true + }, + "include": [ + "src" + ] +} diff --git a/packages/tsconfig/tsconfig.react.json b/packages/tsconfig/tsconfig.react.json new file mode 100644 index 00000000000..4652780dc49 --- /dev/null +++ b/packages/tsconfig/tsconfig.react.json @@ -0,0 +1,6 @@ +{ + "//": "This tsconfig should be used when a package needs React / JSX.", + "compilerOptions": { + "jsx": "react" + } +} \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 5957a10e5ac..4149eb6bddb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,7 +34,6 @@ "typecheck": "tsc", "test": "vitest run", "test:watch": "vitest", - "test:debug": "node --inspect-brk scripts/test.js --runInBand", "coverage": "vitest run --coverage && open coverage/index.html", "coverage:summary": "vitest run --coverage.enabled --reporter=junit --coverage.reporter=json-summary" }, @@ -45,6 +44,7 @@ ] }, "devDependencies": { + "@linode/tsconfig": "workspace:*", "@storybook/addon-actions": "^8.6.7", "@storybook/preview-api": "^8.6.7", "@storybook/react": "^8.6.7", diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index a9b006a91d5..a0428fa04e5 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,15 +1,7 @@ { - "compilerOptions": { - "jsx": "react", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "noEmit": true, - "strict": true, - "noUnusedLocals": true, - "forceConsistentCasingInFileNames": true, - "incremental": true - }, + "extends": [ + "@linode/tsconfig/package", + "@linode/tsconfig/react", + ], "include": ["src"], } diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 58fa22f159d..df630a17e60 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -21,7 +21,6 @@ "typecheck": "tsc", "test": "vitest run", "test:watch": "vitest", - "test:debug": "node --inspect-brk scripts/test.js --runInBand", "coverage": "vitest run --coverage && open coverage/index.html", "coverage:summary": "vitest run --coverage.enabled --reporter=junit --coverage.reporter=json-summary" }, @@ -39,6 +38,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@linode/tsconfig": "workspace:*", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "~6.4.2", "@testing-library/react": "~16.0.0", diff --git a/packages/utilities/tsconfig.json b/packages/utilities/tsconfig.json index b136bfd20b1..84f506343dc 100644 --- a/packages/utilities/tsconfig.json +++ b/packages/utilities/tsconfig.json @@ -1,15 +1,7 @@ { - "compilerOptions": { - "jsx": "react", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "noEmit": true, - "strict": true, - "noUnusedLocals": true, - "forceConsistentCasingInFileNames": true, - "incremental": true - }, + "extends": [ + "@linode/tsconfig/package", + "@linode/tsconfig/react" + ], "include": ["src"] } diff --git a/packages/validation/package.json b/packages/validation/package.json index 9beaee549f7..8312ba8dde0 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -41,6 +41,7 @@ "yup": "^1.4.0" }, "devDependencies": { + "@linode/tsconfig": "workspace:*", "concurrently": "^9.0.1", "tsup": "^8.4.0" }, diff --git a/packages/validation/tsconfig.json b/packages/validation/tsconfig.json index 58df0f1dff7..22d69fd4250 100644 --- a/packages/validation/tsconfig.json +++ b/packages/validation/tsconfig.json @@ -1,18 +1,8 @@ { + "extends": ["@linode/tsconfig/package", "@linode/tsconfig/emit-types"], "compilerOptions": { - "target": "esnext", - "module": "esnext", - "emitDeclarationOnly": true, - "declaration": true, "outDir": "./lib", - "esModuleInterop": true, - "moduleResolution": "bundler", - "skipLibCheck": true, - "strict": true, "baseUrl": ".", - "noUnusedLocals": true, - "declarationMap": true, - "incremental": true }, "include": [ "src" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 103fb1a4a0a..c785b5ad8d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: specifier: ^1.4.0 version: 1.5.0 devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig axios-mock-adapter: specifier: ^1.22.0 version: 1.22.0(axios@1.8.3) @@ -595,6 +598,9 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig '@testing-library/dom': specifier: ^10.1.0 version: 10.4.0 @@ -622,6 +628,10 @@ importers: vite: specifier: ^6.3.4 version: 6.3.4(@types/node@20.17.6)(jiti@2.4.2)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig packages/shared: dependencies: @@ -644,6 +654,9 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig '@storybook/addon-actions': specifier: ^8.6.7 version: 8.6.9(storybook@8.6.9(prettier@3.5.3)) @@ -672,6 +685,8 @@ importers: specifier: ^3.2.0 version: 3.3.0(rollup@4.40.1)(typescript@5.7.3)(vite@6.3.4(@types/node@20.17.6)(jiti@2.4.2)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + packages/tsconfig: {} + packages/ui: dependencies: '@emotion/react': @@ -708,6 +723,9 @@ importers: specifier: ^4.8.2 version: 4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@7.1.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig '@storybook/addon-actions': specifier: ^8.6.7 version: 8.6.9(storybook@8.6.9(prettier@3.5.3)) @@ -760,6 +778,9 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig '@testing-library/dom': specifier: ^10.1.0 version: 10.4.0 @@ -797,6 +818,9 @@ importers: specifier: ^1.4.0 version: 1.5.0 devDependencies: + '@linode/tsconfig': + specifier: workspace:* + version: link:../tsconfig concurrently: specifier: ^9.0.1 version: 9.1.0 From d970ca717c0c2ebb4e9fd70db31ae5e57cbe3a68 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Fri, 23 May 2025 14:31:49 -0400 Subject: [PATCH 40/59] fix: tsconfig emit issue and comments (#12273) * fix emitting issue and comments * clean up --------- Co-authored-by: Banks Nussman --- packages/tsconfig/tsconfig.emit-types.json | 11 ++++++++--- packages/tsconfig/tsconfig.non-strict.json | 9 ++++++--- packages/tsconfig/tsconfig.package.json | 7 +++++-- packages/tsconfig/tsconfig.react.json | 4 +++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/tsconfig/tsconfig.emit-types.json b/packages/tsconfig/tsconfig.emit-types.json index 818c04598aa..4d438eaa544 100644 --- a/packages/tsconfig/tsconfig.emit-types.json +++ b/packages/tsconfig/tsconfig.emit-types.json @@ -1,8 +1,13 @@ { - "//": "This tsconfig makes tsc emit type declaration files.", - "//": "Currently, only api-v4 and validation emit types. All other packages don't have a build step.", - "//": "Note: When using this config, specify outDir in the comsuming tsconfig", + /** + * This tsconfig makes tsc emit type declaration files. + * + * Currently, only api-v4 and validation emit types. All other packages don't have a build step. + * + * Note: When using this config, specify `outDir` in the comsuming tsconfig. + */ "compilerOptions": { + "noEmit": false, "declarationMap": true, "declaration": true, "emitDeclarationOnly": true diff --git a/packages/tsconfig/tsconfig.non-strict.json b/packages/tsconfig/tsconfig.non-strict.json index 8415631881a..34ed04ae3f4 100644 --- a/packages/tsconfig/tsconfig.non-strict.json +++ b/packages/tsconfig/tsconfig.non-strict.json @@ -1,7 +1,10 @@ { - "//": "This tsconfig disables strict mode but enables some nice strict-like options.", - "//": "Note: We want to work towards making all of our packages use strict: true", - "//": "The current blocker is reated to type inference with our React Query query key factory.", + /** + * This tsconfig disables strict mode but enables some nice strict-like options. + * + * Note: We want to work towards making all of our packages use `strict: true`. + * The current blocker is related to type inference with our React Query query key factory. + */ "compilerOptions": { "strict": false, "allowUnreachableCode": false, diff --git a/packages/tsconfig/tsconfig.package.json b/packages/tsconfig/tsconfig.package.json index 6501cc53a23..cd768475207 100644 --- a/packages/tsconfig/tsconfig.package.json +++ b/packages/tsconfig/tsconfig.package.json @@ -1,5 +1,7 @@ { - "//": "This tsconfig is our base config for packages.", + /** + * This tsconfig is our base config for packages. + */ "compilerOptions": { "target": "esnext", "module": "esnext", @@ -8,7 +10,8 @@ "skipLibCheck": true, "strict": true, "noUnusedLocals": true, - "incremental": true + "incremental": true, + "noEmit": true }, "include": [ "src" diff --git a/packages/tsconfig/tsconfig.react.json b/packages/tsconfig/tsconfig.react.json index 4652780dc49..7b97ca65fbc 100644 --- a/packages/tsconfig/tsconfig.react.json +++ b/packages/tsconfig/tsconfig.react.json @@ -1,5 +1,7 @@ { - "//": "This tsconfig should be used when a package needs React / JSX.", + /** + * This tsconfig should be used when a package needs React / JSX. + */ "compilerOptions": { "jsx": "react" } From 62406ececbf394fa2b88818dc822d524e0c66d0e Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Sat, 24 May 2025 13:45:22 -0400 Subject: [PATCH 41/59] tech-story: [M3-9983] - Reroute Support/Help (#12242) * Save progress * Save progress * Save progress * More work on the modal and calls * Linting errors * Linting errors * Revert "Linting errors" This reverts commit 794880bd26dad13e9744071c099699b2cef1a1eb. * Linting errors * Fix all the units * fix the e2e * sorting/pagination * sorting/pagination * moaar test fixes * last test * missing original useEffect * small cleanup * Added changeset: Reroute Support & Help features * feedback @bnussman-akamai @hana-akamai * fix support/search error * feedback @mjac0bs * fix test * feedback @bnussman-akamai --- .../pr-12242-tech-stories-1747759885067.md | 5 + .../account/account-linode-managed.spec.ts | 2 +- .../manager/cypress/support/ui/constants.ts | 4 +- packages/manager/eslint.config.js | 2 + packages/manager/src/MainContent.tsx | 62 ++------- .../AccountActivationLanding.test.tsx | 18 ++- .../AccountActivationLanding.tsx | 95 ++++++------- .../MaintenanceBanner/MaintenanceBanner.tsx | 6 +- .../components/SupportLink/SupportLink.tsx | 3 +- packages/manager/src/env.d.ts | 3 + .../Events/FormattedEventMessage.test.tsx | 2 +- .../src/features/Help/HelpLanding.test.tsx | 14 +- .../features/Help/Panels/AlgoliaSearchBar.tsx | 24 ++-- .../SupportSearchLanding/HelpResources.tsx | 13 +- .../SupportSearchLanding.test.tsx | 28 +++- .../SupportSearchLanding.tsx | 13 +- packages/manager/src/features/Help/index.tsx | 52 -------- .../Images/ImagesLanding/ImagesLanding.tsx | 4 +- .../LinodesLanding/LinodeRow/LinodeRow.tsx | 2 +- .../Linodes/SMTPRestrictionText.test.tsx | 2 +- .../LongviewLanding/LongviewClients.tsx | 17 ++- .../LongviewLanding/LongviewLanding.tsx | 14 +- .../ManagedDashboardCard/MonitorTickets.tsx | 20 +-- .../useFormattedNotifications.tsx | 2 +- .../SupportTicketDetail.test.tsx | 19 +++ .../SupportTicketDetail.tsx | 51 ++++--- .../SupportTicketDialog.test.tsx | 8 +- .../SupportTickets/SupportTicketDialog.tsx | 24 ++-- .../SupportTicketsLanding.test.tsx | 24 +++- .../SupportTickets/SupportTicketsLanding.tsx | 126 ++++++++---------- .../SupportTickets/TicketList.test.tsx | 14 ++ .../Support/SupportTickets/TicketList.tsx | 32 +++-- .../features/Support/SupportTickets/index.tsx | 2 - packages/manager/src/hooks/useOrderV2.ts | 3 +- packages/manager/src/routes/account/index.ts | 10 -- packages/manager/src/routes/images/index.ts | 1 + packages/manager/src/routes/index.tsx | 1 + packages/manager/src/routes/longview/index.ts | 5 - packages/manager/src/routes/support/index.ts | 77 +++++++++-- .../src/routes/support/supportLazyRoutes.tsx | 31 +++++ .../src/utilities/logic-query-parser.d.ts | 3 - .../manager/src/utilities/search-string.d.ts | 3 - 42 files changed, 461 insertions(+), 380 deletions(-) create mode 100644 packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md delete mode 100644 packages/manager/src/features/Help/index.tsx delete mode 100644 packages/manager/src/features/Support/SupportTickets/index.tsx create mode 100644 packages/manager/src/routes/support/supportLazyRoutes.tsx delete mode 100644 packages/manager/src/utilities/logic-query-parser.d.ts delete mode 100644 packages/manager/src/utilities/search-string.d.ts diff --git a/packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md b/packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md new file mode 100644 index 00000000000..e8f94ce475b --- /dev/null +++ b/packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Reroute Support & Help features ([#12242](https://github.com/linode/manager/pull/12242)) diff --git a/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts b/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts index 26a22129f2a..c9466939e48 100644 --- a/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts @@ -173,7 +173,7 @@ describe('Account Linode Managed', () => { // Navigate to the 'Open a Support Ticket' page. cy.findByText('Support Ticket').should('be.visible').click(); - cy.url().should('endWith', '/support/tickets'); + cy.url().should('endWith', '/support/tickets/open?dialogOpen=true'); // Confirm that title and category are related to cancelling Linode Managed. cy.findByLabelText('Title (required)').should( diff --git a/packages/manager/cypress/support/ui/constants.ts b/packages/manager/cypress/support/ui/constants.ts index 34046c5fb5b..e4bb5251d6a 100644 --- a/packages/manager/cypress/support/ui/constants.ts +++ b/packages/manager/cypress/support/ui/constants.ts @@ -11,8 +11,8 @@ export const routes = { profile: '/profile', support: '/support', supportTickets: '/support/tickets', - supportTicketsClosed: '/support/tickets?type=closed', - supportTicketsOpen: '/support/tickets?type=open', + supportTicketsClosed: '/support/tickets/closed', + supportTicketsOpen: '/support/tickets/open', }; /** * due 2 rerender of the page that i could not deterministically check i added this wait diff --git a/packages/manager/eslint.config.js b/packages/manager/eslint.config.js index 54f95455e54..2c0c9c7b8fc 100644 --- a/packages/manager/eslint.config.js +++ b/packages/manager/eslint.config.js @@ -407,6 +407,7 @@ export const baseConfig = [ 'src/features/Domains/**/*', 'src/features/DataStream/**/*', 'src/features/Firewalls/**/*', + 'src/features/Help/**/*', 'src/features/Images/**/*', 'src/features/Longview/**/*', 'src/features/Managed/**/*', @@ -417,6 +418,7 @@ export const baseConfig = [ 'src/features/TopMenu/SearchBar/**/*', 'src/components/Tag/**/*', 'src/features/StackScripts/**/*', + 'src/features/Support/**/*', 'src/features/Volumes/**/*', 'src/features/VPCs/**/*', ], diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 768d7599f7b..ffb41959941 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -13,7 +13,6 @@ import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; -import Logo from 'src/assets/logo/akamai-logo.svg'; import { MainContentBanner } from 'src/components/MainContentBanner'; import { MaintenanceScreen } from 'src/components/MaintenanceScreen'; import { @@ -129,29 +128,11 @@ const Profile = React.lazy(() => default: module.Profile, })) ); -const SupportTickets = React.lazy( - () => import('src/features/Support/SupportTickets') -); -const SupportTicketDetail = React.lazy(() => - import('src/features/Support/SupportTicketDetail/SupportTicketDetail').then( - (module) => ({ - default: module.SupportTicketDetail, - }) - ) -); -const Help = React.lazy(() => - import('./features/Help/index').then((module) => ({ - default: module.HelpAndSupport, - })) -); const EventsLanding = React.lazy(() => import('src/features/Events/EventsLanding').then((module) => ({ default: module.EventsLanding, })) ); -const AccountActivationLanding = React.lazy( - () => import('src/components/AccountActivation/AccountActivationLanding') -); const Databases = React.lazy(() => import('src/features/Databases')); const CloudPulseMetrics = React.lazy(() => @@ -223,6 +204,13 @@ export const MainContent = () => { const { isPageScrollable } = useIsPageScrollable(contentRef); + migrationRouter.update({ + context: { + globalErrors, + queryClient, + }, + }); + /** * this is the case where the user has successfully completed signup * but needs a manual review from Customer Support. In this case, @@ -232,34 +220,13 @@ export const MainContent = () => { */ if (globalErrors.account_unactivated) { return ( -
-
- - - - - - - - - -
-
+ <> + + + ); } @@ -371,7 +338,6 @@ export const MainContent = () => { )} - ({ + useLocation: vi.fn().mockReturnValue({ + state: { + supportTicketFormFields: {}, + }, + }), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useLocation: queryMocks.useLocation, + }; +}); + describe('AccountActivationLanding', () => { it('renders the AccountActivationLanding component', () => { const { getByText, queryByText } = renderWithTheme( diff --git a/packages/manager/src/components/AccountActivation/AccountActivationLanding.tsx b/packages/manager/src/components/AccountActivation/AccountActivationLanding.tsx index 86b3702eab5..2e40219d44f 100644 --- a/packages/manager/src/components/AccountActivation/AccountActivationLanding.tsx +++ b/packages/manager/src/components/AccountActivation/AccountActivationLanding.tsx @@ -1,14 +1,14 @@ -import { ErrorState, StyledLinkButton, Typography } from '@linode/ui'; +import { Box, ErrorState, StyledLinkButton, Typography } from '@linode/ui'; import Warning from '@mui/icons-material/CheckCircle'; -import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; +import Logo from 'src/assets/logo/akamai-logo.svg'; import { SupportTicketDialog } from 'src/features/Support/SupportTickets/SupportTicketDialog'; import type { AttachmentError } from 'src/features/Support/SupportTicketDetail/SupportTicketDetail'; -const AccountActivationLanding = () => { +export const AccountActivationLanding = () => { const history = useHistory(); const [supportDrawerIsOpen, toggleSupportDrawer] = @@ -27,49 +27,50 @@ const AccountActivationLanding = () => { }; return ( - - ({ marginBottom: theme.spacing(2) })} - variant="h2" - > - Your account is currently being reviewed. - - - Thanks for signing up! You’ll receive an email from us once - our review is complete, so hang tight. If you have questions during - this process{' '} - toggleSupportDrawer(true)}> - please open a Support ticket - - . - - toggleSupportDrawer(false)} - onSuccess={handleTicketSubmitSuccess} - open={supportDrawerIsOpen} - prefilledTitle="Help me activate my account" - /> - - } - /> + + + + ({ marginBottom: theme.spacingFunction(16) })} + variant="h2" + > + Your account is currently being reviewed. + + + Thanks for signing up! You’ll receive an email from us once + our review is complete, so hang tight. If you have questions + during this process{' '} + toggleSupportDrawer(true)}> + please open a Support ticket + + . + + toggleSupportDrawer(false)} + onSuccess={handleTicketSubmitSuccess} + open={supportDrawerIsOpen} + prefilledTitle="Help me activate my account" + /> + + } + /> + ); }; - -export const accountActivationLandingLazyRoute = createLazyRoute( - '/account-activation' -)({ - component: AccountActivationLanding, -}); - -export default React.memo(AccountActivationLanding); diff --git a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx index cf8547b7996..4cc5cef55ab 100644 --- a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx +++ b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx @@ -82,7 +82,7 @@ export const MaintenanceBanner = React.memo((props: Props) => { } For more information, please see your{' '} - open support tickets. + open support tickets.
); @@ -105,8 +105,8 @@ const generateIntroText = ( This Linode’s physical host is currently undergoing maintenance.{' '} {maintenanceActionTextMap[type]} Please refer to - your Support tickets for more - information. + your Support tickets for + more information. ); } diff --git a/packages/manager/src/components/SupportLink/SupportLink.tsx b/packages/manager/src/components/SupportLink/SupportLink.tsx index e4c357e9119..0bc52b417f2 100644 --- a/packages/manager/src/components/SupportLink/SupportLink.tsx +++ b/packages/manager/src/components/SupportLink/SupportLink.tsx @@ -39,12 +39,11 @@ const SupportLink = (props: SupportLinkProps) => { { expect(getByText('contact Support')).toBeInTheDocument(); expect(container.querySelector('a')).toHaveAttribute( 'href', - '/support/tickets' + '/support/tickets/open?dialogOpen=true' ); }); }); diff --git a/packages/manager/src/features/Help/HelpLanding.test.tsx b/packages/manager/src/features/Help/HelpLanding.test.tsx index d74a1d32e35..183bfbb89e1 100644 --- a/packages/manager/src/features/Help/HelpLanding.test.tsx +++ b/packages/manager/src/features/Help/HelpLanding.test.tsx @@ -1,25 +1,25 @@ import * as React from 'react'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; import { HelpLanding } from './HelpLanding'; describe('Help Landing', () => { - it('should render search panel', () => { - const { getByText } = renderWithTheme(); + it('should render search panel', async () => { + const { getByText } = await renderWithThemeAndRouter(); expect(getByText('What can we help you with?')).toBeVisible(); }); - it('should render popular posts panel', () => { - const { getByText } = renderWithTheme(); + it('should render popular posts panel', async () => { + const { getByText } = await renderWithThemeAndRouter(); expect(getByText('Most Popular Documentation:')).toBeVisible(); expect(getByText('Most Popular Community Posts:')).toBeVisible(); }); - it('should render other ways panel', () => { - const { getByText } = renderWithTheme(); + it('should render other ways panel', async () => { + const { getByText } = await renderWithThemeAndRouter(); expect(getByText('Other Ways to Get Help')).toBeVisible(); }); diff --git a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx index 54fb12278a5..1a7ed2a0304 100644 --- a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx +++ b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx @@ -1,7 +1,6 @@ import { Autocomplete, InputAdornment, Notice } from '@linode/ui'; +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { withRouter } from 'react-router-dom'; -import type { RouteComponentProps } from 'react-router-dom'; import { debounce } from 'throttle-debounce'; import Search from 'src/assets/icons/search.svg'; @@ -17,16 +16,15 @@ interface SelectedItem { label: string; value: string; } -interface AlgoliaSearchBarProps extends AlgoliaProps, RouteComponentProps<{}> {} /** * For Algolia search to work locally, ensure you have valid values set for * REACT_APP_ALGOLIA_APPLICATION_ID and REACT_APP_ALGOLIA_SEARCH_KEY in your .env file. */ -const AlgoliaSearchBar = (props: AlgoliaSearchBarProps) => { +const AlgoliaSearchBar = (props: AlgoliaProps) => { const [inputValue, setInputValue] = React.useState(''); - const { history, searchAlgolia, searchEnabled, searchError, searchResults } = - props; + const { searchAlgolia, searchEnabled, searchError, searchResults } = props; + const navigate = useNavigate(); const options = React.useMemo(() => { const [docs, community] = searchResults; @@ -50,12 +48,6 @@ const AlgoliaSearchBar = (props: AlgoliaSearchBarProps) => { [searchAlgolia] ); - const getLinkTarget = (inputValue: string) => { - return inputValue - ? `/support/search/?query=${inputValue}` - : '/support/search/'; - }; - const handleSelect = (selected: ConvertedItems | null | SelectedItem) => { if (!selected || !inputValue) { return; @@ -68,8 +60,10 @@ const AlgoliaSearchBar = (props: AlgoliaSearchBarProps) => { window.open(href, '_blank', 'noopener'); } else { // If no href, we redirect to the search landing page. - const link = getLinkTarget(inputValue); - history.push(link); + navigate({ + to: '/support/search', + search: { query: inputValue || undefined }, + }); } }; return ( @@ -128,5 +122,5 @@ const AlgoliaSearchBar = (props: AlgoliaSearchBarProps) => { }; export default withSearch({ highlight: false, hitsPerPage: 10 })( - withRouter(AlgoliaSearchBar) + AlgoliaSearchBar ); diff --git a/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx b/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx index 1a6afe1e2cb..42e2ad4b333 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/HelpResources.tsx @@ -1,7 +1,7 @@ import { Typography } from '@linode/ui'; import Grid from '@mui/material/Grid'; +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; import Community from 'src/assets/icons/community.svg'; @@ -34,7 +34,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ export const HelpResources = () => { const { classes } = useStyles(); - const history = useHistory(); + const navigate = useNavigate(); const [drawerOpen, setDrawerOpen] = React.useState(false); const openTicketDrawer = () => { setDrawerOpen(true); @@ -48,9 +48,12 @@ export const HelpResources = () => { ticketId: number, attachmentErrors: AttachmentError[] = [] ) => { - history.push({ - pathname: `/support/tickets/${ticketId}`, - state: { attachmentErrors }, + navigate({ + to: `/support/tickets/${ticketId}`, + state: (prev) => ({ + ...prev, + attachmentErrors, + }), }); setDrawerOpen(false); }; diff --git a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx index ef4ff4437c3..05fc05dc0a9 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.test.tsx @@ -3,7 +3,6 @@ import { screen } from '@testing-library/react'; import { assocPath } from 'ramda'; import * as React from 'react'; -import { reactRouterProps } from 'src/__data__/reactRouterProps'; import { renderWithTheme } from 'src/utilities/testHelpers'; import SupportSearchLanding from './SupportSearchLanding'; @@ -12,9 +11,25 @@ const props = { searchAlgolia: vi.fn(), searchEnabled: true, searchResults: [[], []], - ...reactRouterProps, + location: { + search: '?query=test', + }, }; +const queryMocks = vi.hoisted(() => ({ + useNavigate: vi.fn(), + useLocation: vi.fn(), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + useLocation: queryMocks.useLocation, + }; +}); + const propsWithMultiWordURLQuery = assocPath( ['location', 'search'], '?query=two%20words', @@ -22,6 +37,15 @@ const propsWithMultiWordURLQuery = assocPath( ); describe('SupportSearchLanding Component', () => { + beforeEach(() => { + queryMocks.useLocation.mockReturnValue({ + search: '?query=test', + state: { + supportTicketFormFields: {}, + }, + }); + }); + it('should render', () => { renderWithTheme(); expect(screen.getByTestId('support-search-landing')).toBeInTheDocument(); diff --git a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx index d6175b1b3f4..08f4638fba2 100644 --- a/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx +++ b/packages/manager/src/features/Help/SupportSearchLanding/SupportSearchLanding.tsx @@ -2,9 +2,8 @@ import { Box, H1Header, InputAdornment, Notice, TextField } from '@linode/ui'; import { getQueryParamFromQueryString } from '@linode/utilities'; import Search from '@mui/icons-material/Search'; import Grid from '@mui/material/Grid'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; import { COMMUNITY_SEARCH_URL, DOCS_SEARCH_URL } from 'src/constants'; @@ -38,7 +37,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); const SupportSearchLanding = (props: AlgoliaProps) => { - const history = useHistory(); + const navigate = useNavigate(); const { searchAlgolia, searchEnabled, searchError, searchResults } = props; const [docs, community] = searchResults; const { classes } = useStyles(); @@ -58,7 +57,7 @@ const SupportSearchLanding = (props: AlgoliaProps) => { const onInputChange = (e: React.ChangeEvent) => { const newQuery = e.target.value ?? ''; setQueryString(newQuery); - history.replace({ search: `?query=${newQuery}` }); + navigate({ to: '/search', search: { query: newQuery } }); searchAlgolia(newQuery); }; @@ -122,9 +121,3 @@ const SupportSearchLanding = (props: AlgoliaProps) => { export default withSearch({ highlight: false, hitsPerPage: 5 })( SupportSearchLanding ); - -export const supportSearchLandingLazyRoute = createLazyRoute('/support/search')( - { - component: SupportSearchLanding, - } -); diff --git a/packages/manager/src/features/Help/index.tsx b/packages/manager/src/features/Help/index.tsx deleted file mode 100644 index 89fd7cc9627..00000000000 --- a/packages/manager/src/features/Help/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; - -import { StatusBanners } from './StatusBanners'; - -const HelpLanding = React.lazy(() => - import('./HelpLanding').then((module) => ({ default: module.HelpLanding })) -); - -const SupportSearchLanding = React.lazy( - () => import('src/features/Help/SupportSearchLanding/SupportSearchLanding') -); - -const SupportTickets = React.lazy( - () => import('src/features/Support/SupportTickets') -); - -const SupportTicketDetail = React.lazy(() => - import('src/features/Support/SupportTicketDetail/SupportTicketDetail').then( - (module) => ({ - default: module.SupportTicketDetail, - }) - ) -); - -export const HelpAndSupport = () => { - return ( - <> - - - - - - - - - - - - - ); -}; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index 31eeed772ec..81014ef59b4 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -71,7 +71,7 @@ import { RebuildImageDrawer } from './RebuildImageDrawer'; import type { Handlers as ImageHandlers } from './ImagesActionMenu'; import type { Filter, Image, ImageStatus } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; -import type { ImageAction, ImagesSearchParams } from 'src/routes/images'; +import type { ImageAction } from 'src/routes/images'; const useStyles = makeStyles()((theme: Theme) => ({ imageTable: { @@ -108,7 +108,7 @@ export const ImagesLanding = () => { }: { action: ImageAction; imageId: string } = useParams({ strict: false, }); - const search: ImagesSearchParams = useSearch({ from: '/images' }); + const search = useSearch({ from: '/images' }); const { query } = search; const history = useHistory(); const navigate = useNavigate(); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx index 457d4e32226..8a391d9c92c 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx @@ -72,7 +72,7 @@ export const LinodeRow = (props: Props) => { <> This Linode’s maintenance window opens at{' '} {parsedMaintenanceStartTime}. For more information, see your{' '} - open support tickets. + open support tickets. ); }; diff --git a/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx b/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx index a80e3967738..640d0aba7cb 100644 --- a/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx +++ b/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx @@ -106,7 +106,7 @@ describe('SMTPRestrictionText component', () => { expect(getByText('open a support ticket')).toHaveAttribute( 'href', - '/support/tickets' + '/support/tickets/open?dialogOpen=true' ); }); }); diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx index 47fd5ac729e..2623dc77496 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClients.tsx @@ -1,6 +1,6 @@ import { useAccountSettings, useGrants, useProfile } from '@linode/queries'; import { Autocomplete, Typography } from '@linode/ui'; -import { useLocation, useNavigate } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; import { connect } from 'react-redux'; @@ -30,7 +30,6 @@ import type { LongviewSubscription, } from '@linode/api-v4/lib/longview/types'; import type { Props as LongviewProps } from 'src/containers/longview.container'; -import type { LongviewState } from 'src/routes/longview'; import type { State as StatsState } from 'src/store/longviewStats/longviewStats.reducer'; import type { MapState } from 'src/store/types'; @@ -52,8 +51,6 @@ type SortKey = 'cpu' | 'load' | 'name' | 'network' | 'ram' | 'storage' | 'swap'; export const LongviewClients = (props: LongviewClientsCombinedProps) => { const { getLongviewClients } = props; const navigate = useNavigate(); - const location = useLocation(); - const locationState = location.state as LongviewState; const { data: profile } = useProfile(); const { data: grants } = useGrants(); const { data: accountSettings } = useAccountSettings(); @@ -127,8 +124,16 @@ export const LongviewClients = (props: LongviewClientsCombinedProps) => { const handleSubmit = () => { if (isManaged) { navigate({ - state: (prev) => ({ ...prev, ...locationState }), - to: '/support/tickets', + state: (prev) => ({ + ...prev, + supportTicketFormFields: { + title: 'Request for additional Longview clients', + }, + }), + search: { + dialogOpen: drawerOpen, + }, + to: '/support/tickets/open', }); return; } diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewLanding.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewLanding.tsx index 1f98e2ca4ae..f7a106471b0 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewLanding.tsx @@ -4,7 +4,7 @@ import { } from '@linode/api-v4/lib/longview'; import { useAccountSettings } from '@linode/queries'; import { styled } from '@mui/material/styles'; -import { useLocation, useNavigate } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; @@ -28,15 +28,12 @@ import type { LongviewSubscription, } from '@linode/api-v4/lib/longview/types'; import type { Props as LongviewProps } from 'src/containers/longview.container'; -import type { LongviewState } from 'src/routes/longview'; const LongviewClients = React.lazy(() => import('./LongviewClients')); const LongviewPlans = React.lazy(() => import('./LongviewPlans')); export const LongviewLanding = (props: LongviewProps) => { const navigate = useNavigate(); - const location = useLocation(); - const locationState = location.state as LongviewState; const { enqueueSnackbar } = useSnackbar(); const activeSubscriptionRequestHook = useAPIRequest( () => getActiveLongviewPlan().then((response) => response), @@ -105,8 +102,13 @@ export const LongviewLanding = (props: LongviewProps) => { const handleSubmit = () => { if (isManaged) { navigate({ - state: (prev) => ({ ...prev, ...locationState }), - to: '/support/tickets', + state: (prev) => ({ + ...prev, + supportTicketFormFields: { + title: 'Request for additional Longview clients', + }, + }), + to: '/support/tickets/open', }); return; } diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx index fd947dbe634..c000061d0d5 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/MonitorTickets.tsx @@ -1,8 +1,7 @@ import { Typography } from '@linode/ui'; import Grid from '@mui/material/Grid'; +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -// eslint-disable-next-line no-restricted-imports -import { useHistory } from 'react-router-dom'; import TicketIcon from 'src/assets/icons/ticket.svg'; import { Link } from 'src/components/Link'; @@ -17,7 +16,7 @@ interface MonitorTicketsProps { export const MonitorTickets = (props: MonitorTicketsProps) => { const { issues } = props; - const history = useHistory(); + const navigate = useNavigate(); const openIssues = issues.filter((thisIssue) => !thisIssue.dateClosed); @@ -57,12 +56,17 @@ export const MonitorTickets = (props: MonitorTicketsProps) => { - history.push({ - pathname: '/support/tickets', - state: { - open: true, - title: 'Managed monitor issue', + navigate({ + search: { + dialogOpen: true, }, + state: (prev) => ({ + ...prev, + supportTicketFormFields: { + title: 'Managed monitor issue', + }, + }), + to: '/support/tickets/open', }) } > diff --git a/packages/manager/src/features/NotificationCenter/useFormattedNotifications.tsx b/packages/manager/src/features/NotificationCenter/useFormattedNotifications.tsx index 5bd2011cdfb..ac9299a44ca 100644 --- a/packages/manager/src/features/NotificationCenter/useFormattedNotifications.tsx +++ b/packages/manager/src/features/NotificationCenter/useFormattedNotifications.tsx @@ -193,7 +193,7 @@ const interceptNotification = ( {' '} resides on a host that is pending critical maintenance. You should have received a{' '} - + support ticket {' '} that details how you will be affected. Please see the aforementioned diff --git a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.test.tsx b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.test.tsx index a3682fa1a2a..940bd16928a 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.test.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.test.tsx @@ -16,9 +16,28 @@ import { import { SupportTicketDetail } from './SupportTicketDetail'; +const queryMocks = vi.hoisted(() => ({ + useLocation: vi.fn(), + useParams: vi.fn(), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useLocation: queryMocks.useLocation, + useParams: queryMocks.useParams, + }; +}); + describe('Support Ticket Detail', () => { beforeAll(() => { resizeScreenSize(breakpoints.values.lg); + queryMocks.useParams.mockReturnValue({ ticketId: '1' }); + queryMocks.useLocation.mockReturnValue({ + state: {}, + pathname: '/support/tickets/1', + }); }); it('should display a loading spinner', () => { diff --git a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx index c625f4c2b91..c9ed4dcf58b 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx @@ -6,10 +6,9 @@ import { import { CircleProgress, ErrorState, Stack } from '@linode/ui'; import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useLocation, useParams } from '@tanstack/react-router'; import { isEmpty } from 'ramda'; import * as React from 'react'; -import { useHistory, useLocation, useParams } from 'react-router-dom'; import { Waypoint } from 'react-waypoint'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; @@ -23,6 +22,7 @@ import { ReplyContainer } from './TabbedReply/ReplyContainer'; import { TicketStatus } from './TicketStatus'; import type { SupportReply } from '@linode/api-v4/lib/support'; +import type { SupportState } from 'src/routes/support'; export interface AttachmentError { error: string; @@ -30,23 +30,26 @@ export interface AttachmentError { } export const SupportTicketDetail = () => { - const history = useHistory<{ attachmentErrors?: AttachmentError[] }>(); const location = useLocation(); - const { ticketId } = useParams<{ ticketId: string }>(); - const id = Number(ticketId); + const { ticketId } = useParams({ from: '/support/tickets/$ticketId' }); - const attachmentErrors = history.location.state?.attachmentErrors; + const locationState = location.state as SupportState; const { data: profile } = useProfile(); - const { data: ticket, error, isLoading, refetch } = useSupportTicketQuery(id); + const { + data: ticket, + error, + isLoading, + refetch, + } = useSupportTicketQuery(ticketId); const { data: repliesData, error: repliesError, fetchNextPage, hasNextPage, isLoading: repliesLoading, - } = useInfiniteSupportTicketRepliesQuery(id); + } = useInfiniteSupportTicketRepliesQuery(ticketId); const replies = repliesData?.pages.flatMap((page) => page.data); @@ -79,11 +82,7 @@ export const SupportTicketDetail = () => { crumbOverrides: [ { linkTo: { - pathname: `/support/tickets`, - // If we're viewing a `Closed` ticket, the Breadcrumb link should take us to `Closed` tickets. - search: `type=${ - ticket.status === 'closed' ? 'closed' : 'open' - }`, + pathname: `/support/tickets/${ticket.status}`, }, position: 2, }, @@ -95,15 +94,17 @@ export const SupportTicketDetail = () => { {/* If a user attached files when creating the ticket and was redirected here, display those errors. */} - {attachmentErrors !== undefined && - !isEmpty(attachmentErrors) && - attachmentErrors?.map((error, idx: number) => ( - - ))} + {locationState?.attachmentErrors !== undefined && + !isEmpty(locationState?.attachmentErrors) && + locationState?.attachmentErrors?.map( + (error: AttachmentError, idx: number) => ( + + ) + )} {/* If the ticket isn't blank, display it, followed by replies (if any). */} @@ -152,9 +153,3 @@ const StyledStack = styled(Stack, { marginLeft: theme.spacing(), marginRight: theme.spacing(), })); - -export const supportTicketDetailLazyRoute = createLazyRoute( - '/support/tickets/$ticketId' -)({ - component: SupportTicketDetail, -}); diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.test.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.test.tsx index bb6e886b69e..79cc7bcd273 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.test.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; import { SupportTicketDialog } from './SupportTicketDialog'; @@ -13,8 +13,10 @@ const props: SupportTicketDialogProps = { }; describe('Support Ticket Drawer', () => { - it('should render', () => { - const { getByText } = renderWithTheme(); + it('should render', async () => { + const { getByText } = await renderWithThemeAndRouter( + + ); expect(getByText('Open a Support Ticket')); }); }); diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx index 3df834f209c..39f3fce124f 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx @@ -12,10 +12,12 @@ import { Typography, } from '@linode/ui'; import { reduceAsync, scrollErrorIntoViewV2 } from '@linode/utilities'; +import { useLocation as useLocationTanstack } from '@tanstack/react-router'; import { update } from 'ramda'; import * as React from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; -import { useLocation } from 'react-router-dom'; +// eslint-disable-next-line no-restricted-imports +import { useLocation as useLocationRouterDom } from 'react-router-dom'; import { debounce } from 'throttle-debounce'; import { sendSupportTicketExitEvent } from 'src/utilities/analytics/customEventAnalytics'; @@ -48,6 +50,7 @@ import type { TicketSeverity, } from '@linode/api-v4'; import type { EntityForTicketDetails } from 'src/components/SupportLink/SupportLink'; +import type { SupportState } from 'src/routes/support'; interface Accumulator { errors: AttachmentError[]; @@ -136,8 +139,11 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { prefilledTitle, } = props; - const location = useLocation(); - const stateParams = location.state; + const locationRouterDom = useLocationRouterDom(); + const locationTanstack = useLocationTanstack(); + const locationTanstackState = locationTanstack.state as SupportState; + const stateParams = + locationRouterDom.state ?? locationTanstackState.supportTicketFormFields; // Collect prefilled data from props or Link parameters. const _prefilledDescription: string = @@ -197,7 +203,7 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { React.useEffect(() => { if (!open) { - resetDrawer(); + resetDialog(); } }, [open]); @@ -220,7 +226,7 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { }, [summary, description, entityId, entityType, selectedSeverity]); /** - * Clear the drawer completely if clearValues is passed (when canceling out of the drawer or successfully submitting) + * Clear the dialog completely if clearValues is passed (when canceling out of the dialog or successfully submitting) * or reset to the default values (from localStorage) otherwise. */ const resetTicket = (clearValues: boolean = false) => { @@ -238,7 +244,7 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { }); }; - const resetDrawer = (clearValues: boolean = false) => { + const resetDialog = (clearValues: boolean = false) => { resetTicket(clearValues); setFiles([]); @@ -249,7 +255,7 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { const handleClose = () => { if (ticketType !== 'general') { - window.setTimeout(() => resetDrawer(true), 500); + window.setTimeout(() => resetDialog(true), 500); } props.onClose(); sendSupportTicketExitEvent('Close'); @@ -257,7 +263,7 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { const handleCancel = () => { props.onClose(); - window.setTimeout(() => resetDrawer(true), 500); + window.setTimeout(() => resetDialog(true), 500); sendSupportTicketExitEvent('Cancel'); }; @@ -378,7 +384,7 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => { attachFiles(response!.id).then(({ errors: _errors }: Accumulator) => { setSubmitting(false); if (!props.keepOpenOnSuccess) { - window.setTimeout(() => resetDrawer(true), 500); + window.setTimeout(() => resetDialog(true), 500); props.onClose(); } /* Errors will be an array of errors, or empty if all attachments succeeded. */ diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.test.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.test.tsx index b5d42c8d843..81a498b40f3 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.test.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.test.tsx @@ -1,11 +1,27 @@ import * as React from 'react'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; -import SupportTicketsLanding from './SupportTicketsLanding'; +import { SupportTicketsLanding } from './SupportTicketsLanding'; -describe('Support Tickets Landing', () => { - const { getByText } = renderWithTheme(); +const queryMocks = vi.hoisted(() => ({ + useNavigate: vi.fn(), + useSearch: vi.fn().mockReturnValue({ dialogOpen: false }), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + useSearch: queryMocks.useSearch, + }; +}); + +describe('Support Tickets Landing', async () => { + const { getByText } = await renderWithThemeAndRouter( + + ); it('should render a header', () => { getByText('Tickets'); diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx index 3bb2c249e2b..f4e766fb8b4 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx @@ -1,107 +1,97 @@ -import { getQueryParamsFromQueryString } from '@linode/utilities'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useNavigate, useSearch } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; -import { Tab } from 'src/components/Tabs/Tab'; -import { TabList } from 'src/components/Tabs/TabList'; -import { TabPanel } from 'src/components/Tabs/TabPanel'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; +import { useTabs } from 'src/hooks/useTabs'; import { SupportTicketDialog } from './SupportTicketDialog'; import { TicketList } from './TicketList'; import type { AttachmentError } from '../SupportTicketDetail/SupportTicketDetail'; -import type { BaseQueryParams } from '@linode/utilities'; -import type { BooleanString } from 'src/features/Linodes/types'; - -interface QueryParams extends BaseQueryParams { - drawerOpen: BooleanString; -} - -const tabs = ['open', 'closed']; - -const SupportTicketsLanding = () => { - const location = useLocation(); - const history = useHistory(); +export const SupportTicketsLanding = () => { + const navigate = useNavigate(); /** ?drawerOpen=true to allow external links to go directly to the ticket drawer */ - const parsedParams = getQueryParamsFromQueryString( - location.search - ); + const { dialogOpen } = useSearch({ + strict: false, + }); - const stateParams = location.state; - - const [drawerOpen, setDrawerOpen] = React.useState( - stateParams ? stateParams.open : parsedParams.drawerOpen === 'true' - ); + const { tabs, tabIndex, handleTabChange } = useTabs([ + { + title: 'Open Tickets', + to: '/support/tickets/open', + }, + { + title: 'Closed Tickets', + to: '/support/tickets/closed', + }, + ]); const handleAddTicketSuccess = ( ticketId: number, attachmentErrors: AttachmentError[] = [] ) => { - history.push({ - pathname: `/support/tickets/${ticketId}`, - state: { attachmentErrors }, + navigate({ + to: '/support/tickets/$ticketId', + state: (prev) => ({ + ...prev, + attachmentErrors, + }), + params: { + ticketId, + }, }); - setDrawerOpen(false); }; - const handleButtonKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - setDrawerOpen(true); - } - }; - - const tabIndex = tabs.indexOf(parsedParams.type); - return ( setDrawerOpen(true)} - onButtonKeyPress={handleButtonKeyPress} + onButtonClick={() => + navigate({ + to: '/support/tickets', + search: { dialogOpen: true }, + }) + } spacingBottom={4} title="Tickets" /> - { - history.push(`/support/tickets?type=${tabs[index]}`); - }} - > - - Open Tickets - Closed Tickets - - - - - - - - - + + + }> + + + + + + + + + setDrawerOpen(false)} + onClose={() => + navigate({ + to: '/support/tickets', + search: { dialogOpen: false }, + }) + } onSuccess={handleAddTicketSuccess} - open={drawerOpen} + open={Boolean(dialogOpen)} /> ); }; - -export default SupportTicketsLanding; - -export const supportTicketsLandingLazyRoute = createLazyRoute( - '/support/tickets' -)({ - component: SupportTicketsLanding, -}); diff --git a/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx b/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx index 4265dbd73d4..1d3efba168d 100644 --- a/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx +++ b/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx @@ -18,6 +18,20 @@ const props: Props = { const loadingTestId = 'table-row-loading'; +const queryMocks = vi.hoisted(() => ({ + useSearch: vi.fn().mockReturnValue({ dialogOpen: false }), + useNavigate: vi.fn(), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useSearch: queryMocks.useSearch, + useNavigate: queryMocks.useNavigate, + }; +}); + describe('TicketList', () => { it('renders loading state', () => { renderWithTheme(); diff --git a/packages/manager/src/features/Support/SupportTickets/TicketList.tsx b/packages/manager/src/features/Support/SupportTickets/TicketList.tsx index 392dd05a253..b465e955c4a 100644 --- a/packages/manager/src/features/Support/SupportTickets/TicketList.tsx +++ b/packages/manager/src/features/Support/SupportTickets/TicketList.tsx @@ -12,8 +12,8 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { TableSortCell } from 'src/components/TableSortCell'; -import { useOrder } from 'src/hooks/useOrder'; -import { usePagination } from 'src/hooks/usePagination'; +import { useOrderV2 } from 'src/hooks/useOrderV2'; +import { usePaginationV2 } from 'src/hooks/usePaginationV2'; import { TicketRow } from './TicketRow'; import { getStatusFilter, useTicketSeverityCapability } from './ticketUtils'; @@ -32,15 +32,27 @@ export const TicketList = (props: Props) => { const hasSeverityCapability = useTicketSeverityCapability(); - const pagination = usePagination(1, preferenceKey); - - const { handleOrderChange, order, orderBy } = useOrder( - { - order: 'desc', - orderBy: 'opened', + const pagination = usePaginationV2({ + currentRoute: + filterStatus === 'open' + ? '/support/tickets/open' + : '/support/tickets/closed', + preferenceKey, + }); + + const { handleOrderChange, order, orderBy } = useOrderV2({ + initialRoute: { + defaultOrder: { + order: 'desc', + orderBy: 'opened', + }, + from: + filterStatus === 'open' + ? '/support/tickets/open' + : '/support/tickets/closed', }, - `${preferenceKey}-order` - ); + preferenceKey: `${preferenceKey}-order`, + }); const filter = { ['+order']: order, diff --git a/packages/manager/src/features/Support/SupportTickets/index.tsx b/packages/manager/src/features/Support/SupportTickets/index.tsx deleted file mode 100644 index d8bb05ee6db..00000000000 --- a/packages/manager/src/features/Support/SupportTickets/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import SupportTicketsLanding from './SupportTicketsLanding'; -export default SupportTicketsLanding; diff --git a/packages/manager/src/hooks/useOrderV2.ts b/packages/manager/src/hooks/useOrderV2.ts index fd778160ea4..22b3729170a 100644 --- a/packages/manager/src/hooks/useOrderV2.ts +++ b/packages/manager/src/hooks/useOrderV2.ts @@ -6,6 +6,7 @@ import { sortData } from 'src/components/OrderBy'; import type { OrderSetWithPrefix } from '@linode/utilities'; import type { LinkProps, RegisteredRouter } from '@tanstack/react-router'; +import type { TableSearchParams } from 'src/routes/types'; export type Order = 'asc' | 'desc'; @@ -109,7 +110,7 @@ export const useOrderV2 = ({ }; navigate({ - search: (prev) => ({ + search: (prev: TableSearchParams) => ({ ...prev, ...searchParams, ...urlData, diff --git a/packages/manager/src/routes/account/index.ts b/packages/manager/src/routes/account/index.ts index 591dce6f2af..2eb5b812bed 100644 --- a/packages/manager/src/routes/account/index.ts +++ b/packages/manager/src/routes/account/index.ts @@ -141,15 +141,6 @@ const accountEntityTransfersCreateRoute = createRoute({ ).then((m) => m.entityTransfersCreateLazyRoute) ); -const accountActivationLandingRoute = createRoute({ - getParentRoute: () => rootRoute, - path: 'account-activation', -}).lazy(() => - import('src/components/AccountActivation/AccountActivationLanding').then( - (m) => m.accountActivationLandingLazyRoute - ) -); - export const accountRouteTree = accountRoute.addChildren([ accountIndexRoute, accountUsersRoute, @@ -161,7 +152,6 @@ export const accountRouteTree = accountRoute.addChildren([ accountUsersUsernameProfileRoute, accountUsersUsernamePermissionsRoute, ]), - accountActivationLandingRoute, accountBillingRoute, accountBillingMakePaymentRoute, accountBillingPaymentMethodsRoute, diff --git a/packages/manager/src/routes/images/index.ts b/packages/manager/src/routes/images/index.ts index 0f0dc49154a..735622127ea 100644 --- a/packages/manager/src/routes/images/index.ts +++ b/packages/manager/src/routes/images/index.ts @@ -38,6 +38,7 @@ const imagesRoute = createRoute({ component: ImagesRoute, getParentRoute: () => rootRoute, path: 'images', + validateSearch: (search: ImagesSearchParams) => search, }); const imagesIndexRoute = createRoute({ diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index 5cf6170bd24..9c848b64230 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -102,6 +102,7 @@ export const migrationRouteTree = migrationRootRoute.addChildren([ placementGroupsRouteTree, searchRouteTree, stackScriptsRouteTree, + supportRouteTree, volumesRouteTree, vpcsRouteTree, ]); diff --git a/packages/manager/src/routes/longview/index.ts b/packages/manager/src/routes/longview/index.ts index 48f3a61668e..58de680dd02 100644 --- a/packages/manager/src/routes/longview/index.ts +++ b/packages/manager/src/routes/longview/index.ts @@ -3,11 +3,6 @@ import { createRoute, redirect } from '@tanstack/react-router'; import { rootRoute } from '../root'; import { LongviewRoute } from './LongviewRoute'; -export type LongviewState = { - open?: boolean; - title?: string; -}; - const longviewRoute = createRoute({ component: LongviewRoute, getParentRoute: () => rootRoute, diff --git a/packages/manager/src/routes/support/index.ts b/packages/manager/src/routes/support/index.ts index b82ce14bbd7..d34d48cf96d 100644 --- a/packages/manager/src/routes/support/index.ts +++ b/packages/manager/src/routes/support/index.ts @@ -1,10 +1,22 @@ -import { createRoute } from '@tanstack/react-router'; +import { createRoute, redirect } from '@tanstack/react-router'; import { rootRoute } from '../root'; +import { SupportSearchLandingWrapper } from './supportLazyRoutes'; import { SupportTicketsRoute } from './SupportRoute'; +import type { AttachmentError } from 'src/features/Support/SupportTicketDetail/SupportTicketDetail'; +import type { SupportTicketFormFields } from 'src/features/Support/SupportTickets/SupportTicketDialog'; + +interface SupportSearchParams { + dialogOpen?: boolean; +} + +export interface SupportState { + attachmentErrors?: AttachmentError[]; + supportTicketFormFields?: SupportTicketFormFields; +} + const supportRoute = createRoute({ - // TODO: TanStackRouter - got to handle the MainContent.tsx `globalErrors.account_unactivated` logic. component: SupportTicketsRoute, getParentRoute: () => rootRoute, path: 'support', @@ -17,13 +29,38 @@ const supportLandingRoute = createRoute({ import('src/features/Help/HelpLanding').then((m) => m.helpLandingLazyRoute) ); -const supportTicketsRoute = createRoute({ +const supportTicketsLandingRoute = createRoute({ getParentRoute: () => supportRoute, path: 'tickets', + validateSearch: (search: SupportSearchParams) => search, +}).lazy(() => + import('./supportLazyRoutes').then((m) => m.supportTicketsLandingLazyRoute) +); + +const supportTicketsNewRoute = createRoute({ + beforeLoad: async () => { + throw redirect({ to: '/support/tickets', search: { dialogOpen: true } }); + }, + getParentRoute: () => supportTicketsLandingRoute, + path: 'new', }).lazy(() => - import('src/features/Support/SupportTickets/SupportTicketsLanding').then( - (m) => m.supportTicketsLandingLazyRoute - ) + import('./supportLazyRoutes').then((m) => m.supportTicketsLandingLazyRoute) +); + +const supportTicketsLandingRouteOpen = createRoute({ + getParentRoute: () => supportTicketsLandingRoute, + path: 'open', + validateSearch: (search: SupportSearchParams) => search, +}).lazy(() => + import('./supportLazyRoutes').then((m) => m.supportTicketsLandingLazyRoute) +); + +const supportTicketsLandingRouteClosed = createRoute({ + getParentRoute: () => supportTicketsLandingRoute, + path: 'closed', + validateSearch: (search: SupportSearchParams) => search, +}).lazy(() => + import('./supportLazyRoutes').then((m) => m.supportTicketsLandingLazyRoute) ); const supportTicketDetailRoute = createRoute({ @@ -33,22 +70,36 @@ const supportTicketDetailRoute = createRoute({ }), path: 'tickets/$ticketId', }).lazy(() => - import('src/features/Support/SupportTicketDetail/SupportTicketDetail').then( - (m) => m.supportTicketDetailLazyRoute - ) + import('./supportLazyRoutes').then((m) => m.supportTicketDetailLazyRoute) ); const supportSearchLandingRoute = createRoute({ + component: SupportSearchLandingWrapper, getParentRoute: () => supportRoute, path: 'search', +}); + +export const accountActivationLandingRoute = createRoute({ + beforeLoad: async ({ context }) => { + if (!context.globalErrors?.account_unactivated) { + throw redirect({ to: '/' }); + } + return true; + }, + getParentRoute: () => rootRoute, + path: 'account-activation', }).lazy(() => - import('src/features/Help/SupportSearchLanding/SupportSearchLanding').then( - (m) => m.supportSearchLandingLazyRoute - ) + import('./supportLazyRoutes').then((m) => m.accountActivationLandingLazyRoute) ); export const supportRouteTree = supportRoute.addChildren([ supportLandingRoute, - supportTicketsRoute.addChildren([supportTicketDetailRoute]), + supportTicketsLandingRoute.addChildren([ + supportTicketsNewRoute, + supportTicketsLandingRouteOpen, + supportTicketsLandingRouteClosed, + supportTicketDetailRoute, + ]), supportSearchLandingRoute, + accountActivationLandingRoute, ]); diff --git a/packages/manager/src/routes/support/supportLazyRoutes.tsx b/packages/manager/src/routes/support/supportLazyRoutes.tsx new file mode 100644 index 00000000000..b91edb33c90 --- /dev/null +++ b/packages/manager/src/routes/support/supportLazyRoutes.tsx @@ -0,0 +1,31 @@ +import { createLazyRoute } from '@tanstack/react-router'; +import * as React from 'react'; + +import { AccountActivationLanding } from 'src/components/AccountActivation/AccountActivationLanding'; +import SupportSearchLanding from 'src/features/Help/SupportSearchLanding/SupportSearchLanding'; +import { SupportTicketDetail } from 'src/features/Support/SupportTicketDetail/SupportTicketDetail'; +import { SupportTicketsLanding } from 'src/features/Support/SupportTickets/SupportTicketsLanding'; + +import type { AlgoliaState as AlgoliaProps } from 'src/features/Help/SearchHOC'; + +export const SupportSearchLandingWrapper = (props: AlgoliaProps) => { + return ; +}; + +export const supportTicketsLandingLazyRoute = createLazyRoute( + '/support/tickets' +)({ + component: SupportTicketsLanding, +}); + +export const supportTicketDetailLazyRoute = createLazyRoute( + '/support/tickets/$ticketId' +)({ + component: SupportTicketDetail, +}); + +export const accountActivationLandingLazyRoute = createLazyRoute( + '/account-activation' +)({ + component: AccountActivationLanding, +}); diff --git a/packages/manager/src/utilities/logic-query-parser.d.ts b/packages/manager/src/utilities/logic-query-parser.d.ts deleted file mode 100644 index f9db6bb57f1..00000000000 --- a/packages/manager/src/utilities/logic-query-parser.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @todo: add types - -declare module 'logic-query-parser'; diff --git a/packages/manager/src/utilities/search-string.d.ts b/packages/manager/src/utilities/search-string.d.ts deleted file mode 100644 index 9312500ad2d..00000000000 --- a/packages/manager/src/utilities/search-string.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @todo: add types - -declare module 'search-string'; From b39a608bca29fbc34116ca8ea2a56adeb5551561 Mon Sep 17 00:00:00 2001 From: mduda-akamai Date: Mon, 26 May 2025 07:55:31 +0200 Subject: [PATCH 42/59] [DPS-33112] Add Streams empty state and Create Stream views (#12235) --- ...r-12235-upcoming-features-1747394318662.md | 5 ++ .../features/DataStream/DataStreamLanding.tsx | 4 +- .../StreamCreate/StreamCreate.styles.ts | 23 +++++++++ .../Streams/StreamCreate/StreamCreate.tsx | 48 +++++++++++++++++++ .../StreamCreate/StreamCreateCheckoutBar.tsx | 20 ++++++++ .../StreamCreate/StreamCreateDataSet.tsx | 19 ++++++++ .../StreamCreate/StreamCreateDelivery.tsx | 19 ++++++++ .../StreamCreate/StreamCreateGeneralInfo.tsx | 10 ++++ .../features/DataStream/Streams/Streams.tsx | 5 -- .../DataStream/Streams/StreamsLanding.tsx | 7 +++ .../Streams/StreamsLandingEmptyState.tsx | 42 ++++++++++++++++ .../Streams/StreamsLandingEmptyStateData.ts | 36 ++++++++++++++ .../routes/datastream/dataStreamLazyRoutes.ts | 7 +++ .../manager/src/routes/datastream/index.ts | 9 +++- 14 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md create mode 100644 packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.styles.ts create mode 100644 packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateCheckoutBar.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDataSet.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDelivery.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateGeneralInfo.tsx delete mode 100644 packages/manager/src/features/DataStream/Streams/Streams.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamsLanding.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyState.tsx create mode 100644 packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyStateData.ts diff --git a/packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md b/packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md new file mode 100644 index 00000000000..92726ce7146 --- /dev/null +++ b/packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DataStream: add Streams empty state and Create Stream views ([#12235](https://github.com/linode/manager/pull/12235)) diff --git a/packages/manager/src/features/DataStream/DataStreamLanding.tsx b/packages/manager/src/features/DataStream/DataStreamLanding.tsx index 8f747d39e28..1480bcf3546 100644 --- a/packages/manager/src/features/DataStream/DataStreamLanding.tsx +++ b/packages/manager/src/features/DataStream/DataStreamLanding.tsx @@ -17,8 +17,8 @@ const Destinations = React.lazy(() => ); const Streams = React.lazy(() => - import('./Streams/Streams').then((module) => ({ - default: module.Streams, + import('./Streams/StreamsLanding').then((module) => ({ + default: module.StreamsLanding, })) ); diff --git a/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.styles.ts b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.styles.ts new file mode 100644 index 00000000000..13e63e40589 --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.styles.ts @@ -0,0 +1,23 @@ +import { makeStyles } from 'tss-react/mui'; + +import type { Theme } from '@mui/material/styles'; + +export const useStyles = makeStyles()((theme: Theme) => ({ + root: { + '& .mlMain': { + [theme.breakpoints.down('lg')]: { + flexBasis: '100%', + maxWidth: '100%', + }, + }, + '& .mlSidebar': { + [theme.breakpoints.down('lg')]: { + background: theme.color.white, + flexBasis: '100%', + maxWidth: '100%', + marginTop: theme.spacingFunction(16), + padding: theme.spacingFunction(8), + }, + }, + }, +})); diff --git a/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.tsx b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.tsx new file mode 100644 index 00000000000..3723785002d --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreate.tsx @@ -0,0 +1,48 @@ +import { Stack } from '@linode/ui'; +import Grid from '@mui/material/Grid'; +import * as React from 'react'; + +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { LandingHeader } from 'src/components/LandingHeader'; +import { StreamCreateCheckoutBar } from 'src/features/DataStream/Streams/StreamCreate/StreamCreateCheckoutBar'; +import { StreamCreateDataSet } from 'src/features/DataStream/Streams/StreamCreate/StreamCreateDataSet'; +import { StreamCreateDelivery } from 'src/features/DataStream/Streams/StreamCreate/StreamCreateDelivery'; +import { StreamCreateGeneralInfo } from 'src/features/DataStream/Streams/StreamCreate/StreamCreateGeneralInfo'; + +import { useStyles } from './StreamCreate.styles'; + +export const StreamCreate = () => { + const { classes } = useStyles(); + + const landingHeaderProps = { + breadcrumbProps: { + pathname: '/datastream/streams/create', + crumbOverrides: [ + { + label: 'DataStream', + position: 1, + }, + ], + }, + removeCrumbX: 2, + title: 'Create Stream', + }; + + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateCheckoutBar.tsx b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateCheckoutBar.tsx new file mode 100644 index 00000000000..3986aac1561 --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateCheckoutBar.tsx @@ -0,0 +1,20 @@ +import { Divider } from '@linode/ui'; +import * as React from 'react'; + +import { CheckoutBar } from 'src/components/CheckoutBar/CheckoutBar'; + +export const StreamCreateCheckoutBar = () => { + const onDeploy = () => {}; + + return ( + + + + ); +}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDataSet.tsx b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDataSet.tsx new file mode 100644 index 00000000000..8c8d29f576c --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDataSet.tsx @@ -0,0 +1,19 @@ +import { Box, Paper, Typography } from '@linode/ui'; +import React from 'react'; + +import { DocsLink } from 'src/components/DocsLink/DocsLink'; + +export const StreamCreateDataSet = () => { + return ( + + + Data Set + + + + ); +}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDelivery.tsx b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDelivery.tsx new file mode 100644 index 00000000000..272d6832638 --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateDelivery.tsx @@ -0,0 +1,19 @@ +import { Box, Paper, Typography } from '@linode/ui'; +import React from 'react'; + +import { DocsLink } from 'src/components/DocsLink/DocsLink'; + +export const StreamCreateDelivery = () => { + return ( + + + Delivery + + + + ); +}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateGeneralInfo.tsx b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateGeneralInfo.tsx new file mode 100644 index 00000000000..e8d6f71f73b --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamCreate/StreamCreateGeneralInfo.tsx @@ -0,0 +1,10 @@ +import { Paper, Typography } from '@linode/ui'; +import React from 'react'; + +export const StreamCreateGeneralInfo = () => { + return ( + + General Information + + ); +}; diff --git a/packages/manager/src/features/DataStream/Streams/Streams.tsx b/packages/manager/src/features/DataStream/Streams/Streams.tsx deleted file mode 100644 index f9caa96733e..00000000000 --- a/packages/manager/src/features/DataStream/Streams/Streams.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as React from 'react'; - -export const Streams = () => { - return

Content for Streams tab

; -}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamsLanding.tsx b/packages/manager/src/features/DataStream/Streams/StreamsLanding.tsx new file mode 100644 index 00000000000..5c8672da8f7 --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamsLanding.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; + +import { StreamsLandingEmptyState } from 'src/features/DataStream/Streams/StreamsLandingEmptyState'; + +export const StreamsLanding = () => { + return ; +}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyState.tsx b/packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyState.tsx new file mode 100644 index 00000000000..6b0db5b613b --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyState.tsx @@ -0,0 +1,42 @@ +import { useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; + +import ComputeIcon from 'src/assets/icons/entityIcons/compute.svg'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; +import { sendEvent } from 'src/utilities/analytics/utils'; + +import { + gettingStartedGuides, + headers, + linkAnalyticsEvent, +} from './StreamsLandingEmptyStateData'; + +export const StreamsLandingEmptyState = () => { + const navigate = useNavigate(); + + return ( + <> + + { + sendEvent({ + action: 'Click:button', + category: linkAnalyticsEvent.category, + label: 'Create Stream', + }); + navigate({ to: '/datastream/streams/create' }); + }, + }, + ]} + gettingStartedGuidesData={gettingStartedGuides} + headers={headers} + icon={ComputeIcon} + linkAnalyticsEvent={linkAnalyticsEvent} + /> + + ); +}; diff --git a/packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyStateData.ts b/packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyStateData.ts new file mode 100644 index 00000000000..f41b778cb95 --- /dev/null +++ b/packages/manager/src/features/DataStream/Streams/StreamsLandingEmptyStateData.ts @@ -0,0 +1,36 @@ +import { + docsLink, + guidesMoreLinkText, +} from 'src/utilities/emptyStateLandingUtils'; + +import type { + ResourcesHeaders, + ResourcesLinks, + ResourcesLinkSection, +} from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; + +export const headers: ResourcesHeaders = { + title: 'Streams', + subtitle: '', + description: 'Create a data stream and configure delivery of cloud logs', +}; + +export const linkAnalyticsEvent: ResourcesLinks['linkAnalyticsEvent'] = { + action: 'Click:link', + category: 'Streams landing page empty', +}; + +export const gettingStartedGuides: ResourcesLinkSection = { + links: [ + { + // TODO: Change the link and text when proper documentation is ready + text: 'Getting started guide', + to: 'https://techdocs.akamai.com/cloud-computing/docs', + }, + ], + moreInfo: { + text: guidesMoreLinkText, + to: docsLink, + }, + title: 'Getting Started Guides', +}; diff --git a/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts b/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts index 4cc2a268043..d6041e32c0c 100644 --- a/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts +++ b/packages/manager/src/routes/datastream/dataStreamLazyRoutes.ts @@ -1,7 +1,14 @@ import { createLazyRoute } from '@tanstack/react-router'; import { DataStreamLanding } from 'src/features/DataStream/DataStreamLanding'; +import { StreamCreate } from 'src/features/DataStream/Streams/StreamCreate/StreamCreate'; export const dataStreamLandingLazyRoute = createLazyRoute('/datastream')({ component: DataStreamLanding, }); + +export const streamCreateLazyRoute = createLazyRoute( + '/datastream/streams/create' +)({ + component: StreamCreate, +}); diff --git a/packages/manager/src/routes/datastream/index.ts b/packages/manager/src/routes/datastream/index.ts index cdd5473bc6f..e9b29011a67 100644 --- a/packages/manager/src/routes/datastream/index.ts +++ b/packages/manager/src/routes/datastream/index.ts @@ -26,6 +26,13 @@ const streamsRoute = createRoute({ import('./dataStreamLazyRoutes').then((m) => m.dataStreamLandingLazyRoute) ); +const streamsCreateRoute = createRoute({ + getParentRoute: () => dataStreamRoute, + path: 'streams/create', +}).lazy(() => + import('./dataStreamLazyRoutes').then((m) => m.streamCreateLazyRoute) +); + const destinationsRoute = createRoute({ getParentRoute: () => dataStreamRoute, path: 'destinations', @@ -35,6 +42,6 @@ const destinationsRoute = createRoute({ export const dataStreamRouteTree = dataStreamRoute.addChildren([ dataStreamLandingRoute, - streamsRoute, + streamsRoute.addChildren([streamsCreateRoute]), destinationsRoute, ]); From b7d87b5cf8b0c2ed74a2bd93c435058fd2d727f0 Mon Sep 17 00:00:00 2001 From: hasyed-akamai Date: Tue, 27 May 2025 12:14:45 +0530 Subject: [PATCH 43/59] fix: [M3-9990] - Image Select overflows off screen on mobile viewports (#12269) * fix: [M3-9990] - Image Select overflows off screen on mobile viewports * Added changeset: Image Select overflows off screen on mobile viewports * revert back the sx property for image select * remove sx prop in imageselect --- packages/manager/.changeset/pr-12269-fixed-1747981241068.md | 5 +++++ packages/manager/src/components/ImageSelect/ImageSelect.tsx | 2 +- .../src/features/Linodes/LinodeCreate/Tabs/Images.tsx | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 packages/manager/.changeset/pr-12269-fixed-1747981241068.md diff --git a/packages/manager/.changeset/pr-12269-fixed-1747981241068.md b/packages/manager/.changeset/pr-12269-fixed-1747981241068.md new file mode 100644 index 00000000000..9cb239837f2 --- /dev/null +++ b/packages/manager/.changeset/pr-12269-fixed-1747981241068.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Image Select overflows off screen on mobile viewports ([#12269](https://github.com/linode/manager/pull/12269)) diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index e53527c5648..aeb6393656d 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -157,7 +157,7 @@ export const ImageSelect = (props: Props) => { } return ( - + { onBlur={field.onBlur} onChange={onChange} siteType={selectedRegion?.site_type} - sx={{ width: '416px' }} value={field.value ?? null} variant="private" /> From 2bf59a3daa81ef454d94ac666bf7a14e90586369 Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Tue, 27 May 2025 10:32:18 -0400 Subject: [PATCH 44/59] upcoming: [M3-9884] - Show Linode Interface firewalls in `LinodeEntityDetail` (#12176) * save work - mounting of query * unsure how i feel about this in light of details landing view * make the switch * remove check for interfaces enabled - this only appears if they are enabled? * abstract lke cluster cell * fix tests, abstract * Added changeset: Show Linode Interface firewalls in `LinodeEntityDetail` * hide behidn feature flag * add test * feedback * fix mui v7 errors --- ...r-12176-upcoming-features-1746717784144.md | 5 + .../Linodes/LinodeEntityDetail.test.tsx | 56 +++++- .../features/Linodes/LinodeEntityDetail.tsx | 9 - .../Linodes/LinodeEntityDetailBody.tsx | 158 ++-------------- .../LinodeEntityDetailRowConfigFirewall.tsx | 163 ++++++++++++++++ ...LinodeEntityDetailRowInterfaceFirewall.tsx | 179 ++++++++++++++++++ 6 files changed, 415 insertions(+), 155 deletions(-) create mode 100644 packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md create mode 100644 packages/manager/src/features/Linodes/LinodeEntityDetailRowConfigFirewall.tsx create mode 100644 packages/manager/src/features/Linodes/LinodeEntityDetailRowInterfaceFirewall.tsx diff --git a/packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md b/packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md new file mode 100644 index 00000000000..63acec314e1 --- /dev/null +++ b/packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Show Linode Interface firewalls in `LinodeEntityDetail` ([#12176](https://github.com/linode/manager/pull/12176)) diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx index 925e8cee43f..273bda62f78 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx @@ -1,6 +1,7 @@ import { linodeConfigInterfaceFactoryWithVPC, linodeFactory, + linodeInterfaceFactoryPublic, linodeInterfaceFactoryVPC, } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; @@ -241,9 +242,8 @@ describe('Linode Entity Detail', () => { }); }); - it('should display the interface type for a Linode with Linode interfaces and does not display firewall link', async () => { + it('should display the interface type for a Linode with Linode interfaces', async () => { const mockLinode = linodeFactory.build({ interface_generation: 'linode' }); - const mockFirewall = firewallFactory.build({ label: 'test-firewall' }); const account = accountFactory.build({ capabilities: ['Linode Interfaces'], }); @@ -254,9 +254,6 @@ describe('Linode Entity Detail', () => { }), http.get('*/linode/instances/:linodeId', () => { return HttpResponse.json(mockLinode); - }), - http.get('*/linode/instances/:linodeId/firewalls', () => { - return HttpResponse.json(makeResourcePage([mockFirewall])); }) ); @@ -279,6 +276,55 @@ describe('Linode Entity Detail', () => { }); }); + it('should display the public and VPC firewalls for a Linode using new interfaces', async () => { + const mockLinode = linodeFactory.build({ interface_generation: 'linode' }); + const mockPublicInterface = linodeInterfaceFactoryPublic.build(); + const mockVPCInterface = linodeInterfaceFactoryVPC.build(); + const mockFirewall = firewallFactory.build(); + const account = accountFactory.build({ + capabilities: ['Linode Interfaces'], + }); + + server.use( + http.get('*/v4/account', () => { + return HttpResponse.json(account); + }), + http.get('*/linode/instances/:linodeId', () => { + return HttpResponse.json(mockLinode); + }), + http.get('*/linode/instances/:linodeId/interfaces', () => { + return HttpResponse.json({ + interfaces: [mockPublicInterface, mockVPCInterface], + }); + }), + http.get( + '*/linode/instances/:linodeId/interfaces/:interfaceId/firewalls', + () => { + return HttpResponse.json(makeResourcePage([mockFirewall])); + } + ) + ); + + const { getByText } = renderWithTheme( + , + { + flags: { + linodeInterfaces: { enabled: true }, + }, + } + ); + + await waitFor(() => { + expect(getByText('Linode')).toBeVisible(); + expect(getByText('Public Interface Firewall:')).toBeVisible(); + expect(getByText('VPC Interface Firewall:')).toBeVisible(); + }); + }); + it('should not display the encryption status of the linode if the account lacks the capability or the feature flag is off', () => { // situation where isDiskEncryptionFeatureEnabled === false const { queryByTestId } = renderWithTheme( diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index 10d6a7e9a48..559a2ef3c61 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -1,6 +1,5 @@ import { useAllImagesQuery, - useLinodeFirewallsQuery, useLinodeVolumesQuery, useRegionsQuery, } from '@linode/queries'; @@ -70,13 +69,6 @@ export const LinodeEntityDetail = (props: Props) => { linodeId: linode.id, }); - const { data: attachedFirewallData } = useLinodeFirewallsQuery( - linode.id, - !isLinodeInterface - ); - - const attachedFirewalls = attachedFirewallData?.data ?? []; - const isLinodesGrantReadOnly = useIsResourceRestricted({ grantLevel: 'read_only', grantType: 'linode', @@ -129,7 +121,6 @@ export const LinodeEntityDetail = (props: Props) => { body={ { const { encryptionStatus, - firewalls, gbRAM, gbStorage, interfaceGeneration, @@ -111,13 +105,6 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => { vpcLinodeIsAssignedTo, } = props; - const location = useLocation(); - const history = useHistory(); - - const openUpgradeInterfacesDialog = () => { - history.replace(`${location.pathname}/upgrade-interfaces`); - }; - const { data: profile } = useProfile(); const { data: maskSensitiveDataPreference } = usePreferences( @@ -130,22 +117,9 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => { const { isDiskEncryptionFeatureEnabled } = useIsDiskEncryptionFeatureEnabled(); - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const isLinodeInterface = interfaceGeneration === 'linode'; const vpcIPv4 = getVPCIPv4(interfaceWithVPC); - const { canUpgradeInterfaces, unableToUpgradeReasons } = - useCanUpgradeInterfaces(linodeLkeClusterId, region, interfaceGeneration); - - const unableToUpgradeTooltipText = getUnableToUpgradeTooltipText( - unableToUpgradeReasons - ); - - // Take the first firewall to display. Linodes with legacy config interfaces can only be assigned to one firewall (currently). We'll only display - // the attached firewall for Linodes with legacy config interfaces - Linodes with new Linode interfaces can be associated with multiple firewalls - // since each interface can have a firewall. - const attachedFirewall = firewalls.length > 0 ? firewalls[0] : undefined; - // @TODO LDE: Remove usages of this variable once LDE is fully rolled out (being used to determine formatting adjustments currently) const isDisplayingEncryptedStatus = isDiskEncryptionFeatureEnabled && Boolean(encryptionStatus); @@ -409,118 +383,20 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
)} - {(linodeLkeClusterId || - attachedFirewall || - isLinodeInterfacesEnabled) && ( - - {linodeLkeClusterId && ( - - LKE Cluster:{' '} - - {cluster?.label ?? `${linodeLkeClusterId}`} - -   - {cluster ? `(ID: ${linodeLkeClusterId})` : undefined} - - )} - {!isLinodeInterface && attachedFirewall && ( - - Firewall:{' '} - - {attachedFirewall.label ?? `${attachedFirewall.id}`} - -   - {attachedFirewall && `(ID: ${attachedFirewall.id})`} - - )} - {isLinodeInterfacesEnabled && ( - - Interfaces:{' '} - {isLinodeInterface ? ( - 'Linode' - ) : ( - - Configuration Profile - - - ({ - backgroundColor: theme.color.tagButtonBg, - color: theme.tokens.color.Neutrals[80], - marginLeft: theme.spacingFunction(12), - })} - /> - - {!canUpgradeInterfaces && unableToUpgradeTooltipText && ( - - )} - - - )} - - )} - + {isLinodeInterface ? ( + + ) : ( + )} ); diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailRowConfigFirewall.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailRowConfigFirewall.tsx new file mode 100644 index 00000000000..5f0b81b1373 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailRowConfigFirewall.tsx @@ -0,0 +1,163 @@ +import { useLinodeFirewallsQuery } from '@linode/queries'; +import { Box, Chip, Tooltip, TooltipIcon, useTheme } from '@linode/ui'; +import Grid from '@mui/material/Grid'; +import * as React from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { useCanUpgradeInterfaces } from 'src/hooks/useCanUpgradeInterfaces'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + +import { + StyledBox, + StyledLabelBox, + StyledListItem, +} from './LinodeEntityDetail.styles'; +import { + FirewallCell, + LKEClusterCell, +} from './LinodeEntityDetailRowInterfaceFirewall'; +import { DEFAULT_UPGRADE_BUTTON_HELPER_TEXT } from './LinodesDetail/LinodeConfigs/LinodeConfigs'; +import { getUnableToUpgradeTooltipText } from './LinodesDetail/LinodeConfigs/UpgradeInterfaces/utils'; + +import type { + InterfaceGenerationType, + KubernetesCluster, +} from '@linode/api-v4'; + +interface Props { + cluster: KubernetesCluster | undefined; + interfaceGeneration: InterfaceGenerationType | undefined; + linodeId: number; + linodeLkeClusterId: null | number; + region: string; +} + +export const LinodeEntityDetailRowConfigFirewall = (props: Props) => { + const { cluster, linodeId, linodeLkeClusterId, interfaceGeneration, region } = + props; + + const location = useLocation(); + const history = useHistory(); + const theme = useTheme(); + + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const { data: attachedFirewallData } = useLinodeFirewallsQuery( + linodeId, + interfaceGeneration !== 'linode' + ); + const attachedFirewalls = attachedFirewallData?.data ?? []; + const attachedFirewall = + attachedFirewalls.find((firewall) => firewall.status === 'enabled') ?? + (attachedFirewalls.length > 0 ? attachedFirewalls[0] : undefined); + + const { canUpgradeInterfaces, unableToUpgradeReasons } = + useCanUpgradeInterfaces(linodeLkeClusterId, region, interfaceGeneration); + + const unableToUpgradeTooltipText = getUnableToUpgradeTooltipText( + unableToUpgradeReasons + ); + + const openUpgradeInterfacesDialog = () => { + history.replace(`${location.pathname}/upgrade-interfaces`); + }; + + if (!isLinodeInterfacesEnabled && !linodeLkeClusterId && !attachedFirewall) { + return null; + } + + return ( + + {(linodeLkeClusterId || attachedFirewall) && ( + + {linodeLkeClusterId && ( + + )} + {attachedFirewall && ( + + )} + + )} + {isLinodeInterfacesEnabled && ( + + Interfaces:{' '} + + Configuration Profile + + + ({ + backgroundColor: theme.color.tagButtonBg, + color: theme.tokens.color.Neutrals[80], + marginLeft: theme.spacingFunction(12), + })} + /> + + {!canUpgradeInterfaces && unableToUpgradeTooltipText && ( + + )} + + + + )} + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailRowInterfaceFirewall.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailRowInterfaceFirewall.tsx new file mode 100644 index 00000000000..ad3842fb8e5 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailRowInterfaceFirewall.tsx @@ -0,0 +1,179 @@ +import { + linodeQueries, + useLinodeInterfacesQuery, + useQueries, +} from '@linode/queries'; +import { useTheme } from '@mui/material'; +import Grid from '@mui/material/Grid'; +import * as React from 'react'; + +import { Link } from 'src/components/Link'; + +import { + StyledBox, + StyledLabelBox, + StyledListItem, +} from './LinodeEntityDetail.styles'; +import { getLinodeInterfaceType } from './LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities'; + +import type { LinodeInterfaceType } from './LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities'; +import type { Firewall, KubernetesCluster } from '@linode/api-v4'; +import type { SxProps } from '@mui/material'; + +interface Props { + cluster: KubernetesCluster | undefined; + linodeId: number; + linodeLkeClusterId: null | number; +} + +export const LinodeEntityDetailRowInterfaceFirewall = (props: Props) => { + const { cluster, linodeId, linodeLkeClusterId } = props; + + const theme = useTheme(); + + const { data: linodeInterfaces } = useLinodeInterfacesQuery(linodeId); + + const nonVlanInterfaces = + linodeInterfaces?.interfaces.filter((iface) => !iface.vlan) ?? []; + + const interfaceFirewalls = useQueries({ + queries: nonVlanInterfaces.map( + (iface) => + linodeQueries.linode(linodeId)._ctx.interfaces._ctx.interface(iface.id) + ._ctx.firewalls + ), + combine(result) { + return result.reduce>( + (acc, res, index) => { + if (res.data) { + const firewalls = res.data.data; + const shownFirewall = + (firewalls.find((firewall) => firewall.status === 'enabled') ?? + firewalls.length > 0) + ? firewalls[0] + : undefined; + const iface = nonVlanInterfaces[index]; + acc[getLinodeInterfaceType(iface)] = shownFirewall; + } + return acc; + }, + { VPC: undefined, Public: undefined, VLAN: undefined } + ); + }, + }); + + const publicInterfaceFirewall = interfaceFirewalls.Public; + const vpcInterfaceFirewall = interfaceFirewalls.VPC; + + return ( + + + {linodeLkeClusterId && ( + + )} + {publicInterfaceFirewall && ( + + )} + {vpcInterfaceFirewall && ( + + )} + + Interfaces: Linode + + + + ); +}; + +export const LKEClusterCell = ({ + hideLKECellRightBorder, + cluster, + linodeLkeClusterId, +}: { + cluster: KubernetesCluster | undefined; + hideLKECellRightBorder: boolean; + linodeLkeClusterId: number; +}) => { + return ( + + LKE Cluster:{' '} + + {cluster?.label ?? `${linodeLkeClusterId}`} + +   + {cluster ? `(ID: ${linodeLkeClusterId})` : undefined} + + ); +}; + +export const FirewallCell = ({ + additionalSx, + cellLabel, + firewall, + hidePaddingLeft, +}: { + additionalSx?: SxProps; + cellLabel: string; + firewall: Firewall; + hidePaddingLeft: boolean; +}) => { + return ( + + {cellLabel}{' '} + + {firewall.label} + +   + {`(ID: ${firewall.id})`} + + ); +}; From d119b1d65838d623ec87692bec7ab4733334165c Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Tue, 27 May 2025 14:24:09 -0400 Subject: [PATCH 45/59] change: [M3-9687] - Prepare LKE APL for GA (#12268) * initial logic * Added changeset: Update LKE flows for APL General Availability --- .../pr-12268-changed-1747968191432.md | 5 +++ .../e2e/core/kubernetes/lke-create.spec.ts | 5 ++- .../manager/src/dev-tools/FeatureFlagTool.tsx | 1 + packages/manager/src/featureFlags.ts | 1 + .../CreateCluster/ApplicationPlatform.tsx | 25 +++++++------- .../CreateCluster/CreateCluster.tsx | 19 ++++++----- .../KubernetesClusterDetail.tsx | 5 ++- .../src/features/Kubernetes/kubeUtils.ts | 34 +++++++++++++++---- 8 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 packages/manager/.changeset/pr-12268-changed-1747968191432.md diff --git a/packages/manager/.changeset/pr-12268-changed-1747968191432.md b/packages/manager/.changeset/pr-12268-changed-1747968191432.md new file mode 100644 index 00000000000..9c8e4615986 --- /dev/null +++ b/packages/manager/.changeset/pr-12268-changed-1747968191432.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update LKE flows for APL General Availability ([#12268](https://github.com/linode/manager/pull/12268)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 3275b515699..9c4438e070d 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -426,9 +426,8 @@ describe('LKE Cluster Creation with APL enabled', () => { nanodeType, ]; mockAppendFeatureFlags({ - apl: { - enabled: true, - }, + apl: true, + aplGeneralAvailability: false, }).as('getFeatureFlags'); mockGetAccountBeta({ description: diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index baab8e30e64..75fb525728f 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -24,6 +24,7 @@ const options: { flag: keyof Flags; label: string }[] = [ { flag: 'aclpIntegration', label: 'ACLP Integration' }, { flag: 'aclpLogs', label: 'ACLP Logs' }, { flag: 'apl', label: 'Akamai App Platform' }, + { flag: 'aplGeneralAvailability', label: 'Akamai App Platform GA' }, { flag: 'blockStorageEncryption', label: 'Block Storage Encryption (BSE)' }, { flag: 'disableLargestGbPlans', label: 'Disable Largest GB Plans' }, { flag: 'gecko2', label: 'Gecko' }, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index f718a289ddd..aef1233905b 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -116,6 +116,7 @@ export interface Flags { apicliButtonCopy: string; apiMaintenance: APIMaintenance; apl: boolean; + aplGeneralAvailability: boolean; blockStorageEncryption: boolean; cloudManagerDesignUpdatesBanner: DesignUpdatesBannerFlag; databaseAdvancedConfig: boolean; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx index f8355900fe8..41d835fff4c 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx @@ -11,7 +11,7 @@ import * as React from 'react'; import { FormLabel } from 'src/components/FormLabel'; import { Link } from 'src/components/Link'; - +import { useAPLAvailability } from 'src/features/Kubernetes/kubeUtils'; export interface APLProps { isSectionDisabled: boolean; setAPL: (apl: boolean) => void; @@ -28,17 +28,16 @@ export const APLCopy = () => ( ); -const APL_UNSUPPORTED_CHIP_COPY = ' - COMING SOON'; - export const ApplicationPlatform = (props: APLProps) => { const { isSectionDisabled, setAPL, setHighAvailability } = props; - + const { isAPLGeneralAvailability } = useAPLAvailability(); const [isAPLChecked, setIsAPLChecked] = React.useState( isSectionDisabled ? false : undefined ); const [isAPLNotChecked, setIsAPLNotChecked] = React.useState< boolean | undefined >(isSectionDisabled ? true : undefined); + const APL_UNSUPPORTED_CHIP_COPY = `${!isAPLGeneralAvailability ? ' - ' : ''}${isSectionDisabled ? 'COMING SOON' : ''}`; /** * Reset the radio buttons to the correct default state once the user toggles cluster tiers. @@ -48,7 +47,7 @@ export const ApplicationPlatform = (props: APLProps) => { setIsAPLNotChecked(isSectionDisabled ? true : undefined); }, [isSectionDisabled]); - const CHIP_COPY = `BETA${isSectionDisabled ? APL_UNSUPPORTED_CHIP_COPY : ''}`; + const CHIP_COPY = `${!isAPLGeneralAvailability ? 'BETA' : ''}${isSectionDisabled ? APL_UNSUPPORTED_CHIP_COPY : ''}`; const handleChange = (e: React.ChangeEvent) => { setAPL(e.target.value === 'yes'); @@ -69,13 +68,15 @@ export const ApplicationPlatform = (props: APLProps) => { > Akamai App Platform - + {(!isAPLGeneralAvailability || isSectionDisabled) && ( + + )} diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index a800d448e18..e471df521b4 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -35,6 +35,7 @@ import { getLatestVersion, useAPLAvailability, useIsLkeEnterpriseEnabled, + useKubernetesBetaEndpoint, useLkeStandardOrEnterpriseVersions, } from 'src/features/Kubernetes/kubeUtils'; import { useFlags } from 'src/hooks/useFlags'; @@ -104,13 +105,14 @@ export const CreateCluster = () => { const { mutateAsync: updateAccountAgreements } = useMutateAccountAgreements(); const [highAvailability, setHighAvailability] = React.useState(); const [controlPlaneACL, setControlPlaneACL] = React.useState(false); - const [apl_enabled, setApl_enabled] = React.useState(false); + const [aplEnabled, setAplEnabled] = React.useState(false); const { data, error: regionsError } = useRegionsQuery(); const regionsData = data ?? []; const history = useHistory(); const { data: account } = useAccount(); const { showAPL } = useAPLAvailability(); + const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); const { showHighAvailability } = getKubeHighAvailability(account); const { showControlPlaneACL } = getKubeControlPlaneACL(account); const [ipV4Addr, setIPv4Addr] = React.useState([ @@ -272,17 +274,16 @@ export const CreateCluster = () => { }; if (isAPLSupported) { - payload = { ...payload, apl_enabled }; + payload = { ...payload, apl_enabled: aplEnabled }; } if (isLkeEnterpriseLAFeatureEnabled) { payload = { ...payload, tier: selectedTier }; } - const createClusterFn = - isAPLSupported || isLkeEnterpriseLAFeatureEnabled - ? createKubernetesClusterBeta - : createKubernetesCluster; + const createClusterFn = isUsingBetaEndpoint + ? createKubernetesClusterBeta + : createKubernetesCluster; // Since ACL is enabled by default for LKE-E clusters, run validation on the ACL IP Address fields if the acknowledgement is not explicitly checked. if (selectedTier === 'enterprise' && !isACLAcknowledgementChecked) { @@ -498,7 +499,7 @@ export const CreateCluster = () => { @@ -519,7 +520,7 @@ export const CreateCluster = () => { ? UNKNOWN_PRICE : highAvailabilityPrice } - isAPLEnabled={apl_enabled} + isAPLEnabled={aplEnabled} isErrorKubernetesTypes={isErrorKubernetesTypes} isLoadingKubernetesTypes={isLoadingKubernetesTypes} selectedRegionId={selectedRegion?.id} @@ -568,7 +569,7 @@ export const CreateCluster = () => { addNodePool={(pool: KubeNodePoolResponse) => addPool(pool)} apiError={errorMap.node_pools} hasSelectedRegion={hasSelectedRegion} - isAPLEnabled={apl_enabled} + isAPLEnabled={aplEnabled} isPlanPanelDisabled={isPlanPanelDisabled} isSelectedRegionEligibleForPlan={isSelectedRegionEligibleForPlan} regionsData={regionsData} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx index 58b61971281..965b539b856 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx @@ -6,11 +6,11 @@ import { useLocation, useParams } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; -import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { - getKubeHighAvailability, useAPLAvailability, + useKubernetesBetaEndpoint, } from 'src/features/Kubernetes/kubeUtils'; +import { getKubeHighAvailability } from 'src/features/Kubernetes/kubeUtils'; import { useKubernetesClusterMutation, useKubernetesClusterQuery, @@ -29,7 +29,6 @@ export const KubernetesClusterDetail = () => { const id = Number(clusterID); const location = useLocation(); const { showAPL } = useAPLAvailability(); - const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); const { diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.ts b/packages/manager/src/features/Kubernetes/kubeUtils.ts index 953bb118938..2d2aa099c7f 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.ts @@ -187,16 +187,30 @@ export const getKubeHighAvailability = ( export const useAPLAvailability = () => { const flags = useFlags(); + const isAPLEnabled = Boolean(flags.apl); + const isAPLGeneralAvailability = Boolean(flags.aplGeneralAvailability); - // Only fetch the account beta if the APL flag is enabled + // Only fetch the account beta if: + // 1. the APL flag is enabled + // 2. we're not in GA const { data: beta, isLoading } = useAccountBetaQuery( 'apl', - Boolean(flags.apl) + isAPLEnabled && !isAPLGeneralAvailability ); - const showAPL = beta !== undefined && getBetaStatus(beta) === 'active'; + // In order to show the APL panel, we either: + // 1. Confirm the user is in the beta group (and the APL flag is enabled) + // or + // 2. Are in GA (which supersedes the beta group check and the APL flag) + const showAPL = + (beta !== undefined && getBetaStatus(beta) === 'active') || + isAPLGeneralAvailability; - return { isLoading: flags.apl && isLoading, showAPL }; + return { + isLoading: isAPLEnabled && isLoading, + showAPL, + isAPLGeneralAvailability, + }; }; export const getKubeControlPlaneACL = ( @@ -334,9 +348,17 @@ export const useLkeStandardOrEnterpriseVersions = ( }; export const useKubernetesBetaEndpoint = () => { - const { isLoading: isAPLAvailabilityLoading, showAPL } = useAPLAvailability(); + const { + isLoading: isAPLAvailabilityLoading, + showAPL, + isAPLGeneralAvailability, + } = useAPLAvailability(); const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); - const isUsingBetaEndpoint = showAPL || isLkeEnterpriseLAFeatureEnabled; + // Use beta endpoint if either: + // 1. LKE Enterprise is enabled + // 2. APL is supported but not in GA + const isUsingBetaEndpoint = + (showAPL && !isAPLGeneralAvailability) || isLkeEnterpriseLAFeatureEnabled; return { isAPLAvailabilityLoading, From 18189806b675982231bfe919cbccabb65f46cbd2 Mon Sep 17 00:00:00 2001 From: bill-akamai Date: Tue, 27 May 2025 14:31:17 -0400 Subject: [PATCH 46/59] fix: [M3-9978] - Linode Create flow global error looks broken (#12276) * Remove Paper wrapper * Added changeset: LinodeCreateError notice not spanning full width --- .../.changeset/pr-12276-fixed-1748030613590.md | 5 +++++ .../features/Linodes/LinodeCreate/Error.tsx | 18 ++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 packages/manager/.changeset/pr-12276-fixed-1748030613590.md diff --git a/packages/manager/.changeset/pr-12276-fixed-1748030613590.md b/packages/manager/.changeset/pr-12276-fixed-1748030613590.md new file mode 100644 index 00000000000..e8f51773580 --- /dev/null +++ b/packages/manager/.changeset/pr-12276-fixed-1748030613590.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +LinodeCreateError notice not spanning full width ([#12276](https://github.com/linode/manager/pull/12276)) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Error.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Error.tsx index c59a271021c..28734e10c95 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Error.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Error.tsx @@ -1,4 +1,4 @@ -import { Notice, Paper } from '@linode/ui'; +import { Notice } from '@linode/ui'; import React from 'react'; import { useFormContext } from 'react-hook-form'; @@ -20,14 +20,12 @@ export const LinodeCreateError = () => { } return ( - - - - - + + + ); }; From ca22f7fae6c6d29660400237a47cea8ac6b8cf66 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 27 May 2025 21:07:13 -0400 Subject: [PATCH 47/59] chore: [M3-10045] - Mark event read endpoint as deprecated (#12274) * mark `markEventRea` as deprecated * clean up comments * Added changeset: Mark `markEventRead` as deprecated * Update packages/api-v4/src/account/events.ts Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> --------- Co-authored-by: Banks Nussman Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> --- .../api-v4/.changeset/pr-12274-changed-1748026726555.md | 5 +++++ packages/api-v4/src/account/events.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 packages/api-v4/.changeset/pr-12274-changed-1748026726555.md diff --git a/packages/api-v4/.changeset/pr-12274-changed-1748026726555.md b/packages/api-v4/.changeset/pr-12274-changed-1748026726555.md new file mode 100644 index 00000000000..c9888ee0a66 --- /dev/null +++ b/packages/api-v4/.changeset/pr-12274-changed-1748026726555.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Mark `markEventRead` as deprecated ([#12274](https://github.com/linode/manager/pull/12274)) diff --git a/packages/api-v4/src/account/events.ts b/packages/api-v4/src/account/events.ts index 1cf4934a3f3..4f8c8d21e4b 100644 --- a/packages/api-v4/src/account/events.ts +++ b/packages/api-v4/src/account/events.ts @@ -33,7 +33,7 @@ export const getEvent = (eventId: number) => /** * markEventSeen * - * Set the "seen" property of an event to true + * Marks all events up to and including the referenced event ID as "seen" * * @param eventId { number } ID of the event to designate as seen */ @@ -50,6 +50,10 @@ export const markEventSeen = (eventId: number) => * * @param eventId { number } ID of the event to designate as read * + * @deprecated As of `5/20/2025`, this endpoint is deprecated. It will be sunset on `6/17/2025`. + * + * If you depend on using `read`, you may be able to use `markEventSeen` and `seen` instead. + * Please note that the `seen` endpoint functions differently and will mark all events up to and including the referenced event ID as "seen" rather than individual events. */ export const markEventRead = (eventId: number) => Request<{}>( From 59de7f83b0e990157ee4c3a6469fce1b07402d38 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 28 May 2025 09:00:03 -0400 Subject: [PATCH 48/59] chore: [M3-10032] - Stop MSW and DevTools from existing in production bundles (#12263) * fix * clean up * clean up * revert some extra changes * I think this is safe to clean up? * revert change affecting storybook * Added changeset: Stop MSW and DevTools from existing in production bundles --------- Co-authored-by: Banks Nussman --- .../pr-12263-tech-stories-1747925853379.md | 5 + packages/manager/src/constants.ts | 7 +- packages/manager/src/dev-tools/DevTools.tsx | 118 ++++++++---------- packages/manager/src/dev-tools/load.ts | 21 +--- packages/manager/src/index.tsx | 38 +++--- packages/manager/src/utilities/storage.ts | 4 +- packages/utilities/src/helpers/index.ts | 1 - .../utilities/src/helpers/rootManager.test.ts | 39 ------ packages/utilities/src/helpers/rootManager.ts | 20 --- 9 files changed, 88 insertions(+), 165 deletions(-) create mode 100644 packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md delete mode 100644 packages/utilities/src/helpers/rootManager.test.ts delete mode 100644 packages/utilities/src/helpers/rootManager.ts diff --git a/packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md b/packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md new file mode 100644 index 00000000000..926cbd5fc9c --- /dev/null +++ b/packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Stop MSW and DevTools from existing in production bundles ([#12263](https://github.com/linode/manager/pull/12263)) diff --git a/packages/manager/src/constants.ts b/packages/manager/src/constants.ts index 3f3ff81983d..f6b29323012 100644 --- a/packages/manager/src/constants.ts +++ b/packages/manager/src/constants.ts @@ -7,9 +7,10 @@ import { getBooleanEnv } from '@linode/utilities'; export const isProductionBuild = import.meta.env.PROD; // allow us to explicity enable dev tools -export const ENABLE_DEV_TOOLS = getBooleanEnv( - import.meta.env.REACT_APP_ENABLE_DEV_TOOLS -); +export const ENABLE_DEV_TOOLS = + import.meta.env.REACT_APP_ENABLE_DEV_TOOLS === undefined + ? import.meta.env.DEV + : getBooleanEnv(import.meta.env.REACT_APP_ENABLE_DEV_TOOLS); // allow us to explicity enable maintenance mode export const ENABLE_MAINTENANCE_MODE = diff --git a/packages/manager/src/dev-tools/DevTools.tsx b/packages/manager/src/dev-tools/DevTools.tsx index 0627f8bf657..c3454d40852 100644 --- a/packages/manager/src/dev-tools/DevTools.tsx +++ b/packages/manager/src/dev-tools/DevTools.tsx @@ -1,5 +1,4 @@ import { CloseIcon } from '@linode/ui'; -import { getRoot } from '@linode/utilities'; import Handyman from '@mui/icons-material/Handyman'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { styled } from '@mui/material'; @@ -27,66 +26,71 @@ const reactQueryDevtoolsStyle = { width: '100%', }; -export const install = (store: ApplicationStore, queryClient: QueryClient) => { - const DevTools = () => { - const [isOpen, setIsOpen] = React.useState(false); - const [isDraggable, setIsDraggable] = React.useState(false); - const [view, setView] = React.useState('mocks'); - const devToolsMainRef = React.useRef(null); +interface Props { + queryClient: QueryClient; + store: ApplicationStore; +} - const handleOpenReactQuery = () => { - setView('react-query'); - }; +export const DevTools = (props: Props) => { + const [isOpen, setIsOpen] = React.useState(false); + const [isDraggable, setIsDraggable] = React.useState(false); + const [view, setView] = React.useState('mocks'); + const devToolsMainRef = React.useRef(null); - const handleOpenMocks = () => { - setView('mocks'); - }; + const handleOpenReactQuery = () => { + setView('react-query'); + }; - const handleOpenDesignTokens = () => { - setView('design-tokens'); - }; + const handleOpenMocks = () => { + setView('mocks'); + }; - const handleDraggableToggle = () => { - setIsDraggable(!isDraggable); - if (isDraggable) { - setIsOpen(false); - } - }; + const handleOpenDesignTokens = () => { + setView('design-tokens'); + }; - const handleGoToPreferences = () => { - window.location.assign('/profile/settings?preferenceEditor=true'); - }; + const handleDraggableToggle = () => { + setIsDraggable(!isDraggable); + if (isDraggable) { + setIsOpen(false); + } + }; - React.useEffect(() => { - // Prevent scrolling of the window when scrolling start/end of the dev tools - // Particularly useful when in draggable mode - if (!isDraggable) { + const handleGoToPreferences = () => { + window.location.assign('/profile/settings?preferenceEditor=true'); + }; + + React.useEffect(() => { + // Prevent scrolling of the window when scrolling start/end of the dev tools + // Particularly useful when in draggable mode + if (!isDraggable) { + return; + } + + const handleWheel = (e: WheelEvent) => { + if (!devToolsMainRef.current?.contains(e.target as Node)) { return; } - const handleWheel = (e: WheelEvent) => { - if (!devToolsMainRef.current?.contains(e.target as Node)) { - return; - } + const target = devToolsMainRef.current; + const isAtTop = target.scrollTop === 0; + const isAtBottom = + target.scrollHeight - target.clientHeight <= target.scrollTop + 1; - const target = devToolsMainRef.current; - const isAtTop = target.scrollTop === 0; - const isAtBottom = - target.scrollHeight - target.clientHeight <= target.scrollTop + 1; - - if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) { - e.preventDefault(); - } - }; + if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) { + e.preventDefault(); + } + }; - window.addEventListener('wheel', handleWheel, { passive: false }); + window.addEventListener('wheel', handleWheel, { passive: false }); - return () => { - window.removeEventListener('wheel', handleWheel); - }; - }, []); + return () => { + window.removeEventListener('wheel', handleWheel); + }; + }, []); - return ( + return ( +
{ - + @@ -182,22 +186,6 @@ export const install = (store: ApplicationStore, queryClient: QueryClient) => {
- ); - }; - - const devToolsRoot = - document.getElementById('dev-tools-root') || - (() => { - const newRoot = document.createElement('div'); - newRoot.id = 'dev-tools-root'; - document.body.appendChild(newRoot); - return newRoot; - })(); - - const root = getRoot(devToolsRoot); - root.render( - - ); }; diff --git a/packages/manager/src/dev-tools/load.ts b/packages/manager/src/dev-tools/load.ts index a796e64717b..489e4989c09 100644 --- a/packages/manager/src/dev-tools/load.ts +++ b/packages/manager/src/dev-tools/load.ts @@ -1,4 +1,3 @@ -import { ENABLE_DEV_TOOLS } from 'src/constants'; import { mswDB } from 'src/mocks/indexedDB'; import { resolveMockPreset } from 'src/mocks/mockPreset'; import { createInitialMockStore, emptyStore } from 'src/mocks/mockState'; @@ -12,9 +11,7 @@ import { isMSWEnabled, } from './utils'; -import type { QueryClient } from '@tanstack/react-query'; import type { MockPresetExtra, MockSeeder, MockState } from 'src/mocks/types'; -import type { ApplicationStore } from 'src/store'; export let mockState: MockState; @@ -24,12 +21,7 @@ export let mockState: MockState; * * @param store Redux store to control */ -export async function loadDevTools( - store: ApplicationStore, - client: QueryClient -) { - const devTools = await import('./DevTools'); - +export async function loadDevTools() { if (isMSWEnabled) { const { worker: mswWorker } = await import('../mocks/mswWorkers'); const mswPresetId = getBaselinePreset() ?? defaultBaselineMockPreset.id; @@ -163,15 +155,4 @@ export async function loadDevTools( const worker = mswWorker(extraHandlers, baseHandlers); await worker.start({ onUnhandledRequest: 'bypass' }); } - - devTools.install(store, client); } - -/** - * Defaults to `true` for development - * Default to `false` in production builds - * - * Define `REACT_APP_ENABLE_DEV_TOOLS` to explicitly enable or disable dev tools - */ -export const shouldLoadDevTools = - ENABLE_DEV_TOOLS !== undefined ? ENABLE_DEV_TOOLS : import.meta.env.DEV; diff --git a/packages/manager/src/index.tsx b/packages/manager/src/index.tsx index f32238e29da..a94eb33ec81 100644 --- a/packages/manager/src/index.tsx +++ b/packages/manager/src/index.tsx @@ -1,30 +1,25 @@ import { queryClientFactory } from '@linode/queries'; -import { getRoot } from '@linode/utilities'; import CssBaseline from '@mui/material/CssBaseline'; import { QueryClientProvider } from '@tanstack/react-query'; -import * as React from 'react'; +import React from 'react'; +import { createRoot } from 'react-dom/client'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { Route, BrowserRouter as Router, Switch } from 'react-router-dom'; import { CookieWarning } from 'src/components/CookieWarning'; import { Snackbar } from 'src/components/Snackbar/Snackbar'; -import { SplashScreen } from 'src/components/SplashScreen'; import 'src/exceptionReporting'; +import { SplashScreen } from 'src/components/SplashScreen'; import Logout from 'src/layouts/Logout'; import { setupInterceptors } from 'src/request'; import { storeFactory } from 'src/store'; import { App } from './App'; import NullComponent from './components/NullComponent'; -import { loadDevTools, shouldLoadDevTools } from './dev-tools/load'; import './index.css'; +import { ENABLE_DEV_TOOLS } from './constants'; import { LinodeThemeWrapper } from './LinodeThemeWrapper'; -const queryClient = queryClientFactory('longLived'); -const store = storeFactory(); - -setupInterceptors(store); - const Lish = React.lazy(() => import('src/features/Lish')); const CancelLanding = React.lazy(() => @@ -38,6 +33,11 @@ const LoginAsCustomerCallback = React.lazy( ); const OAuthCallbackPage = React.lazy(() => import('src/layouts/OAuth')); +const queryClient = queryClientFactory('longLived'); +const store = storeFactory(); + +setupInterceptors(store); + const Main = () => { if (!navigator.cookieEnabled) { return ; @@ -89,15 +89,23 @@ const Main = () => { }; async function loadApp() { - if (shouldLoadDevTools) { - await loadDevTools(store, queryClient); + if (ENABLE_DEV_TOOLS) { + const devTools = await import('./dev-tools/load'); + await devTools.loadDevTools(); + + const { DevTools } = await import('./dev-tools/DevTools'); + + const devToolsRootContainer = document.createElement('div'); + devToolsRootContainer.id = 'dev-tools-root'; + document.body.appendChild(devToolsRootContainer); + + const root = createRoot(devToolsRootContainer); + + root.render(); } const container = document.getElementById('root'); - if (container) { - const root = getRoot(container); - root.render(
); - } + createRoot(container!).render(
); } loadApp(); diff --git a/packages/manager/src/utilities/storage.ts b/packages/manager/src/utilities/storage.ts index c93a41782fd..798fc97b2aa 100644 --- a/packages/manager/src/utilities/storage.ts +++ b/packages/manager/src/utilities/storage.ts @@ -1,4 +1,4 @@ -import { shouldLoadDevTools } from 'src/dev-tools/load'; +import { ENABLE_DEV_TOOLS } from 'src/constants'; import type { RegionSite } from '@linode/api-v4'; import type { StackScriptPayload } from '@linode/api-v4/lib/stackscripts/types'; @@ -244,7 +244,7 @@ export const { export const getEnvLocalStorageOverrides = () => { // This is broken into two logical branches so that local storage is accessed // ONLY if the dev tools are enabled and it's a development build. - if (shouldLoadDevTools && import.meta.env.DEV) { + if (ENABLE_DEV_TOOLS && import.meta.env.DEV) { const localStorageOverrides = storage.devToolsEnv.get(); if (localStorageOverrides) { return localStorageOverrides; diff --git a/packages/utilities/src/helpers/index.ts b/packages/utilities/src/helpers/index.ts index 732c3b9ac29..b8c74575725 100644 --- a/packages/utilities/src/helpers/index.ts +++ b/packages/utilities/src/helpers/index.ts @@ -46,7 +46,6 @@ export * from './redactAccessToken'; export * from './reduceAsync'; export * from './regions'; export * from './replaceNewlinesWithLineBreaks'; -export * from './rootManager'; export * from './roundTo'; export * from './scrollErrorIntoView'; export * from './scrollErrorIntoViewV2'; diff --git a/packages/utilities/src/helpers/rootManager.test.ts b/packages/utilities/src/helpers/rootManager.test.ts deleted file mode 100644 index 4cb1fc6bf06..00000000000 --- a/packages/utilities/src/helpers/rootManager.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createRoot } from 'react-dom/client'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { getRoot, rootInstances } from './rootManager'; - -vi.mock('react-dom/client', () => ({ - createRoot: vi.fn().mockImplementation((container) => ({ - _internalRoot: container, // Mock implementation detail - render: vi.fn(), - })), -})); - -describe('getRoot', () => { - beforeEach(() => { - vi.clearAllMocks(); - rootInstances.clear(); - }); - - it('should create a new root for a new container', () => { - const container = document.createElement('div'); - const root = getRoot(container); - - expect(createRoot).toHaveBeenCalledWith(container); - expect(rootInstances.get(container)).toBe(root); - expect(createRoot).toHaveBeenCalledTimes(1); - }); - - it('should return the existing root for an existing container', () => { - const container = document.createElement('div'); - // Call getRoot twice with the same container - const firstCallRoot = getRoot(container); - const secondCallRoot = getRoot(container); - - // createRoot should only have been called once - expect(createRoot).toHaveBeenCalledTimes(1); - expect(firstCallRoot).toBe(secondCallRoot); - expect(rootInstances.size).toBe(1); - }); -}); diff --git a/packages/utilities/src/helpers/rootManager.ts b/packages/utilities/src/helpers/rootManager.ts deleted file mode 100644 index 267ee9f5a33..00000000000 --- a/packages/utilities/src/helpers/rootManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createRoot } from 'react-dom/client'; -import type { Root } from 'react-dom/client'; - -export const rootInstances = new Map(); - -/** - * This utility helps manage React roots efficiently, - * ensuring that only one root is created per container and allowing reuse of existing roots when possible. - * It's particularly useful in scenarios where you need to dynamically create and manage multiple React roots in an application. (In our case, the APP and DevTools) - */ -export const getRoot = (container: HTMLElement): Root => { - let root = rootInstances.get(container); - - if (!root) { - root = createRoot(container); - rootInstances.set(container, root); - } - - return root; -}; From cbf6d0b9f0c239050c709a8f76b70bdb70a0d9b3 Mon Sep 17 00:00:00 2001 From: mpolotsk-akamai <157619599+mpolotsk-akamai@users.noreply.github.com> Date: Wed, 28 May 2025 17:10:38 +0200 Subject: [PATCH 49/59] feat: [UIE-8731] - Roles Table and Users Table ui fix (#12233) * feat: [UIE-8731] - Roles Table and Users Table ui fix * Added changeset: IAM RBAC: fix UI issues in Users and Roles tabs, including button styling, layout, and permissions toggle * feat: [UIE-8679, UIE-8532] - fix expand/hide issue * feat: [UIE-8679, UIE-8532] - remove sortable from description column * feat: [UIE-8679, UIE-8532] - useCalculateHiddenItems fix * remove fix for expand/hide issue --- ...r-12233-upcoming-features-1747392110404.md | 5 +++ .../manager/src/features/IAM/Roles/Roles.tsx | 3 +- .../IAM/Roles/RolesTable/RolesTable.tsx | 27 +++++++++----- .../Roles/RolesTable/RolesTableActionMenu.tsx | 8 +++- .../IAM/Shared/Permissions/Permissions.tsx | 1 + .../features/IAM/Users/UsersTable/Users.tsx | 37 +++++++------------ 6 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md diff --git a/packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md b/packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md new file mode 100644 index 00000000000..b2f497d57fa --- /dev/null +++ b/packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +IAM RBAC: fix UI issues in Users and Roles tabs, including button styling, layout, and permissions toggle ([#12233](https://github.com/linode/manager/pull/12233)) diff --git a/packages/manager/src/features/IAM/Roles/Roles.tsx b/packages/manager/src/features/IAM/Roles/Roles.tsx index 7717a801027..5b24e26cdcd 100644 --- a/packages/manager/src/features/IAM/Roles/Roles.tsx +++ b/packages/manager/src/features/IAM/Roles/Roles.tsx @@ -1,4 +1,4 @@ -import { CircleProgress, Paper } from '@linode/ui'; +import { CircleProgress, Paper, Typography } from '@linode/ui'; import React from 'react'; import { RolesTable } from 'src/features/IAM/Roles/RolesTable/RolesTable'; @@ -22,6 +22,7 @@ export const RolesLanding = () => { return ( ({ marginTop: theme.tokens.spacing.S16 })}> + Roles ); diff --git a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx index d711b67861d..0eaaec86c99 100644 --- a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx +++ b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx @@ -125,7 +125,10 @@ export const RolesTable = ({ roles }: Props) => { container direction="row" spacing={2} - sx={{ justifyContent: 'space-between' }} + sx={(theme) => ({ + justifyContent: 'space-between', + marginBottom: theme.tokens.spacing.S12, + })} > { > Role Type - handleSort(event, 'description')} - sortable - sorted={sort?.column === 'description' ? sort.order : undefined} - style={{ minWidth: '38%' }} - > + Description @@ -212,7 +210,9 @@ export const RolesTable = ({ roles }: Props) => { {!rows?.length ? ( - No items to display. + + No items to display. + ) : ( rows.map((roleRow) => ( @@ -225,7 +225,9 @@ export const RolesTable = ({ roles }: Props) => { selectable selected={selectedRows.includes(roleRow)} > - + {roleRow.name} @@ -242,7 +244,12 @@ export const RolesTable = ({ roles }: Props) => { )} - + { assignRoleRow(roleRow); diff --git a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTableActionMenu.tsx b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTableActionMenu.tsx index 6a57242ee2e..640202e8ba6 100644 --- a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTableActionMenu.tsx +++ b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTableActionMenu.tsx @@ -8,5 +8,11 @@ interface Props { export const RolesTableActionMenu = ({ onClick }: Props) => { // This menu has evolved over time to where it isn't much of a menu at all, but rather a single action. - return ; + return ( + + ); }; diff --git a/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx b/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx index ca53d0e9d09..16f7308b065 100644 --- a/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx +++ b/packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx @@ -42,6 +42,7 @@ export const Permissions = ({ permissions }: Props) => { }; }, [calculateHiddenItems, handleResize]); + // TODO: update the link for TooltipIcon when it's ready - UIE-8534 return ( Permissions diff --git a/packages/manager/src/features/IAM/Users/UsersTable/Users.tsx b/packages/manager/src/features/IAM/Users/UsersTable/Users.tsx index d164342867d..c609872f099 100644 --- a/packages/manager/src/features/IAM/Users/UsersTable/Users.tsx +++ b/packages/manager/src/features/IAM/Users/UsersTable/Users.tsx @@ -1,6 +1,6 @@ import { useAccountUsers, useProfile } from '@linode/queries'; import { getAPIFilterFromQuery } from '@linode/search'; -import { Box, Button, Paper, Typography } from '@linode/ui'; +import { Button, Paper, Stack, Typography } from '@linode/ui'; import { useMediaQuery } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import React from 'react'; @@ -21,8 +21,6 @@ import { UsersLandingTableHead } from './UsersLandingTableHead'; import type { Filter } from '@linode/api-v4'; -const XS_TO_SM_BREAKPOINT = 475; - export const UsersLanding = () => { const [isCreateDrawerOpen, setIsCreateDrawerOpen] = React.useState(false); @@ -101,24 +99,18 @@ export const UsersLanding = () => { order={order} /> )} - ({ marginTop: theme.spacing(2) })}> - ({ - alignItems: 'center', - display: 'flex', - justifyContent: 'space-between', - marginBottom: theme.spacing(2), - [theme.breakpoints.down(XS_TO_SM_BREAKPOINT)]: { - alignItems: 'flex-start', - flexDirection: 'column', - }, - })} + ({ marginTop: theme.tokens.spacing.S16 })}> + {isProxyUser ? ( ({ [theme.breakpoints.down('md')]: { - marginLeft: theme.spacing(1), + marginLeft: theme.tokens.spacing.S8, }, })} variant="h3" @@ -130,7 +122,7 @@ export const UsersLanding = () => { clearable containerProps={{ sx: { - width: { md: '320px', xs: '100%' }, + width: '320px', }, }} debounceTime={250} @@ -147,12 +139,9 @@ export const UsersLanding = () => { buttonType="primary" disabled={isRestrictedUser} onClick={() => setIsCreateDrawerOpen(true)} - sx={(theme) => ({ - [theme.breakpoints.down(XS_TO_SM_BREAKPOINT)]: { - marginTop: theme.spacing(1), - width: '100%', - }, - })} + sx={{ + maxWidth: '120px', + }} tooltipText={ isRestrictedUser ? 'You cannot create other users as a restricted user.' @@ -161,7 +150,7 @@ export const UsersLanding = () => { > Add a User - +
From c78e24120b62387dd3fb539d2c510afea6a04e2b Mon Sep 17 00:00:00 2001 From: smans-akamai Date: Wed, 28 May 2025 11:22:59 -0400 Subject: [PATCH 50/59] feat: [UIE-8615] - DBaaS - Adding Configure Networking section to Create with Assign VPC functionality (#12281) * feat: [UIE-8615] - DBaaS - Creating new Networking section with VPC for DB Create flow * Adding changesets * Applying feedback to refactor create and summary * Updating changset to upcoming feature since it will be behind feature flag * Adding comment to clean up private_network prop post VPC release --- .../pr-12281-added-1748363215356.md | 5 + packages/api-v4/src/databases/types.ts | 7 + ...r-12281-upcoming-features-1748381133121.md | 5 + .../manager/src/dev-tools/FeatureFlagTool.tsx | 1 + packages/manager/src/featureFlags.ts | 1 + .../DatabaseCreate/DatabaseClusterData.tsx | 2 + .../DatabaseCreate/DatabaseCreate.test.tsx | 11 + .../DatabaseCreate/DatabaseCreate.tsx | 81 ++- .../DatabaseCreateAccessControls.tsx | 20 +- ...baseCreateNetworkingConfiguration.test.tsx | 59 +++ .../DatabaseCreateNetworkingConfiguration.tsx | 58 +++ .../DatabaseSummarySection.test.tsx | 36 +- .../DatabaseCreate/DatabaseSummarySection.tsx | 46 +- .../DatabaseVPCSelector.test.tsx | 489 ++++++++++++++++++ .../DatabaseCreate/DatabaseVPCSelector.tsx | 190 +++++++ .../DatabaseResize/DatabaseResize.tsx | 2 +- .../pr-12281-added-1748363298759.md | 5 + packages/validation/src/databases.schema.ts | 12 + 18 files changed, 1006 insertions(+), 24 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-12281-added-1748363215356.md create mode 100644 packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md create mode 100644 packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx create mode 100644 packages/validation/.changeset/pr-12281-added-1748363298759.md diff --git a/packages/api-v4/.changeset/pr-12281-added-1748363215356.md b/packages/api-v4/.changeset/pr-12281-added-1748363215356.md new file mode 100644 index 00000000000..261cdb0c7d9 --- /dev/null +++ b/packages/api-v4/.changeset/pr-12281-added-1748363215356.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +PrivateNetwork type for Use in DBaaS requests ([#12281](https://github.com/linode/manager/pull/12281)) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 05d0e4076a4..c70d7726d8b 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -127,6 +127,12 @@ export interface DatabaseInstance { export type ClusterSize = 1 | 2 | 3; +export interface PrivateNetwork { + public_access: boolean; + subnet_id: null | number; + vpc_id: null | number; +} + type ReadonlyCount = 0 | 2; /** @deprecated TODO (UIE-8214) remove POST GA */ @@ -139,6 +145,7 @@ export interface CreateDatabasePayload { encrypted?: boolean; engine?: Engine; label: string; + private_network?: null | PrivateNetwork; // TODO (UIE-8831): Remove optional (?) post VPC release, since it will always be in create payload region: string; /** @Deprecated used by rdbms-legacy only */ replication_type?: MySQLReplicationType | PostgresReplicationType; diff --git a/packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md b/packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md new file mode 100644 index 00000000000..931c2cca335 --- /dev/null +++ b/packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Configure Networking section and VPC functionality for DBaaS Create view ([#12281](https://github.com/linode/manager/pull/12281)) diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 75fb525728f..923e1eb366f 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -42,6 +42,7 @@ const options: { flag: keyof Flags; label: string }[] = [ { flag: 'dbaasV2MonitorMetrics', label: 'Databases V2 Monitor' }, { flag: 'databaseResize', label: 'Database Resize' }, { flag: 'databaseAdvancedConfig', label: 'Database Advanced Config' }, + { flag: 'databaseVpc', label: 'Database VPC' }, { flag: 'apicliButtonCopy', label: 'APICLI Button Copy' }, { flag: 'iam', label: 'Identity and Access Beta' }, { diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index aef1233905b..ddccd63a06e 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -123,6 +123,7 @@ export interface Flags { databaseBeta: boolean; databaseResize: boolean; databases: boolean; + databaseVpc: boolean; dbaasV2: BetaFeatureFlag; dbaasV2MonitorMetrics: BetaFeatureFlag; disableLargestGbPlans: boolean; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx index 259ef2f7203..ae6c3ff1b62 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx @@ -17,6 +17,7 @@ import type { ClusterSize, DatabaseEngine, Engine, + PrivateNetwork, Region, } from '@linode/api-v4'; import type { FormikErrors } from 'formik'; @@ -28,6 +29,7 @@ export interface DatabaseCreateValues { cluster_size: ClusterSize; engine: Engine; label: string; + private_network: PrivateNetwork; region: string; type: string; } diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx index 5cf9f444798..e09e01513a4 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx @@ -45,6 +45,17 @@ describe('Database Create', () => { getAllByText('Create Database Cluster'); }); + it('should render VPC content when feature flag is present', async () => { + const { getAllByTestId, getAllByText } = renderWithTheme( + , + { + flags: { databaseVpc: true }, + } + ); + await waitForElementToBeRemoved(getAllByTestId(loadingTestId)); + getAllByText('Configure Networking'); + }); + it('should display the correct node price and disable 3 nodes for 1 GB plans', async () => { const standardTypes = [ databaseTypeFactory.build({ diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index ea01b8ff886..dfd6a4e1865 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -1,7 +1,7 @@ import { useRegionsQuery } from '@linode/queries'; import { CircleProgress, Divider, ErrorState, Notice, Paper } from '@linode/ui'; import { formatStorageUnits, scrollErrorIntoViewV2 } from '@linode/utilities'; -import { createDatabaseSchema } from '@linode/validation/lib/databases.schema'; +import { getDynamicDatabaseSchema } from '@linode/validation/lib/databases.schema'; import Grid from '@mui/material/Grid'; import { createLazyRoute } from '@tanstack/react-router'; import { useFormik } from 'formik'; @@ -24,6 +24,7 @@ import { DatabaseSummarySection } from 'src/features/Databases/DatabaseCreate/Da import { DatabaseLogo } from 'src/features/Databases/DatabaseLanding/DatabaseLogo'; import { enforceIPMasks } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils'; import { typeLabelDetails } from 'src/features/Linodes/presentation'; +import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useCreateDatabaseMutation, @@ -35,11 +36,14 @@ import { validateIPs } from 'src/utilities/ipUtils'; import { ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT } from '../constants'; import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls'; +import { DatabaseCreateNetworkingConfiguration } from './DatabaseCreateNetworkingConfiguration'; +import type { AccessProps } from './DatabaseCreateAccessControls'; import type { ClusterSize, CreateDatabasePayload, Engine, + VPC, } from '@linode/api-v4/lib/databases/types'; import type { APIError } from '@linode/api-v4/lib/types'; import type { PlanSelectionWithDatabaseType } from 'src/features/components/PlansPanel/types'; @@ -71,12 +75,17 @@ const DatabaseCreate = () => { platform: 'rdbms-default', }); + const flags = useFlags(); + const isVPCEnabled = flags.databaseVpc; + const formRef = React.useRef(null); const { mutateAsync: createDatabase } = useCreateDatabaseMutation(); const [createError, setCreateError] = React.useState(); const [ipErrorsFromAPI, setIPErrorsFromAPI] = React.useState(); const [selectedTab, setSelectedTab] = React.useState(0); + const [selectedVPC, setSelectedVPC] = React.useState(null); + const isVPCSelected = Boolean(selectedVPC); const handleIPBlur = (ips: ExtendedIP[]) => { const ipsWithMasks = enforceIPMasks(ips); @@ -118,11 +127,20 @@ const DatabaseCreate = () => { } return accum; }, []); + const hasVpc = + values.private_network.vpc_id && values.private_network.subnet_id; + const privateNetwork = hasVpc ? values.private_network : null; const createPayload: CreateDatabasePayload = { ...values, allow_list: _allow_list, + private_network: privateNetwork, }; + + // TODO (UIE-8831): Remove post VPC release, since it will always be in create payload + if (!isVPCEnabled) { + delete createPayload.private_network; + } try { const response = await createDatabase(createPayload); history.push(`/databases/${response.engine}/${response.id}`); @@ -151,12 +169,18 @@ const DatabaseCreate = () => { label: '', region: '', type: '', + private_network: { + vpc_id: null, + subnet_id: null, + public_access: false, + }, }; const { errors, handleSubmit, isSubmitting, + resetForm, setFieldError, setFieldValue, setSubmitting, @@ -169,7 +193,7 @@ const DatabaseCreate = () => { scrollErrorIntoViewV2(formRef); }, validateOnChange: false, - validationSchema: createDatabaseSchema, + validationSchema: getDynamicDatabaseSchema(isVPCSelected), }); React.useEffect(() => { @@ -215,6 +239,15 @@ const DatabaseCreate = () => { return displayTypes?.find((type) => type.id === values.type); }, [displayTypes, values.type]); + const accessControlsConfiguration: AccessProps = { + disabled: isRestricted, + errors: ipErrorsFromAPI, + ips: values.allow_list, + onBlur: handleIPBlur, + onChange: (ips: ExtendedIP[]) => setFieldValue('allow_list', ips), + variant: isVPCEnabled ? 'networking' : 'standard', + }; + const handleTabChange = (index: number) => { setSelectedTab(index); setFieldValue('type', undefined); @@ -232,6 +265,24 @@ const DatabaseCreate = () => { const handleNodeChange = (size: ClusterSize | undefined) => { setFieldValue('cluster_size', size); }; + + const handleNetworkingConfigurationChange = (vpc: null | VPC) => { + setSelectedVPC(vpc); + }; + + const handleResetForm = (partialValues?: Partial) => { + if (partialValues) { + resetForm({ + values: { + ...values, + ...partialValues, + }, + }); + } else { + resetForm(); + } + }; + return ( <> @@ -309,19 +360,31 @@ const DatabaseCreate = () => { /> - setFieldValue('allow_list', ips)} - /> + {isVPCEnabled ? ( + + setFieldValue(field, value) + } + onNetworkingConfigurationChange={ + handleNetworkingConfigurationChange + } + privateNetworkValues={values.private_network} + resetFormFields={handleResetForm} + selectedRegionId={values.region} + /> + ) : ( + + )} diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx index 935f0cb8c48..da30ca59413 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx @@ -36,17 +36,26 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); export type AccessOption = 'none' | 'specific'; +export type AccessVariant = 'networking' | 'standard'; -interface Props { +export interface AccessProps { disabled?: boolean; errors?: APIError[]; ips: ExtendedIP[]; onBlur: (ips: ExtendedIP[]) => void; onChange: (ips: ExtendedIP[]) => void; + variant?: AccessVariant; } -export const DatabaseCreateAccessControls = (props: Props) => { - const { disabled = false, errors, ips, onBlur, onChange } = props; +export const DatabaseCreateAccessControls = (props: AccessProps) => { + const { + disabled = false, + errors, + ips, + onBlur, + onChange, + variant = 'standard', + } = props; const { classes } = useStyles(); const [accessOption, setAccessOption] = useState('specific'); @@ -59,7 +68,10 @@ export const DatabaseCreateAccessControls = (props: Props) => { return ( - + Manage Access diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.test.tsx new file mode 100644 index 00000000000..a9e7bd4cc0c --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { describe, it, vi } from 'vitest'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DatabaseCreateNetworkingConfiguration } from './DatabaseCreateNetworkingConfiguration'; + +import type { AccessProps } from './DatabaseCreateAccessControls'; +import type { PrivateNetwork } from '@linode/api-v4'; +describe('DatabaseCreateNetworkingConfiguration', () => { + const mockAccessControlConfig: AccessProps = { + disabled: false, + errors: [], + ips: [], + onBlur: vi.fn(), + onChange: vi.fn(), + variant: 'networking', + }; + + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: null, + subnet_id: null, + public_access: false, + }; + + const mockProps = { + accessControlsConfiguration: mockAccessControlConfig, + errors: {}, + onChange: vi.fn(), + onNetworkingConfigurationChange: vi.fn(), + privateNetworkValues: mockPrivateNetwork, + resetFormFields: vi.fn(), + selectedRegionId: 'us-east', + }; + + it('renders the networking configuration heading and description', () => { + const { getByText } = renderWithTheme( + + ); + expect( + getByText('Configure Networking', { + exact: true, + }) + ).toBeInTheDocument(); + expect( + getByText('Configure networking options for the cluster.', { + exact: true, + }) + ).toBeInTheDocument(); + }); + + it('renders DatabaseCreateAccessControls and DatabaseVPCSelector', () => { + const { getByText, getByTestId } = renderWithTheme( + + ); + expect(getByTestId('database-vpc-selector')).toBeInTheDocument(); + expect(getByText('Manage Access')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx new file mode 100644 index 00000000000..917308ee5b1 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx @@ -0,0 +1,58 @@ +import { Typography } from '@linode/ui'; +import * as React from 'react'; + +import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls'; +import { DatabaseVPCSelector } from './DatabaseVPCSelector'; + +import type { DatabaseCreateValues } from './DatabaseClusterData'; +import type { AccessProps } from './DatabaseCreateAccessControls'; +import type { PrivateNetwork, VPC } from '@linode/api-v4'; +import type { Theme } from '@mui/material/styles'; +import type { FormikErrors } from 'formik'; + +interface NetworkingConfigurationProps { + accessControlsConfiguration: AccessProps; + errors: FormikErrors; + onChange: (field: string, value: boolean | null | number) => void; + onNetworkingConfigurationChange: (vpcSelected: null | VPC) => void; + privateNetworkValues: PrivateNetwork; + resetFormFields: (partialValues?: Partial) => void; + selectedRegionId: string; +} + +export const DatabaseCreateNetworkingConfiguration = ( + props: NetworkingConfigurationProps +) => { + const { + accessControlsConfiguration, + errors, + onNetworkingConfigurationChange, + onChange, + selectedRegionId, + resetFormFields, + privateNetworkValues, + } = props; + + return ( + <> + Configure Networking + ({ + marginBottom: theme.spacingFunction(20), + })} + > + Configure networking options for the cluster. + + + + + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.test.tsx index 1938f246c6c..909e632edb4 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.test.tsx @@ -2,7 +2,11 @@ import { waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { databaseFactory, databaseTypeFactory } from 'src/factories'; +import { + databaseFactory, + databaseTypeFactory, + vpcFactory, +} from 'src/factories'; import DatabaseCreate from 'src/features/Databases/DatabaseCreate/DatabaseCreate'; import { DatabaseResize } from 'src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize'; import { makeResourcePage } from 'src/mocks/serverHandlers'; @@ -19,9 +23,10 @@ describe('database summary section', () => { beta: false, enabled: true, }, + databaseVpc: true, }; - it('should render the correct number of node radio buttons, associated costs, and summary', async () => { + it('should render the correct number of node radio buttons, associated costs, vpc label and summary', async () => { const standardTypes = databaseTypeFactory.buildList(7, { class: 'standard', }); @@ -40,10 +45,15 @@ describe('database summary section', () => { return HttpResponse.json( makeResourcePage([...mockDedicatedTypes, ...standardTypes]) ); + }), + http.get('*/vpcs', () => { + return HttpResponse.json( + makeResourcePage([vpcFactory.build({ label: 'VPC 1' })]) + ); }) ); - const { getByTestId } = renderWithTheme(, { + const { getByTestId, findByText } = renderWithTheme(, { MemoryRouter: { initialEntries: ['/databases/create'] }, flags, }); @@ -53,6 +63,26 @@ describe('database summary section', () => { ); await userEvent.click(selectedPlan); + // Simulate Region Selection + const regionSelect = getByTestId('region-select').querySelector( + 'input' + ) as HTMLInputElement; + + // Open the autocomplete dropdown + await userEvent.click(regionSelect); + + const regionOption = await findByText('US, Newark, NJ (us-east)'); + await userEvent.click(regionOption); + + // Simulate VPC Selection + const vpcSelector = getByTestId('database-vpc-selector').querySelector( + 'input' + ) as HTMLInputElement; + await userEvent.click(vpcSelector); + const newVPC = await findByText('VPC 1'); + await userEvent.click(newVPC); + + // Check summary contents (ie. plan, nodes, VPC) const summary = getByTestId('currentSummary'); const selectedPlanText = 'Dedicated 4 GB $60/month'; expect(summary).toHaveTextContent(selectedPlanText); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.tsx index db9a71260ef..a00c8d9e33c 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseSummarySection.tsx @@ -1,6 +1,8 @@ import { Box, Typography } from '@linode/ui'; import React from 'react'; +import { useFlags } from 'src/hooks/useFlags'; + import { StyledPlanSummarySpan } from '../DatabaseDetail/DatabaseResize/DatabaseResize.style'; import { useIsDatabasesEnabled } from '../utilities'; import { StyledSpan } from './DatabaseCreate.style'; @@ -11,6 +13,7 @@ import type { DatabaseClusterSizeObject, DatabasePriceObject, Engine, + VPC, } from '@linode/api-v4'; import type { Theme } from '@mui/material'; import type { PlanSelectionWithDatabaseType } from 'src/features/components/PlansPanel/types'; @@ -19,8 +22,8 @@ interface Props { currentClusterSize: ClusterSize; currentEngine: Engine; currentPlan: PlanSelectionWithDatabaseType | undefined; - isResize?: boolean; label?: string; + mode: 'create' | 'resize'; platform?: string; resizeData?: { basePrice: string; @@ -28,6 +31,7 @@ interface Props { plan: string; price: string; }; + selectedVPC?: null | VPC; } export const DatabaseSummarySection = (props: Props) => { @@ -35,12 +39,19 @@ export const DatabaseSummarySection = (props: Props) => { currentClusterSize, currentEngine, currentPlan, - isResize, + selectedVPC, label, + mode, platform, resizeData, } = props; const { isDatabasesV2GA } = useIsDatabasesEnabled(); + const flags = useFlags(); + const isVPCEnabled = flags.databaseVpc; + const isResize = mode === 'resize'; + const isCreate = mode === 'create'; + const isVPCSelected = Boolean(selectedVPC); + const displayVPC = isCreate && isVPCEnabled; const currentPrice = currentPlan?.engines[currentEngine].find( (cluster: DatabaseClusterSizeObject) => @@ -66,11 +77,32 @@ export const DatabaseSummarySection = (props: Props) => { ) : ( {currentPlanPrice} )} - - {currentClusterSize} Node - {getSuffix(isNewDatabase, currentClusterSize)} - - {currentNodePrice} + {displayVPC ? ( + <> + + {currentClusterSize} Node + {getSuffix(isNewDatabase, currentClusterSize)} + + + {currentNodePrice} + + {isVPCSelected && ( + + {selectedVPC?.label} VPC + + )} + + ) : ( + <> + + {currentClusterSize} Node + {getSuffix(isNewDatabase, currentClusterSize)} + + {currentNodePrice} + + )} ) : ( 'Please specify your cluster configuration' diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx new file mode 100644 index 00000000000..c36d4a229da --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx @@ -0,0 +1,489 @@ +import { regionFactory } from '@linode/utilities'; +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { subnetFactory, vpcFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DatabaseVPCSelector } from './DatabaseVPCSelector'; + +import type { PrivateNetwork } from '@linode/api-v4'; + +// Hoist query mocks +const queryMocks = vi.hoisted(() => ({ + useRegionQuery: vi.fn().mockReturnValue({ data: {} }), + useAllVPCsQuery: vi.fn().mockReturnValue({ data: [], isLoading: false }), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useRegionQuery: queryMocks.useRegionQuery, + useAllVPCsQuery: queryMocks.useAllVPCsQuery, + }; +}); + +const mockIpv4 = '10.0.0.0/24'; + +const mockRegion = regionFactory.build({ + capabilities: ['VPCs'], + id: 'us-east', + label: 'Newark, NJ', +}); + +const mockSubnets = subnetFactory.buildList(1, { + ipv4: mockIpv4, + id: 123, + label: 'Subnet 1', +}); + +const mockVPCWithSubnet = vpcFactory.build({ + id: 1234, + label: 'VPC 1', + region: 'us-east', + subnets: mockSubnets, +}); + +const setUpBaseMocks = () => { + queryMocks.useRegionQuery.mockReturnValue({ data: mockRegion }); + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: [mockVPCWithSubnet], + isLoading: false, + }); +}; + +describe('DatabaseVPCSelector', () => { + const mockProps = { + errors: {}, + onChange: vi.fn(), + onConfigurationChange: vi.fn(), + privateNetworkValues: { + vpc_id: null, + subnet_id: null, + public_access: false, + }, + resetFormFields: vi.fn(), + selectedRegionId: '', + }; + + beforeEach(() => { + vi.resetAllMocks(); + queryMocks.useRegionQuery.mockReturnValue({ + data: null, + }); + + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: null, + isLoading: false, + }); + }); + + it('Should render the VPC selector heading', () => { + const { getByText } = renderWithTheme( + + ); + expect(getByText('Assign a VPC', { exact: true })).toBeInTheDocument(); + }); + + it('Should render VPC autocomplete in initial disabled state', () => { + const { getByTestId, getByText } = renderWithTheme( + + ); + const vpcSelector = getByTestId('database-vpc-selector'); + expect(vpcSelector).toBeInTheDocument(); + expect(vpcSelector.querySelector('input')).toBeDisabled(); + expect( + getByText( + 'In the Select Engine and Region section, select a region with an existing VPC to see available VPCs.', + { exact: true } + ) + ).toBeInTheDocument(); + }); + + it('Should enable VPC autocomplete when VPCs are available', () => { + const subnets = subnetFactory.buildList(3, { ipv4: mockIpv4 }); + + const vpcWithSubnet = vpcFactory.build({ + subnets, + region: 'us-east', + }); + + queryMocks.useRegionQuery.mockReturnValue({ + data: mockRegion, + }); + + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: [vpcWithSubnet], + isLoading: false, + }); + + const mockEnabledProps = { ...mockProps, selectedRegionId: 'us-east' }; + const { getByTestId } = renderWithTheme( + + ); + + const vpcSelector = getByTestId('database-vpc-selector'); + expect(vpcSelector).toBeInTheDocument(); + expect(vpcSelector.querySelector('input')).toBeEnabled(); + }); + + it('Should enable Subnet autocomplete when VPC is selected', async () => { + queryMocks.useRegionQuery.mockReturnValue({ + data: mockRegion, + }); + + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: [mockVPCWithSubnet], + isLoading: false, + }); + + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: 1234, + subnet_id: null, + public_access: false, + }; + + const mockEnabledProps = { + ...mockProps, + privateNetworkValues: mockPrivateNetwork, + selectedRegionId: 'us-east', + }; + const { getByTestId } = renderWithTheme( + + ); + + const vpcSelector = getByTestId('database-vpc-selector'); + const vpcSelectorInput = vpcSelector.querySelector( + 'input' + ) as HTMLInputElement; + expect(vpcSelectorInput?.value).toBe(mockVPCWithSubnet.label); + const subnetSelector = getByTestId('database-subnet-selector'); + expect(subnetSelector).toBeInTheDocument(); + expect(subnetSelector.querySelector('input')).toBeEnabled(); + }); + + it('Should set fields for VPC, Subnet, and Public Access based on privateNetworkValues values', async () => { + queryMocks.useRegionQuery.mockReturnValue({ + data: mockRegion, + }); + + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: [mockVPCWithSubnet], + isLoading: false, + }); + + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: 1234, + subnet_id: 123, + public_access: true, + }; + + const mockEnabledProps = { + ...mockProps, + privateNetworkValues: mockPrivateNetwork, + selectedRegionId: 'us-east', + }; + const { getByTestId } = renderWithTheme( + + ); + + const vpcSelector = getByTestId('database-vpc-selector'); + const vpcSelectorInput = vpcSelector.querySelector( + 'input' + ) as HTMLInputElement; + const subnetSelector = getByTestId('database-subnet-selector'); + const expectedSubnetValue = `${mockSubnets[0].label} (${mockSubnets[0].ipv4})`; + const publicAccessCheckbox = getByTestId('database-public-access-checkbox'); + + expect(vpcSelectorInput?.value).toBe(mockVPCWithSubnet.label); + expect(subnetSelector).toBeInTheDocument(); + expect(subnetSelector.querySelector('input')?.value).toBe( + expectedSubnetValue + ); + expect(publicAccessCheckbox).toBeInTheDocument(); + expect(publicAccessCheckbox.querySelector('input')).toBeChecked(); + }); + + it('Should clear VPC and subnet when selectedRegionId changes', () => { + // Initial region, VPC, and subnet + const region1 = regionFactory.build({ + capabilities: ['VPCs'], + id: 'us-east', + label: 'Newark, NJ', + }); + const region2 = regionFactory.build({ + capabilities: ['VPCs'], + id: 'us-west', + label: 'Fremont, CA', + }); + + // Set up mocks for initial render + queryMocks.useRegionQuery.mockReturnValue({ data: region1 }); + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: [mockVPCWithSubnet], + isLoading: false, + }); + + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: 1234, + subnet_id: 123, + public_access: true, + }; + + const resetFormFields = vi.fn(); + const onConfigurationChange = vi.fn(); + + const { rerender, getByTestId } = renderWithTheme( + + ); + + // Change region to a new one + queryMocks.useRegionQuery.mockReturnValue({ data: region2 }); + queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); + + rerender( + + ); + + expect(resetFormFields).toHaveBeenCalled(); + expect(onConfigurationChange).toHaveBeenCalledWith(null); + const vpcSelector = getByTestId('database-vpc-selector'); + expect((vpcSelector.querySelector('input') as HTMLInputElement).value).toBe( + '' + ); + }); + + it('Should NOT clear VPC and subnet when selectedRegionId changes from undefined to a valid region', () => { + // Initial render with no region selected + queryMocks.useRegionQuery.mockReturnValue({ data: null }); + queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); + + const resetFormFields = vi.fn(); + const onConfigurationChange = vi.fn(); + + const { rerender } = renderWithTheme( + + ); + + // Now render with a valid region + queryMocks.useRegionQuery.mockReturnValue({ data: mockRegion }); + queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); + + rerender( + + ); + + expect(resetFormFields).not.toHaveBeenCalled(); + expect(onConfigurationChange).not.toHaveBeenCalledWith(null); + }); + + it('Should show long helper text when no region is selected', () => { + const { getByText } = renderWithTheme( + + ); + expect( + getByText( + 'In the Select Engine and Region section, select a region with an existing VPC to see available VPCs.', + { exact: true } + ) + ).toBeInTheDocument(); + }); + + it('Should show short helper text when a region is selected but no VPCs are available', () => { + queryMocks.useRegionQuery.mockReturnValue({ data: mockRegion }); + queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); + + const { getByText } = renderWithTheme( + + ); + expect( + getByText('No VPC is available in the selected region.', { exact: true }) + ).toBeInTheDocument(); + }); + + it('Should NOT show helper text when VPCs are available', () => { + const vpcWithSubnet = vpcFactory.build({ + region: 'us-east', + subnets: subnetFactory.buildList(1, { ipv4: mockIpv4 }), + }); + queryMocks.useRegionQuery.mockReturnValue({ data: mockRegion }); + queryMocks.useAllVPCsQuery.mockReturnValue({ + data: [vpcWithSubnet], + isLoading: false, + }); + + const { queryByText } = renderWithTheme( + + ); + expect( + queryByText('No VPC is available in the selected region.') + ).not.toBeInTheDocument(); + expect( + queryByText( + 'In the Select Engine and Region section, select a region with an existing VPC to see available VPCs.' + ) + ).not.toBeInTheDocument(); + }); + + it('Should show subnet validation error text when there is a subnet error', () => { + setUpBaseMocks(); + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: 1234, + subnet_id: null, + public_access: false, + }; + + const mockErrors = { + private_network: { + subnet_id: 'Subnet is required.', + }, + }; + + const { getByTestId, getByText } = renderWithTheme( + + ); + + const subnetSelector = getByTestId('database-subnet-selector'); + expect(subnetSelector).toBeInTheDocument(); + expect(getByText('Subnet is required.')).toBeInTheDocument(); + }); + + it('Should clear subnet field when the VPC field is cleared', async () => { + setUpBaseMocks(); + const onChange = vi.fn(); + + // Start with both VPC and subnet selected + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: 1234, + subnet_id: 123, + public_access: false, + }; + + const { getByTestId } = renderWithTheme( + + ); + + // Simulate clearing the VPC field (user clears the Autocomplete) + const vpcSelector = getByTestId('database-vpc-selector'); + const clearButton = vpcSelector.querySelector( + 'button[title="Clear"]' + ) as HTMLElement; + await userEvent.click(clearButton); + // ...assertions as above... + expect(onChange).toHaveBeenCalledWith('private_network.vpc_id', null); + expect(onChange).toHaveBeenCalledWith('private_network.subnet_id', null); + expect(onChange).toHaveBeenCalledWith( + 'private_network.public_access', + false + ); + }); + + it('Should call onChange for the VPC field when a value is selected', async () => { + setUpBaseMocks(); + const onChange = vi.fn(); + + // Start with no VPC selected + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: null, + subnet_id: null, + public_access: false, + }; + + const { getByTestId, findByText } = renderWithTheme( + + ); + + // Simulate selecting a VPC from the Autocomplete + const vpcSelector = getByTestId('database-vpc-selector').querySelector( + 'input' + ) as HTMLInputElement; + // Open the autocomplete dropdown + await userEvent.click(vpcSelector); + + // Select the option + const newVPC = await findByText('VPC 1'); + await userEvent.click(newVPC); + + expect(onChange).toHaveBeenCalledWith( + 'private_network.vpc_id', + mockVPCWithSubnet.id + ); + }); + + it('Should call onChange for the Subnet field when subnet value is selected', async () => { + setUpBaseMocks(); + const onChange = vi.fn(); + + // Start with VPC selected and no subnet selection + const mockPrivateNetwork: PrivateNetwork = { + vpc_id: 1234, + subnet_id: null, + public_access: false, + }; + + const { getByTestId, findByText } = renderWithTheme( + + ); + + // Simulate selecting a Subnet from the Autocomplete + const subnetSelector = getByTestId( + 'database-subnet-selector' + ).querySelector('input') as HTMLInputElement; + + await userEvent.click(subnetSelector); + + // Select the option + const expectedSubnetLabel = `${mockSubnets[0].label} (${mockSubnets[0].ipv4})`; + const newSubnet = await findByText(expectedSubnetLabel); + await userEvent.click(newSubnet); + + expect(onChange).toHaveBeenCalledWith( + 'private_network.subnet_id', + mockSubnets[0].id + ); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx new file mode 100644 index 00000000000..589eb88a6a4 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx @@ -0,0 +1,190 @@ +import { useAllVPCsQuery, useRegionQuery } from '@linode/queries'; +import { + Autocomplete, + Box, + Checkbox, + Notice, + TooltipIcon, + Typography, +} from '@linode/ui'; +import * as React from 'react'; + +import type { DatabaseCreateValues } from './DatabaseClusterData'; +import type { PrivateNetwork, VPC } from '@linode/api-v4'; +import type { Theme } from '@mui/material/styles'; +import type { FormikErrors } from 'formik'; + +interface DatabaseVPCSelectorProps { + errors: FormikErrors; + onChange: (field: string, value: boolean | null | number) => void; + onConfigurationChange: (vpc: null | VPC) => void; + privateNetworkValues: PrivateNetwork; + resetFormFields: (partialValues?: Partial) => void; + selectedRegionId: string; +} + +export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { + const { + errors, + onConfigurationChange, + onChange, + selectedRegionId, + resetFormFields, + privateNetworkValues, + } = props; + + const { data: selectedRegion } = useRegionQuery(selectedRegionId); + const regionSupportsVPCs = selectedRegion?.capabilities.includes('VPCs'); + + const { + data: vpcs, + error, + isLoading, + } = useAllVPCsQuery({ + enabled: regionSupportsVPCs, + filter: { region: selectedRegionId }, + }); + + const selectedVPC = React.useMemo( + () => vpcs?.find((vpc) => vpc.id === privateNetworkValues.vpc_id), + [vpcs, privateNetworkValues.vpc_id] + ); + const selectedSubnet = React.useMemo( + () => + selectedVPC?.subnets.find( + (subnet) => subnet.id === privateNetworkValues.subnet_id + ), + [selectedVPC, privateNetworkValues.subnet_id] + ); + + const prevRegionId = React.useRef(); + const regionHasVPCs = Boolean(vpcs && vpcs.length > 0); + const disableVPCSelectors = !regionSupportsVPCs || !regionHasVPCs; + + const resetVPCConfiguration = () => { + resetFormFields({ + private_network: { + vpc_id: null, + subnet_id: null, + public_access: false, + }, + }); + }; + + React.useEffect(() => { + // When the selected region has changed, reset VPC configuration. + // Then switch back to default validation behavior + if (prevRegionId.current && prevRegionId.current !== selectedRegionId) { + resetVPCConfiguration(); + onConfigurationChange(null); + } + prevRegionId.current = selectedRegionId; + }, [selectedRegionId]); + + const vpcHelperTextCopy = !selectedRegionId + ? 'In the Select Engine and Region section, select a region with an existing VPC to see available VPCs.' + : 'No VPC is available in the selected region.'; + + /** Returns dynamic marginTop value used to center TooltipIcon in different scenarios */ + const getVPCTooltipIconMargin = () => { + const margins = { + longHelperText: '.75rem', + shortHelperText: '1.75rem', + noHelperText: '2.75rem', + }; + if (disableVPCSelectors && !selectedRegionId) return margins.longHelperText; + if (disableVPCSelectors && selectedRegionId) return margins.shortHelperText; + return margins.noHelperText; + }; + + return ( + <> + ({ + marginTop: theme.spacingFunction(20), + marginBottom: theme.spacingFunction(4), + })} + variant="h3" + > + Assign a VPC + + + Assign this cluster to an existing VPC. + + { + if (!value) { + onChange('private_network.subnet_id', null); + onChange('private_network.public_access', false); + } + onConfigurationChange(value ?? null); + onChange('private_network.vpc_id', value?.id ?? null); + }} + options={vpcs ?? []} + placeholder="Select a VPC" + sx={{ width: '354px' }} + value={selectedVPC ?? null} + /> + + + + {selectedVPC ? ( + <> + `${subnet.label} (${subnet.ipv4})`} + label="Subnet" + onChange={(e, value) => { + onChange('private_network.subnet_id', value?.id ?? null); + }} + options={selectedVPC?.subnets ?? []} + placeholder="Select a subnet" + value={selectedSubnet ?? null} + /> + ({ + marginTop: theme.spacingFunction(20), + })} + > + { + onChange('private_network.public_access', value ?? null); + }} + text={'Enable public access'} + toolTipText={ + 'Adds a public endpoint to the database in addition to the private VPC endpoint.' + } + /> + + + ) : ( + ({ + marginTop: theme.spacingFunction(20), + })} + text="The cluster will have public access by default if a VPC is not assigned." + variant="info" + /> + )} + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx index 52394b69799..db37103f221 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx @@ -318,8 +318,8 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { currentClusterSize={database.cluster_size} currentEngine={selectedEngine} currentPlan={currentPlan} - isResize={true} label={database.label} + mode="resize" platform={database.platform} resizeData={summaryText} /> diff --git a/packages/validation/.changeset/pr-12281-added-1748363298759.md b/packages/validation/.changeset/pr-12281-added-1748363298759.md new file mode 100644 index 00000000000..fe22246cece --- /dev/null +++ b/packages/validation/.changeset/pr-12281-added-1748363298759.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Added +--- + +Method to retrieve dynamic validation for Create database schema ([#12281](https://github.com/linode/manager/pull/12281)) diff --git a/packages/validation/src/databases.schema.ts b/packages/validation/src/databases.schema.ts index 68a0bfbc5e8..e4a6d08d210 100644 --- a/packages/validation/src/databases.schema.ts +++ b/packages/validation/src/databases.schema.ts @@ -18,6 +18,18 @@ export const createDatabaseSchema = object({ replication_commit_type: string().notRequired().nullable(), // TODO (UIE-8214) remove POST GA }); +export const getDynamicDatabaseSchema = (isVPCSelected: boolean) => { + if (!isVPCSelected) { + return createDatabaseSchema; + } + + return createDatabaseSchema.shape({ + private_network: object().shape({ + subnet_id: number().nullable().required('Subnet is required.'), + }), + }); +}; + export const updateDatabaseSchema = object({ label: string().notRequired().min(3, LABEL_MESSAGE).max(32, LABEL_MESSAGE), allow_list: array().of(string()).notRequired(), From 952e0c913f3a42163f0dae3f44c3a404804c896f Mon Sep 17 00:00:00 2001 From: Harsh Shankar Rao Date: Wed, 28 May 2025 21:17:17 +0530 Subject: [PATCH 51/59] upcoming: M3-9792 - Add VPC column to the Nodebalancer Landing table (#12256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🔄 - Add VPC column to the Nodebalancer Landing table ## How to test 🧪 ### Prerequisites - Add the customer tags mentioned in the ticket description - Enable the `NodeBalancer-VPC Integration` flag - Create a Nodebalancer integrated with VPC ### Verification steps - [ ] Verify that VPC column is shown in the Nodebalancer landing table --- ...r-12256-upcoming-features-1747817841341.md | 5 +++ .../NodeBalancerActionMenu.tsx | 44 ++++++++++--------- .../NodeBalancerTableRow.tsx | 10 +++++ .../NodeBalancersLanding/NodeBalancerVPC.tsx | 42 ++++++++++++++++++ .../NodeBalancersLanding.tsx | 8 ++++ 5 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md create mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx diff --git a/packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md b/packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md new file mode 100644 index 00000000000..4cbc46418bf --- /dev/null +++ b/packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add VPC column to the Nodebalancer Landing table ([#12256](https://github.com/linode/manager/pull/12256)) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index ef4515b3cd5..b473e8497ed 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -1,4 +1,3 @@ -import { Hidden } from '@linode/ui'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useNavigate } from '@tanstack/react-router'; @@ -9,6 +8,8 @@ import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuActi import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; +import { useIsNodebalancerVPCEnabled } from '../utils'; + import type { Theme } from '@mui/material/styles'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; @@ -29,6 +30,8 @@ export const NodeBalancerActionMenu = (props: Props) => { id: nodeBalancerId, }); + const { isNodebalancerVPCEnabled } = useIsNodebalancerVPCEnabled(); + const actions: Action[] = [ { onClick: () => { @@ -72,26 +75,25 @@ export const NodeBalancerActionMenu = (props: Props) => { }, ]; - return ( - <> - {!matchesMdDown && - actions.map((action) => { - return ( - - ); - })} - - { + return ( + - - + ); + }); + } + return ( + ); }; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index 9deb553141c..51ee4b7f613 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -10,12 +10,15 @@ import { TableRow } from 'src/components/TableRow'; import { IPAddress } from 'src/features/Linodes/LinodesLanding/IPAddress'; import { RegionIndicator } from 'src/features/Linodes/LinodesLanding/RegionIndicator'; +import { useIsNodebalancerVPCEnabled } from '../utils'; import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; +import { NodeBalancerVPC } from './NodeBalancerVPC'; import type { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; export const NodeBalancerTableRow = (props: NodeBalancer) => { const { id, ipv4, label, region, transfer } = props; + const { isNodebalancerVPCEnabled } = useIsNodebalancerVPCEnabled(); const { data: configs } = useAllNodeBalancerConfigsQuery(id); @@ -68,6 +71,13 @@ export const NodeBalancerTableRow = (props: NodeBalancer) => { + {isNodebalancerVPCEnabled && ( + + + + + + )} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx new file mode 100644 index 00000000000..c28242278cc --- /dev/null +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx @@ -0,0 +1,42 @@ +import { + useNodeBalancerVPCConfigsBetaQuery, + useVPCQuery, +} from '@linode/queries'; +import React from 'react'; + +import { Link } from 'src/components/Link'; +import { Skeleton } from 'src/components/Skeleton'; + +interface Props { + nodeBalancerId: number; +} + +export const NodeBalancerVPC = ({ nodeBalancerId }: Props) => { + const { data: vpcConfig, isLoading: isVPCConfigLoading } = + useNodeBalancerVPCConfigsBetaQuery(nodeBalancerId, Boolean(nodeBalancerId)); + + const { data: vpcDetails, isLoading: isVPCDetailsLoading } = useVPCQuery( + Number(vpcConfig?.data[0]?.vpc_id), + Boolean(vpcConfig?.data[0]?.vpc_id) + ); + + if (isVPCConfigLoading || isVPCDetailsLoading) { + return ; + } + + if (vpcConfig?.data?.length === 0) { + return 'None'; + } + + return vpcConfig?.data.map(({ vpc_id: vpcId }, i) => ( + + + {vpcDetails?.label} + + {i < vpcConfig.data.length - 1 ? ', ' : ''} + + )); +}; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index c14b28e5aa7..78e3cf26537 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -20,6 +20,7 @@ import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; +import { useIsNodebalancerVPCEnabled } from '../utils'; import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; import { NodeBalancerTableRow } from './NodeBalancerTableRow'; @@ -61,6 +62,8 @@ export const NodeBalancersLanding = () => { error: selectedNodeBalancerError, } = useNodeBalancerQuery(Number(params.id), !!params.id); + const { isNodebalancerVPCEnabled } = useIsNodebalancerVPCEnabled(); + if (error) { return ( { Region + {isNodebalancerVPCEnabled && ( + + VPC + + )} From fa98452b27139ad1990ccc262e45e616df08a020 Mon Sep 17 00:00:00 2001 From: Harsh Shankar Rao Date: Wed, 28 May 2025 22:08:11 +0530 Subject: [PATCH 52/59] upcoming: [M3-9988] - Allow auto-assign of Subnet IPv4 ranges when a Nodebalancer is assigned to a VPC (#12247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 Allow auto-assign of Subnet IPv4 ranges when a Nodebalancer is assigned to a VPC ## How to test 🧪 ### Prerequisites - Add the customer tags mentioned in the Project Tracker - Enable the NodeBalancer-VPC Integration flag ### Verification steps - [ ] Navigate to the Create Nodebalancer page - [ ] Select a region that has a vpc/subnet - [ ] Select the vpc & subnet in the "Assign VPC" section - [ ] Observe the checkbox to be checked - [ ] Create a nodebalancer --- packages/api-v4/src/nodebalancers/types.ts | 2 +- .../nodebalancer-create-with-vpc.spec.ts | 8 +- .../NodeBalancers/NodeBalancerCreate.tsx | 37 ++++-- .../features/NodeBalancers/VPCPanel.test.tsx | 10 ++ .../src/features/NodeBalancers/VPCPanel.tsx | 114 +++++++++++++----- .../manager/src/features/VPCs/constants.ts | 3 + .../validation/src/nodebalancers.schema.ts | 105 +++++----------- 7 files changed, 165 insertions(+), 114 deletions(-) diff --git a/packages/api-v4/src/nodebalancers/types.ts b/packages/api-v4/src/nodebalancers/types.ts index 6b31d809195..77d577d33c6 100644 --- a/packages/api-v4/src/nodebalancers/types.ts +++ b/packages/api-v4/src/nodebalancers/types.ts @@ -135,7 +135,7 @@ export interface NodeBalancerStats { } export interface NodeBalancerVpcPayload { - ipv4_range: string; + ipv4_range?: string; ipv6_range?: string; subnet_id: number; } diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-with-vpc.spec.ts index becf26a31e9..7c4278d2f67 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-with-vpc.spec.ts @@ -104,7 +104,13 @@ describe('Create a NodeBalancer with VPCs', () => { `${mockSubnet.label} (${mockSubnet.ipv4})` ); - cy.findByText(`NodeBalancer IPv4 CIDR for ${mockSubnet.label}`); + // Confirm that the auto-assign for VPC IPv4 range is checked + cy.get('[data-testid="vpc-ipv4-checkbox"]') + .find('[type="checkbox"]') + .should('be.checked') + .click(); + + cy.findByText(`NodeBalancer IPv4 CIDR for ${mockSubnet.label}`).click(); cy.focused().clear(); cy.focused().type(`${mockUpdatedSubnet.nodebalancers[0].ipv4_range}`); // node backend config diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index 197cd6baa71..f95ded0b087 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -337,13 +337,15 @@ const NodeBalancerCreate = () => { nodeBalancerRequestData?.vpcs && nodeBalancerRequestData.vpcs.length > 0 ) { - nodeBalancerRequestData.vpcs = nodeBalancerRequestData.vpcs.map( - (vpc) => ({ - ...vpc, - ipv4_range: vpc.ipv4_range.endsWith('/30') - ? vpc.ipv4_range - : `${vpc.ipv4_range}/30`, - }) + nodeBalancerRequestData.vpcs = nodeBalancerRequestData.vpcs.map((vpc) => + vpc.ipv4_range + ? { + ...vpc, + ipv4_range: vpc.ipv4_range.endsWith('/30') + ? vpc.ipv4_range + : `${vpc.ipv4_range}/30`, + } + : vpc ); } nodeBalancerRequestData.configs = transformConfigsForRequest( @@ -512,7 +514,7 @@ const NodeBalancerCreate = () => { return { ...rest }; }); } else { - const vpcs = subnetIds.map((id) => ({ subnet_id: id, ipv4_range: '' })); + const vpcs = subnetIds.map((id) => ({ subnet_id: id })); setNodeBalancerFields((prev) => ({ ...prev, vpcs, @@ -520,19 +522,28 @@ const NodeBalancerCreate = () => { } }; - const ipv4Change = (ipv4Range: string, index: number) => { + const ipv4Change = (ipv4Range: null | string, index: number) => { if (nodeBalancerFields?.vpcs?.[index].ipv4_range === ipv4Range) { return; } - if (ipv4Range === '') { + const vpcs = nodeBalancerFields?.vpcs; + if (ipv4Range === null) { + // handling auto-assign ipv4 ranges for all subnets + setNodeBalancerFields((prev) => { + const { vpcs: vpcs, ...rest } = prev; + const updatedVpcs = vpcs?.map(({ subnet_id }) => ({ + subnet_id, + })); + return { ...rest, vpcs: updatedVpcs }; + }); + } else if (ipv4Range === '') { + // removing vpcs from the payload if ipv4 ranges are removed setNodeBalancerFields((prev) => { // eslint-disable-next-line no-unused-vars, sonarjs/no-unused-vars const { vpcs: _, ...rest } = prev; return { ...rest }; }); - } - const vpcs = nodeBalancerFields?.vpcs; - if (vpcs) { + } else if (vpcs) { vpcs[index].ipv4_range = ipv4Range; setNodeBalancerFields((prev) => ({ ...prev, diff --git a/packages/manager/src/features/NodeBalancers/VPCPanel.test.tsx b/packages/manager/src/features/NodeBalancers/VPCPanel.test.tsx index bd420e6f69d..ce4c5826371 100644 --- a/packages/manager/src/features/NodeBalancers/VPCPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/VPCPanel.test.tsx @@ -146,6 +146,16 @@ describe('VPCPanel', () => { await screen.findByText(vpcWithSubnet.label, { exact: false }) ); + const checkbox = screen.getByTestId('vpc-ipv4-checkbox'); + + await userEvent.click(checkbox); + + expect( + screen.getByLabelText( + 'Auto-assign a /30 CIDR in each subnet for this NodeBalancer' + ) + ).not.toBeChecked(); + expect( screen.getByLabelText(`${subnets[0].label}`, { exact: false, diff --git a/packages/manager/src/features/NodeBalancers/VPCPanel.tsx b/packages/manager/src/features/NodeBalancers/VPCPanel.tsx index 99fe752a144..dda32540825 100644 --- a/packages/manager/src/features/NodeBalancers/VPCPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/VPCPanel.tsx @@ -1,24 +1,32 @@ import { useAllVPCsQuery, useRegionQuery } from '@linode/queries'; import { Autocomplete, + Box, + Checkbox, + FormControlLabel, InputAdornment, Paper, Stack, TextField, + TooltipIcon, Typography, } from '@linode/ui'; +import { useMediaQuery, useTheme } from '@mui/material'; import React from 'react'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { NODEBALANCER_REGION_CAVEAT_HELPER_TEXT } from '../VPCs/constants'; +import { + NB_AUTO_ASSIGN_CIDR_TOOLTIP, + NODEBALANCER_REGION_CAVEAT_HELPER_TEXT, +} from '../VPCs/constants'; import type { APIError, NodeBalancerVpcPayload, VPC } from '@linode/api-v4'; export interface Props { disabled?: boolean; errors?: APIError[]; - ipv4Change: (ipv4Range: string, index: number) => void; + ipv4Change: (ipv4Range: null | string, index: number) => void; regionSelected: string; setIsVpcSelected: (vpc: boolean) => void; subnetChange: (subnetIds: null | number[]) => void; @@ -36,6 +44,9 @@ export const VPCPanel = (props: Props) => { subnetChange, } = props; + const theme = useTheme(); + const isSmallBp = useMediaQuery(theme.breakpoints.down('sm')); + const { data: region } = useRegionQuery(regionSelected); const regionSupportsVPC = region?.capabilities.includes('VPCs') || false; @@ -49,6 +60,8 @@ export const VPCPanel = (props: Props) => { filter: { region: regionSelected }, }); + const [autoAssignIPv4WithinVPC, toggleAutoAssignIPv4Range] = + React.useState(true); const [VPCSelected, setVPCSelected] = React.useState(null); React.useEffect(() => { @@ -126,31 +139,78 @@ export const VPCPanel = (props: Props) => { ) ?? null } /> - {subnets && - subnets.map((vpc, index) => ( - - err.field?.includes(`vpcs[${index}].ipv4_range`) - )?.reason - } - key={`${vpc.subnet_id}`} - label={`NodeBalancer IPv4 CIDR for ${getVPCSubnetLabelFromId(vpc.subnet_id)}`} - noMarginTop - onChange={(e) => ipv4Change(e.target.value ?? '', index)} - // eslint-disable-next-line sonarjs/no-hardcoded-ip - placeholder="10.0.0.24" - required - slotProps={{ - input: { - endAdornment: ( - /30 - ), - }, - }} - value={vpc.ipv4_range} - /> - ))} + {subnets && ( + <> + ({ + marginLeft: theme.spacingFunction(2), + paddingTop: theme.spacingFunction(8), + })} + > + { + if (checked) { + ipv4Change(null, 0); + } + toggleAutoAssignIPv4Range(checked); + }} + /> + } + data-testid="vpc-ipv4-checkbox" + label={ + + + Auto-assign a /30 CIDR in each subnet for this + NodeBalancer + + + + } + /> + + {!autoAssignIPv4WithinVPC && + subnets.map((vpc, index) => ( + + err.field?.includes(`vpcs[${index}].ipv4_range`) + )?.reason + } + key={`${vpc.subnet_id}`} + label={`NodeBalancer IPv4 CIDR for ${getVPCSubnetLabelFromId(vpc.subnet_id)}`} + noMarginTop + onChange={(e) => + ipv4Change(e.target.value ?? '', index) + } + // eslint-disable-next-line sonarjs/no-hardcoded-ip + placeholder="10.0.0.24" + slotProps={{ + input: { + endAdornment: ( + + /30 + + ), + }, + }} + value={vpc.ipv4_range} + /> + ))} + + )} )} diff --git a/packages/manager/src/features/VPCs/constants.ts b/packages/manager/src/features/VPCs/constants.ts index 6511e2b4af2..a6387b047b0 100644 --- a/packages/manager/src/features/VPCs/constants.ts +++ b/packages/manager/src/features/VPCs/constants.ts @@ -23,6 +23,9 @@ export const REGIONAL_LINODE_MESSAGE = export const MULTIPLE_CONFIGURATIONS_MESSAGE = 'This Linode has multiple configurations. Select which configuration you would like added to the subnet.'; +export const NB_AUTO_ASSIGN_CIDR_TOOLTIP = + 'A /30 CIDR is needed for the NodeBalancer to communicate with the subnet. Deselect this option to define a custom IP range.'; + export const VPC_AUTO_ASSIGN_IPV4_TOOLTIP = 'Automatically assign an IPv4 address as the private IP address for this Linode in the VPC.'; diff --git a/packages/validation/src/nodebalancers.schema.ts b/packages/validation/src/nodebalancers.schema.ts index 00886159f53..f983ac45abc 100644 --- a/packages/validation/src/nodebalancers.schema.ts +++ b/packages/validation/src/nodebalancers.schema.ts @@ -1,6 +1,6 @@ -import { array, boolean, lazy, mixed, number, object, string } from 'yup'; +import { array, boolean, mixed, number, object, string } from 'yup'; -import { IP_EITHER_BOTH_NOT_NEITHER, vpcsValidateIP } from './vpcs.schema'; +import { vpcsValidateIP } from './vpcs.schema'; const PORT_WARNING = 'Port must be between 1 and 65535.'; const LABEL_WARNING = 'Label must be between 3 and 32 characters.'; @@ -265,79 +265,40 @@ const clientUdpSessThrottle = number() ) .typeError('UDP Session Throttle must be a number.'); -const createNodeBalancerVPCsSchema = object().shape( - { - subnet_id: number() - .typeError('Subnet ID must be a number.') - .required('Subnet ID is required.'), - ipv4_range: string().when('ipv6_range', { - is: (value: unknown) => - value === '' || value === null || value === undefined, - then: (schema) => - schema - .required(IP_EITHER_BOTH_NOT_NEITHER) - .matches(PRIVATE_IPV4_REGEX, PRIVATE_IPV4_WARNING) - .test({ - name: 'IPv4 CIDR format', - message: 'The IPv4 range must be in CIDR format.', - test: (value) => - vpcsValidateIP({ - value, - shouldHaveIPMask: true, - mustBeIPMask: false, - }), - }), - otherwise: (schema) => - lazy((value: string | undefined) => { - switch (typeof value) { - case 'string': - return schema - .notRequired() - .matches(PRIVATE_IPV4_REGEX, PRIVATE_IPV4_WARNING) - .test({ - name: 'IPv4 CIDR format', - message: 'The IPv4 range must be in CIDR format.', - test: (value) => - vpcsValidateIP({ - value, - shouldHaveIPMask: true, - mustBeIPMask: false, - }), - }); - - case 'undefined': - return schema.notRequired().nullable(); - - default: - return schema.notRequired().nullable(); - } +const createNodeBalancerVPCsSchema = object().shape({ + subnet_id: number() + .typeError('Subnet ID must be a number.') + .required('Subnet ID is required.'), + ipv4_range: string() + .notRequired() + .matches(PRIVATE_IPV4_REGEX, PRIVATE_IPV4_WARNING) + .test({ + name: 'IPv4 CIDR format', + message: 'The IPv4 range must be in CIDR format.', + test: (value) => + !value || + vpcsValidateIP({ + value, + shouldHaveIPMask: true, + mustBeIPMask: false, }), }), - ipv6_range: string().when('ipv4_range', { - is: (value: unknown) => - value === '' || value === null || value === undefined, - then: (schema) => - schema - .required(IP_EITHER_BOTH_NOT_NEITHER) - .matches(PRIVATE_IPV6_REGEX, 'Must be a valid private IPv6 address.') - .test({ - name: 'valid-ipv6-range', - message: - 'Must be a valid private IPv6 range, e.g. fd12:3456:789a:1::1/64.', - test: (value) => - vpcsValidateIP({ - value, - shouldHaveIPMask: true, - mustBeIPMask: false, - }), - }), + ipv6_range: string() + .notRequired() + .matches(PRIVATE_IPV6_REGEX, 'Must be a valid private IPv6 address.') + .test({ + name: 'valid-ipv6-range', + message: + 'Must be a valid private IPv6 range, e.g. fd12:3456:789a:1::1/64.', + test: (value) => + !value || + vpcsValidateIP({ + value, + shouldHaveIPMask: true, + mustBeIPMask: false, + }), }), - }, - [ - ['ipv4_range', 'ipv6_range'], - ['ipv6_range', 'ipv4_range'], - ], -); +}); export const NodeBalancerSchema = object({ label: string() From 332aa04c569e3db6b7c6b7750c5e981ceba91959 Mon Sep 17 00:00:00 2001 From: aaleksee-akamai Date: Wed, 28 May 2025 18:39:28 +0200 Subject: [PATCH 53/59] feat: [UIE-8766] - IAM RBAC: fix bugs for the assigned roles table (#12249) * feat: [UIE-8766] - IAM RBAC: fix bugs for the assigned roles table * Added changeset: IAM RBAC: Fix bugs for the assigned roles table --------- Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> Co-authored-by: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> --- ...r-12249-upcoming-features-1747752128426.md | 5 ++++ .../AssignedPermissionsPanel.style.ts | 4 ++- .../Users/UserRoles/AssignNewRoleDrawer.tsx | 28 +++++++++++-------- packages/manager/src/queries/iam/iam.ts | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md diff --git a/packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md b/packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md new file mode 100644 index 00000000000..3ede6de1854 --- /dev/null +++ b/packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +IAM RBAC: Fix bugs for the assigned roles table ([#12249](https://github.com/linode/manager/pull/12249)) diff --git a/packages/manager/src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel.style.ts b/packages/manager/src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel.style.ts index f308d7b8d84..7374c6693f0 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel.style.ts +++ b/packages/manager/src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel.style.ts @@ -23,7 +23,9 @@ export const StyledDescription = styled(Typography)(({ theme }) => ({ wordBreak: 'normal', })); -export const StyledEntityBox = styled(Box)<{ +export const StyledEntityBox = styled(Box, { + shouldForwardProp: (prop) => prop !== 'hideDetails', +})<{ hideDetails: boolean | undefined; }>(({ theme, hideDetails }) => ({ marginTop: !hideDetails ? theme.tokens.spacing.S12 : undefined, diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx index a8cfbb81785..efe557c192b 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx @@ -1,6 +1,7 @@ import { ActionsPanel, Drawer, Notice, Typography } from '@linode/ui'; import { useTheme } from '@mui/material'; import Grid from '@mui/material/Grid'; +import { useQueryClient } from '@tanstack/react-query'; import { enqueueSnackbar } from 'notistack'; import React, { useState } from 'react'; import { FormProvider, useFieldArray, useForm } from 'react-hook-form'; @@ -12,9 +13,9 @@ import { StyledLinkButtonBox } from 'src/components/SelectFirewallPanel/SelectFi import { AssignSingleRole } from 'src/features/IAM/Users/UserRoles/AssignSingleRole'; import { useAccountPermissions, - useAccountUserPermissions, useAccountUserPermissionsMutation, } from 'src/queries/iam/iam'; +import { iamQueries } from 'src/queries/iam/queries'; import { getAllRoles, @@ -22,6 +23,7 @@ import { } from '../../Shared/utilities'; import type { AssignNewRoleFormValues } from '../../Shared/utilities'; +import type { IamUserPermissions } from '@linode/api-v4'; interface Props { onClose: () => void; @@ -31,11 +33,10 @@ interface Props { export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { const theme = useTheme(); const { username } = useParams<{ username: string }>(); + const queryClient = useQueryClient(); const { data: accountPermissions } = useAccountPermissions(); - const { data: existingRoles } = useAccountUserPermissions(username ?? ''); - const form = useForm({ defaultValues: { roles: [ @@ -68,22 +69,25 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { const { mutateAsync: updateUserRolePermissions, isPending } = useAccountUserPermissionsMutation(username); - const onSubmit = handleSubmit(async (values: AssignNewRoleFormValues) => { + const onSubmit = async (values: AssignNewRoleFormValues) => { try { + const queryKey = iamQueries.user(username)._ctx.permissions.queryKey; + const currentRoles = + queryClient.getQueryData(queryKey); + const mergedRoles = mergeAssignedRolesIntoExistingRoles( values, - existingRoles + structuredClone(currentRoles) ); await updateUserRolePermissions(mergedRoles); - enqueueSnackbar(`Roles added.`, { - variant: 'success', - }); + + enqueueSnackbar(`Roles added.`, { variant: 'success' }); handleClose(); } catch (error) { setError(error.field ?? 'root', { message: error[0].reason }); } - }); + }; const handleClose = () => { reset(); @@ -92,9 +96,9 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { // TODO - add a link 'Learn more" - UIE-8534 return ( - + - + {formState.errors.root?.message && ( @@ -157,7 +161,7 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { 'data-testid': 'submit', label: 'Assign', type: 'submit', - loading: formState.isSubmitting || isPending, + loading: isPending || formState.isSubmitting, }} secondaryButtonProps={{ 'data-testid': 'cancel', diff --git a/packages/manager/src/queries/iam/iam.ts b/packages/manager/src/queries/iam/iam.ts index ed6d4536949..ccb7dd863a7 100644 --- a/packages/manager/src/queries/iam/iam.ts +++ b/packages/manager/src/queries/iam/iam.ts @@ -30,7 +30,7 @@ export const useAccountUserPermissionsMutation = (username: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data) => updateUserPermissions(username, data), - onSuccess(role) { + onSuccess: (role) => { queryClient.setQueryData( iamQueries.user(username)._ctx.permissions.queryKey, role From 9eff3456a42170470f5ce34ab6750cb9d01e6bb5 Mon Sep 17 00:00:00 2001 From: hasyed-akamai Date: Wed, 28 May 2025 23:43:30 +0530 Subject: [PATCH 54/59] upcoming: [M3-9785, M3-9788, M3-10040] - Added NodeBalacer Table and replace Linodes with Resources and remove "Label" text from Subnet and Linode Column (#12232) * upcoming: [M3-9785, M3-9788] - Added NodeBalacer Table and replace Linodes with Resources * fix unit test and e2e * Added changeset: Added NodeBalacer Table under Subnets Table and replace Linodes Text with Resources * added unit tests for the `SubnetNodebalancerRow` component * fix unit tests * added full width for the table rows * fix e2e tests * implemented changes suggested by Dajahi * added a feature flag for the Linodes column and revert back the changes in the unit and e2e tests * change changeset description and center loading and error state for subnetLinodeRow * added unit test for `getUniqueResourcesFromSubnets` utils * added unit test in for UI testing of Resources text * remove label text from subnet and linode column * fix unit test for `vpcsubnetstable` * update error state * align error message --- ...r-12232-upcoming-features-1747645656510.md | 5 + packages/manager/src/factories/subnets.ts | 15 +- .../VPCs/VPCDetail/SubnetLinodeRow.tsx | 10 +- .../VPCDetail/SubnetNodebalancerRow.test.tsx | 97 +++++++++++++ .../VPCs/VPCDetail/SubnetNodebalancerRow.tsx | 121 ++++++++++++++++ .../VPCs/VPCDetail/VPCDetail.test.tsx | 37 +++++ .../src/features/VPCs/VPCDetail/VPCDetail.tsx | 12 +- .../VPCs/VPCDetail/VPCSubnetsTable.test.tsx | 129 +++++++++++++++++- .../VPCs/VPCDetail/VPCSubnetsTable.tsx | 100 +++++++++----- .../VPCs/VPCLanding/VPCLanding.test.tsx | 30 ++++ .../features/VPCs/VPCLanding/VPCLanding.tsx | 6 +- .../features/VPCs/VPCLanding/VPCRow.test.tsx | 16 ++- .../src/features/VPCs/VPCLanding/VPCRow.tsx | 23 +++- .../manager/src/features/VPCs/utils.test.ts | 72 ++++++++++ packages/manager/src/features/VPCs/utils.ts | 18 +++ 15 files changed, 638 insertions(+), 53 deletions(-) create mode 100644 packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md create mode 100644 packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx create mode 100644 packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx diff --git a/packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md b/packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md new file mode 100644 index 00000000000..ef9fd1033c6 --- /dev/null +++ b/packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add NodeBalancer Table under VPC Subnets Table and rename "Linodes" column to "Resources" ([#12232](https://github.com/linode/manager/pull/12232)) diff --git a/packages/manager/src/factories/subnets.ts b/packages/manager/src/factories/subnets.ts index f06f91a0adf..d81bf06a94c 100644 --- a/packages/manager/src/factories/subnets.ts +++ b/packages/manager/src/factories/subnets.ts @@ -3,6 +3,7 @@ import { Factory } from '@linode/utilities'; import type { Subnet, SubnetAssignedLinodeData, + SubnetAssignedNodeBalancerData, } from '@linode/api-v4/lib/vpcs/types'; // NOTE: Changing to fixed array length for the interfaces and linodes fields of the @@ -20,6 +21,12 @@ export const subnetAssignedLinodeDataFactory = ), }); +export const subnetAssignedNodebalancerDataFactory = + Factory.Sync.makeFactory({ + id: Factory.each((i) => i), + ipv4_range: Factory.each((i) => `192.168.${i}.0/30`), + }); + export const subnetFactory = Factory.Sync.makeFactory({ created: '2023-07-12T16:08:53', id: Factory.each((i) => i), @@ -32,6 +39,12 @@ export const subnetFactory = Factory.Sync.makeFactory({ }) ) ), - nodebalancers: [], + nodebalancers: Factory.each((i) => + Array.from({ length: 3 }, (_, arrIdx) => + subnetAssignedNodebalancerDataFactory.build({ + id: i * 10 + arrIdx, + }) + ) + ), updated: '2023-07-12T16:08:53', }); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx index 4ba7f209e6e..995d2de1df2 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx @@ -111,20 +111,20 @@ export const SubnetLinodeRow = (props: Props) => { ) : _hasUnrecommendedConfiguration(config, subnetId); - if (linodeLoading || !linode) { + if (linodeLoading) { return ( - + ); } - if (linodeError) { + if (linodeError || !linode) { return ( - + - Linode Label + Linode Status VPC IPv4 diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx new file mode 100644 index 00000000000..c296c4806a4 --- /dev/null +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx @@ -0,0 +1,97 @@ +import { waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import * as React from 'react'; +import { afterAll, afterEach, beforeAll, describe, it } from 'vitest'; + +import { + firewallFactory, + subnetAssignedNodebalancerDataFactory, +} from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { + mockMatchMedia, + renderWithTheme, + wrapWithTableBody, +} from 'src/utilities/testHelpers'; + +import { SubnetNodeBalancerRow } from './SubnetNodebalancerRow'; + +const LOADING_TEST_ID = 'circle-progress'; + +beforeAll(() => mockMatchMedia()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('SubnetNodeBalancerRow', () => { + const nodebalancer = { + id: 123, + label: 'test-nodebalancer', + }; + + const configs = [ + { nodes_status: { up: 3, down: 1 } }, + { nodes_status: { up: 2, down: 2 } }, + ]; + + const firewalls = makeResourcePage( + firewallFactory.buildList(1, { label: 'mock-firewall' }) + ); + + const subnetNodebalancer = subnetAssignedNodebalancerDataFactory.build({ + id: nodebalancer.id, + ipv4_range: '192.168.99.0/30', + }); + + it('renders loading state', async () => { + const { getByTestId } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument(); + await waitForElementToBeRemoved(() => getByTestId(LOADING_TEST_ID)); + }); + + it('renders nodebalancer row with data', async () => { + server.use( + http.get('*/nodebalancers/:id', () => { + return HttpResponse.json(nodebalancer); + }), + http.get('*/nodebalancers/:id/configs', () => { + return HttpResponse.json(configs); + }), + http.get('*/nodebalancers/:id/firewalls', () => { + return HttpResponse.json(firewalls); + }) + ); + + const { getByText, getByRole } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + await waitFor(() => { + expect(getByText(nodebalancer.label)).toBeInTheDocument(); + }); + + expect(getByText(subnetNodebalancer.ipv4_range)).toBeInTheDocument(); + expect(getByText('mock-firewall')).toBeInTheDocument(); + + const nodebalancerLink = getByRole('link', { + name: nodebalancer.label, + }); + + expect(nodebalancerLink).toHaveAttribute( + 'href', + `/nodebalancers/${nodebalancer.id}/summary` + ); + }); +}); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx new file mode 100644 index 00000000000..a46a950fb91 --- /dev/null +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx @@ -0,0 +1,121 @@ +import { + useAllNodeBalancerConfigsQuery, + useNodeBalancerQuery, + useNodeBalancersFirewallsQuery, +} from '@linode/queries'; +import { Box, CircleProgress, Hidden } from '@linode/ui'; +import ErrorOutline from '@mui/icons-material/ErrorOutline'; +import { Typography } from '@mui/material'; +import * as React from 'react'; + +import { Link } from 'src/components/Link'; +import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; + +interface Props { + hover?: boolean; + ipv4: string; + nodeBalancerId: number; +} + +export const SubnetNodeBalancerRow = ({ + nodeBalancerId, + hover = false, + ipv4, +}: Props) => { + const { + data: nodebalancer, + error: nodebalancerError, + isLoading: nodebalancerLoading, + } = useNodeBalancerQuery(nodeBalancerId); + const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery( + Number(nodeBalancerId) + ); + const { data: configs } = useAllNodeBalancerConfigsQuery( + Number(nodeBalancerId) + ); + + const firewallLabel = attachedFirewallData?.data[0]?.label; + const firewallId = attachedFirewallData?.data[0]?.id; + + const down = configs?.reduce((acc: number, config) => { + return acc + config.nodes_status.down; + }, 0); // add the downtime for each config together + + const up = configs?.reduce((acc: number, config) => { + return acc + config.nodes_status.up; + }, 0); // add the uptime for each config together + + if (nodebalancerLoading) { + return ( + + + + + + ); + } + + if (nodebalancerError || !nodebalancer) { + return ( + + + + ({ color: theme.color.red, marginRight: 1 })} + /> + + There was an error loading{' '} + + Nodebalancer {nodeBalancerId} + + + + + + ); + } + + return ( + + + + {nodebalancer?.label} + + + + + {`${up} up, ${down} down`} + + {ipv4} + + + {firewallLabel} + + + + ); +}; + +export const SubnetNodebalancerTableRowHead = ( + + NodeBalancer + Backend Status + + VPC IPv4 Range + + + Firewalls + + + +); diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx index 3992a0b6aad..aefd1aa0e45 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx @@ -76,6 +76,43 @@ describe('VPC Detail Summary section', () => { getAllByText(vpcFactory1.updated); }); + it('should display number of subnets and resources, region, id, creation and update dates', async () => { + const vpcFactory1 = vpcFactory.build({ id: 1, subnets: [] }); + server.use( + http.get('*/vpcs/:vpcId', () => { + return HttpResponse.json(vpcFactory1); + }) + ); + + const { getAllByText, queryByTestId } = await renderWithThemeAndRouter( + , + { + flags: { nodebalancerVpc: true }, + } + ); + + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } + + getAllByText('Subnets'); + getAllByText('Resources'); + getAllByText('0'); + + getAllByText('Region'); + getAllByText('US, Newark, NJ'); + + getAllByText('VPC ID'); + getAllByText(vpcFactory1.id); + + getAllByText('Created'); + getAllByText(vpcFactory1.created); + + getAllByText('Updated'); + getAllByText(vpcFactory1.updated); + }); + it('should display description if one is provided', async () => { const vpcFactory1 = vpcFactory.build({ description: `VPC for webserver and database.`, diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx index 20d86349282..557cea1daea 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx @@ -16,11 +16,13 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { EntityHeader } from 'src/components/EntityHeader/EntityHeader'; import { LandingHeader } from 'src/components/LandingHeader'; import { LKE_ENTERPRISE_VPC_WARNING } from 'src/features/Kubernetes/constants'; +import { useIsNodebalancerVPCEnabled } from 'src/features/NodeBalancers/utils'; import { VPC_DOCS_LINK, VPC_LABEL } from 'src/features/VPCs/constants'; import { getIsVPCLKEEnterpriseCluster, getUniqueLinodesFromSubnets, + getUniqueResourcesFromSubnets, } from '../utils'; import { VPCDeleteDialog } from '../VPCLanding/VPCDeleteDialog'; import { VPCEditDrawer } from '../VPCLanding/VPCEditDrawer'; @@ -48,6 +50,8 @@ const VPCDetail = () => { isLoading, } = useVPCQuery(Number(vpcId) || -1, Boolean(vpcId)); + const flags = useIsNodebalancerVPCEnabled(); + const { data: regions } = useRegionsQuery(); const handleEditVPC = (vpc: VPC) => { @@ -93,7 +97,9 @@ const VPCDetail = () => { const regionLabel = regions?.find((r) => r.id === vpc.region)?.label ?? vpc.region; - const numLinodes = getUniqueLinodesFromSubnets(vpc.subnets); + const numResources = flags.isNodebalancerVPCEnabled + ? getUniqueResourcesFromSubnets(vpc.subnets) + : getUniqueLinodesFromSubnets(vpc.subnets); const summaryData = [ [ @@ -102,8 +108,8 @@ const VPCDetail = () => { value: vpc.subnets.length, }, { - label: 'Linodes', - value: numLinodes, + label: flags.isNodebalancerVPCEnabled ? 'Resources' : 'Linodes', + value: numResources, }, ], [ diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx index 01f937e46c9..6d32fc39632 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx @@ -70,7 +70,7 @@ describe('VPC Subnets table', () => { } getByPlaceholderText('Filter Subnets by label or id'); - getByText('Subnet Label'); + getByText('Subnet'); getByText(subnet.label); getByText('Subnet ID'); getAllByText(subnet.id); @@ -90,6 +90,66 @@ describe('VPC Subnets table', () => { getByText('Delete'); }); + it('should display filter input, subnet label, id, ip range, number of resources, and action menu', async () => { + const subnet = subnetFactory.build({ + linodes: [ + subnetAssignedLinodeDataFactory.build({ id: 1 }), + subnetAssignedLinodeDataFactory.build({ id: 2 }), + subnetAssignedLinodeDataFactory.build({ id: 3 }), + ], + }); + server.use( + http.get('*/vpcs/:vpcId/subnets', () => { + return HttpResponse.json(makeResourcePage([subnet])); + }), + http.get('*/networking/firewalls/settings', () => { + return HttpResponse.json(firewallSettingsFactory.build()); + }) + ); + + const { + getAllByRole, + getAllByText, + getByPlaceholderText, + getByText, + queryByTestId, + } = await renderWithThemeAndRouter( + , + { + flags: { nodebalancerVpc: true }, + } + ); + + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } + + getByPlaceholderText('Filter Subnets by label or id'); + getByText('Subnet'); + getByText(subnet.label); + getByText('Subnet ID'); + getAllByText(subnet.id); + + getByText('Subnet IP Range'); + getByText(subnet.ipv4!); + + getByText('Resources'); + getByText(subnet.linodes.length + subnet.nodebalancers.length); + + const actionMenuButton = getAllByRole('button')[4]; + await userEvent.click(actionMenuButton); + + getByText('Assign Linodes'); + getByText('Unassign Linodes'); + getByText('Edit'); + getByText('Delete'); + }); + it('should display no linodes text if there are no linodes associated with the subnet', async () => { const subnet = subnetFactory.build({ linodes: [] }); server.use( @@ -149,12 +209,77 @@ describe('VPC Subnets table', () => { const expandTableButton = getAllByRole('button')[3]; await userEvent.click(expandTableButton); - getByText('Linode Label'); + getByText('Linode'); getByText('Status'); getByText('VPC IPv4'); getByText('Firewalls'); }); + it('should display no nodeBalancers text if there are no nodeBalancers associated with the subnet', async () => { + const subnet = subnetFactory.build({ nodebalancers: [] }); + + server.use( + http.get('*/vpcs/:vpcId/subnets', () => { + return HttpResponse.json(makeResourcePage([subnet])); + }), + http.get('*/networking/firewalls/settings', () => { + return HttpResponse.json(firewallSettingsFactory.build()); + }) + ); + + const { getAllByRole, getByText, queryByTestId } = + await renderWithThemeAndRouter( + , + { flags: { nodebalancerVpc: true } } + ); + + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } + + const expandTableButton = getAllByRole('button')[3]; + await userEvent.click(expandTableButton); + getByText('No NodeBalancers'); + }); + + it('should show Nodebalancer table head data when table is expanded', async () => { + const subnet = subnetFactory.build(); + server.use( + http.get('*/vpcs/:vpcId/subnets', () => { + return HttpResponse.json(makeResourcePage([subnet])); + }), + http.get('*/networking/firewalls/settings', () => { + return HttpResponse.json(firewallSettingsFactory.build()); + }) + ); + const { getAllByRole, getByText, queryByTestId } = + await renderWithThemeAndRouter( + , + { flags: { nodebalancerVpc: true } } + ); + + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } + + const expandTableButton = getAllByRole('button')[3]; + await userEvent.click(expandTableButton); + + getByText('NodeBalancer'); + getByText('Backend Status'); + getByText('VPC IPv4 Range'); + }); + it('should disable Create Subnet button if the VPC is associated with a LKE-E cluster', async () => { server.use( http.get('*/networking/firewalls/settings', () => { diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx index 84d4ce1578c..b509b94c2f0 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx @@ -26,6 +26,7 @@ import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableSortCell } from 'src/components/TableSortCell'; import { PowerActionsDialog } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; +import { useIsNodebalancerVPCEnabled } from 'src/features/NodeBalancers/utils'; import { SubnetActionMenu } from 'src/features/VPCs/VPCDetail/SubnetActionMenu'; import { useOrderV2 } from 'src/hooks/useOrderV2'; import { usePaginationV2 } from 'src/hooks/usePaginationV2'; @@ -37,6 +38,10 @@ import { SubnetCreateDrawer } from './SubnetCreateDrawer'; import { SubnetDeleteDialog } from './SubnetDeleteDialog'; import { SubnetEditDrawer } from './SubnetEditDrawer'; import { SubnetLinodeRow, SubnetLinodeTableRowHead } from './SubnetLinodeRow'; +import { + SubnetNodeBalancerRow, + SubnetNodebalancerTableRowHead, +} from './SubnetNodebalancerRow'; import { SubnetUnassignLinodesDrawer } from './SubnetUnassignLinodesDrawer'; import type { Linode } from '@linode/api-v4/lib/linodes/types'; @@ -81,6 +86,8 @@ export const VPCSubnetsTable = (props: Props) => { }); const { query } = search; + const flags = useIsNodebalancerVPCEnabled(); + const pagination = usePaginationV2({ currentRoute: VPC_DETAILS_ROUTE, preferenceKey, @@ -271,7 +278,7 @@ export const VPCSubnetsTable = (props: Props) => { width: '24%', })} > - Subnet Label + Subnet { Subnet IP Range - Linodes + {`${flags.isNodebalancerVPCEnabled ? 'Resources' : 'Linodes'}`} @@ -301,7 +310,9 @@ export const VPCSubnetsTable = (props: Props) => { {subnet.ipv4} - {subnet.linodes.length} + + {`${flags.isNodebalancerVPCEnabled ? subnet.linodes.length + subnet.nodebalancers.length : subnet.linodes.length}`} + { ); const InnerTable = ( -
- - {SubnetLinodeTableRowHead} - - - {subnet.linodes.length > 0 ? ( - subnet.linodes.map((linodeInfo) => ( - - )) - ) : ( - - )} - -
+ <> + + + {SubnetLinodeTableRowHead} + + + {subnet.linodes.length > 0 ? ( + subnet.linodes.map((linodeInfo) => ( + + )) + ) : ( + + )} + +
+ {flags.isNodebalancerVPCEnabled && ( + + + {SubnetNodebalancerTableRowHead} + + + {subnet.nodebalancers?.length > 0 ? ( + subnet.nodebalancers.map((nb) => ( + + )) + ) : ( + + )} + +
+ )} + ); return { @@ -398,7 +434,7 @@ export const VPCSubnetsTable = (props: Props) => { + } TableRowHead={SubnetTableRowHead} /> diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx index 91d88a6b56e..504db8f4532 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx @@ -44,6 +44,36 @@ describe('VPC Landing Table', () => { getAllByText('Linodes'); }); + it('should render vpc landing table with items with nodebalancerVpc flag enabled', async () => { + server.use( + http.get('*/vpcs', () => { + const vpcsWithSubnet = vpcFactory.buildList(3, { + subnets: subnetFactory.buildList(Math.floor(Math.random() * 10) + 1), + }); + return HttpResponse.json(makeResourcePage(vpcsWithSubnet)); + }) + ); + + const { getAllByText, queryByTestId } = await renderWithThemeAndRouter( + , + { + flags: { nodebalancerVpc: true }, + } + ); + + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } + + // Static text and table column headers + getAllByText('Label'); + getAllByText('Region'); + getAllByText('VPC ID'); + getAllByText('Subnets'); + getAllByText('Resources'); + }); + it('should render vpc landing with empty state', async () => { server.use( http.get('*/vpcs', () => { diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx index 7b872674e24..cb1947226a3 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx @@ -13,6 +13,7 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { useIsNodebalancerVPCEnabled } from 'src/features/NodeBalancers/utils'; import { VPC_CREATE_ROUTE, VPC_LANDING_ROUTE, @@ -100,6 +101,8 @@ const VPCLanding = () => { error: selectedVPCError, } = useVPCQuery(params.vpcId ?? -1, !!params.vpcId); + const flags = useIsNodebalancerVPCEnabled(); + if (error) { return ( { Subnets - Linodes + {`${flags.isNodebalancerVPCEnabled ? 'Resources' : 'Linodes'}`} @@ -178,6 +181,7 @@ const VPCLanding = () => { handleDeleteVPC(vpc)} handleEditVPC={() => handleEditVPC(vpc)} + isNodebalancerVPCEnabled={flags.isNodebalancerVPCEnabled} key={vpc.id} vpc={vpc} /> diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.test.tsx index f7864e78675..ab70607450b 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.test.tsx @@ -17,7 +17,12 @@ describe('VPC Table Row', () => { const { getAllByText, getByText } = renderWithTheme( wrapWithTableBody( - + ) ); @@ -38,6 +43,7 @@ describe('VPC Table Row', () => { ) @@ -55,6 +61,7 @@ describe('VPC Table Row', () => { ) @@ -71,7 +78,12 @@ describe('VPC Table Row', () => { }); const { getAllByRole } = renderWithTheme( wrapWithTableBody( - + ) ); const actionButtons = getAllByRole('button'); diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx index 6e7ccba7130..dacac7209b4 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx @@ -10,7 +10,11 @@ import { getRestrictedResourceText } from 'src/features/Account/utils'; import { LKE_ENTERPRISE_VPC_WARNING } from 'src/features/Kubernetes/constants'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; -import { getIsVPCLKEEnterpriseCluster } from '../utils'; +import { + getIsVPCLKEEnterpriseCluster, + getUniqueLinodesFromSubnets, + getUniqueResourcesFromSubnets, +} from '../utils'; import type { VPC } from '@linode/api-v4/lib/vpcs/types'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; @@ -18,18 +22,23 @@ import type { Action } from 'src/components/ActionMenu/ActionMenu'; interface Props { handleDeleteVPC: () => void; handleEditVPC: () => void; + isNodebalancerVPCEnabled: boolean; vpc: VPC; } -export const VPCRow = ({ handleDeleteVPC, handleEditVPC, vpc }: Props) => { +export const VPCRow = ({ + handleDeleteVPC, + handleEditVPC, + isNodebalancerVPCEnabled, + vpc, +}: Props) => { const { id, label, subnets } = vpc; const { data: regions } = useRegionsQuery(); const regionLabel = regions?.find((r) => r.id === vpc.region)?.label ?? ''; - const numLinodes = subnets.reduce( - (acc, subnet) => acc + subnet.linodes.length, - 0 - ); + const numResources = isNodebalancerVPCEnabled + ? getUniqueResourcesFromSubnets(vpc.subnets) + : getUniqueLinodesFromSubnets(vpc.subnets); const isVPCReadOnly = useIsResourceRestricted({ grantLevel: 'read_only', @@ -83,7 +92,7 @@ export const VPCRow = ({ handleDeleteVPC, handleEditVPC, vpc }: Props) => { {subnets.length} - {numLinodes} + {numResources} {actions.map((action) => ( diff --git a/packages/manager/src/features/VPCs/utils.test.ts b/packages/manager/src/features/VPCs/utils.test.ts index 21c48cb1982..11cf3a75b63 100644 --- a/packages/manager/src/features/VPCs/utils.test.ts +++ b/packages/manager/src/features/VPCs/utils.test.ts @@ -7,6 +7,7 @@ import { import { linodeConfigFactory } from 'src/factories/linodeConfigs'; import { subnetAssignedLinodeDataFactory, + subnetAssignedNodebalancerDataFactory, subnetFactory, } from 'src/factories/subnets'; @@ -14,6 +15,7 @@ import { getLinodeInterfacePrimaryIPv4, getLinodeInterfaceRanges, getUniqueLinodesFromSubnets, + getUniqueResourcesFromSubnets, getVPCInterfacePayload, hasUnrecommendedConfiguration, hasUnrecommendedConfigurationLinodeInterface, @@ -26,6 +28,76 @@ const subnetLinodeInfoList1 = subnetAssignedLinodeDataFactory.buildList(4); const subnetLinodeInfoId1 = subnetAssignedLinodeDataFactory.build({ id: 1 }); const subnetLinodeInfoId3 = subnetAssignedLinodeDataFactory.build({ id: 3 }); +const subnetNodeBalancerInfoList1 = + subnetAssignedNodebalancerDataFactory.buildList(4); +const subnetNodeBalancerInfoId1 = subnetAssignedNodebalancerDataFactory.build({ + id: 1, +}); +const subnetNodeBalancerInfoId3 = subnetAssignedNodebalancerDataFactory.build({ + id: 3, +}); + +describe('getUniqueResourcesFromSubnets', () => { + it(`returns the number of unique linodes and nodeBalancers within a VPC's subnets`, () => { + const subnets0 = [subnetFactory.build({ linodes: [], nodebalancers: [] })]; + const subnets1 = [ + subnetFactory.build({ + linodes: subnetLinodeInfoList1, + nodebalancers: subnetNodeBalancerInfoList1, + }), + ]; + const subnets2 = [ + subnetFactory.build({ + linodes: [ + subnetLinodeInfoId1, + subnetLinodeInfoId1, + subnetLinodeInfoId3, + subnetLinodeInfoId3, + ], + nodebalancers: [ + subnetNodeBalancerInfoId1, + subnetNodeBalancerInfoId1, + subnetNodeBalancerInfoId3, + subnetNodeBalancerInfoId3, + ], + }), + ]; + const subnets3 = [ + subnetFactory.build({ + linodes: subnetLinodeInfoList1, + nodebalancers: subnetNodeBalancerInfoList1, + }), + subnetFactory.build({ linodes: [], nodebalancers: [] }), + subnetFactory.build({ + linodes: [subnetLinodeInfoId3], + nodebalancers: [subnetNodeBalancerInfoId3], + }), + subnetFactory.build({ + linodes: [ + subnetAssignedLinodeDataFactory.build({ id: 6 }), + subnetAssignedLinodeDataFactory.build({ id: 7 }), + subnetAssignedLinodeDataFactory.build({ id: 8 }), + subnetAssignedLinodeDataFactory.build({ id: 9 }), + subnetLinodeInfoId1, + ], + nodebalancers: [ + subnetAssignedNodebalancerDataFactory.build({ id: 6 }), + subnetAssignedNodebalancerDataFactory.build({ id: 7 }), + subnetAssignedNodebalancerDataFactory.build({ id: 8 }), + subnetAssignedNodebalancerDataFactory.build({ id: 9 }), + subnetNodeBalancerInfoId1, + ], + }), + ]; + + expect(getUniqueResourcesFromSubnets(subnets0)).toBe(0); + expect(getUniqueResourcesFromSubnets(subnets1)).toBe(8); + expect(getUniqueResourcesFromSubnets(subnets2)).toBe(4); + // updated factory for generating linode ids, so unique linodes will be different + expect(getUniqueResourcesFromSubnets(subnets3)).toBe(16); + }); +}); + describe('getUniqueLinodesFromSubnets', () => { it(`returns the number of unique linodes within a VPC's subnets`, () => { const subnets0 = [subnetFactory.build({ linodes: [] })]; diff --git a/packages/manager/src/features/VPCs/utils.ts b/packages/manager/src/features/VPCs/utils.ts index 65781321421..31a171caef3 100644 --- a/packages/manager/src/features/VPCs/utils.ts +++ b/packages/manager/src/features/VPCs/utils.ts @@ -23,6 +23,24 @@ export const getUniqueLinodesFromSubnets = (subnets: Subnet[]) => { return linodes.length; }; +export const getUniqueResourcesFromSubnets = (subnets: Subnet[]) => { + const linodes: number[] = []; + const nodeBalancer: number[] = []; + for (const subnet of subnets) { + subnet.linodes.forEach((linodeInfo) => { + if (!linodes.includes(linodeInfo.id)) { + linodes.push(linodeInfo.id); + } + }); + subnet.nodebalancers.forEach((nodeBalancerInfo) => { + if (!nodeBalancer.includes(nodeBalancerInfo.id)) { + nodeBalancer.push(nodeBalancerInfo.id); + } + }); + } + return linodes.length + nodeBalancer.length; +}; + // Linode Interfaces: show unrecommended notice if (active) VPC interface has an IPv4 nat_1_1 address but isn't the default IPv4 route export const hasUnrecommendedConfigurationLinodeInterface = ( linodeInterface: LinodeInterface | undefined, From 3b1465d2ce9a64530e3bd589b85c39cfcce85c9c Mon Sep 17 00:00:00 2001 From: Hussain Khalil <122488130+hkhalil-akamai@users.noreply.github.com> Date: Wed, 28 May 2025 14:23:03 -0400 Subject: [PATCH 55/59] upcoming: [M3-9163] - QEMU reboot notices (#12231) * Add new security reboot notification type * Introduce PlatformMaintenanceBanner and usePlatformMaintenance hook * Finalize QEMU banners * Added changeset: QEMU reboot notices * Added changeset: Notification type for QEMU maintenance * Add Platform maintenance notification template to MSW * Final changes to reflect devcloud data * Add unit tests * Feedback @jdamore-linode -- add gap in banner reboot button * Feedback @bnussman-akamai: remove async test queries --- .../pr-12231-added-1747351003855.md | 5 + packages/api-v4/src/account/types.ts | 3 +- ...r-12231-upcoming-features-1747350971547.md | 5 + .../DateTimeDisplay/DateTimeDisplay.tsx | 9 +- .../MaintenanceBanner/MaintenanceBanner.tsx | 8 +- .../LinodePlatformMaintenanceBanner.test.tsx | 127 ++++++++++++++++++ .../LinodePlatformMaintenanceBanner.tsx | 112 +++++++++++++++ .../PlatformMaintenanceBanner.test.tsx | 108 +++++++++++++++ .../PlatformMaintenanceBanner.tsx | 37 +++++ .../components/ExtraPresetMaintenance.tsx | 20 +++ .../components/ExtraPresetNotifications.tsx | 9 ++ .../src/factories/accountMaintenance.ts | 16 ++- packages/manager/src/factories/databases.ts | 2 +- .../src/features/Account/AccountLanding.tsx | 2 + .../features/Account/Maintenance/utilities.ts | 10 +- .../LinodesDetailHeader/Notifications.tsx | 17 ++- .../Linodes/LinodesLanding/LinodesLanding.tsx | 2 + .../src/hooks/usePlatformMaintenance.test.ts | 99 ++++++++++++++ .../src/hooks/usePlatformMaintenance.ts | 81 +++++++++++ packages/utilities/src/helpers/random.ts | 8 +- 20 files changed, 663 insertions(+), 17 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-12231-added-1747351003855.md create mode 100644 packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md create mode 100644 packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.test.tsx create mode 100644 packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.tsx create mode 100644 packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.test.tsx create mode 100644 packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.tsx create mode 100644 packages/manager/src/hooks/usePlatformMaintenance.test.ts create mode 100644 packages/manager/src/hooks/usePlatformMaintenance.ts diff --git a/packages/api-v4/.changeset/pr-12231-added-1747351003855.md b/packages/api-v4/.changeset/pr-12231-added-1747351003855.md new file mode 100644 index 00000000000..cbb0459ad18 --- /dev/null +++ b/packages/api-v4/.changeset/pr-12231-added-1747351003855.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +Notification type for QEMU maintenance ([#12231](https://github.com/linode/manager/pull/12231)) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 82cc6f644dd..3920a8820e6 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -285,6 +285,7 @@ export type NotificationType = | 'payment_due' | 'promotion' | 'reboot_scheduled' + | 'security_reboot_maintenance_scheduled' | 'tax_id_verifying' | 'ticket_abuse' | 'ticket_important' @@ -573,7 +574,7 @@ export interface AccountMaintenance { entity: { id: number; label: string; - type: string; + type: 'linode' | 'volume'; url: string; }; maintenance_policy_set: MaintenancePolicyType; diff --git a/packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md b/packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md new file mode 100644 index 00000000000..9d4cb0cd3ac --- /dev/null +++ b/packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +QEMU reboot notices ([#12231](https://github.com/linode/manager/pull/12231)) diff --git a/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx b/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx index c2fdfaeee14..da43b4ae11a 100644 --- a/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx +++ b/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { formatDate } from 'src/utilities/formatDate'; +import type { SxProps, Theme } from '@linode/ui'; import type { TimeInterval } from 'src/utilities/formatDate'; export interface DateTimeDisplayProps { @@ -23,6 +24,10 @@ export interface DateTimeDisplayProps { * If the date and time provided is within the designated time frame then the date is displayed as a relative date */ humanizeCutoff?: TimeInterval; + /** + * Styles to pass through to the sx prop. + */ + sx?: SxProps; /** * The date and time string to display */ @@ -30,10 +35,10 @@ export interface DateTimeDisplayProps { } const DateTimeDisplay = (props: DateTimeDisplayProps) => { - const { className, displayTime, format, humanizeCutoff, value } = props; + const { className, displayTime, format, humanizeCutoff, value, sx } = props; const { data: profile } = useProfile(); return ( - + {formatDate(value, { displayTime, format, diff --git a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx index 4cc5cef55ab..18eef546841 100644 --- a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx +++ b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { Link } from 'src/components/Link'; import { PENDING_MAINTENANCE_FILTER } from 'src/features/Account/Maintenance/utilities'; +import { isPlatformMaintenance } from 'src/hooks/usePlatformMaintenance'; import { formatDate } from 'src/utilities/formatDate'; import { isPast } from 'src/utilities/isPast'; @@ -19,11 +20,16 @@ interface Props { export const MaintenanceBanner = React.memo((props: Props) => { const { maintenanceEnd, maintenanceStart, type } = props; - const { data: accountMaintenanceData } = useAllAccountMaintenanceQuery( + const { data: rawAccountMaintenanceData } = useAllAccountMaintenanceQuery( {}, PENDING_MAINTENANCE_FILTER ); + // Filter out platform maintenance, since that is handled separately + const accountMaintenanceData = rawAccountMaintenanceData?.filter( + (maintenance) => !isPlatformMaintenance(maintenance) + ); + const { data: profile, error: profileError, diff --git a/packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.test.tsx b/packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.test.tsx new file mode 100644 index 00000000000..a7a5ee043b4 --- /dev/null +++ b/packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.test.tsx @@ -0,0 +1,127 @@ +/* eslint-disable testing-library/prefer-screen-queries */ +import { linodeFactory } from '@linode/utilities'; +import React from 'react'; + +import { accountMaintenanceFactory, notificationFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { LinodePlatformMaintenanceBanner } from './LinodePlatformMaintenanceBanner'; + +const queryMocks = vi.hoisted(() => ({ + useNotificationsQuery: vi.fn().mockReturnValue({}), + useAllAccountMaintenanceQuery: vi.fn().mockReturnValue({}), + useLinodeQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + ...queryMocks, + }; +}); + +beforeEach(() => { + vi.stubEnv('TZ', 'UTC'); +}); + +describe('LinodePlatformMaintenanceBanner', () => { + it("doesn't render when there is no platform maintenance", () => { + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: accountMaintenanceFactory.buildList(3, { + type: 'reboot', + entity: { + type: 'linode', + }, + }), + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: [], + }); + + const { queryByText } = renderWithTheme( + + ); + + expect( + queryByText('needs to be rebooted for critical platform maintenance.') + ).not.toBeInTheDocument(); + }); + + it('does not render if there is a notification but not a maintenance item', () => { + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: accountMaintenanceFactory.buildList(3, { + type: 'reboot', + entity: { + type: 'linode', + }, + reason: 'Unrelated maintenance', + }), + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: notificationFactory.buildList(1, { + type: 'security_reboot_maintenance_scheduled', + label: 'Platform Maintenance Scheduled', + }), + }); + + const { queryByText } = renderWithTheme( + + ); + + expect( + queryByText('needs to be rebooted for critical platform maintenance.') + ).not.toBeInTheDocument(); + }); + + it('renders when a maintenance item is returned', () => { + const mockPlatformMaintenance = accountMaintenanceFactory.buildList(2, { + type: 'reboot', + entity: { type: 'linode' }, + reason: 'Your Linode needs a critical security update', + when: '2020-01-01T00:00:00', + start_time: '2020-01-01T00:00:00', + }); + const mockMaintenance = [ + ...mockPlatformMaintenance, + accountMaintenanceFactory.build({ + type: 'reboot', + entity: { type: 'linode' }, + reason: 'Unrelated maintenance item', + }), + ]; + + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: mockMaintenance, + }); + + queryMocks.useLinodeQuery.mockReturnValue({ + data: linodeFactory.build({ + id: mockPlatformMaintenance[0].entity.id, + label: 'linode-with-platform-maintenance', + }), + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: notificationFactory.buildList(1, { + type: 'security_reboot_maintenance_scheduled', + label: 'Platform Maintenance Scheduled', + }), + }); + + const { getByText } = renderWithTheme( + + ); + + expect(getByText('linode-with-platform-maintenance')).toBeVisible(); + expect( + getByText((el) => + el.includes('needs to be rebooted for critical platform maintenance.') + ) + ).toBeVisible(); + }); +}); diff --git a/packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.tsx b/packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.tsx new file mode 100644 index 00000000000..7e5151163c5 --- /dev/null +++ b/packages/manager/src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner.tsx @@ -0,0 +1,112 @@ +import { useLinodeQuery } from '@linode/queries'; +import { Notice } from '@linode/ui'; +import { Box, Button, Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import { PowerActionsDialog } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; +import { usePlatformMaintenance } from 'src/hooks/usePlatformMaintenance'; + +import { DateTimeDisplay } from '../DateTimeDisplay'; +import { Link } from '../Link'; + +import type { AccountMaintenance, Linode } from '@linode/api-v4'; + +export const LinodePlatformMaintenanceBanner = (props: { + linodeId: Linode['id']; +}) => { + const { linodeId } = props; + + const { linodesWithPlatformMaintenance, platformMaintenanceByLinode } = + usePlatformMaintenance(); + + const { data: linode } = useLinodeQuery( + linodeId, + linodesWithPlatformMaintenance.has(linodeId) + ); + + const [isRebootDialogOpen, setIsRebootDialogOpen] = React.useState(false); + + if (!linodesWithPlatformMaintenance.has(linodeId)) return null; + + const earliestMaintenance = platformMaintenanceByLinode[linodeId].reduce( + (earliest, current) => { + const currentMaintenanceStartTime = getMaintenanceStartTime(current); + const earliestMaintenanceStartTime = getMaintenanceStartTime(earliest); + + if (currentMaintenanceStartTime && earliestMaintenanceStartTime) { + return currentMaintenanceStartTime < earliestMaintenanceStartTime + ? current + : earliest; + } + + return earliest; + }, + platformMaintenanceByLinode[linodeId][0] + ); + + const startTime = getMaintenanceStartTime(earliestMaintenance); + + return ( + <> + + + + + Linode{' '} + + {linode?.label ?? linodeId} + {' '} + needs to be rebooted for critical platform maintenance.{' '} + {startTime && ( + <> + A reboot is scheduled for{' '} + + ({ + fontWeight: theme.tokens.font.FontWeight.Bold, + })} + value={startTime} + />{' '} + at{' '} + ({ + fontWeight: theme.tokens.font.FontWeight.Bold, + })} + value={startTime} + /> + + . + + )} + + + + + + setIsRebootDialogOpen(false)} + /> + + ); +}; + +// The 'start_time' field might not be available, so fallback to 'when' +const getMaintenanceStartTime = ( + maintenance: AccountMaintenance +): null | string | undefined => maintenance.start_time ?? maintenance.when; diff --git a/packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.test.tsx b/packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.test.tsx new file mode 100644 index 00000000000..250b3cc821f --- /dev/null +++ b/packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.test.tsx @@ -0,0 +1,108 @@ +/* eslint-disable testing-library/prefer-screen-queries */ +import React from 'react'; + +import { accountMaintenanceFactory, notificationFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { PlatformMaintenanceBanner } from './PlatformMaintenanceBanner'; + +const queryMocks = vi.hoisted(() => ({ + useNotificationsQuery: vi.fn().mockReturnValue({}), + useAllAccountMaintenanceQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + ...queryMocks, + }; +}); + +describe('PlatformMaintenanceBanner', () => { + it("doesn't render when there is no platform maintenance", () => { + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: accountMaintenanceFactory.buildList(3, { + type: 'reboot', + entity: { + type: 'linode', + }, + }), + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: [], + }); + + const { queryByText } = renderWithTheme(); + + expect(queryByText('One or more Linodes')).not.toBeInTheDocument(); + expect( + queryByText('needs to be rebooted for critical platform maintenance.') + ).not.toBeInTheDocument(); + }); + + it('renders with generic message when there is a notification', () => { + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: accountMaintenanceFactory.buildList(3, { + type: 'reboot', + entity: { + type: 'linode', + }, + reason: 'Unrelated maintenance', + }), + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: notificationFactory.buildList(1, { + type: 'security_reboot_maintenance_scheduled', + label: 'Platform Maintenance Scheduled', + }), + }); + + const { getByText } = renderWithTheme(); + + expect(getByText('One or more Linodes')).toBeVisible(); + expect( + getByText((el) => + el.includes('need to be rebooted for critical platform maintenance.') + ) + ).toBeVisible(); + }); + + it('renders with count of affected linodes', () => { + const mockPlatformMaintenance = accountMaintenanceFactory.buildList(2, { + type: 'reboot', + entity: { type: 'linode' }, + reason: 'Your Linode needs a critical security update', + }); + const mockMaintenance = [ + ...mockPlatformMaintenance, + accountMaintenanceFactory.build({ + type: 'reboot', + entity: { type: 'linode' }, + reason: 'Unrelated maintenance item', + }), + ]; + + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: mockMaintenance, + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: notificationFactory.buildList(1, { + type: 'security_reboot_maintenance_scheduled', + label: 'Platform Maintenance Scheduled', + }), + }); + + const { getByText } = renderWithTheme(); + + expect(getByText('2 Linodes')).toBeVisible(); + expect( + getByText((el) => + el.includes('need to be rebooted for critical platform maintenance.') + ) + ).toBeVisible(); + }); +}); diff --git a/packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.tsx b/packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.tsx new file mode 100644 index 00000000000..57c08bca624 --- /dev/null +++ b/packages/manager/src/components/PlatformMaintenanceBanner/PlatformMaintenanceBanner.tsx @@ -0,0 +1,37 @@ +import { Notice, Typography } from '@linode/ui'; +import React from 'react'; + +import { usePlatformMaintenance } from 'src/hooks/usePlatformMaintenance'; + +import { Link } from '../Link'; + +/** + * This banner will be used in the event of VM platform maintenance, + * that requires a reboot, e.g., QEMU upgrades. Since these are + * urgent and affect a large portion of Linodes, we are displaying + * them separately from the standard MaintenanceBanner. + */ + +export const PlatformMaintenanceBanner = () => { + const { accountHasPlatformMaintenance, linodesWithPlatformMaintenance } = + usePlatformMaintenance(); + + if (!accountHasPlatformMaintenance) return null; + + return ( + + + + {linodesWithPlatformMaintenance.size > 0 + ? linodesWithPlatformMaintenance.size + : 'One or more'}{' '} + Linode{linodesWithPlatformMaintenance.size !== 1 && 's'} + {' '} + need{linodesWithPlatformMaintenance.size === 1 && 's'} to be rebooted + for critical platform maintenance. See which Linodes are scheduled for + reboot on the Account Maintenance{' '} + page. + + + ); +}; diff --git a/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx b/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx index 7efbd9c374c..c6489317505 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetMaintenance.tsx @@ -130,8 +130,11 @@ const renderMaintenanceFields = ( onChange={onChange} value={maintenance.status} > + + + , @@ -158,7 +161,24 @@ const renderMaintenanceFields = ( const maintenanceTemplates = { Default: () => accountMaintenanceFactory.build(), + Canceled: () => accountMaintenanceFactory.build({ status: 'canceled' }), Completed: () => accountMaintenanceFactory.build({ status: 'completed' }), + 'In Progress': () => + accountMaintenanceFactory.build({ status: 'in-progress' }), Pending: () => accountMaintenanceFactory.build({ status: 'pending' }), + Scheduled: () => accountMaintenanceFactory.build({ status: 'scheduled' }), Started: () => accountMaintenanceFactory.build({ status: 'started' }), + 'Platform Maintenance': () => + accountMaintenanceFactory.build({ + entity: { + type: 'linode', + id: 1, + label: 'linode-1', + url: '/v4/linode/instances/1', + }, + status: 'scheduled', + type: 'reboot', + reason: + "In this case we must apply a critical security update to your Linode's host.", + }), } as const; diff --git a/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx b/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx index e835102588e..8f908038f2e 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetNotifications.tsx @@ -252,4 +252,13 @@ const notificationTemplates = { until: '2021-10-16T04:00:00', when: '2021-09-30T04:00:00', }), + 'Platform Maintenance Scheduled': () => + notificationFactory.build({ + label: + 'One or more of your Linodes has a scheduled reboot for a critical platform security update.', + message: + 'One or more of your Linodes has a scheduled reboot for a critical platform security update.', + severity: 'major', + type: 'security_reboot_maintenance_scheduled', + }), } as const; diff --git a/packages/manager/src/factories/accountMaintenance.ts b/packages/manager/src/factories/accountMaintenance.ts index 66501b16bed..23b4c1d268a 100644 --- a/packages/manager/src/factories/accountMaintenance.ts +++ b/packages/manager/src/factories/accountMaintenance.ts @@ -45,8 +45,16 @@ export const accountMaintenanceFactory = type: Factory.each(() => pickRandom(['cold_migration', 'live_migration', 'reboot']) ), - when: Factory.each(() => randomDate().toISOString()), - not_before: Factory.each(() => randomDate().toISOString()), - start_time: Factory.each(() => randomDate().toISOString()), - complete_time: Factory.each(() => randomDate().toISOString()), + when: Factory.each( + () => randomDate().toISO({ includeOffset: false }) ?? '' + ), + not_before: Factory.each( + () => randomDate().toISO({ includeOffset: false }) ?? '' + ), + start_time: Factory.each( + () => randomDate().toISO({ includeOffset: false }) ?? '' + ), + complete_time: Factory.each( + () => randomDate().toISO({ includeOffset: false }) ?? '' + ), }); diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index f2a7f34ecbe..e8a2a7ae4e9 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -258,7 +258,7 @@ export const databaseBackupFactory = Factory.Sync.makeFactory({ created: Factory.each(() => { const now = new Date(); const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); - return randomDate(tenDaysAgo, now).toISOString(); + return randomDate(tenDaysAgo, now).toISO() ?? ''; }), id: Factory.each((i) => i), label: Factory.each(() => `backup-${crypto.randomUUID()}`), diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index 9247d2de527..04df40cba27 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -17,6 +17,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { sendSwitchAccountEvent } from 'src/utilities/analytics/customEventAnalytics'; +import { PlatformMaintenanceBanner } from '../../components/PlatformMaintenanceBanner/PlatformMaintenanceBanner'; import AccountLogins from './AccountLogins'; import { SwitchAccountButton } from './SwitchAccountButton'; import { SwitchAccountDrawer } from './SwitchAccountDrawer'; @@ -193,6 +194,7 @@ const AccountLanding = () => { return ( + diff --git a/packages/manager/src/features/Account/Maintenance/utilities.ts b/packages/manager/src/features/Account/Maintenance/utilities.ts index 70841a9ecfa..4c3c3658999 100644 --- a/packages/manager/src/features/Account/Maintenance/utilities.ts +++ b/packages/manager/src/features/Account/Maintenance/utilities.ts @@ -1,3 +1,11 @@ export const PENDING_MAINTENANCE_FILTER = Object.freeze({ - status: { '+or': ['pending', 'started'] }, + status: { '+or': ['pending', 'started', 'scheduled'] }, }); + +export const PLATFORM_MAINTENANCE_TYPE = + 'security_reboot_maintenance_scheduled'; + +export const PLATFORM_MAINTENANCE_REASON_MATCH = [ + 'critical platform update', + 'critical security update', +]; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/Notifications.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/Notifications.tsx index a9b681fad93..c6b31ac9ef2 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/Notifications.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/Notifications.tsx @@ -7,8 +7,10 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { MaintenanceBanner } from 'src/components/MaintenanceBanner/MaintenanceBanner'; +import { LinodePlatformMaintenanceBanner } from 'src/components/PlatformMaintenanceBanner/LinodePlatformMaintenanceBanner'; import { ProductNotification } from 'src/components/ProductNotification/ProductNotification'; import { PENDING_MAINTENANCE_FILTER } from 'src/features/Account/Maintenance/utilities'; +import { isPlatformMaintenance } from 'src/hooks/usePlatformMaintenance'; import { MigrationNotification } from './MigrationNotification'; @@ -31,11 +33,13 @@ const Notifications = () => { PENDING_MAINTENANCE_FILTER ); - const maintenanceForThisLinode = accountMaintenanceData?.find( - (thisMaintenance) => - thisMaintenance.entity.type === 'linode' && - thisMaintenance.entity.id === linode?.id - ); + const maintenanceForThisLinode = accountMaintenanceData + ?.filter((maintenance) => !isPlatformMaintenance(maintenance)) // Platform maintenance is handled separately + ?.find( + (thisMaintenance) => + thisMaintenance.entity.type === 'linode' && + thisMaintenance.entity.id === linode?.id + ); const generateNotificationBody = (notification: Notification) => { switch (notification.type) { @@ -78,6 +82,9 @@ const Notifications = () => { ); })} + {linode ? ( + + ) : null} {maintenanceForThisLinode ? ( { /> )} + {this.props.someLinodesHaveScheduledMaintenance && ( )} diff --git a/packages/manager/src/hooks/usePlatformMaintenance.test.ts b/packages/manager/src/hooks/usePlatformMaintenance.test.ts new file mode 100644 index 00000000000..acf7d15330b --- /dev/null +++ b/packages/manager/src/hooks/usePlatformMaintenance.test.ts @@ -0,0 +1,99 @@ +import { renderHook } from '@testing-library/react'; + +import { accountMaintenanceFactory, notificationFactory } from 'src/factories'; + +import { usePlatformMaintenance } from './usePlatformMaintenance'; + +const queryMocks = vi.hoisted(() => ({ + useNotificationsQuery: vi.fn().mockReturnValue({}), + useAllAccountMaintenanceQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + ...queryMocks, + }; +}); + +describe('usePlatformMaintenace', () => { + it('returns false when there is no platform maintenance notification', () => { + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: accountMaintenanceFactory.buildList(3, { + type: 'reboot', + entity: { + type: 'linode', + }, + }), + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: [], + }); + + const { result } = renderHook(() => usePlatformMaintenance()); + + expect(result.current.accountHasPlatformMaintenance).toBe(false); + }); + + it('returns true when there is a platform maintenance notifications', () => { + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: [], + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: notificationFactory.buildList(1, { + type: 'security_reboot_maintenance_scheduled', + label: 'Platform Maintenance Scheduled', + }), + }); + + const { result } = renderHook(() => usePlatformMaintenance()); + + expect(result.current).toEqual({ + accountHasPlatformMaintenance: true, + linodesWithPlatformMaintenance: new Set(), + platformMaintenanceByLinode: {}, + }); + }); + + it('includes linodes with platform maintenance', () => { + const mockPlatformMaintenance = accountMaintenanceFactory.buildList(2, { + type: 'reboot', + entity: { type: 'linode' }, + reason: 'Your Linode needs a critical security update', + }); + const mockMaintenance = [ + ...mockPlatformMaintenance, + accountMaintenanceFactory.build({ + type: 'reboot', + entity: { type: 'linode' }, + reason: 'Unrelated maintenance item', + }), + ]; + + queryMocks.useAllAccountMaintenanceQuery.mockReturnValue({ + data: mockMaintenance, + }); + + queryMocks.useNotificationsQuery.mockReturnValue({ + data: notificationFactory.buildList(1, { + type: 'security_reboot_maintenance_scheduled', + label: 'Platform Maintenance Scheduled', + }), + }); + + const { result } = renderHook(() => usePlatformMaintenance()); + + expect(result.current).toEqual({ + accountHasPlatformMaintenance: true, + linodesWithPlatformMaintenance: new Set( + mockPlatformMaintenance.map((m) => m.entity.id) + ), + platformMaintenanceByLinode: Object.fromEntries( + mockPlatformMaintenance.map((m) => [m.entity.id, [m]]) + ), + }); + }); +}); diff --git a/packages/manager/src/hooks/usePlatformMaintenance.ts b/packages/manager/src/hooks/usePlatformMaintenance.ts new file mode 100644 index 00000000000..adb69f8faef --- /dev/null +++ b/packages/manager/src/hooks/usePlatformMaintenance.ts @@ -0,0 +1,81 @@ +import { + useAllAccountMaintenanceQuery, + useNotificationsQuery, +} from '@linode/queries'; + +import { + PENDING_MAINTENANCE_FILTER, + PLATFORM_MAINTENANCE_REASON_MATCH, + PLATFORM_MAINTENANCE_TYPE, +} from 'src/features/Account/Maintenance/utilities'; + +import type { AccountMaintenance, Linode } from '@linode/api-v4'; + +interface UsePlatformMaintenanceResult { + accountHasPlatformMaintenance: boolean; + linodesWithPlatformMaintenance: Set; + platformMaintenanceByLinode: Record; +} + +/** + * Determines whether an account has platform maintenance, which + * is system-wide maintenance requiring a reboot, and returns + * associated maintenance items. + */ +export const usePlatformMaintenance = (): UsePlatformMaintenanceResult => { + const { data: accountNotifications } = useNotificationsQuery(); + + const accountHasPlatformMaintenance = + accountNotifications?.find( + (notification) => notification.type === PLATFORM_MAINTENANCE_TYPE + ) !== undefined; + + const { data: accountMaintenanceData } = useAllAccountMaintenanceQuery( + {}, + PENDING_MAINTENANCE_FILTER, + accountHasPlatformMaintenance + ); + + const platformMaintenanceItems = accountMaintenanceData?.filter( + isPlatformMaintenance + ); + + return getPlatformMaintenanceResult( + accountHasPlatformMaintenance, + platformMaintenanceItems + ); +}; + +export const isPlatformMaintenance = ( + maintenance: AccountMaintenance +): boolean => + maintenance.type === 'reboot' && + maintenance.entity.type === 'linode' && + PLATFORM_MAINTENANCE_REASON_MATCH.some((match) => + maintenance.reason.includes(match) + ); + +const getPlatformMaintenanceResult = ( + accountHasPlatformMaintenance: boolean, + maintenanceItems: AccountMaintenance[] = [] +): UsePlatformMaintenanceResult => { + const platformMaintenanceByLinode: Record< + Linode['id'], + AccountMaintenance[] + > = {}; + for (const maintenance of maintenanceItems) { + if (!(maintenance.entity.id in platformMaintenanceByLinode)) + platformMaintenanceByLinode[maintenance.entity.id] = []; + platformMaintenanceByLinode[maintenance.entity.id].push(maintenance); + } + + const linodesWithPlatformMaintenance = new Set( + Object.keys(platformMaintenanceByLinode).map(Number) + ); + + return { + accountHasPlatformMaintenance, + platformMaintenanceByLinode, + linodesWithPlatformMaintenance, + }; +}; diff --git a/packages/utilities/src/helpers/random.ts b/packages/utilities/src/helpers/random.ts index 67e8aafa653..c780a7d4bca 100644 --- a/packages/utilities/src/helpers/random.ts +++ b/packages/utilities/src/helpers/random.ts @@ -1,3 +1,5 @@ +import { DateTime } from 'luxon'; + /** * Picks a random element from an array * @param items { T[] } an array of any kind @@ -18,5 +20,7 @@ export const randomDate = ( start: Date = new Date(), end: Date = new Date(2021, 10, 25), ) => - // eslint-disable-next-line sonarjs/pseudo-random - new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); + DateTime.fromMillis( + // eslint-disable-next-line sonarjs/pseudo-random + start.getTime() + Math.random() * (end.getTime() - start.getTime()), + ); From e4fa5828d2d8e4502a50269bd27e1269b7489986 Mon Sep 17 00:00:00 2001 From: cliu-akamai <126020611+cliu-akamai@users.noreply.github.com> Date: Wed, 28 May 2025 15:17:20 -0400 Subject: [PATCH 56/59] test: [M3-9176] - Add integration test for Upgrade to new Linode Interface flow (#12259) * M3-9176 Add integration test for Upgrade to new Linode Interface flow * Added changeset: Add integration test for Upgrade to new Linode Interface flow * Fixed comments * Fixing linting issue. * Fixed comments --- .../pr-12259-tests-1747849345030.md | 5 + .../e2e/core/linodes/linode-config.spec.ts | 396 ++++++++++++---- .../linodes/upgrade-linode-interface.spec.ts | 441 ++++++++++++++++++ .../support/constants/linode-interfaces.ts | 31 ++ .../cypress/support/intercepts/linodes.ts | 40 ++ .../manager/cypress/support/util/linodes.ts | 96 ++++ .../ConfigSelectDialogContent.tsx | 1 + .../src/factories/linodeInterface.ts | 13 +- 8 files changed, 925 insertions(+), 98 deletions(-) create mode 100644 packages/manager/.changeset/pr-12259-tests-1747849345030.md create mode 100644 packages/manager/cypress/e2e/core/linodes/upgrade-linode-interface.spec.ts create mode 100644 packages/manager/cypress/support/constants/linode-interfaces.ts diff --git a/packages/manager/.changeset/pr-12259-tests-1747849345030.md b/packages/manager/.changeset/pr-12259-tests-1747849345030.md new file mode 100644 index 00000000000..86591aac0bb --- /dev/null +++ b/packages/manager/.changeset/pr-12259-tests-1747849345030.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add integration test for Upgrade to new Linode Interface flow ([#12259](https://github.com/linode/manager/pull/12259)) diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index c2a7fce210c..c4d8ad31c55 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -3,7 +3,9 @@ import { linodeConfigInterfaceFactory, linodeConfigInterfaceFactoryWithVPC, linodeFactory, + linodeInterfaceFactoryPublic, regionFactory, + upgradeLinodeInterfaceFactory, } from '@linode/utilities'; import { accountFactory, @@ -15,6 +17,12 @@ import { } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; +import { + dryRunButtonText, + upgradeInterfacesButtonText, + upgradeTooltipText1, + upgradeTooltipText2, +} from 'support/constants/linode-interfaces'; import { LINODE_CLONE_TIMEOUT } from 'support/constants/linodes'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -36,13 +44,21 @@ import { mockGetLinodeKernel, mockGetLinodeKernels, mockGetLinodeVolumes, + mockUpgradeNewLinodeInterface, + mockUpgradeNewLinodeInterfaceError, } from 'support/intercepts/linodes'; +import { mockGetRegion, mockGetRegions } from 'support/intercepts/regions'; import { mockGetVLANs } from 'support/intercepts/vlans'; import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { fetchAllKernels, findKernelById } from 'support/util/kernels'; -import { createTestLinode, fetchLinodeConfigs } from 'support/util/linodes'; +import { + assertPromptDialogContent, + assertUpgradeSummary, + createTestLinode, + fetchLinodeConfigs, +} from 'support/util/linodes'; import { randomIp, randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; @@ -466,7 +482,7 @@ describe('Linode Config management', () => { describe('Mocked', () => { const region: Region = regionFactory.build({ - capabilities: ['Linodes'], + capabilities: ['Linodes', 'Linode Interfaces'], country: 'us', id: 'us-southeast', }); @@ -497,101 +513,6 @@ describe('Linode Config management', () => { const mockVLANs: VLAN[] = VLANFactory.buildList(2); - /* - * - Confirms that config dialog interfaces section is absent on Linodes that use new interfaces. - * - Confirms absence on edit and add config dialog. - */ - it('Does not show interfaces section when managing configs using new Linode interfaces', () => { - // TODO M3-9775: Remove mock when `linodeInterfaces` feature flag is removed. - mockAppendFeatureFlags({ - linodeInterfaces: { - enabled: true, - }, - }); - - // TODO Remove account mock when 'Linode Interfaces' capability is generally available. - mockGetAccount( - accountFactory.build({ - capabilities: ['Linodes', 'Linode Interfaces'], - }) - ); - - const mockLinode = linodeFactory.build({ - id: randomNumber(1000, 99999), - label: randomLabel(), - region: chooseRegion().id, - interface_generation: 'linode', - }); - - const mockConfig = configFactory.build({ - label: randomLabel(), - id: randomNumber(1000, 99999), - interfaces: null, - }); - - mockGetLinodeDetails(mockLinode.id, mockLinode); - mockGetLinodeConfigs(mockLinode.id, [mockConfig]); - mockGetLinodeConfig(mockLinode.id, mockConfig); - - cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); - - cy.findByLabelText('List of Configurations') - .should('be.visible') - .within(() => { - ui.button - .findByTitle('Edit') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm absence of the interfaces section when editing an existing config. - ui.dialog - .findByTitle('Edit Configuration') - .should('be.visible') - .within(() => { - // Scroll "Networking" section into view, and confirm that Interfaces - // options are absent and informational text is shown instead. - cy.findByText('Networking').scrollIntoView(); - cy.contains( - "Go to Network to view your Linode's Network interfaces." - ).should('be.visible'); - cy.findByText('Primary Interface (Default Route)').should( - 'not.exist' - ); - cy.findByText('eth0').should('not.exist'); - cy.findByText('eth1').should('not.exist'); - cy.findByText('eth2').should('not.exist'); - - ui.button.findByTitle('Cancel').click(); - }); - - // Confirm asbence of the interfaces section when adding a new config. - ui.button - .findByTitle('Add Configuration') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.dialog - .findByTitle('Add Configuration') - .should('be.visible') - .within(() => { - // Scroll "Networking" section into view, and confirm that Interfaces - // options are absent and informational text is shown instead. - cy.findByText('Networking').scrollIntoView(); - cy.contains( - "Go to Network to view your Linode's Network interfaces." - ).should('be.visible'); - cy.findByText('Primary Interface (Default Route)').should( - 'not.exist' - ); - cy.findByText('eth0').should('not.exist'); - cy.findByText('eth1').should('not.exist'); - cy.findByText('eth2').should('not.exist'); - }); - }); - /* * - Tests Linode config create and VPC interface assignment UI flows using mock API data. * - Confirms that VPC can be assigned as eth0, eth1, and eth2. @@ -939,5 +860,286 @@ describe('Linode Config management', () => { cy.findByText('REBOOT NEEDED').should('be.visible'); }); + + describe('Upgrade new Linode Interfaces flow', () => { + beforeEach(() => { + // TODO M3-9775: Remove mock when `linodeInterfaces` feature flag is removed. + mockAppendFeatureFlags({ + linodeInterfaces: { + enabled: true, + }, + }); + + // TODO Remove account mock when 'Linode Interfaces' capability is generally available. + mockGetAccount( + accountFactory.build({ + capabilities: ['Linodes', 'Linode Interfaces'], + }) + ); + + const mockRegion: Region = regionFactory.build({ + id: 'us-east', + label: 'Newark, NJ', + capabilities: ['Linodes', 'Linode Interfaces'], + }); + mockGetRegions([mockRegion]); + mockGetRegion(mockRegion); + }); + + /* + * - Confirms that config dialog interfaces section is absent on Linodes that use new interfaces. + * - Confirms absence on edit and add config dialog. + */ + it('Does not show interfaces section when managing configs using new Linode interfaces', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: 'us-east', + interface_generation: 'linode', + }); + + const mockConfig = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]); + mockGetLinodeConfig(mockLinode.id, mockConfig); + + cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); + + cy.findByLabelText('List of Configurations') + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Edit') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm absence of the interfaces section when editing an existing config. + ui.dialog + .findByTitle('Edit Configuration') + .should('be.visible') + .within(() => { + // Scroll "Networking" section into view, and confirm that Interfaces + // options are absent and informational text is shown instead. + cy.findByText('Networking').scrollIntoView(); + cy.contains( + "Go to Network to view your Linode's Network interfaces." + ).should('be.visible'); + cy.findByText('Primary Interface (Default Route)').should( + 'not.exist' + ); + cy.findByText('eth0').should('not.exist'); + cy.findByText('eth1').should('not.exist'); + cy.findByText('eth2').should('not.exist'); + + ui.button.findByTitle('Cancel').click(); + }); + + // Confirm absence of the interfaces section when adding a new config. + ui.button + .findByTitle('Add Configuration') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Add Configuration') + .should('be.visible') + .within(() => { + // Scroll "Networking" section into view, and confirm that Interfaces + // options are absent and informational text is shown instead. + cy.findByText('Networking').scrollIntoView(); + cy.contains( + "Go to Network to view your Linode's Network interfaces." + ).should('be.visible'); + cy.findByText('Primary Interface (Default Route)').should( + 'not.exist' + ); + cy.findByText('eth0').should('not.exist'); + cy.findByText('eth1').should('not.exist'); + cy.findByText('eth2').should('not.exist'); + }); + }); + + /* + * - Confirm button appears in Details footer for linodes with legacy interfaces. + * - Confirm clicking 'Upgrade Interfaces' button flow. + */ + it('Upgrades from legacy configuration interfaces to new Linode interfaces (Public)', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: 'us-east', + }); + + const mockConfig = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + const mockPublicInterface = linodeInterfaceFactoryPublic.build({ + id: randomNumber(1000, 99999), + }); + + const mockUpgradeLinodeInterface = upgradeLinodeInterfaceFactory.build({ + config_id: mockConfig.id, + dry_run: true, + interfaces: [mockPublicInterface], + }); + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]); + mockGetLinodeConfig(mockLinode.id, mockConfig); + mockUpgradeNewLinodeInterface( + mockLinode.id, + mockUpgradeLinodeInterface + ); + + cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); + + // Confirm the tooltip shows up + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .trigger('mouseover'); + cy.findByText(upgradeTooltipText1, { exact: false }).should( + 'be.visible' + ); + cy.findByText(upgradeTooltipText2, { exact: false }).should( + 'be.visible' + ); + + // Confirm the "Upgrade Interfaces" button appears and works as expected. + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + // Assert the prompt dialog content. + assertPromptDialogContent(); + + // Check "Dry Run" flow + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + ui.button + .findByTitle(dryRunButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, true); + + ui.button + .findByTitle('Continue to Upgrade') + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, false); + + ui.button.findByTitle('Close').should('be.visible').click(); + }); + + // Check "Upgrade Interfaces" flow + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, false); + + ui.button + .findByTitle('View Network Settings') + .should('be.visible') + .click(); + }); + + // Confirm can navigate to linode/networking after success + cy.url().should('endWith', `linodes/${mockLinode.id}/networking`); + }); + + /* + * - Confirm upgrade error flow. + * - Confirm the error message shows up. + */ + it('Displays error message when having upgrade issue', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: 'us-east', + }); + + const mockConfig = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + const mockErrorMessage = 'Custom Error'; + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]); + mockGetLinodeConfig(mockLinode.id, mockConfig); + mockUpgradeNewLinodeInterfaceError( + mockLinode.id, + mockErrorMessage, + 500 + ).as('upgradeError'); + + cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); + + // Confirm the "Upgrade Interfaces" button appears. + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + // Check error flow + ui.button + .findByTitle(dryRunButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@upgradeError'); + cy.findByText(mockErrorMessage).should('be.visible'); + + // Confirm "Return to Overview" button back to the dialog. + ui.button + .findByTitle('Return to Overview') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + assertPromptDialogContent(); + }); + }); }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/upgrade-linode-interface.spec.ts b/packages/manager/cypress/e2e/core/linodes/upgrade-linode-interface.spec.ts new file mode 100644 index 00000000000..95c1dbcd082 --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/upgrade-linode-interface.spec.ts @@ -0,0 +1,441 @@ +import { + configFactory, + linodeFactory, + linodeInterfaceFactoryPublic, + upgradeLinodeInterfaceFactory, +} from '@linode/utilities'; +import { accountFactory } from '@src/factories'; +import { authenticate } from 'support/api/authentication'; +import { + configSelectSharedText, + dryRunButtonText, + errorDryRunText, + upgradeInterfacesButtonText, + upgradeInterfacesWarningText, +} from 'support/constants/linode-interfaces'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { mockGetAccount } from 'support/intercepts/account'; +import { + mockGetLinodeConfig, + mockGetLinodeConfigs, +} from 'support/intercepts/configs'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockGetLinodeDetails, + mockUpgradeNewLinodeInterface, + mockUpgradeNewLinodeInterfaceError, +} from 'support/intercepts/linodes'; +import { ui } from 'support/ui'; +import { cleanUp } from 'support/util/cleanup'; +import { + assertPromptDialogContent, + assertUpgradeSummary, +} from 'support/util/linodes'; +import { randomLabel, randomNumber } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; + +authenticate(); +describe('upgrade to new Linode Interface flow', () => { + beforeEach(() => { + cleanUp(['linodes']); + cy.tag('method:e2e'); + + // TODO M3-9775: Remove mock when `linodeInterfaces` feature flag is removed. + mockAppendFeatureFlags({ + linodeInterfaces: { + enabled: true, + }, + }); + + // TODO Remove account mock when 'Linode Interfaces' capability is generally available. + mockGetAccount( + accountFactory.build({ + capabilities: ['Linodes', 'Linode Interfaces'], + }) + ); + }); + + /* + * - Confirms that config dialog interfaces section is absent on Linodes that use new interfaces. + * - Confirms absence on edit and add config dialog. + */ + it('does not show interfaces section in the config dialog for Linodes using new interfaces', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: chooseRegion().id, + interface_generation: 'linode', + }); + + const mockConfig = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]); + mockGetLinodeConfig(mockLinode.id, mockConfig); + + cy.visitWithLogin(`/linodes/${mockLinode.id}`); + cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( + 'be.visible' + ); + + // "UPGRADE" button is absent + cy.findByText('Linode').should('be.visible'); + cy.findByText('Configuration Profile').should('not.exist'); + cy.findByText('UPGRADE').should('not.exist'); + + cy.get('[data-testid="Configurations"]').should('be.visible').click(); + + cy.findByLabelText('List of Configurations') + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Edit') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm absence of the interfaces section when editing an existing config. + ui.dialog + .findByTitle('Edit Configuration') + .should('be.visible') + .within(() => { + // Scroll "Networking" section into view, and confirm that Interfaces + // options are absent and informational text is shown instead. + cy.findByText('Networking').scrollIntoView(); + cy.contains( + "Go to Network to view your Linode's Network interfaces." + ).should('be.visible'); + cy.findByText('Primary Interface (Default Route)').should('not.exist'); + cy.findByText('eth0').should('not.exist'); + cy.findByText('eth1').should('not.exist'); + cy.findByText('eth2').should('not.exist'); + + ui.button.findByTitle('Cancel').click(); + }); + + // Confirm absence of the interfaces section when adding a new config. + ui.button + .findByTitle('Add Configuration') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Add Configuration') + .should('be.visible') + .within(() => { + // Scroll "Networking" section into view, and confirm that Interfaces + // options are absent and informational text is shown instead. + cy.findByText('Networking').scrollIntoView(); + cy.contains( + "Go to Network to view your Linode's Network interfaces." + ).should('be.visible'); + cy.findByText('Primary Interface (Default Route)').should('not.exist'); + cy.findByText('eth0').should('not.exist'); + cy.findByText('eth1').should('not.exist'); + cy.findByText('eth2').should('not.exist'); + }); + }); + + /* + * - Confirm button appears in Details footer for linodes with legacy interfaces. + * - Confirm clicking 'UPGRADE' button flow. + */ + it('upgrades from a single legacy configuration to new Linode interfaces (Public) from details page', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: chooseRegion().id, + }); + + const mockConfig = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + const mockPublicInterface = linodeInterfaceFactoryPublic.build({ + id: randomNumber(1000, 99999), + }); + + const mockUpgradeLinodeInterface = upgradeLinodeInterfaceFactory.build({ + config_id: mockConfig.id, + dry_run: true, + interfaces: [mockPublicInterface], + }); + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]); + mockGetLinodeConfig(mockLinode.id, mockConfig); + mockUpgradeNewLinodeInterface(mockLinode.id, mockUpgradeLinodeInterface); + + cy.visitWithLogin(`/linodes/${mockLinode.id}`); + cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( + 'be.visible' + ); + + // "UPGRADE" button appears and works as expected. + cy.findByText('Configuration Profile').should('be.visible'); + cy.findByText('UPGRADE').should('be.visible').click({ force: true }); + + // Assert the prompt dialog content. + assertPromptDialogContent(); + + // Check "Dry Run" flow + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + ui.button + .findByTitle(dryRunButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, true); + + ui.button + .findByTitle('Continue to Upgrade') + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, false); + + ui.button.findByTitle('Close').should('be.visible').click(); + }); + + // Check "Upgrade Interfaces" flow + cy.findByText('UPGRADE').should('be.visible').click({ force: true }); + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, false); + + ui.button + .findByTitle('View Network Settings') + .should('be.visible') + .click(); + }); + + // Confirm can navigate to linode/networking after success + cy.url().should('endWith', `linodes/${mockLinode.id}/networking`); + }); + + /* + * - Confirm Linode with multiple configurations can be upgraded to new Linode Interfaces. + */ + it('upgrades from multiple legacy configurations to new Linode interfaces from details page', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: chooseRegion().id, + }); + + const mockConfig1 = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + const mockConfig2 = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + const mockPublicInterface = linodeInterfaceFactoryPublic.build({ + id: randomNumber(1000, 99999), + }); + + const mockUpgradeLinodeInterface = upgradeLinodeInterfaceFactory.build({ + config_id: mockConfig1.id, + dry_run: true, + interfaces: [mockPublicInterface], + }); + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig1, mockConfig2]); + mockGetLinodeConfig(mockLinode.id, mockConfig1); + mockUpgradeNewLinodeInterface(mockLinode.id, mockUpgradeLinodeInterface); + + cy.visitWithLogin(`/linodes/${mockLinode.id}`); + cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( + 'be.visible' + ); + + // "UPGRADE" button appears and works as expected. + cy.findByText('Configuration Profile').should('be.visible'); + cy.findByText('UPGRADE').should('be.visible').click({ force: true }); + + // Assert the prompt dialog content. + assertPromptDialogContent(); + + // Check "Dry Run" flow + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + ui.button + .findByTitle(dryRunButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + // Find the config select and open it + cy.get('[placeholder="Select Configuration Profile"]') + .should('be.visible') + .click(); + cy.focused().type(`${mockConfig1.label}{enter}`); + + // Select the config + ui.autocompletePopper + .findByTitle(mockConfig1.label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm config select text for multiple configurations + cy.findByText(configSelectSharedText, { exact: false }).should( + 'be.visible' + ); + + cy.findAllByText(dryRunButtonText) + .last() + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, true); + + ui.button + .findByTitle('Continue to Upgrade') + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, false); + + ui.button.findByTitle('Close').should('be.visible').click(); + }); + + // Check "Upgrade Interfaces" flow + cy.findByText('UPGRADE').should('be.visible').click({ force: true }); + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText(configSelectSharedText).should('be.visible'); + + // Find the config select and open it + cy.get('[placeholder="Select Configuration Profile"]') + .should('be.visible') + .click(); + cy.focused().type(`${mockConfig1.label}{enter}`); + + // Select the config + ui.autocompletePopper + .findByTitle(mockConfig1.label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm multiple configuration warning text for multiple configurations + cy.findByText(upgradeInterfacesWarningText).should('be.visible'); + + cy.findAllByText(upgradeInterfacesButtonText) + .last() + .should('be.visible') + .should('be.enabled') + .click(); + + assertUpgradeSummary(mockPublicInterface, false); + + ui.button.findByTitle('Close').should('be.visible').click(); + }); + + // Confirm can navigate to Linode after success + cy.url().should('endWith', `linodes/${mockLinode.id}`); + }); + + /* + * - Confirm upgrade error flow. + * - Confirm "Return to Overview" works. + * - Confirm the error message shows up. + */ + it('Displays error message when having upgrade issue', () => { + const mockLinode = linodeFactory.build({ + id: randomNumber(1000, 99999), + label: randomLabel(), + region: chooseRegion().id, + }); + + const mockConfig = configFactory.build({ + label: randomLabel(), + id: randomNumber(1000, 99999), + interfaces: null, + }); + + const mockErrorMessage = 'Custom Error'; + + mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]); + mockGetLinodeConfig(mockLinode.id, mockConfig); + mockUpgradeNewLinodeInterfaceError(mockLinode.id, mockErrorMessage, 500).as( + 'upgradeError' + ); + + cy.visitWithLogin(`/linodes/${mockLinode.id}`); + cy.contains('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( + 'be.visible' + ); + + // "UPGRADE" button appears and works as expected. + cy.findByText('Configuration Profile').should('be.visible'); + cy.findByText('UPGRADE').should('be.visible').click({ force: true }); + + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + // Check error flow + ui.button + .findByTitle(dryRunButtonText) + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm the error message shows up. + cy.wait('@upgradeError'); + cy.findByText(mockErrorMessage).should('be.visible'); + cy.findByText(errorDryRunText).should('be.visible'); + + // Confirm "Return to Overview" button back to the dialog. + ui.button + .findByTitle('Return to Overview') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + assertPromptDialogContent(); + }); +}); diff --git a/packages/manager/cypress/support/constants/linode-interfaces.ts b/packages/manager/cypress/support/constants/linode-interfaces.ts new file mode 100644 index 00000000000..8f5c8212b7e --- /dev/null +++ b/packages/manager/cypress/support/constants/linode-interfaces.ts @@ -0,0 +1,31 @@ +export const dryRunButtonText = 'Perform Dry Run'; + +export const upgradeInterfacesButtonText = 'Upgrade Interfaces'; + +export const upgradeTooltipText1 = 'Configuration Profile interfaces from a single profile can be upgraded to Linode Interfaces.'; + +export const upgradeTooltipText2 = 'After the upgrade, the Linode can only use Linode Interfaces and cannot revert to Configuration Profile interfaces. Use the dry-run feature to review the changes before committing.'; + +export const promptDialogDescription1 = 'Upgrading allows interface connections to be associated directly with the Linode, rather than its configuration profile.'; + +export const promptDialogDescription2 = 'We recommend performing a dry run before upgrading to identify and resolve any potential conflicts.'; + +export const promptDialogUpgradeWhatHappensTitle = 'What happens after the upgrade:'; + +export const promptDialogUpgradeDetails = [ + 'New Linode Interfaces are created to match the existing Configuration Profile Interfaces.', + 'The Linode will only use Linode Interfaces and cannot revert to Configuration Profile Interfaces.', + 'Private IPv4 addresses are not supported on public Linode Interfaces—services relying on a private IPv4 will no longer function.', + 'All firewalls are removed from the Linode. Any previously attached firewalls are reassigned to the new public and VPC interfaces. Default firewalls are not applied if none were originally attached.', + 'Public interfaces retain the Linode’s existing MAC address and SLAAC IPv6 address.', + 'Configuration Profile Interfaces are removed from the Configurations tab. The new Linode Interfaces will appear in the Network tab.', +]; + +export const configSelectSharedText = + 'This Linode has multiple configuration profiles. Choose one to continue.'; + +export const upgradeInterfacesWarningText = + 'After upgrading, the Linode will use only Linode Interfaces and cannot revert back to Configuration Profile Interfaces. Private IPv4 addresses are not supported on public Linode Interfaces. Services depending on a private IPv4 will no longer function.'; + +export const errorDryRunText = + 'The dry run found the following issues. After correcting them, perform another dry run.'; diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 7f2a01d2d67..7ee9fb7f817 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -17,6 +17,7 @@ import type { LinodeInterfaces, LinodeIPsResponse, LinodeType, + UpgradeInterfaceData, Volume, } from '@linode/api-v4'; @@ -701,3 +702,42 @@ export const mockCreateLinodeInterfaceError = ( makeErrorResponse(errorMessage, statusCode) ); }; + +/** + * Intercepts POST request to create a Linode Interface. + * + * @param linodeId - the Linodes ID to add the interface to. + * @param linodeInterface - a mock upgrade linode interface object. + * + * @returns Cypress chainable. + */ +export const mockUpgradeNewLinodeInterface = ( + linodeId: number, + linodeInterface: UpgradeInterfaceData +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linodeId}/upgrade-interfaces`), + makeResponse(linodeInterface) + ); +}; + +/** + * Intercepts POST request to create a Linode Interface and mocks an error response. + * + * @param errorMessage - Error message to be included in the mocked HTTP response. + * @param statusCode - HTTP status code for mocked error response. Default is `400`. + * + * @returns Cypress chainable. + */ +export const mockUpgradeNewLinodeInterfaceError = ( + linodeId: number, + errorMessage: string, + statusCode: number = 400 +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linodeId}/upgrade-interfaces`), + makeErrorResponse(errorMessage, statusCode) + ); +}; diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index 3ebe1b4eb1d..816cf6c5990 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -3,7 +3,16 @@ import { createLinodeRequestFactory } from '@linode/utilities'; import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; import { findOrCreateDependencyVlan } from 'support/api/vlans'; import { pageSize } from 'support/constants/api'; +import { + dryRunButtonText, + promptDialogDescription1, + promptDialogDescription2, + promptDialogUpgradeDetails, + promptDialogUpgradeWhatHappensTitle, + upgradeInterfacesButtonText, +} from 'support/constants/linode-interfaces'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { ui } from 'support/ui'; import { SimpleBackoffMethod } from 'support/util/backoff'; import { pollLinodeDiskStatuses, pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomString } from 'support/util/random'; @@ -16,6 +25,7 @@ import type { CreateLinodeRequest, InterfacePayload, Linode, + LinodeInterface, } from '@linode/api-v4'; /** @@ -213,3 +223,89 @@ export const fetchLinodeConfigs = async ( getLinodeConfigs(linodeId, { page, page_size: pageSize }) ); }; + +/** + * Check the content of prompt dialog + */ +export const assertPromptDialogContent = () => { + ui.dialog + .findByTitle('Upgrade to Linode Interfaces') + .should('be.visible') + .within(() => { + cy.findByText(promptDialogDescription1, { exact: false }).should( + 'be.visible' + ); + cy.findByText(promptDialogDescription2, { exact: false }).should( + 'be.visible' + ); + cy.findByText(promptDialogUpgradeWhatHappensTitle, { + exact: false, + }).should('be.visible'); + promptDialogUpgradeDetails.forEach((item) => { + cy.findByText(item).should('be.visible'); + }); + + ui.button + .findByTitle(dryRunButtonText) + .should('be.visible') + .should('be.enabled'); + ui.button + .findByTitle(upgradeInterfacesButtonText) + .should('be.visible') + .should('be.enabled'); + }); +}; + +/** + * Check the upgrade summary + * + * @param linodeInterface - Linode interface to check. + * @param isDryRun - Boolean to indicate if the upgrade performs dry run. + * + */ +export const assertUpgradeSummary = ( + linodeInterface: LinodeInterface, + isDryRun: boolean = false +) => { + if (isDryRun) { + // Confirm that dry run status is successful + cy.findByText('Dry run successful').should('be.visible'); + cy.findByText( + 'No issues were found. You can proceed with upgrading to Linode Interfaces.' + ).should('be.visible'); + + // Confirm that dry run summary details display. + cy.findByText('Dry Run Summary').should('be.visible'); + cy.findByText('Interface Meta Info').should('be.visible'); + cy.findByText(`MAC Address: ${linodeInterface.mac_address}`).should( + 'be.visible' + ); + cy.findByText(`Created: ${linodeInterface.created}`).should('be.visible'); + cy.findByText(`Updated: ${linodeInterface.updated}`).should('be.visible'); + cy.findByText(`Version: ${linodeInterface.version}`).should('be.visible'); + cy.findByText('Public Interface dry run successful.').should('be.visible'); + } else { + // Confirm that upgrade status is successful + cy.findByText('Upgrade successful').should('be.visible'); + cy.findByText( + 'Your Linode now uses Linode Interfaces. Existing interfaces were migrated, firewalls reassigned, and changes are visible', + { exact: false } + ).should('be.visible'); + + // Confirm that upgrade summary details display. + cy.findByText('Upgrade Summary').should('be.visible'); + cy.findByText( + `Interface Meta Info: Interface #${linodeInterface.id}` + ).should('be.visible'); + cy.findByText(`ID: ${linodeInterface.id}`).should('be.visible'); + cy.findByText(`MAC Address: ${linodeInterface.mac_address}`).should( + 'be.visible' + ); + cy.findByText(`Created: ${linodeInterface.created}`).should('be.visible'); + cy.findByText(`Updated: ${linodeInterface.updated}`).should('be.visible'); + cy.findByText(`Version: ${linodeInterface.version}`).should('be.visible'); + cy.findByText('Public Interface successfully upgraded.').should( + 'be.visible' + ); + } +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/ConfigSelectDialogContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/ConfigSelectDialogContent.tsx index c02c30781ee..180234b6e3a 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/ConfigSelectDialogContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/ConfigSelectDialogContent.tsx @@ -63,6 +63,7 @@ export const ConfigSelectDialogContent = ( onChange={(_, item) => setSelectedConfigId(item.value)} options={configOptions} placeholder="Select Configuration Profile" + searchable value={ configOptions.find((options) => options.value === selectedConfigId) ?? null diff --git a/packages/utilities/src/factories/linodeInterface.ts b/packages/utilities/src/factories/linodeInterface.ts index e1aef5c9cc0..220aedd19de 100644 --- a/packages/utilities/src/factories/linodeInterface.ts +++ b/packages/utilities/src/factories/linodeInterface.ts @@ -2,7 +2,11 @@ import { Factory } from './factoryProxy'; -import type { LinodeInterface, LinodeInterfaceSettings } from '@linode/api-v4'; +import type { + LinodeInterface, + LinodeInterfaceSettings, + UpgradeInterfaceData, +} from '@linode/api-v4'; export const linodeInterfaceSettingsFactory = Factory.Sync.makeFactory({ @@ -15,6 +19,13 @@ export const linodeInterfaceSettingsFactory = }, }); +export const upgradeLinodeInterfaceFactory = + Factory.Sync.makeFactory({ + config_id: Factory.each((i) => i), + dry_run: true, + interfaces: [], + }); + export const linodeInterfaceFactoryVlan = Factory.Sync.makeFactory({ created: '2025-03-19T03:58:04', From 47085feac199286e64b6d4a761fcb4923e9d0c6a Mon Sep 17 00:00:00 2001 From: mpolotsk-akamai <157619599+mpolotsk-akamai@users.noreply.github.com> Date: Wed, 28 May 2025 21:34:03 +0200 Subject: [PATCH 57/59] feat: [UIE-8792] - add pagination to Roles table (#12264) * feat: [UIE-8792] - add pagination to Roles table * Added changeset: IAM RBAC: add pagination to the Roles table * feat: [UIE-8792] - unit test fix * feat: [UIE-8792] - update default page size * feat: [UIE-8792] - use usePagination hook to handle page state --------- Co-authored-by: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Co-authored-by: corya-akamai <136115382+corya-akamai@users.noreply.github.com> --- ...r-12264-upcoming-features-1747926067938.md | 5 + .../IAM/Roles/RolesTable/RolesTable.tsx | 107 +++++++++++------- .../src/features/IAM/Shared/constants.ts | 2 + 3 files changed, 75 insertions(+), 39 deletions(-) create mode 100644 packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md diff --git a/packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md b/packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md new file mode 100644 index 00000000000..7b9a24c0242 --- /dev/null +++ b/packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +IAM RBAC: add pagination to the Roles table ([#12264](https://github.com/linode/manager/pull/12264)) diff --git a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx index 0eaaec86c99..85b59d58645 100644 --- a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx +++ b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx @@ -2,6 +2,7 @@ import { Button, Select, Typography } from '@linode/ui'; import { capitalizeAllWords } from '@linode/utilities'; import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; +import { Pagination } from 'akamai-cds-react-components/Pagination'; import { sortRows, Table, @@ -23,58 +24,85 @@ import { getFacadeRoleDescription, mapEntityTypesForSelect, } from 'src/features/IAM/Shared/utilities'; +import { usePagination } from 'src/hooks/usePagination'; + +import { ROLES_TABLE_PREFERENCE_KEY } from '../../Shared/constants'; import type { RoleView } from '../../Shared/types'; import type { SelectOption } from '@linode/ui'; import type { Order } from 'akamai-cds-react-components/Table'; - const ALL_ROLES_OPTION: SelectOption = { label: 'All Roles', value: 'all', }; interface Props { - roles: RoleView[]; + roles?: RoleView[]; } -export const RolesTable = ({ roles }: Props) => { - const [rows, setRows] = useState(roles); - +export const RolesTable = ({ roles = [] }: Props) => { // Filter string for the search bar const [filterString, setFilterString] = React.useState(''); - - // Get just the list of entity types from this list of roles, to be used in the selection filter - const filterableOptions = React.useMemo(() => { - return [ALL_ROLES_OPTION, ...mapEntityTypesForSelect(roles, ' Roles')]; - }, [roles]); - const [filterableEntityType, setFilterableEntityType] = useState(ALL_ROLES_OPTION); - const [sort, setSort] = useState< undefined | { column: string; order: Order } >(undefined); - const [selectedRows, setSelectedRows] = useState([]); const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const pagination = usePagination(1, ROLES_TABLE_PREFERENCE_KEY); + + // Filtering + const getFilteredRows = ( + text: string, + entityTypeVal = ALL_ROLES_OPTION.value + ) => { + return roles.filter( + (r) => + (entityTypeVal === ALL_ROLES_OPTION.value || + entityTypeVal === r.entity_type) && + (r.name.includes(text) || + r.description.includes(text) || + r.access.includes(text)) + ); + }; + + const filteredRows = React.useMemo( + () => getFilteredRows(filterString, filterableEntityType?.value), + [roles, filterString, filterableEntityType] + ); + + // Get just the list of entity types from this list of roles, to be used in the selection filter + const filterableOptions = React.useMemo(() => { + return [ALL_ROLES_OPTION, ...mapEntityTypesForSelect(roles, ' Roles')]; + }, [roles]); + + const sortedRows = React.useMemo(() => { + if (!sort) return filteredRows; + return sortRows(filteredRows, sort.order, sort.column); + }, [filteredRows, sort]); + + const paginatedRows = React.useMemo(() => { + const start = (pagination.page - 1) * pagination.pageSize; + return sortedRows.slice(start, start + pagination.pageSize); + }, [sortedRows, pagination.page, pagination.pageSize]); + const areAllSelected = React.useMemo(() => { return ( - !!rows?.length && + !!paginatedRows?.length && !!selectedRows?.length && - rows?.length === selectedRows?.length + paginatedRows?.length === selectedRows?.length ); - }, [rows, selectedRows]); + }, [paginatedRows, selectedRows]); const handleSort = (event: CustomEvent, column: string) => { setSort({ column, order: event.detail as Order }); - const visibleRows = sortRows(rows, event.detail as Order, column); - setRows(visibleRows); }; const handleSelect = (event: CustomEvent, row: 'all' | RoleView) => { if (row === 'all') { - setSelectedRows(areAllSelected ? [] : rows); + setSelectedRows(areAllSelected ? [] : paginatedRows); } else if (selectedRows.includes(row)) { setSelectedRows(selectedRows.filter((r) => r !== row)); } else { @@ -82,30 +110,14 @@ export const RolesTable = ({ roles }: Props) => { } }; - const getFilteredRows = ( - text: string, - entityTypeVal = ALL_ROLES_OPTION.value - ) => { - return roles.filter( - (r) => - (entityTypeVal === ALL_ROLES_OPTION.value || - entityTypeVal === r.entity_type) && - (r.name.includes(text) || - r.description.includes(text) || - r.access.includes(text)) - ); - }; - const handleTextFilter = (fs: string) => { setFilterString(fs); - const filteredRows = getFilteredRows(fs, filterableEntityType?.value); - setRows(filteredRows); + pagination.handlePageChange(1); }; const handleChangeEntityTypeFilter = (_: never, entityType: SelectOption) => { setFilterableEntityType(entityType ?? ALL_ROLES_OPTION); - const filteredRows = getFilteredRows(filterString, entityType?.value); - setRows(filteredRows); + pagination.handlePageChange(1); }; const assignRoleRow = (row: RoleView) => { @@ -118,6 +130,15 @@ export const RolesTable = ({ roles }: Props) => { setIsDrawerOpen(true); }; + const handlePageChange = (event: CustomEvent<{ page: number }>) => { + pagination.handlePageChange(Number(event.detail)); + }; + + const handlePageSizeChange = (event: CustomEvent<{ pageSize: number }>) => { + const newSize = event.detail.pageSize; + pagination.handlePageSizeChange(newSize); + pagination.handlePageChange(1); + }; return ( <> ({ marginTop: theme.tokens.spacing.S16 })}> @@ -208,14 +229,14 @@ export const RolesTable = ({ roles }: Props) => { - {!rows?.length ? ( + {!paginatedRows?.length ? ( No items to display. ) : ( - rows.map((roleRow) => ( + paginatedRows.map((roleRow) => ( { )} + setIsDrawerOpen(false)} diff --git a/packages/manager/src/features/IAM/Shared/constants.ts b/packages/manager/src/features/IAM/Shared/constants.ts index db7006eba78..bd63357dff6 100644 --- a/packages/manager/src/features/IAM/Shared/constants.ts +++ b/packages/manager/src/features/IAM/Shared/constants.ts @@ -19,3 +19,5 @@ export const PAID_ENTITY_TYPES = [ 'volume', 'image', ]; + +export const ROLES_TABLE_PREFERENCE_KEY = 'roles'; From af67aa772fd7b2c7450e83ec4653086e34b1683e Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 28 May 2025 16:49:51 -0400 Subject: [PATCH 58/59] Cloud version 1.143.0, API v4 version 0.141.0, Validation version 0.67.0, UI version 0.13.0, and Queries version 0.6.0 --- .../pr-12217-changed-1747239928776.md | 5 -- .../pr-12223-removed-1747346374792.md | 5 -- .../pr-12231-added-1747351003855.md | 5 -- .../pr-12272-fixed-1748009649223.md | 5 -- .../pr-12274-changed-1748026726555.md | 5 -- .../pr-12281-added-1748363215356.md | 5 -- packages/api-v4/CHANGELOG.md | 20 ++++++ packages/api-v4/package.json | 2 +- .../pr-12052-tech-stories-1744889150021.md | 5 -- ...r-12155-upcoming-features-1746446424705.md | 5 -- ...r-12162-upcoming-features-1746512536804.md | 5 -- ...r-12176-upcoming-features-1746717784144.md | 5 -- ...r-12181-upcoming-features-1746797606970.md | 5 -- .../pr-12201-tech-stories-1747327902316.md | 5 -- .../pr-12203-changed-1747094831129.md | 5 -- .../pr-12203-fixed-1747094899948.md | 5 -- .../pr-12203-fixed-1747094966781.md | 5 -- .../pr-12204-tech-stories-1747102404898.md | 5 -- .../pr-12205-tech-stories-1747256053421.md | 5 -- .../pr-12207-fixed-1747120917933.md | 5 -- .../pr-12210-fixed-1747239552927.md | 5 -- .../pr-12212-tech-stories-1747235320136.md | 5 -- .../pr-12215-changed-1747223125401.md | 5 -- .../pr-12215-fixed-1747230804629.md | 5 -- .../pr-12217-fixed-1747241027159.md | 5 -- .../pr-12218-tests-1747241849916.md | 5 -- .../pr-12219-tech-stories-1747251063145.md | 5 -- .../pr-12221-tech-stories-1747257942556.md | 5 -- .../pr-12223-fixed-1747346453830.md | 5 -- .../pr-12223-tech-stories-1747346603680.md | 5 -- .../pr-12226-tests-1747314916451.md | 5 -- ...r-12227-upcoming-features-1747318764898.md | 5 -- .../pr-12230-tests-1747340294690.md | 5 -- ...r-12231-upcoming-features-1747350971547.md | 5 -- ...r-12232-upcoming-features-1747645656510.md | 5 -- ...r-12233-upcoming-features-1747392110404.md | 5 -- ...r-12235-upcoming-features-1747394318662.md | 5 -- ...r-12236-upcoming-features-1747395107850.md | 5 -- .../pr-12237-tests-1747403119751.md | 5 -- .../pr-12238-tests-1747405842720.md | 5 -- .../pr-12239-tech-stories-1747407424807.md | 5 -- .../pr-12242-tech-stories-1747759885067.md | 5 -- ...r-12243-upcoming-features-1747670800164.md | 5 -- .../pr-12244-tech-stories-1747680484622.md | 5 -- ...r-12246-upcoming-features-1747750319787.md | 5 -- ...r-12249-upcoming-features-1747752128426.md | 5 -- .../pr-12250-tests-1747757135754.md | 5 -- ...r-12251-upcoming-features-1747763494658.md | 5 -- .../pr-12252-fixed-1747765254237.md | 5 -- ...r-12256-upcoming-features-1747817841341.md | 5 -- .../pr-12258-tech-stories-1747847887739.md | 5 -- .../pr-12259-tests-1747849345030.md | 5 -- .../pr-12263-tech-stories-1747925853379.md | 5 -- ...r-12264-upcoming-features-1747926067938.md | 5 -- .../pr-12265-tech-stories-1747931205927.md | 5 -- ...r-12266-upcoming-features-1747938663753.md | 5 -- .../pr-12267-tech-stories-1747942707347.md | 5 -- .../pr-12268-changed-1747968191432.md | 5 -- .../pr-12269-fixed-1747981241068.md | 5 -- .../pr-12272-fixed-1748009662401.md | 5 -- .../pr-12276-fixed-1748030613590.md | 5 -- ...r-12281-upcoming-features-1748381133121.md | 5 -- packages/manager/CHANGELOG.md | 71 +++++++++++++++++++ packages/manager/package.json | 2 +- .../pr-12204-added-1747102460258.md | 5 -- .../pr-12205-added-1747256092908.md | 5 -- .../pr-12217-removed-1747241598724.md | 5 -- .../pr-12221-added-1747257990334.md | 5 -- packages/queries/CHANGELOG.md | 12 ++++ packages/queries/package.json | 2 +- .../pr-12208-fixed-1747157830376.md | 5 -- .../pr-12224-changed-1747294477261.md | 5 -- packages/ui/CHANGELOG.md | 10 +++ packages/ui/package.json | 2 +- .../pr-12181-fixed-1746797740109.md | 5 -- .../pr-12281-added-1748363298759.md | 5 -- packages/validation/CHANGELOG.md | 10 +++ packages/validation/package.json | 2 +- 78 files changed, 128 insertions(+), 345 deletions(-) delete mode 100644 packages/api-v4/.changeset/pr-12217-changed-1747239928776.md delete mode 100644 packages/api-v4/.changeset/pr-12223-removed-1747346374792.md delete mode 100644 packages/api-v4/.changeset/pr-12231-added-1747351003855.md delete mode 100644 packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md delete mode 100644 packages/api-v4/.changeset/pr-12274-changed-1748026726555.md delete mode 100644 packages/api-v4/.changeset/pr-12281-added-1748363215356.md delete mode 100644 packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md delete mode 100644 packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md delete mode 100644 packages/manager/.changeset/pr-12162-upcoming-features-1746512536804.md delete mode 100644 packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md delete mode 100644 packages/manager/.changeset/pr-12181-upcoming-features-1746797606970.md delete mode 100644 packages/manager/.changeset/pr-12201-tech-stories-1747327902316.md delete mode 100644 packages/manager/.changeset/pr-12203-changed-1747094831129.md delete mode 100644 packages/manager/.changeset/pr-12203-fixed-1747094899948.md delete mode 100644 packages/manager/.changeset/pr-12203-fixed-1747094966781.md delete mode 100644 packages/manager/.changeset/pr-12204-tech-stories-1747102404898.md delete mode 100644 packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md delete mode 100644 packages/manager/.changeset/pr-12207-fixed-1747120917933.md delete mode 100644 packages/manager/.changeset/pr-12210-fixed-1747239552927.md delete mode 100644 packages/manager/.changeset/pr-12212-tech-stories-1747235320136.md delete mode 100644 packages/manager/.changeset/pr-12215-changed-1747223125401.md delete mode 100644 packages/manager/.changeset/pr-12215-fixed-1747230804629.md delete mode 100644 packages/manager/.changeset/pr-12217-fixed-1747241027159.md delete mode 100644 packages/manager/.changeset/pr-12218-tests-1747241849916.md delete mode 100644 packages/manager/.changeset/pr-12219-tech-stories-1747251063145.md delete mode 100644 packages/manager/.changeset/pr-12221-tech-stories-1747257942556.md delete mode 100644 packages/manager/.changeset/pr-12223-fixed-1747346453830.md delete mode 100644 packages/manager/.changeset/pr-12223-tech-stories-1747346603680.md delete mode 100644 packages/manager/.changeset/pr-12226-tests-1747314916451.md delete mode 100644 packages/manager/.changeset/pr-12227-upcoming-features-1747318764898.md delete mode 100644 packages/manager/.changeset/pr-12230-tests-1747340294690.md delete mode 100644 packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md delete mode 100644 packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md delete mode 100644 packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md delete mode 100644 packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md delete mode 100644 packages/manager/.changeset/pr-12236-upcoming-features-1747395107850.md delete mode 100644 packages/manager/.changeset/pr-12237-tests-1747403119751.md delete mode 100644 packages/manager/.changeset/pr-12238-tests-1747405842720.md delete mode 100644 packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md delete mode 100644 packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md delete mode 100644 packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md delete mode 100644 packages/manager/.changeset/pr-12244-tech-stories-1747680484622.md delete mode 100644 packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md delete mode 100644 packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md delete mode 100644 packages/manager/.changeset/pr-12250-tests-1747757135754.md delete mode 100644 packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md delete mode 100644 packages/manager/.changeset/pr-12252-fixed-1747765254237.md delete mode 100644 packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md delete mode 100644 packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md delete mode 100644 packages/manager/.changeset/pr-12259-tests-1747849345030.md delete mode 100644 packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md delete mode 100644 packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md delete mode 100644 packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md delete mode 100644 packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md delete mode 100644 packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md delete mode 100644 packages/manager/.changeset/pr-12268-changed-1747968191432.md delete mode 100644 packages/manager/.changeset/pr-12269-fixed-1747981241068.md delete mode 100644 packages/manager/.changeset/pr-12272-fixed-1748009662401.md delete mode 100644 packages/manager/.changeset/pr-12276-fixed-1748030613590.md delete mode 100644 packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md delete mode 100644 packages/queries/.changeset/pr-12204-added-1747102460258.md delete mode 100644 packages/queries/.changeset/pr-12205-added-1747256092908.md delete mode 100644 packages/queries/.changeset/pr-12217-removed-1747241598724.md delete mode 100644 packages/queries/.changeset/pr-12221-added-1747257990334.md delete mode 100644 packages/ui/.changeset/pr-12208-fixed-1747157830376.md delete mode 100644 packages/ui/.changeset/pr-12224-changed-1747294477261.md delete mode 100644 packages/validation/.changeset/pr-12181-fixed-1746797740109.md delete mode 100644 packages/validation/.changeset/pr-12281-added-1748363298759.md diff --git a/packages/api-v4/.changeset/pr-12217-changed-1747239928776.md b/packages/api-v4/.changeset/pr-12217-changed-1747239928776.md deleted file mode 100644 index 0361ae81a37..00000000000 --- a/packages/api-v4/.changeset/pr-12217-changed-1747239928776.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Changed ---- - -Made `lke_cluster` and `type` defined in the `NodeBalancer` type ([#12217](https://github.com/linode/manager/pull/12217)) diff --git a/packages/api-v4/.changeset/pr-12223-removed-1747346374792.md b/packages/api-v4/.changeset/pr-12223-removed-1747346374792.md deleted file mode 100644 index d6ffa9a75e6..00000000000 --- a/packages/api-v4/.changeset/pr-12223-removed-1747346374792.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Removed ---- - -`add_buckets` from `GlobalGrantTypes` ([#12223](https://github.com/linode/manager/pull/12223)) diff --git a/packages/api-v4/.changeset/pr-12231-added-1747351003855.md b/packages/api-v4/.changeset/pr-12231-added-1747351003855.md deleted file mode 100644 index cbb0459ad18..00000000000 --- a/packages/api-v4/.changeset/pr-12231-added-1747351003855.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Added ---- - -Notification type for QEMU maintenance ([#12231](https://github.com/linode/manager/pull/12231)) diff --git a/packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md b/packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md deleted file mode 100644 index f8ae3ecb7ed..00000000000 --- a/packages/api-v4/.changeset/pr-12272-fixed-1748009649223.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Fixed ---- - -Make quota_id a string ([#12272](https://github.com/linode/manager/pull/12272)) diff --git a/packages/api-v4/.changeset/pr-12274-changed-1748026726555.md b/packages/api-v4/.changeset/pr-12274-changed-1748026726555.md deleted file mode 100644 index c9888ee0a66..00000000000 --- a/packages/api-v4/.changeset/pr-12274-changed-1748026726555.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Changed ---- - -Mark `markEventRead` as deprecated ([#12274](https://github.com/linode/manager/pull/12274)) diff --git a/packages/api-v4/.changeset/pr-12281-added-1748363215356.md b/packages/api-v4/.changeset/pr-12281-added-1748363215356.md deleted file mode 100644 index 261cdb0c7d9..00000000000 --- a/packages/api-v4/.changeset/pr-12281-added-1748363215356.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Added ---- - -PrivateNetwork type for Use in DBaaS requests ([#12281](https://github.com/linode/manager/pull/12281)) diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 4beb99843f3..f136d5f743b 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,23 @@ +## [2025-06-03] - v0.141.0 + +### Added: + +- Notification type for QEMU maintenance ([#12231](https://github.com/linode/manager/pull/12231)) +- PrivateNetwork type for Use in DBaaS requests ([#12281](https://github.com/linode/manager/pull/12281)) + +### Changed: + +- Make `lke_cluster` and `type` defined in the `NodeBalancer` type ([#12217](https://github.com/linode/manager/pull/12217)) +- Mark `markEventRead` as deprecated ([#12274](https://github.com/linode/manager/pull/12274)) + +### Fixed: + +- Make quota_id a string ([#12272](https://github.com/linode/manager/pull/12272)) + +### Removed: + +- `add_buckets` from `GlobalGrantTypes` ([#12223](https://github.com/linode/manager/pull/12223)) + ## [2025-05-20] - v0.140.0 ### Upcoming Features: diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index a8bfff35ce3..8f71898954d 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.140.0", + "version": "0.141.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md b/packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md deleted file mode 100644 index 06a423492e9..00000000000 --- a/packages/manager/.changeset/pr-12052-tech-stories-1744889150021.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Reduce api requests made for every keystroke in Volume attach drawer ([#12052](https://github.com/linode/manager/pull/12052)) diff --git a/packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md b/packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md deleted file mode 100644 index 7d32e4d1d26..00000000000 --- a/packages/manager/.changeset/pr-12155-upcoming-features-1746446424705.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -DataStream: routes, feature flag, tabs ([#12155](https://github.com/linode/manager/pull/12155)) diff --git a/packages/manager/.changeset/pr-12162-upcoming-features-1746512536804.md b/packages/manager/.changeset/pr-12162-upcoming-features-1746512536804.md deleted file mode 100644 index e226c4b8f1d..00000000000 --- a/packages/manager/.changeset/pr-12162-upcoming-features-1746512536804.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Show VPC details in the Nodebalancer summary page ([#12162](https://github.com/linode/manager/pull/12162)) diff --git a/packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md b/packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md deleted file mode 100644 index 63acec314e1..00000000000 --- a/packages/manager/.changeset/pr-12176-upcoming-features-1746717784144.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Show Linode Interface firewalls in `LinodeEntityDetail` ([#12176](https://github.com/linode/manager/pull/12176)) diff --git a/packages/manager/.changeset/pr-12181-upcoming-features-1746797606970.md b/packages/manager/.changeset/pr-12181-upcoming-features-1746797606970.md deleted file mode 100644 index b3d066cf077..00000000000 --- a/packages/manager/.changeset/pr-12181-upcoming-features-1746797606970.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add VPC Section in the Nodebalancer create flow ([#12181](https://github.com/linode/manager/pull/12181)) diff --git a/packages/manager/.changeset/pr-12201-tech-stories-1747327902316.md b/packages/manager/.changeset/pr-12201-tech-stories-1747327902316.md deleted file mode 100644 index dfa17c00f7a..00000000000 --- a/packages/manager/.changeset/pr-12201-tech-stories-1747327902316.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Add support for NB-VPC related /v4/vpcs changes in CRUD mocks ([#12201](https://github.com/linode/manager/pull/12201)) diff --git a/packages/manager/.changeset/pr-12203-changed-1747094831129.md b/packages/manager/.changeset/pr-12203-changed-1747094831129.md deleted file mode 100644 index 22782cff002..00000000000 --- a/packages/manager/.changeset/pr-12203-changed-1747094831129.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Switch to self-hosting the Pendo agent with Adobe Launch ([#12203](https://github.com/linode/manager/pull/12203)) diff --git a/packages/manager/.changeset/pr-12203-fixed-1747094899948.md b/packages/manager/.changeset/pr-12203-fixed-1747094899948.md deleted file mode 100644 index 75f893b6cad..00000000000 --- a/packages/manager/.changeset/pr-12203-fixed-1747094899948.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Bug in loadScript function not resolving promise if script already existed ([#12203](https://github.com/linode/manager/pull/12203)) diff --git a/packages/manager/.changeset/pr-12203-fixed-1747094966781.md b/packages/manager/.changeset/pr-12203-fixed-1747094966781.md deleted file mode 100644 index f8707444078..00000000000 --- a/packages/manager/.changeset/pr-12203-fixed-1747094966781.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Bug where first pageview of landing page was not fired in Adobe Analytics ([#12203](https://github.com/linode/manager/pull/12203)) diff --git a/packages/manager/.changeset/pr-12204-tech-stories-1747102404898.md b/packages/manager/.changeset/pr-12204-tech-stories-1747102404898.md deleted file mode 100644 index 985f3f7dc52..00000000000 --- a/packages/manager/.changeset/pr-12204-tech-stories-1747102404898.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Removed ---- - -Move domain related queries and dependencies to shared `queries` package ([#12204](https://github.com/linode/manager/pull/12204)) diff --git a/packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md b/packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md deleted file mode 100644 index bfac9d35693..00000000000 --- a/packages/manager/.changeset/pr-12205-tech-stories-1747256053421.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Moved images related queries and dependencies to shared `queries` package ([#12205](https://github.com/linode/manager/pull/12205)) diff --git a/packages/manager/.changeset/pr-12207-fixed-1747120917933.md b/packages/manager/.changeset/pr-12207-fixed-1747120917933.md deleted file mode 100644 index 498774f077b..00000000000 --- a/packages/manager/.changeset/pr-12207-fixed-1747120917933.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Formatting of the volume status and broken spacing in MultipleIPInput component ([#12207](https://github.com/linode/manager/pull/12207)) diff --git a/packages/manager/.changeset/pr-12210-fixed-1747239552927.md b/packages/manager/.changeset/pr-12210-fixed-1747239552927.md deleted file mode 100644 index 4609dea254a..00000000000 --- a/packages/manager/.changeset/pr-12210-fixed-1747239552927.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Do not set ACL Revision ID to empty string on LKE clusters ([#12210](https://github.com/linode/manager/pull/12210)) diff --git a/packages/manager/.changeset/pr-12212-tech-stories-1747235320136.md b/packages/manager/.changeset/pr-12212-tech-stories-1747235320136.md deleted file mode 100644 index 2dd5b00875a..00000000000 --- a/packages/manager/.changeset/pr-12212-tech-stories-1747235320136.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Add MSW presets for Events, Maintenance, and Notifications ([#12212](https://github.com/linode/manager/pull/12212)) diff --git a/packages/manager/.changeset/pr-12215-changed-1747223125401.md b/packages/manager/.changeset/pr-12215-changed-1747223125401.md deleted file mode 100644 index c85f18440bb..00000000000 --- a/packages/manager/.changeset/pr-12215-changed-1747223125401.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Remove the `Accordion` wrapper from the default Alerts tab and replace it with `Paper` on the Linode details page ([#12215](https://github.com/linode/manager/pull/12215)) diff --git a/packages/manager/.changeset/pr-12215-fixed-1747230804629.md b/packages/manager/.changeset/pr-12215-fixed-1747230804629.md deleted file mode 100644 index 325b8148f49..00000000000 --- a/packages/manager/.changeset/pr-12215-fixed-1747230804629.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@linode/manager': Fixed ---- - -Manual clearing of default Alerts fields now resets values to zero, preventing empty string/NaN and ensuring consistency with toggle off state ([#12215](https://github.com/linode/manager/pull/12215)) diff --git a/packages/manager/.changeset/pr-12217-fixed-1747241027159.md b/packages/manager/.changeset/pr-12217-fixed-1747241027159.md deleted file mode 100644 index 11dd932502e..00000000000 --- a/packages/manager/.changeset/pr-12217-fixed-1747241027159.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -NodeBalancer label and connection throttle not updating until page refresh ([#12217](https://github.com/linode/manager/pull/12217)) diff --git a/packages/manager/.changeset/pr-12218-tests-1747241849916.md b/packages/manager/.changeset/pr-12218-tests-1747241849916.md deleted file mode 100644 index 08ed1fb0b41..00000000000 --- a/packages/manager/.changeset/pr-12218-tests-1747241849916.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Unskip Cypress Firewall end-to-end tests ([#12218](https://github.com/linode/manager/pull/12218)) diff --git a/packages/manager/.changeset/pr-12219-tech-stories-1747251063145.md b/packages/manager/.changeset/pr-12219-tech-stories-1747251063145.md deleted file mode 100644 index 091cec3227e..00000000000 --- a/packages/manager/.changeset/pr-12219-tech-stories-1747251063145.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Upgrade @sentry/react to v9 ([#12219](https://github.com/linode/manager/pull/12219)) diff --git a/packages/manager/.changeset/pr-12221-tech-stories-1747257942556.md b/packages/manager/.changeset/pr-12221-tech-stories-1747257942556.md deleted file mode 100644 index 601a68e326a..00000000000 --- a/packages/manager/.changeset/pr-12221-tech-stories-1747257942556.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Move quotas related queries and dependencies to shared `queries` package ([#12221](https://github.com/linode/manager/pull/12221)) diff --git a/packages/manager/.changeset/pr-12223-fixed-1747346453830.md b/packages/manager/.changeset/pr-12223-fixed-1747346453830.md deleted file mode 100644 index 33456e48fc5..00000000000 --- a/packages/manager/.changeset/pr-12223-fixed-1747346453830.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Inconsistent restricted user notices on landing pages ([#12223](https://github.com/linode/manager/pull/12223)) diff --git a/packages/manager/.changeset/pr-12223-tech-stories-1747346603680.md b/packages/manager/.changeset/pr-12223-tech-stories-1747346603680.md deleted file mode 100644 index 80ee93eb37a..00000000000 --- a/packages/manager/.changeset/pr-12223-tech-stories-1747346603680.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Remove `useAccountManagement` hook ([#12223](https://github.com/linode/manager/pull/12223)) diff --git a/packages/manager/.changeset/pr-12226-tests-1747314916451.md b/packages/manager/.changeset/pr-12226-tests-1747314916451.md deleted file mode 100644 index 50aaf97105d..00000000000 --- a/packages/manager/.changeset/pr-12226-tests-1747314916451.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Exclude distributed regions when selecting regions for API operations ([#12226](https://github.com/linode/manager/pull/12226)) diff --git a/packages/manager/.changeset/pr-12227-upcoming-features-1747318764898.md b/packages/manager/.changeset/pr-12227-upcoming-features-1747318764898.md deleted file mode 100644 index 60f9d8d254b..00000000000 --- a/packages/manager/.changeset/pr-12227-upcoming-features-1747318764898.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -IAM RBAC: fix bugs in the assign new roles drawer ([#12227](https://github.com/linode/manager/pull/12227)) diff --git a/packages/manager/.changeset/pr-12230-tests-1747340294690.md b/packages/manager/.changeset/pr-12230-tests-1747340294690.md deleted file mode 100644 index 96a8ac60a7d..00000000000 --- a/packages/manager/.changeset/pr-12230-tests-1747340294690.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Cypress test for Longview create page for restricted users ([#12230](https://github.com/linode/manager/pull/12230)) diff --git a/packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md b/packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md deleted file mode 100644 index 9d4cb0cd3ac..00000000000 --- a/packages/manager/.changeset/pr-12231-upcoming-features-1747350971547.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -QEMU reboot notices ([#12231](https://github.com/linode/manager/pull/12231)) diff --git a/packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md b/packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md deleted file mode 100644 index ef9fd1033c6..00000000000 --- a/packages/manager/.changeset/pr-12232-upcoming-features-1747645656510.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add NodeBalancer Table under VPC Subnets Table and rename "Linodes" column to "Resources" ([#12232](https://github.com/linode/manager/pull/12232)) diff --git a/packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md b/packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md deleted file mode 100644 index b2f497d57fa..00000000000 --- a/packages/manager/.changeset/pr-12233-upcoming-features-1747392110404.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -IAM RBAC: fix UI issues in Users and Roles tabs, including button styling, layout, and permissions toggle ([#12233](https://github.com/linode/manager/pull/12233)) diff --git a/packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md b/packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md deleted file mode 100644 index 92726ce7146..00000000000 --- a/packages/manager/.changeset/pr-12235-upcoming-features-1747394318662.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -DataStream: add Streams empty state and Create Stream views ([#12235](https://github.com/linode/manager/pull/12235)) diff --git a/packages/manager/.changeset/pr-12236-upcoming-features-1747395107850.md b/packages/manager/.changeset/pr-12236-upcoming-features-1747395107850.md deleted file mode 100644 index 8a4877318b2..00000000000 --- a/packages/manager/.changeset/pr-12236-upcoming-features-1747395107850.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add beta ACLP contextual alerts to the Alerts tab on the Linode details page ([#12236](https://github.com/linode/manager/pull/12236)) diff --git a/packages/manager/.changeset/pr-12237-tests-1747403119751.md b/packages/manager/.changeset/pr-12237-tests-1747403119751.md deleted file mode 100644 index 1984157b2bb..00000000000 --- a/packages/manager/.changeset/pr-12237-tests-1747403119751.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Test for firewall create page for restricted users ([#12237](https://github.com/linode/manager/pull/12237)) diff --git a/packages/manager/.changeset/pr-12238-tests-1747405842720.md b/packages/manager/.changeset/pr-12238-tests-1747405842720.md deleted file mode 100644 index 1cd86d9b903..00000000000 --- a/packages/manager/.changeset/pr-12238-tests-1747405842720.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Vpc tests for restricted user ([#12238](https://github.com/linode/manager/pull/12238)) diff --git a/packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md b/packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md deleted file mode 100644 index 2f2d224a88d..00000000000 --- a/packages/manager/.changeset/pr-12239-tech-stories-1747407424807.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Remove recompose from Longview ([#12239](https://github.com/linode/manager/pull/12239)) diff --git a/packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md b/packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md deleted file mode 100644 index e8f94ce475b..00000000000 --- a/packages/manager/.changeset/pr-12242-tech-stories-1747759885067.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Reroute Support & Help features ([#12242](https://github.com/linode/manager/pull/12242)) diff --git a/packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md b/packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md deleted file mode 100644 index f650f6fef4b..00000000000 --- a/packages/manager/.changeset/pr-12243-upcoming-features-1747670800164.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Event message tweaks for Linode Interfaces ([#12243](https://github.com/linode/manager/pull/12243)) diff --git a/packages/manager/.changeset/pr-12244-tech-stories-1747680484622.md b/packages/manager/.changeset/pr-12244-tech-stories-1747680484622.md deleted file mode 100644 index 7258b006d94..00000000000 --- a/packages/manager/.changeset/pr-12244-tech-stories-1747680484622.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Use `unstable_createBreakpoints` to define our MUI breakpoints ([#12244](https://github.com/linode/manager/pull/12244)) diff --git a/packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md b/packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md deleted file mode 100644 index 923f6501d3a..00000000000 --- a/packages/manager/.changeset/pr-12246-upcoming-features-1747750319787.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Fix newest LKE-E kubernetes version not being selected by default in create flow ([#12246](https://github.com/linode/manager/pull/12246)) diff --git a/packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md b/packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md deleted file mode 100644 index 3ede6de1854..00000000000 --- a/packages/manager/.changeset/pr-12249-upcoming-features-1747752128426.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -IAM RBAC: Fix bugs for the assigned roles table ([#12249](https://github.com/linode/manager/pull/12249)) diff --git a/packages/manager/.changeset/pr-12250-tests-1747757135754.md b/packages/manager/.changeset/pr-12250-tests-1747757135754.md deleted file mode 100644 index fdb60ba9f6f..00000000000 --- a/packages/manager/.changeset/pr-12250-tests-1747757135754.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Account quotas navigation and permissions ([#12250](https://github.com/linode/manager/pull/12250)) diff --git a/packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md b/packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md deleted file mode 100644 index c03060d6768..00000000000 --- a/packages/manager/.changeset/pr-12251-upcoming-features-1747763494658.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Update estimated time for LKE-E node pool pending creation message ([#12251](https://github.com/linode/manager/pull/12251)) diff --git a/packages/manager/.changeset/pr-12252-fixed-1747765254237.md b/packages/manager/.changeset/pr-12252-fixed-1747765254237.md deleted file mode 100644 index 37e28d21234..00000000000 --- a/packages/manager/.changeset/pr-12252-fixed-1747765254237.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Make `linode_resize` started event generic ([#12252](https://github.com/linode/manager/pull/12252)) diff --git a/packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md b/packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md deleted file mode 100644 index 4cbc46418bf..00000000000 --- a/packages/manager/.changeset/pr-12256-upcoming-features-1747817841341.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add VPC column to the Nodebalancer Landing table ([#12256](https://github.com/linode/manager/pull/12256)) diff --git a/packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md b/packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md deleted file mode 100644 index 40a3fb0767d..00000000000 --- a/packages/manager/.changeset/pr-12258-tech-stories-1747847887739.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Reroute search feature ([#12258](https://github.com/linode/manager/pull/12258)) diff --git a/packages/manager/.changeset/pr-12259-tests-1747849345030.md b/packages/manager/.changeset/pr-12259-tests-1747849345030.md deleted file mode 100644 index 86591aac0bb..00000000000 --- a/packages/manager/.changeset/pr-12259-tests-1747849345030.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add integration test for Upgrade to new Linode Interface flow ([#12259](https://github.com/linode/manager/pull/12259)) diff --git a/packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md b/packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md deleted file mode 100644 index 926cbd5fc9c..00000000000 --- a/packages/manager/.changeset/pr-12263-tech-stories-1747925853379.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Stop MSW and DevTools from existing in production bundles ([#12263](https://github.com/linode/manager/pull/12263)) diff --git a/packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md b/packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md deleted file mode 100644 index 7b9a24c0242..00000000000 --- a/packages/manager/.changeset/pr-12264-upcoming-features-1747926067938.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -IAM RBAC: add pagination to the Roles table ([#12264](https://github.com/linode/manager/pull/12264)) diff --git a/packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md b/packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md deleted file mode 100644 index addb44a5fcd..00000000000 --- a/packages/manager/.changeset/pr-12265-tech-stories-1747931205927.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Fix erroneous Sentry error in useAdobeAnalytics hook ([#12265](https://github.com/linode/manager/pull/12265)) diff --git a/packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md b/packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md deleted file mode 100644 index 60825e38963..00000000000 --- a/packages/manager/.changeset/pr-12266-upcoming-features-1747938663753.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Disable the Kubernetes Dashboard request for LKE-E clusters ([#12266](https://github.com/linode/manager/pull/12266)) diff --git a/packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md b/packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md deleted file mode 100644 index eac0ae4b063..00000000000 --- a/packages/manager/.changeset/pr-12267-tech-stories-1747942707347.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Re-add `eslint-plugin-react-refresh` eslint plugin ([#12267](https://github.com/linode/manager/pull/12267)) diff --git a/packages/manager/.changeset/pr-12268-changed-1747968191432.md b/packages/manager/.changeset/pr-12268-changed-1747968191432.md deleted file mode 100644 index 9c8e4615986..00000000000 --- a/packages/manager/.changeset/pr-12268-changed-1747968191432.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Update LKE flows for APL General Availability ([#12268](https://github.com/linode/manager/pull/12268)) diff --git a/packages/manager/.changeset/pr-12269-fixed-1747981241068.md b/packages/manager/.changeset/pr-12269-fixed-1747981241068.md deleted file mode 100644 index 9cb239837f2..00000000000 --- a/packages/manager/.changeset/pr-12269-fixed-1747981241068.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Image Select overflows off screen on mobile viewports ([#12269](https://github.com/linode/manager/pull/12269)) diff --git a/packages/manager/.changeset/pr-12272-fixed-1748009662401.md b/packages/manager/.changeset/pr-12272-fixed-1748009662401.md deleted file mode 100644 index d15f3149018..00000000000 --- a/packages/manager/.changeset/pr-12272-fixed-1748009662401.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Make quota_id a string ([#12272](https://github.com/linode/manager/pull/12272)) diff --git a/packages/manager/.changeset/pr-12276-fixed-1748030613590.md b/packages/manager/.changeset/pr-12276-fixed-1748030613590.md deleted file mode 100644 index e8f51773580..00000000000 --- a/packages/manager/.changeset/pr-12276-fixed-1748030613590.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -LinodeCreateError notice not spanning full width ([#12276](https://github.com/linode/manager/pull/12276)) diff --git a/packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md b/packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md deleted file mode 100644 index 931c2cca335..00000000000 --- a/packages/manager/.changeset/pr-12281-upcoming-features-1748381133121.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Configure Networking section and VPC functionality for DBaaS Create view ([#12281](https://github.com/linode/manager/pull/12281)) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 01b777aca2c..0c7d2a69cb0 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,77 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2025-06-03] - v1.143.0 + +### Changed: + +- Remove the `Accordion` wrapper from the default Alerts tab and replace it with `Paper` on the Linode details page ([#12215](https://github.com/linode/manager/pull/12215)) +- Update LKE flows for APL General Availability ([#12268](https://github.com/linode/manager/pull/12268)) + +### Fixed: + +- Bug where first pageview of landing page was not fired in Adobe Analytics ([#12203](https://github.com/linode/manager/pull/12203)) +- Formatting of the volume status and broken spacing in MultipleIPInput component ([#12207](https://github.com/linode/manager/pull/12207)) +- ACL Revision ID being set to empty string on LKE clusters ([#12210](https://github.com/linode/manager/pull/12210)) +- NodeBalancer label and connection throttle not updating until page refresh ([#12217](https://github.com/linode/manager/pull/12217)) +- Inconsistent restricted user notices on landing pages ([#12223](https://github.com/linode/manager/pull/12223)) +- `linode_resize` started event referencing the wrong linode ([#12252](https://github.com/linode/manager/pull/12252)) +- Image Select overflows off screen on mobile viewports ([#12269](https://github.com/linode/manager/pull/12269)) +- LinodeCreateError notice not spanning full width ([#12276](https://github.com/linode/manager/pull/12276)) +- Manual clearing of default Alerts fields now resets values to zero, preventing empty string/NaN and ensuring consistency with toggle off state ([#12215](https://github.com/linode/manager/pull/12215)) + +### Tech Stories: + +- Reduce api requests made for every keystroke in Volume attach drawer ([#12052](https://github.com/linode/manager/pull/12052)) +- Add support for NB-VPC related /v4/vpcs changes in CRUD mocks ([#12201](https://github.com/linode/manager/pull/12201)) +- Move images related queries and dependencies to shared `queries` package ([#12205](https://github.com/linode/manager/pull/12205)) +- Move domain related queries and dependencies to shared `queries` package ([#12204](https://github.com/linode/manager/pull/12204)) +- Move quotas related queries and dependencies to shared `queries` package ([#12221](https://github.com/linode/manager/pull/12221)) +- Add MSW presets for Events, Maintenance, and Notifications ([#12212](https://github.com/linode/manager/pull/12212)) +- Upgrade @sentry/react to v9 ([#12219](https://github.com/linode/manager/pull/12219)) +- Remove `useAccountManagement` hook ([#12223](https://github.com/linode/manager/pull/12223)) +- Remove recompose from Longview ([#12239](https://github.com/linode/manager/pull/12239)) +- Reroute Support & Help features ([#12242](https://github.com/linode/manager/pull/12242)) +- Use `unstable_createBreakpoints` to define our MUI breakpoints ([#12244](https://github.com/linode/manager/pull/12244)) +- Reroute search feature ([#12258](https://github.com/linode/manager/pull/12258)) +- Stop MSW and DevTools from existing in production bundles ([#12263](https://github.com/linode/manager/pull/12263)) +- Fix erroneous Sentry error in useAdobeAnalytics hook ([#12265](https://github.com/linode/manager/pull/12265)) +- Re-add `eslint-plugin-react-refresh` eslint plugin ([#12267](https://github.com/linode/manager/pull/12267)) +- Switch to self-hosting the Pendo agent with Adobe Launch ([#12203](https://github.com/linode/manager/pull/12203)) +- Fix bug in loadScript function not resolving promise if script already existed ([#12203](https://github.com/linode/manager/pull/12203)) +- Make quota_id a string ([#12272](https://github.com/linode/manager/pull/12272)) + +### Tests: + +- Unskip Cypress Firewall end-to-end tests ([#12218](https://github.com/linode/manager/pull/12218)) +- Exclude distributed regions when selecting regions for API operations ([#12226](https://github.com/linode/manager/pull/12226)) +- Add Cypress test for Longview create page for restricted users ([#12230](https://github.com/linode/manager/pull/12230)) +- Add test for firewall create page for restricted users ([#12237](https://github.com/linode/manager/pull/12237)) +- Add VPC tests for restricted user ([#12238](https://github.com/linode/manager/pull/12238)) +- Add Cypress test for Account quotas navigation and permissions ([#12250](https://github.com/linode/manager/pull/12250)) +- Add integration test for Upgrade to new Linode Interface flow ([#12259](https://github.com/linode/manager/pull/12259)) + +### Upcoming Features: + +- DataStream: routes, feature flag, tabs ([#12155](https://github.com/linode/manager/pull/12155)) +- Show VPC details in the Nodebalancer summary page ([#12162](https://github.com/linode/manager/pull/12162)) +- Show Linode Interface firewalls in `LinodeEntityDetail` ([#12176](https://github.com/linode/manager/pull/12176)) +- Add VPC Section in the Nodebalancer create flow ([#12181](https://github.com/linode/manager/pull/12181)) +- IAM RBAC: fix bugs in the assign new roles drawer ([#12227](https://github.com/linode/manager/pull/12227)) +- QEMU reboot notices ([#12231](https://github.com/linode/manager/pull/12231)) +- Add NodeBalancer Table under VPC Subnets Table and rename "Linodes" column to "Resources" ([#12232](https://github.com/linode/manager/pull/12232)) +- IAM RBAC: fix UI issues in Users and Roles tabs, including button styling, layout, and permissions toggle ([#12233](https://github.com/linode/manager/pull/12233)) +- DataStream: add Streams empty state and Create Stream views ([#12235](https://github.com/linode/manager/pull/12235)) +- Add beta ACLP contextual alerts to the Alerts tab on the Linode details page ([#12236](https://github.com/linode/manager/pull/12236)) +- Event message tweaks for Linode Interfaces ([#12243](https://github.com/linode/manager/pull/12243)) +- Fix newest LKE-E kubernetes version not being selected by default in create flow ([#12246](https://github.com/linode/manager/pull/12246)) +- IAM RBAC: Fix bugs for the assigned roles table ([#12249](https://github.com/linode/manager/pull/12249)) +- Update estimated time for LKE-E node pool pending creation message ([#12251](https://github.com/linode/manager/pull/12251)) +- Add VPC column to the Nodebalancer Landing table ([#12256](https://github.com/linode/manager/pull/12256)) +- IAM RBAC: add pagination to the Roles table ([#12264](https://github.com/linode/manager/pull/12264)) +- Disable the Kubernetes Dashboard request for LKE-E clusters ([#12266](https://github.com/linode/manager/pull/12266)) +- Configure Networking section and VPC functionality for DBaaS Create view ([#12281](https://github.com/linode/manager/pull/12281)) + ## [2025-05-20] - v1.142.1 ### Fixed: diff --git a/packages/manager/package.json b/packages/manager/package.json index e9640c0f39d..1e3b318fdfc 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.142.1", + "version": "1.143.0", "private": true, "type": "module", "bugs": { diff --git a/packages/queries/.changeset/pr-12204-added-1747102460258.md b/packages/queries/.changeset/pr-12204-added-1747102460258.md deleted file mode 100644 index cd22b20be0a..00000000000 --- a/packages/queries/.changeset/pr-12204-added-1747102460258.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/queries": Added ---- - -Create `domains/` directory and migrate relevant query keys and hooks ([#12204](https://github.com/linode/manager/pull/12204)) diff --git a/packages/queries/.changeset/pr-12205-added-1747256092908.md b/packages/queries/.changeset/pr-12205-added-1747256092908.md deleted file mode 100644 index b08e36a1bbd..00000000000 --- a/packages/queries/.changeset/pr-12205-added-1747256092908.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/queries": Added ---- - -Created `images/` directory and migrated relevant query keys and hooks ([#12205](https://github.com/linode/manager/pull/12205)) diff --git a/packages/queries/.changeset/pr-12217-removed-1747241598724.md b/packages/queries/.changeset/pr-12217-removed-1747241598724.md deleted file mode 100644 index 3db68b94ce2..00000000000 --- a/packages/queries/.changeset/pr-12217-removed-1747241598724.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/queries": Removed ---- - -`isUsingBetaEndpoint` parameter from `useNodeBalancerQuery` ([#12217](https://github.com/linode/manager/pull/12217)) diff --git a/packages/queries/.changeset/pr-12221-added-1747257990334.md b/packages/queries/.changeset/pr-12221-added-1747257990334.md deleted file mode 100644 index 8ddde4e28a3..00000000000 --- a/packages/queries/.changeset/pr-12221-added-1747257990334.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/queries": Added ---- - -`quotas/` directory and migrated relevant query keys and hook ([#12221](https://github.com/linode/manager/pull/12221)) diff --git a/packages/queries/CHANGELOG.md b/packages/queries/CHANGELOG.md index a59105aebfc..076a3deb0fd 100644 --- a/packages/queries/CHANGELOG.md +++ b/packages/queries/CHANGELOG.md @@ -1,3 +1,15 @@ +## [2025-06-03] - v0.6.0 + +### Added: + +- Create `domains/` directory and migrate relevant query keys and hooks ([#12204](https://github.com/linode/manager/pull/12204)) +- Create `images/` directory and migrate relevant query keys and hooks ([#12205](https://github.com/linode/manager/pull/12205)) +- `quotas/` directory and migrated relevant query keys and hook ([#12221](https://github.com/linode/manager/pull/12221)) + +### Removed: + +- `isUsingBetaEndpoint` parameter from `useNodeBalancerQuery` ([#12217](https://github.com/linode/manager/pull/12217)) + ## [2025-05-20] - v0.5.0 ### Added: diff --git a/packages/queries/package.json b/packages/queries/package.json index 7db5871f40e..1e31c46ab8f 100644 --- a/packages/queries/package.json +++ b/packages/queries/package.json @@ -1,6 +1,6 @@ { "name": "@linode/queries", - "version": "0.5.0", + "version": "0.6.0", "description": "Linode Utility functions library", "main": "src/index.js", "module": "src/index.ts", diff --git a/packages/ui/.changeset/pr-12208-fixed-1747157830376.md b/packages/ui/.changeset/pr-12208-fixed-1747157830376.md deleted file mode 100644 index 4ba34ba63fd..00000000000 --- a/packages/ui/.changeset/pr-12208-fixed-1747157830376.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/ui": Fixed ---- - -Input placholder opacity ([#12208](https://github.com/linode/manager/pull/12208)) diff --git a/packages/ui/.changeset/pr-12224-changed-1747294477261.md b/packages/ui/.changeset/pr-12224-changed-1747294477261.md deleted file mode 100644 index caa83585785..00000000000 --- a/packages/ui/.changeset/pr-12224-changed-1747294477261.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/ui": Changed ---- - -Akamai Design System - Label component ([#12224](https://github.com/linode/manager/pull/12224)) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 14edca3263e..9ecd73c9959 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,3 +1,13 @@ +## [2025-06-03] - v0.13.0 + +### Changed: + +- Akamai Design System - Label component ([#12224](https://github.com/linode/manager/pull/12224)) + +### Fixed: + +- Input placeholder opacity ([#12208](https://github.com/linode/manager/pull/12208)) + ## [2025-05-20] - v0.12.0 ### Added: diff --git a/packages/ui/package.json b/packages/ui/package.json index 4149eb6bddb..c1534476d93 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,7 +2,7 @@ "name": "@linode/ui", "author": "Linode", "description": "Linode UI component library", - "version": "0.12.0", + "version": "0.13.0", "type": "module", "main": "src/index.ts", "module": "src/index.ts", diff --git a/packages/validation/.changeset/pr-12181-fixed-1746797740109.md b/packages/validation/.changeset/pr-12181-fixed-1746797740109.md deleted file mode 100644 index 4e4419d73e7..00000000000 --- a/packages/validation/.changeset/pr-12181-fixed-1746797740109.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/validation": Fixed ---- - -Handling duplicate subnet-ids during Nodebalancer creation with VPC enabled ([#12181](https://github.com/linode/manager/pull/12181)) diff --git a/packages/validation/.changeset/pr-12281-added-1748363298759.md b/packages/validation/.changeset/pr-12281-added-1748363298759.md deleted file mode 100644 index fe22246cece..00000000000 --- a/packages/validation/.changeset/pr-12281-added-1748363298759.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/validation": Added ---- - -Method to retrieve dynamic validation for Create database schema ([#12281](https://github.com/linode/manager/pull/12281)) diff --git a/packages/validation/CHANGELOG.md b/packages/validation/CHANGELOG.md index 27f7bf0ddce..4e4a952154c 100644 --- a/packages/validation/CHANGELOG.md +++ b/packages/validation/CHANGELOG.md @@ -1,3 +1,13 @@ +## [2025-06-03] - v0.67.0 + +### Added: + +- Method to retrieve dynamic validation for Create database schema ([#12281](https://github.com/linode/manager/pull/12281)) + +### Fixed: + +- Handling duplicate subnet-ids during Nodebalancer creation with VPC enabled ([#12181](https://github.com/linode/manager/pull/12181)) + ## [2025-05-20] - v0.66.0 ### Upcoming Features: diff --git a/packages/validation/package.json b/packages/validation/package.json index 8312ba8dde0..0151262ef18 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@linode/validation", - "version": "0.66.0", + "version": "0.67.0", "description": "Yup validation schemas for use with the Linode APIv4", "type": "module", "main": "lib/index.cjs", From 4a7a1c243150ee39f6bbd5c37f06f225d440fc72 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Fri, 30 May 2025 11:12:56 -0700 Subject: [PATCH 59/59] change: [M3-10061] - Update copy for LKE premium CPU notice (#12300) * Make requested copy change * Add changelog entry * Use lowercase for 'enterprise' * Remove unneeded word --- packages/manager/CHANGELOG.md | 1 + .../Kubernetes/CreateCluster/PremiumCPUPlanNotice.tsx | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 0c7d2a69cb0..e9f168eec09 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Remove the `Accordion` wrapper from the default Alerts tab and replace it with `Paper` on the Linode details page ([#12215](https://github.com/linode/manager/pull/12215)) - Update LKE flows for APL General Availability ([#12268](https://github.com/linode/manager/pull/12268)) +- Copy for premium plan recommendation for LKE ([#12300](https://github.com/linode/manager/pull/12300)) ### Fixed: diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/PremiumCPUPlanNotice.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/PremiumCPUPlanNotice.tsx index a4e6a8dbc82..a5bd8e67769 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/PremiumCPUPlanNotice.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/PremiumCPUPlanNotice.tsx @@ -6,8 +6,9 @@ import type { NoticeProps } from '@linode/ui'; export const PremiumCPUPlanNotice = (props: NoticeProps) => { return ( - Select Premium CPU instances for scenarios where low latency or high - throughput is expected. + To accommodate enterprise workloads, especially where resource contention + can impact your workloads, using Premium instances is highly recommended + and required to achieve peak performance. ); };