From 5d263880214d41cf352f5a829c34e5d458ed4255 Mon Sep 17 00:00:00 2001 From: dmcintyr-akamai Date: Wed, 19 Feb 2025 17:02:08 -0500 Subject: [PATCH 001/219] test: [M3 8672] - use eslint to flag createLinode function (#11689) * M3-8672 initial commit of good new branch * Added changeset: Apply new custom eslint rule --- .../manager/.changeset/pr-11689-tests-1739986270581.md | 5 +++++ packages/manager/.eslintrc.cjs | 8 ++++++++ packages/manager/cypress/support/util/linodes.ts | 1 + packages/manager/package.json | 2 +- packages/ui/package.json | 2 +- yarn.lock | 8 ++++---- 6 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 packages/manager/.changeset/pr-11689-tests-1739986270581.md diff --git a/packages/manager/.changeset/pr-11689-tests-1739986270581.md b/packages/manager/.changeset/pr-11689-tests-1739986270581.md new file mode 100644 index 00000000000..f4063941cea --- /dev/null +++ b/packages/manager/.changeset/pr-11689-tests-1739986270581.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Apply new custom eslint rule ([#11689](https://github.com/linode/manager/pull/11689)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 07bb61bc97c..6939a648538 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -142,6 +142,13 @@ module.exports = { ], }, }, + // turn off no-createLinode rule for src files. this rule should be applied only to cypress test files + { + "files": ["src/**"], + "rules": { + "@linode/cloud-manager/no-createLinode": "off" + } + } ], parser: '@typescript-eslint/parser', // Specifies the ESLint parser parserOptions: { @@ -174,6 +181,7 @@ module.exports = { rules: { '@linode/cloud-manager/deprecate-formik': 'warn', '@linode/cloud-manager/no-custom-fontWeight': 'error', + '@linode/cloud-manager/no-createLinode': 'error', '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index da303ff0bb0..af05524555c 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -138,6 +138,7 @@ export const createTestLinode = async ( ); } + // eslint-disable-next-line @linode/cloud-manager/no-createLinode const linode = await createLinode(resolvedCreatePayload); // Wait for disks to become available if `waitForDisks` option is set. diff --git a/packages/manager/package.json b/packages/manager/package.json index 7d4fc3e9972..03836a8e065 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -120,7 +120,7 @@ }, "devDependencies": { "@4tw/cypress-drag-drop": "^2.3.0", - "@linode/eslint-plugin-cloud-manager": "^0.0.5", + "@linode/eslint-plugin-cloud-manager": "^0.0.7", "@storybook/addon-a11y": "^8.4.7", "@storybook/addon-actions": "^8.4.7", "@storybook/addon-controls": "^8.4.7", diff --git a/packages/ui/package.json b/packages/ui/package.json index 7dd09bfe9cb..f77f0e672d8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -40,7 +40,7 @@ ] }, "devDependencies": { - "@linode/eslint-plugin-cloud-manager": "^0.0.5", + "@linode/eslint-plugin-cloud-manager": "^0.0.7", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "~6.4.2", "@testing-library/react": "~16.0.0", diff --git a/yarn.lock b/yarn.lock index d7f1e90304b..ded92fed170 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1173,10 +1173,10 @@ resolved "https://registry.yarnpkg.com/@linode/design-language-system/-/design-language-system-3.0.0.tgz#6bfede8d2b375aaefd2d6a092f1a1e70fe4bee3d" integrity sha512-LbrDgN0YbwDa1qsbPBV+BreFjk06R/CbcPCvWM4BvU6zrxqCkPVdErwfqJvdtDxRColXbIfn6NeFgq0sJaQ+sw== -"@linode/eslint-plugin-cloud-manager@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@linode/eslint-plugin-cloud-manager/-/eslint-plugin-cloud-manager-0.0.5.tgz#d35a80870e301ff43c4a2ade7d53d866c6a87bb8" - integrity sha512-zOT3iFnFTG5h4ylczvA3fUk8xiJPKzmzNTVsszjfyTDqFr6FiCDhGzUgj7znToW6J3Mr3CATobid2GCR2Ls19Q== +"@linode/eslint-plugin-cloud-manager@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@linode/eslint-plugin-cloud-manager/-/eslint-plugin-cloud-manager-0.0.7.tgz#bdfbb613d85e6d1299a3d1655675620c75d8ee10" + integrity sha512-83ZDbDQGsXCKxagX6CWszFZbsuX/fHSFn/i+P1FGYDm/0qnIo2XypB/lTdJhHJwvq1j2z+0VDZIMP7YLF5U6Sg== "@lukemorales/query-key-factory@^1.3.4": version "1.3.4" From a6bd28eacf35e3af6e92b72037b039498e2ec4de Mon Sep 17 00:00:00 2001 From: hasyed-akamai Date: Thu, 20 Feb 2025 12:22:02 +0530 Subject: [PATCH 002/219] =?UTF-8?q?fix:=20[M3-9281]=20-=20Ensure=20documen?= =?UTF-8?q?t=20titles=20for=20Cloud=20Pulse=20pages=20have=20ap=E2=80=A6?= =?UTF-8?q?=20(#11662)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: [M3-9281] - Ensure document titles for Cloud Pulse pages have appropriate keywords * Added changeset: Document titles of ACPL with appropriate keyword --- .../pr-11662-fixed-1739467765713.md | 5 + .../Alerts/AlertsDetail/AlertDetail.tsx | 3 + .../Alerts/AlertsLanding/AlertsLanding.tsx | 54 ++++--- .../CreateAlert/CreateAlertDefinition.tsx | 142 +++++++++--------- .../Dashboard/CloudPulseDashboardLanding.tsx | 51 ++++--- 5 files changed, 138 insertions(+), 117 deletions(-) create mode 100644 packages/manager/.changeset/pr-11662-fixed-1739467765713.md diff --git a/packages/manager/.changeset/pr-11662-fixed-1739467765713.md b/packages/manager/.changeset/pr-11662-fixed-1739467765713.md new file mode 100644 index 00000000000..55c42293fc6 --- /dev/null +++ b/packages/manager/.changeset/pr-11662-fixed-1739467765713.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Document titles of ACPL with appropriate keyword ([#11662](https://github.com/linode/manager/pull/11662)) diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx index 25c2a929a28..03f380e4d1d 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx @@ -5,6 +5,7 @@ import { useParams } from 'react-router-dom'; import AlertsIcon from 'src/assets/icons/entityIcons/alerts.svg'; import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { useAlertDefinitionQuery } from 'src/queries/cloudpulse/alerts'; @@ -96,8 +97,10 @@ export const AlertDetail = () => { service_type: alertServiceType, type, } = alertDetails; + return ( <> + diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx index a3afae0ab7c..04f84733822 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx @@ -9,6 +9,7 @@ import { useRouteMatch, } from 'react-router-dom'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { Tabs } from 'src/components/Tabs/Tabs'; import { useFlags } from 'src/hooks/useFlags'; @@ -51,31 +52,34 @@ export const AlertsLanding = React.memo(() => { }; return ( - - - + + + - - - - - - - - + + + + + + + + + + ); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx index 4aa79c8d0d3..0971d003a6f 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx @@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts'; import { MetricCriteriaField } from './Criteria/MetricCriteria'; @@ -109,74 +110,77 @@ export const CreateAlertDefinition = () => { }); return ( - - - -
- - 1. General Information - - ( - field.onChange(e.target.value)} - placeholder="Enter Name" - value={field.value ?? ''} - /> - )} - control={control} - name="label" - /> - ( - field.onChange(e.target.value)} - optional - placeholder="Enter Description" - value={field.value ?? ''} - /> - )} - control={control} - name="description" - /> - - - - - setMaxScrapeInterval(interval) - } - name="rule_criteria.rules" - serviceType={serviceTypeWatcher!} - /> - - - - -
-
+ + + + + +
+ + 1. General Information + + ( + field.onChange(e.target.value)} + placeholder="Enter Name" + value={field.value ?? ''} + /> + )} + control={control} + name="label" + /> + ( + field.onChange(e.target.value)} + optional + placeholder="Enter Description" + value={field.value ?? ''} + /> + )} + control={control} + name="description" + /> + + + + + setMaxScrapeInterval(interval) + } + name="rule_criteria.rules" + serviceType={serviceTypeWatcher!} + /> + + + + +
+
+
); }; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index a2d4574955e..85a3c07f199 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -2,6 +2,8 @@ import { Box, Paper } from '@linode/ui'; import { Grid } from '@mui/material'; import * as React from 'react'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; + import { GlobalFilters } from '../Overview/GlobalFilters'; import { CloudPulseAppliedFilterRenderer } from '../shared/CloudPulseAppliedFilterRenderer'; import { defaultTimeDuration } from '../Utils/CloudPulseDateTimePickerUtils'; @@ -75,30 +77,33 @@ export const CloudPulseDashboardLanding = () => { [] ); return ( - - - - - - {dashboard?.service_type && showAppliedFilters && ( - + + + + + + - )} - - + {dashboard?.service_type && showAppliedFilters && ( + + )} + + + + - - + ); }; From d4aea221370d5cd3860bb43c08e2473937ddd9cb Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Thu, 20 Feb 2025 09:33:25 -0500 Subject: [PATCH 003/219] refactor: [M3-9340] - Refactor CreateFirewallDrawer to use react-hook-form (#11677) * a lot of tweaks to that schema...... * convert createFirewallDrawer to react-hook-form * error update * changesets * address feedback, investigate more * address feedback @abailly-akamai @dwiley-akamai --- .../pr-11677-changed-1739995208483.md | 5 + packages/api-v4/src/firewalls/types.ts | 4 +- .../pr-11677-tech-stories-1739897652667.md | 5 + .../FirewallLanding/CreateFirewallDrawer.tsx | 353 ++++++++---------- .../Firewalls/FirewallLanding/constants.ts | 9 + .../pr-11677-changed-1739897615658.md | 5 + packages/validation/src/firewalls.schema.ts | 29 +- 7 files changed, 203 insertions(+), 207 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11677-changed-1739995208483.md create mode 100644 packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md create mode 100644 packages/validation/.changeset/pr-11677-changed-1739897615658.md diff --git a/packages/api-v4/.changeset/pr-11677-changed-1739995208483.md b/packages/api-v4/.changeset/pr-11677-changed-1739995208483.md new file mode 100644 index 00000000000..add94420352 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11677-changed-1739995208483.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Make `label` field in `CreateFirewallPayload` required ([#11677](https://github.com/linode/manager/pull/11677)) diff --git a/packages/api-v4/src/firewalls/types.ts b/packages/api-v4/src/firewalls/types.ts index 859b3ad9402..4f5eca2dbce 100644 --- a/packages/api-v4/src/firewalls/types.ts +++ b/packages/api-v4/src/firewalls/types.ts @@ -67,14 +67,14 @@ export interface FirewallTemplate { } export interface CreateFirewallPayload { - label?: string; + label: string; tags?: string[]; rules: UpdateFirewallRules; devices?: { linodes?: number[]; nodebalancers?: number[]; interfaces?: number[]; - }; + } | null; } export interface UpdateFirewallPayload { diff --git a/packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md b/packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md new file mode 100644 index 00000000000..a2a30278ccc --- /dev/null +++ b/packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Refactor CreateFirewallDrawer to use `react-hook-form` ([#11677](https://github.com/linode/manager/pull/11677)) diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx index 1465f91002b..2f25514fadd 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx @@ -1,4 +1,5 @@ /* eslint-disable jsx-a11y/anchor-is-valid */ +import { yupResolver } from '@hookform/resolvers/yup'; import { Box, FormControlLabel, @@ -9,9 +10,9 @@ import { Typography, } from '@linode/ui'; import { CreateFirewallSchema } from '@linode/validation/lib/firewalls.schema'; -import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { useLocation } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; @@ -28,17 +29,16 @@ import { sendLinodeCreateFormInputEvent, sendLinodeCreateFormStepEvent, } from 'src/utilities/analytics/formEventAnalytics'; -import { getErrorMap } from 'src/utilities/errorUtils'; -import { - handleFieldErrors, - handleGeneralErrors, -} from 'src/utilities/formikErrorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; import { + FIREWALL_HELPER_TEXT, + FIREWALL_LABEL_TEXT, LINODE_CREATE_FLOW_TEXT, NODEBALANCER_CREATE_FLOW_TEXT, + NODEBALANCER_HELPER_TEXT, + READ_ONLY_DEVICES_HIDDEN_MESSAGE, } from './constants'; import type { @@ -51,10 +51,6 @@ import type { import type { LinodeCreateQueryParams } from 'src/features/Linodes/types'; import type { LinodeCreateFormEventOptions } from 'src/utilities/analytics/types'; -export const READ_ONLY_DEVICES_HIDDEN_MESSAGE = - 'Only services you have permission to modify are shown.'; -const NODEBALANCER_HELPER_TEXT = `Only the firewall's inbound rules apply to NodeBalancers.`; - export interface CreateFirewallDrawerProps { createFlow: FirewallDeviceEntityType | undefined; onClose: () => void; @@ -80,7 +76,7 @@ export const CreateFirewallDrawer = React.memo( const { createFlow, onClose, onFirewallCreated, open } = props; const { _hasGrant, _isRestrictedUser } = useAccountManagement(); const { data: grants } = useGrants(); - const { mutateAsync } = useCreateFirewall(); + const { mutateAsync: createFirewall } = useCreateFirewall(); const { data } = useAllFirewallsQuery(open); const { enqueueSnackbar } = useSnackbar(); @@ -99,104 +95,47 @@ export const CreateFirewallDrawer = React.memo( }; const { - errors, - handleBlur, - handleChange, + control, + formState: { errors, isSubmitting }, handleSubmit, - isSubmitting, - resetForm, - setFieldValue, - status, - values, - } = useFormik({ - initialValues, - onSubmit( - values: CreateFirewallPayload, - { setErrors, setStatus, setSubmitting } - ) { - // Clear drawer error state - setStatus(undefined); - setErrors({}); - const payload = { ...values }; + reset, + setError, + } = useForm({ + defaultValues: initialValues, + mode: 'onBlur', + resolver: yupResolver(CreateFirewallSchema), + values: initialValues, + }); - if (payload.label === '') { - payload.label = undefined; - } + const handleClose = () => { + onClose(); + reset(); + }; - if ( - Array.isArray(payload.rules.inbound) && - payload.rules.inbound.length === 0 - ) { - payload.rules.inbound = undefined; - } + const createCustomFirewall = async (values: CreateFirewallPayload) => { + try { + const firewall = await createFirewall(values); + enqueueSnackbar(`Firewall ${values.label} successfully created`, { + variant: 'success', + }); - if ( - Array.isArray(payload.rules.outbound) && - payload.rules.outbound.length === 0 - ) { - payload.rules.outbound = undefined; + if (onFirewallCreated) { + onFirewallCreated(firewall); } - - mutateAsync(payload) - .then((response) => { - setSubmitting(false); - enqueueSnackbar(`Firewall ${payload.label} successfully created`, { - variant: 'success', - }); - - if (onFirewallCreated) { - onFirewallCreated(response); - } - onClose(); - - // Fire analytics form submit upon successful firewall creation from Linode Create flow. - if (isFromLinodeCreate) { - sendLinodeCreateFormStepEvent({ - ...firewallFormEventOptions, - label: 'Create Firewall', - }); - } - }) - .catch((err) => { - const mapErrorToStatus = () => - setStatus({ generalError: getErrorMap([], err).none }); - - setSubmitting(false); - handleFieldErrors(setErrors, err); - handleGeneralErrors( - mapErrorToStatus, - err, - 'Error creating Firewall.' - ); + handleClose(); + // Fire analytics form submit upon successful firewall creation from Linode Create flow. + if (isFromLinodeCreate) { + sendLinodeCreateFormStepEvent({ + ...firewallFormEventOptions, + label: 'Create Firewall', }); - }, - validateOnBlur: false, - validateOnChange: false, - validationSchema: CreateFirewallSchema, - }); - - const FirewallLabelText = `Assign services to the Firewall`; - const FirewallHelperText = `Assign one or more services to this firewall. You can add services later if you want to customize your rules first.`; - - React.useEffect(() => { - if (open) { - resetForm(); + } + } catch (errors) { + for (const error of errors) { + setError(error?.field ?? 'root', { message: error.reason }); + } } - }, [open, resetForm]); - - const handleInboundPolicyChange = React.useCallback( - (e: React.ChangeEvent, value: 'ACCEPT' | 'DROP') => { - setFieldValue('rules.inbound_policy', value); - }, - [setFieldValue] - ); - - const handleOutboundPolicyChange = React.useCallback( - (e: React.ChangeEvent, value: 'ACCEPT' | 'DROP') => { - setFieldValue('rules.outbound_policy', value); - }, - [setFieldValue] - ); + }; const userCannotAddFirewall = _isRestrictedUser && !_hasGrant('add_firewalls'); @@ -257,82 +196,97 @@ export const CreateFirewallDrawer = React.memo( ); - const generalError = - status?.generalError || - // @ts-expect-error this form intentionally breaks Formik's error type - errors['rules.inbound'] || - // @ts-expect-error this form intentionally breaks Formik's error type - errors['rules.outbound'] || - errors.rules; - return ( - -
+ + {userCannotAddFirewall ? ( ) : null} - {generalError && ( - + {errors.root?.message && ( + )} - ( + + )} + control={control} name="label" - onBlur={handleBlur} - onChange={handleChange} - required - value={values.label} /> - Default Inbound Policy - - } - label="Accept" - value="ACCEPT" - /> - } label="Drop" value="DROP" /> - - + ( + + } + label="Accept" + value="ACCEPT" + /> + } + label="Drop" + value="DROP" + /> + + )} + control={control} + name="rules.inbound_policy" + /> Default Outbound Policy - - } - label="Accept" - value="ACCEPT" - /> - } label="Drop" value="DROP" /> - - + ( + + } + label="Accept" + value="ACCEPT" + /> + } + label="Drop" + value="DROP" + /> + + )} + control={control} + name="rules.outbound_policy" + /> ({ @@ -340,11 +294,11 @@ export const CreateFirewallDrawer = React.memo( })} variant="h3" > - {FirewallLabelText} + {FIREWALL_LABEL_TEXT} - {FirewallHelperText} - {deviceSelectGuidance ? ` ${deviceSelectGuidance}` : null} + {FIREWALL_HELPER_TEXT} + {deviceSelectGuidance && ` ${deviceSelectGuidance}`} ({ @@ -356,41 +310,47 @@ export const CreateFirewallDrawer = React.memo( {learnMoreLink}. - { - setFieldValue( - 'devices.linodes', - linodes.map((linode) => linode.id) - ); - }} - // @ts-expect-error this form intentionally breaks Formik's error type - errorText={errors['devices.linodes']} - helperText={deviceSelectGuidance} - multiple - optionsFilter={linodeOptionsFilter} - value={values.devices?.linodes ?? null} + ( + { + field.onChange(linodes.map((linode) => linode.id)); + }} + errorText={fieldState.error?.message} + helperText={deviceSelectGuidance} + multiple + optionsFilter={linodeOptionsFilter} + value={field.value ?? null} + /> + )} + control={control} + name="devices.linodes" /> - { - setFieldValue( - 'devices.nodebalancers', - nodebalancers.map((nodebalancer) => nodebalancer.id) - ); - }} - // @ts-expect-error this form intentionally breaks Formik's error type - errorText={errors['devices.nodebalancers']} - helperText={deviceSelectGuidance} - multiple - optionsFilter={nodebalancerOptionsFilter} - value={values.devices?.nodebalancers ?? null} + ( + { + field.onChange( + nodebalancers.map((nodebalancer) => nodebalancer.id) + ); + }} + errorText={fieldState.error?.message} + helperText={deviceSelectGuidance} + multiple + optionsFilter={nodebalancerOptionsFilter} + value={field.value ?? null} + /> + )} + control={control} + name="devices.nodebalancers" /> diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts b/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts index 6e1825920b7..fd1bf8479b4 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts +++ b/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts @@ -1,2 +1,11 @@ export const LINODE_CREATE_FLOW_TEXT = 'Additional Linodes'; export const NODEBALANCER_CREATE_FLOW_TEXT = 'Additional NodeBalancers'; + +export const FIREWALL_LABEL_TEXT = 'Assign services to the Firewall'; +export const FIREWALL_HELPER_TEXT = + 'Assign one or more services to this firewall. You can add services later if you want to customize your rules first.'; + +export const READ_ONLY_DEVICES_HIDDEN_MESSAGE = + 'Only services you have permission to modify are shown.'; +export const NODEBALANCER_HELPER_TEXT = + "Only the firewall's inbound rules apply to NodeBalancers."; diff --git a/packages/validation/.changeset/pr-11677-changed-1739897615658.md b/packages/validation/.changeset/pr-11677-changed-1739897615658.md new file mode 100644 index 00000000000..e3978aca978 --- /dev/null +++ b/packages/validation/.changeset/pr-11677-changed-1739897615658.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Changed +--- + +Update CreateFirewallSchema to match API types ([#11677](https://github.com/linode/manager/pull/11677)) diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index d3c5a423586..dbda3097726 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -1,7 +1,7 @@ // We must use a default export for ipaddr.js so our packages node compatability // Refer to https://github.com/linode/manager/issues/8675 import ipaddr from 'ipaddr.js'; -import { array, mixed, number, object, string } from 'yup'; +import { array, number, object, string } from 'yup'; export const IP_ERROR_MESSAGE = 'Must be a valid IPv4 or IPv6 address or range.'; @@ -34,7 +34,7 @@ export const CreateFirewallDeviceSchema = object({ nodebalancers: array().of(number()), }); -export const ipAddress = string().test({ +export const ipAddress = string().defined().test({ name: 'validateIP', message: IP_ERROR_MESSAGE, test: validateIP, @@ -139,11 +139,12 @@ const validateFirewallPorts = string().test({ }, }); -const validFirewallRuleProtocol = ['ALL', 'TCP', 'UDP', 'ICMP', 'IPENCAP']; export const FirewallRuleTypeSchema = object().shape({ - action: mixed().oneOf(['ACCEPT', 'DROP']).required('Action is required'), - protocol: mixed() - .oneOf(validFirewallRuleProtocol) + action: string().oneOf(['ACCEPT', 'DROP']).required('Action is required'), + description: string().nullable(), + label: string().nullable(), + protocol: string() + .oneOf(['ALL', 'TCP', 'UDP', 'ICMP', 'IPENCAP']) .required('Protocol is required.'), ports: string().when('protocol', { is: (val: any) => val !== 'ICMP' && val !== 'IPENCAP', @@ -162,28 +163,38 @@ export const FirewallRuleTypeSchema = object().shape({ ipv6: array().of(ipAddress).nullable(), }) .strict(true) + .notRequired() .nullable(), }); export const FirewallRuleSchema = object().shape({ inbound: array(FirewallRuleTypeSchema).nullable(), outbound: array(FirewallRuleTypeSchema).nullable(), - inbound_policy: mixed() + inbound_policy: string() .oneOf(['ACCEPT', 'DROP']) .required('Inbound policy is required.'), - outbound_policy: mixed() + outbound_policy: string() .oneOf(['ACCEPT', 'DROP']) .required('Outbound policy is required.'), }); +const CreateFirewallDevicesSchema = object() + .shape({ + linodes: array().of(number().defined()), + nodebalancers: array().of(number().defined()), + interfaces: array().of(number().defined()), + }) + .notRequired(); + export const CreateFirewallSchema = object().shape({ label: string() .required('Label is required.') .min(3, 'Label must be between 3 and 32 characters.') .max(32, 'Label must be between 3 and 32 characters.'), // Label validation on the back end is more complicated, we only do basic checks here. - tags: array().of(string()), + tags: array().of(string().defined()), rules: FirewallRuleSchema, + devices: CreateFirewallDevicesSchema, }); export const UpdateFirewallSchema = object().shape({ From 873976d0959328e4a60bfe38ab94c54882b63e80 Mon Sep 17 00:00:00 2001 From: dmcintyr-akamai Date: Thu, 20 Feb 2025 09:43:56 -0500 Subject: [PATCH 004/219] test: [M3-8070] - Part 2 of applying eslint to cypress files (#11675) * M3-8070 part 2 of test changes * M3-8070 fix broken billing tests * M3-8070 fix linting error * M3-8070 cleanup * M3-8070 more cleanup --- packages/manager/.eslintrc.cjs | 22 ++- .../core/account/account-cancellation.spec.ts | 6 +- .../e2e/core/account/display-settings.spec.ts | 4 +- .../e2e/core/account/email-bounce.spec.ts | 4 +- .../e2e/core/account/oauth-apps.spec.ts | 50 +++--- .../account/personal-access-tokens.spec.ts | 36 ++-- .../core/account/security-questions.spec.ts | 8 +- .../e2e/core/account/service-transfer.spec.ts | 12 +- .../e2e/core/account/sms-verification.spec.ts | 17 +- .../cypress/e2e/core/account/ssh-keys.spec.ts | 27 ++- .../e2e/core/account/user-profile.spec.ts | 12 +- .../core/account/users-landing-page.spec.ts | 50 +++--- .../e2e/core/billing/billing-contact.spec.ts | 156 +++++++----------- .../billing/smoke-billing-activity.spec.ts | 20 ++- .../cloudpulse/alerts-listing-page.spec.ts | 14 +- .../dbaas-widgets-verification.spec.ts | 3 +- .../core/cloudpulse/edit-system-alert.spec.ts | 21 +-- .../linode-widget-verification.spec.ts | 8 +- .../core/databases/create-database.spec.ts | 17 +- .../core/databases/delete-database.spec.ts | 6 +- .../core/databases/resize-database.spec.ts | 3 +- .../core/databases/update-database.spec.ts | 13 +- 22 files changed, 248 insertions(+), 261 deletions(-) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 6939a648538..b5a0aac9099 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -22,7 +22,27 @@ module.exports = { 'build', 'storybook-static', '.storybook', - 'e2e/core', + 'e2e/core/domains', + 'e2e/core/firewalls', + 'e2e/core/general', + 'e2e/core/helpAndSupport', + 'e2e/core/images', + 'e2e/core/kubernetes', + 'e2e/core/linodes', + 'e2e/core/longview', + 'e2e/core/managed', + 'e2e/core/nodebalancers', + 'e2e/core/notificationsAndEvents', + 'e2e/core/objectStorage', + 'e2e/core/objectStorageGen2', + 'e2e/core/objectStorageMulticluster', + 'e2e/core/oneClickApps', + 'e2e/core/parentChild', + 'e2e/core/placementGroups', + 'e2e/core/stackscripts', + 'e2e/core/volumes', + 'e2e/core/vpc', + 'e2e/core/cloudpulse/timerange-verification.spec.ts', 'public', '!.eslintrc.js', ], diff --git a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts index 6211b624c99..dfe7a8ac1b8 100644 --- a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts @@ -130,7 +130,8 @@ describe('Account cancellation', () => { // Enter account cancellation comments, click "Close Account" again, // and this time mock a successful account cancellation response. mockCancelAccount(mockCancellationResponse).as('cancelAccount'); - cy.contains('Comments (optional)').click().type(cancellationComments); + cy.contains('Comments (optional)').click(); + cy.focused().type(cancellationComments); ui.button .findByTitle('Close Account') @@ -412,7 +413,8 @@ describe('Parent/Child account cancellation', () => { // Enter account cancellation comments, click "Close Account" again, // and this time mock a successful account cancellation response. mockCancelAccount(mockCancellationResponse).as('cancelAccount'); - cy.contains('Comments (optional)').click().type(cancellationComments); + cy.contains('Comments (optional)').click(); + cy.focused().type(cancellationComments); ui.button .findByTitle('Close Account') diff --git a/packages/manager/cypress/e2e/core/account/display-settings.spec.ts b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts index 49eb9c30630..b9691c3e8cd 100644 --- a/packages/manager/cypress/e2e/core/account/display-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts @@ -68,8 +68,8 @@ describe('Display Settings', () => { cy.findByLabelText('Username') .should('be.visible') .should('have.value', username) - .clear() - .type(newUsername); + .clear(); + cy.focused().type(newUsername); ui.button .findByTitle('Update Username') diff --git a/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts b/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts index 7e8443fe8ad..792471191c8 100644 --- a/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts +++ b/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts @@ -104,8 +104,8 @@ describe('Email bounce banners', () => { cy.get('[id="email"]') .should('be.visible') .should('have.value', userprofileEmail) - .clear() - .type(newEmail); + .clear(); + cy.focused().type(newEmail); cy.get('[data-qa-textfield-label="Email"]') .parent() diff --git a/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts b/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts index cef3136b233..5650f74dc9f 100644 --- a/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts @@ -30,11 +30,12 @@ const createOAuthApp = (oauthApp: OAuthClient) => { .findByTitle('Create OAuth App') .should('be.visible') .within(() => { - cy.findByLabelText('Label').click().clear().type(oauthApp.label); - cy.findByLabelText('Callback URL') - .click() - .clear() - .type(oauthApp.redirect_uri); + cy.findByLabelText('Label').click(); + cy.focused().clear(); + cy.focused().type(oauthApp.label); + cy.findByLabelText('Callback URL').click(); + cy.focused().clear(); + cy.focused().type(oauthApp.redirect_uri); ui.buttonGroup .findButtonByTitle('Cancel') .should('be.visible') @@ -56,11 +57,12 @@ const createOAuthApp = (oauthApp: OAuthClient) => { .findByTitle('Create OAuth App') .should('be.visible') .within(() => { - cy.findByLabelText('Label').click().clear().type(oauthApp.label); - cy.findByLabelText('Callback URL') - .click() - .clear() - .type(oauthApp.redirect_uri); + cy.findByLabelText('Label').click(); + cy.focused().clear(); + cy.focused().type(oauthApp.label); + cy.findByLabelText('Callback URL').click(); + cy.focused().clear(); + cy.focused().type(oauthApp.redirect_uri); }); ui.drawerCloseButton.find().click(); @@ -80,8 +82,10 @@ const createOAuthApp = (oauthApp: OAuthClient) => { .should('be.visible') .within(() => { // An error message appears when attempting to create an OAuth App without a label - cy.findByLabelText('Label').click().clear(); - cy.findByLabelText('Callback URL').click().clear(); + cy.findByLabelText('Label').click(); + cy.focused().clear(); + cy.findByLabelText('Callback URL').click(); + cy.focused().clear(); ui.button .findByTitle('Create') .should('be.visible') @@ -91,11 +95,12 @@ const createOAuthApp = (oauthApp: OAuthClient) => { cy.findByText('Redirect URI is required.'); // Fill out and submit OAuth App create form. - cy.findByLabelText('Label').click().clear().type(oauthApp.label); - cy.findByLabelText('Callback URL') - .click() - .clear() - .type(oauthApp.redirect_uri); + cy.findByLabelText('Label').click(); + cy.focused().clear(); + cy.focused().type(oauthApp.label); + cy.findByLabelText('Callback URL').click(); + cy.focused().clear(); + cy.focused().type(oauthApp.redirect_uri); // Check the 'public' checkbox if (oauthApp.public) { cy.get('[data-qa-checked]').should('be.visible').click(); @@ -320,11 +325,12 @@ describe('OAuth Apps', () => { .should('be.visible') .should('be.disabled'); - cy.findByLabelText('Label').click().clear().type(updatedApps[0].label); - cy.findByLabelText('Callback URL') - .click() - .clear() - .type(updatedApps[0].label); + cy.findByLabelText('Label').click(); + cy.focused().clear(); + cy.focused().type(updatedApps[0].label); + cy.findByLabelText('Callback URL').click(); + cy.focused().clear(); + cy.focused().type(updatedApps[0].label); ui.buttonGroup .findButtonByTitle('Save Changes') diff --git a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts index 7df2637bbc7..8e2a17e63c3 100644 --- a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts +++ b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts @@ -69,11 +69,8 @@ describe('Personal access tokens', () => { cy.findAllByText('Child Account Access').should('not.exist'); // Confirm submit button is disabled without specifying scopes. - ui.buttonGroup - .findButtonByTitle('Create Token') - .scrollIntoView() - .should('be.visible') - .should('be.disabled'); + ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); + ui.buttonGroup.findButtonByTitle('Create Token').should('be.disabled'); // Select just one scope. cy.get('[data-qa-row="Account"]').within(() => { @@ -81,9 +78,9 @@ describe('Personal access tokens', () => { }); // Confirm submit button is still disabled without specifying ALL scopes. + ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); ui.buttonGroup .findButtonByTitle('Create Token') - .scrollIntoView() .should('be.visible') .should('be.disabled'); @@ -96,29 +93,32 @@ describe('Personal access tokens', () => { ); // Confirm submit button is enabled; attempt to submit form without specifying a label. + ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); ui.buttonGroup .findButtonByTitle('Create Token') - .scrollIntoView() .should('be.visible') .should('be.enabled') .click(); // Confirm validation error. - cy.findByText('Label must be between 1 and 100 characters.') - .scrollIntoView() - .should('be.visible'); + cy.findByText( + 'Label must be between 1 and 100 characters.' + ).scrollIntoView(); + cy.findByText('Label must be between 1 and 100 characters.').should( + 'be.visible' + ); // Specify a label and re-submit. + cy.findByLabelText('Label').scrollIntoView(); cy.findByLabelText('Label') - .scrollIntoView() .should('be.visible') .should('be.enabled') - .click() - .type(token.label); + .click(); + cy.findByLabelText('Label').type(token.label); + ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); ui.buttonGroup .findButtonByTitle('Create Token') - .scrollIntoView() .should('be.visible') .should('be.enabled') .click(); @@ -219,11 +219,9 @@ describe('Personal access tokens', () => { .findByTitle('Edit Personal Access Token') .should('be.visible') .within(() => { - cy.findByLabelText('Label') - .should('be.visible') - .click() - .clear() - .type(newToken.label); + cy.findByLabelText('Label').as('qaLabel').should('be.visible').click(); + cy.get('@qaLabel').clear(); + cy.get('@qaLabel').type(newToken.label); ui.buttonGroup .findButtonByTitle('Save') diff --git a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts index bc280f36e5a..f0a11d32b51 100644 --- a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts @@ -95,16 +95,16 @@ const setSecurityQuestionAnswer = ( getSecurityQuestion(questionNumber).within(() => { cy.findByLabelText(`Question ${questionNumber}`) .should('be.visible') - .click() - .type(`${question}{enter}`); + .click(); + cy.focused().type(`${question}{enter}`); }); getSecurityQuestionAnswer(questionNumber).within(() => { cy.findByLabelText(`Answer ${questionNumber}`) .should('be.visible') .should('be.enabled') - .click() - .type(answer); + .click(); + cy.focused().type(answer); }); }; 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 460c2ff49ba..492bf919e97 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -79,10 +79,8 @@ const initiateLinodeTransfer = (linodeLabel: string) => { * @param token - Token to attempt to redeem. */ const redeemToken = (token: string) => { - cy.findByLabelText('Receive a Service Transfer') - .should('be.visible') - .click() - .type(token); + cy.findByLabelText('Receive a Service Transfer').should('be.visible').click(); + cy.focused().type(token); ui.button .findByTitle('Review Details') @@ -483,10 +481,8 @@ describe('Account service transfers', () => { ui.toast.assertMessage('Transfer accepted successfully.'); cy.get('[data-qa-panel="Received Service Transfers"]') .should('be.visible') - .click() - .within(() => { - cy.findByText(token).should('be.visible'); - }); + .click(); + cy.findByText(token).should('be.visible'); }); /* diff --git a/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts b/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts index 68227b396b9..8c7fe9700ac 100644 --- a/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts @@ -53,7 +53,8 @@ describe('SMS phone verification', () => { // @TODO Add steps to change country code before typing phone number. - cy.findByLabelText('Phone Number').click().type(optInPhoneNumber); + cy.findByLabelText('Phone Number').click(); + cy.focused().type(optInPhoneNumber); ui.button .findByTitle('Send Verification Code') @@ -65,10 +66,8 @@ describe('SMS phone verification', () => { cy.findByText(confirmationMessage, { exact: false }).should('be.visible'); // Mock invalid verification code for first attempt. - cy.findByLabelText('Verification Code') - .should('be.visible') - .click() - .type(`${randomNumber(10000, 50000)}`); + cy.findByLabelText('Verification Code').should('be.visible').click(); + cy.focused().type(`${randomNumber(10000, 50000)}`); ui.button .findByTitle('Verify Phone Number') @@ -87,11 +86,9 @@ describe('SMS phone verification', () => { // Mock successful verification code for second attempt. mockVerifyVerificationCode().as('verifyCode'); - cy.findByLabelText('Verification Code') - .should('be.visible') - .click() - .clear() - .type(`${randomNumber(10000, 50000)}`); + cy.findByLabelText('Verification Code').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(`${randomNumber(10000, 50000)}`); ui.button .findByTitle('Verify Phone Number') diff --git a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts index d0cf29ac00d..b88ade1096a 100644 --- a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts +++ b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts @@ -57,7 +57,8 @@ describe('SSH keys', () => { cy.findByText('Label is required.'); // When a user tries to create an SSH key without the SSH Public Key, a form validation error appears - cy.get('[id="label"]').clear().type(mockSSHKey.label); + cy.get('[id="label"]').clear(); + cy.focused().type(mockSSHKey.label); ui.button .findByTitle('Add Key') .should('be.visible') @@ -66,7 +67,8 @@ describe('SSH keys', () => { cy.findAllByText(sshFormatErrorMessage).should('be.visible'); // An alert displays when the format of SSH key is incorrect - cy.get('[id="ssh-public-key"]').clear().type('WrongFormatSshKey'); + cy.get('[id="ssh-public-key"]').clear(); + cy.focused().type('WrongFormatSshKey'); ui.button .findByTitle('Add Key') .should('be.visible') @@ -74,7 +76,8 @@ describe('SSH keys', () => { .click(); cy.findAllByText(sshFormatErrorMessage).should('be.visible'); - cy.get('[id="ssh-public-key"]').clear().type(mockSSHKey.ssh_key); + cy.get('[id="ssh-public-key"]').clear(); + cy.focused().type(mockSSHKey.ssh_key); ui.button .findByTitle('Cancel') .should('be.visible') @@ -101,8 +104,10 @@ describe('SSH keys', () => { cy.get('[id="ssh-public-key"]').should('be.empty'); // Create a new ssh key - cy.get('[id="label"]').clear().type(mockSSHKey.label); - cy.get('[id="ssh-public-key"]').clear().type(mockSSHKey.ssh_key); + cy.get('[id="label"]').clear(); + cy.focused().type(mockSSHKey.label); + cy.get('[id="ssh-public-key"]').clear(); + cy.focused().type(mockSSHKey.ssh_key); ui.button .findByTitle('Add Key') .should('be.visible') @@ -157,8 +162,10 @@ describe('SSH keys', () => { cy.get('[id="ssh-public-key"]').should('be.empty'); // Create a new ssh key - cy.get('[id="label"]').clear().type(sshKeyLabel); - cy.get('[id="ssh-public-key"]').clear().type(sshPublicKey); + cy.get('[id="label"]').clear(); + cy.focused().type(sshKeyLabel); + cy.get('[id="ssh-public-key"]').clear(); + cy.focused().type(sshPublicKey); ui.button .findByTitle('Add Key') .should('be.visible') @@ -228,7 +235,8 @@ describe('SSH keys', () => { cy.findByText('Label is required.'); // SSH label is not modified when the operation is cancelled - cy.get('[id="label"]').clear().type(newSSHKeyLabel); + cy.get('[id="label"]').clear(); + cy.focused().type(newSSHKeyLabel); ui.button .findByTitle('Cancel') .should('be.visible') @@ -250,7 +258,8 @@ describe('SSH keys', () => { .should('be.visible') .within(() => { // Update a new ssh key - cy.get('[id="label"]').clear().type(newSSHKeyLabel); + cy.get('[id="label"]').clear(); + cy.focused().type(newSSHKeyLabel); ui.button .findByTitle('Save') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/account/user-profile.spec.ts b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts index fc41156e199..98b9dc65be4 100644 --- a/packages/manager/cypress/e2e/core/account/user-profile.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts @@ -55,8 +55,8 @@ describe('User Profile', () => { cy.get('[id="email"]') .should('be.visible') .should('have.value', activeEmail) - .clear() - .type(newEmail); + .clear(); + cy.focused().type(newEmail); cy.get('[data-qa-textfield-label="Email"]') .parent() @@ -79,8 +79,8 @@ describe('User Profile', () => { cy.get('[id="username"]') .should('be.visible') .should('have.value', activeUsername) - .clear() - .type(newUsername); + .clear(); + cy.focused().type(newUsername); cy.get('[data-qa-textfield-label="Username"]') .parent() @@ -167,8 +167,8 @@ describe('User Profile', () => { cy.get('[id="username"]') .should('be.visible') .should('have.value', additionalUsername) - .clear() - .type(newUsername); + .clear(); + cy.focused().type(newUsername); cy.get('[data-qa-textfield-label="Username"]') .parent() diff --git a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts index eb9db1dcfaa..bac3a3d11fc 100644 --- a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts @@ -333,10 +333,10 @@ describe('Users landing page', () => { .findByTitle('Add a User') .should('be.visible') .within(() => { - cy.findByText('Username').click().type(`${newUser.username}{enter}`); - cy.findByText('Email') - .click() - .type(`${newUser.username}@test.com{enter}`); + cy.findByText('Username').click(); + cy.focused().type(`${newUser.username}{enter}`); + cy.findByText('Email').click(); + cy.focused().type(`${newUser.username}@test.com{enter}`); ui.buttonGroup .findButtonByTitle('Cancel') .should('be.visible') @@ -358,10 +358,10 @@ describe('Users landing page', () => { .findByTitle('Add a User') .should('be.visible') .within(() => { - cy.findByText('Username').click().type(`${newUser.username}{enter}`); - cy.findByText('Email') - .click() - .type(`${newUser.username}@test.com{enter}`); + cy.findByText('Username').click(); + cy.focused().type(`${newUser.username}{enter}`); + cy.findByText('Email').click(); + cy.focused().type(`${newUser.username}@test.com{enter}`); ui.buttonGroup .findButtonByTitle('Cancel') .should('be.visible') @@ -394,10 +394,12 @@ describe('Users landing page', () => { cy.findByText('Email address is required.').should('be.visible'); // type username - cy.findByText('Username').click().type(`${newUser.username}{enter}`); + cy.findByText('Username').click(); + cy.focused().type(`${newUser.username}{enter}`); // an inline error message will be displayed when the email address is invalid - cy.findByText('Email').click().type(`not_valid_email_address{enter}`); + cy.findByText('Email').click(); + cy.focused().type(`not_valid_email_address{enter}`); ui.buttonGroup .findButtonByTitle('Add User') .should('be.visible') @@ -406,10 +408,9 @@ describe('Users landing page', () => { cy.findByText('Must be a valid Email address.').should('be.visible'); // type email address - cy.get('[id="email"]') - .click() - .clear() - .type(`${newUser.username}@test.com{enter}`); + cy.get('[id="email"]').click(); + cy.focused().clear(); + cy.focused().type(`${newUser.username}@test.com{enter}`); ui.buttonGroup .findButtonByTitle('Add User') @@ -467,10 +468,10 @@ describe('Users landing page', () => { .findByTitle('Add a User') .should('be.visible') .within(() => { - cy.findByText('Username').click().type(`${newUser.username}{enter}`); - cy.findByText('Email') - .click() - .type(`${newUser.username}@test.com{enter}`); + cy.findByText('Username').click(); + cy.focused().type(`${newUser.username}{enter}`); + cy.findByText('Email').click(); + cy.focused().type(`${newUser.username}@test.com{enter}`); ui.buttonGroup .findButtonByTitle('Cancel') .should('be.visible') @@ -504,10 +505,12 @@ describe('Users landing page', () => { cy.findByText('Email address is required.').should('be.visible'); // type username - cy.findByText('Username').click().type(`${newUser.username}{enter}`); + cy.findByText('Username').click(); + cy.focused().type(`${newUser.username}{enter}`); // an inline error message will be displayed when the email address is invalid - cy.findByText('Email').click().type(`not_valid_email_address{enter}`); + cy.findByText('Email').click(); + cy.focused().type(`not_valid_email_address{enter}`); ui.buttonGroup .findButtonByTitle('Add User') .should('be.visible') @@ -516,10 +519,9 @@ describe('Users landing page', () => { cy.findByText('Must be a valid Email address.').should('be.visible'); // type email address - cy.get('[id="email"]') - .click() - .clear() - .type(`${newUser.username}@test.com{enter}`); + cy.get('[id="email"]').click(); + cy.focused().clear(); + cy.focused().type(`${newUser.username}@test.com{enter}`); // toggle to disable full access cy.get('[data-qa-create-restricted="true"]') diff --git a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts index b311d11937b..dd706091247 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts @@ -125,79 +125,49 @@ describe('Billing Contact', () => { .findByTitle('Edit Billing Contact Info') .should('be.visible') .within(() => { - cy.findByLabelText('First Name') - .should('be.visible') - .click() - .clear() - .type(newAccountData['first_name']); - cy.findByLabelText('Last Name') - .should('be.visible') - .click() - .clear() - .type(newAccountData['last_name']); - cy.findByLabelText('Company Name') - .should('be.visible') - .click() - .clear() - .type(newAccountData['company']); - cy.findByLabelText('Address') - .should('be.visible') - .click() - .clear() - .type(newAccountData['address_1']); - cy.findByLabelText('Address 2') - .should('be.visible') - .click() - .clear() - .type(newAccountData['address_2']); - cy.findByLabelText('Email (required)') - .should('be.visible') - .click() - .clear() - .type(newAccountData['email']); - cy.findByLabelText('City') - .should('be.visible') - .click() - .clear() - .type(newAccountData['city']); - cy.findByLabelText('Postal Code') - .should('be.visible') - .click() - .clear() - .type(newAccountData['zip']); - cy.findByLabelText('Phone') - .should('be.visible') - .click() - .clear() - .type(newAccountData['phone']); - ui.autocomplete - .findByLabel('State') - .should('be.visible') - .click() - .type(`${newAccountData['state']}`); + cy.findByLabelText('First Name').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['first_name']); + cy.findByLabelText('Last Name').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['last_name']); + cy.findByLabelText('Company Name').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['company']); + cy.findByLabelText('Address').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['address_1']); + cy.findByLabelText('Address 2').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['address_2']); + cy.findByLabelText('Email (required)').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['email']); + cy.findByLabelText('City').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['city']); + cy.findByLabelText('Postal Code').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['zip']); + cy.findByLabelText('Phone').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['phone']); + // need alias to be able to switch focus to modal popup + ui.autocomplete.findByLabel('State').should('be.visible').click(); + cy.focused().type(`${newAccountData['state']}`); ui.autocompletePopper .findByTitle(newAccountData['state']) .should('be.visible') .click(); - cy.findByLabelText('Tax ID') - .should('be.visible') - .click() - .clear() - .type(newAccountData['tax_id']); + cy.findByLabelText('Tax ID').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['tax_id']); cy.findByText(TAX_ID_HELPER_TEXT).should('not.exist'); - cy.get('[data-qa-save-contact-info="true"]') - .click() - .then(() => { - cy.wait('@updateAccount').then((xhr) => { - expect(xhr.response?.body).to.eql(newAccountData); - }); - }); + cy.get('[data-qa-save-contact-info="true"]').click(); + cy.wait('@updateAccount').then((xhr) => { + expect(xhr.response?.body).to.eql(newAccountData); + }); }); - - // check the page updates to reflect the edits - cy.get('[data-qa-contact-summary]').within(() => { - checkAccountContactDisplay(newAccountData); - }); }); it('Edit Contact Info: Tax ID Agreement', () => { @@ -222,48 +192,36 @@ describe('Billing Contact', () => { .findByTitle('Edit Billing Contact Info') .should('be.visible') .within(() => { - cy.findByLabelText('City') - .should('be.visible') - .click() - .clear() - .type(newAccountData['city']); - cy.findByLabelText('Postal Code') - .should('be.visible') - .click() - .clear() - .type(newAccountData['zip']); - ui.autocomplete - .findByLabel('Country') - .should('be.visible') - .click() - .type('Afghanistan'); + cy.findByLabelText('City').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['city']); + cy.findByLabelText('Postal Code').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['zip']); + ui.autocomplete.findByLabel('Country').should('be.visible').click(); + cy.focused().type('Afghanistan'); ui.autocompletePopper .findByTitle('Afghanistan') .should('be.visible') .click(); - cy.findByLabelText('Tax ID') - .should('be.visible') - .click() - .clear() - .type(newAccountData['tax_id']); + cy.findByLabelText('Tax ID').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newAccountData['tax_id']); cy.findByText(TAX_ID_HELPER_TEXT).should('be.visible'); - cy.findByText(TAX_ID_AGREEMENT_TEXT) - .scrollIntoView() - .should('be.visible'); + cy.findByText(TAX_ID_AGREEMENT_TEXT).scrollIntoView(); + cy.findByText(TAX_ID_AGREEMENT_TEXT).should('be.visible'); cy.findByText('Akamai Privacy Statement.').should('be.visible'); cy.get('[data-qa-save-contact-info="true"]').should('be.disabled'); cy.get('[data-testid="tax-id-checkbox"]').click(); cy.get('[data-qa-save-contact-info="true"]') .should('be.enabled') - .click() - .then(() => { - cy.wait('@updateAccount').then((xhr) => { - expect(xhr.response?.body).to.eql(newAccountData); - }); - cy.wait('@updateAccountAgreements').then((xhr) => { - expect(xhr.response?.body).to.eql(newAccountAgreement); - }); - }); + .click(); + cy.wait('@updateAccount').then((xhr) => { + expect(xhr.response?.body).to.eql(newAccountData); + }); + cy.wait('@updateAccountAgreements').then((xhr) => { + expect(xhr.response?.body).to.eql(newAccountAgreement); + }); }); // check the page updates to reflect the edits diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index 4a1f57eeb9b..c4536e153fa 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -167,8 +167,9 @@ describe('Billing Activity Feed', () => { cy.visitWithLogin('/account/billing'); cy.wait(['@getInvoices', '@getPayments']); cy.findByText('Billing & Payment History') - .scrollIntoView() - .should('be.visible'); + .as('qaBilling') + .scrollIntoView(); + cy.get('@qaBilling').should('be.visible'); // Confirm that payments and invoices from the past 6 months are displayed, // and that payments and invoices beyond 6 months are not displayed. @@ -196,7 +197,8 @@ describe('Billing Activity Feed', () => { mockGetInvoices(invoiceMocks).as('getInvoices'); mockGetPayments(paymentMocks).as('getPayments'); - cy.findByText('Transaction Dates').click().type(`All Time`); + cy.findByText('Transaction Dates').click(); + cy.focused().type(`All Time`); ui.autocompletePopper .findByTitle(`All Time`) .should('be.visible') @@ -214,7 +216,8 @@ describe('Billing Activity Feed', () => { }); // Change transaction type drop-down to "Payments" only. - cy.findByText('Transaction Types').click().type(`Payments`); + cy.findByText('Transaction Types').click(); + cy.focused().type(`Payments`); ui.autocompletePopper .findByTitle(`Payments`) .should('be.visible') @@ -268,7 +271,8 @@ describe('Billing Activity Feed', () => { cy.wait(['@getInvoices', '@getPayments', '@getPaymentMethods']); // Change invoice date selection from "6 Months" to "All Time". - cy.findByText('Transaction Dates').click().type('All Time'); + cy.findByText('Transaction Dates').click(); + cy.focused().type('All Time'); ui.autocompletePopper.findByTitle('All Time').should('be.visible').click(); cy.get('[data-qa-billing-activity-panel]') @@ -372,10 +376,8 @@ describe('Billing Activity Feed', () => { // This isn't strictly necessary, but is the most straightforward way to // get Cloud to re-fetch the user's profile data with the new timezone // applied. - cy.findByText('Timezone') - .should('be.visible') - .click() - .type(`${timezoneLabel}{enter}`); + cy.findByText('Timezone').should('be.visible').click(); + cy.focused().type(`${timezoneLabel}{enter}`); ui.button .findByTitle('Update Timezone') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index 2cdabf80e0c..3917af76e95 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -93,10 +93,8 @@ const verifyTableSorting = ( sortOrder: 'ascending' | 'descending', expectedValues: number[] ) => { - ui.heading - .findByText(header) - .click() - .should('have.attr', 'aria-sort', sortOrder); + ui.heading.findByText(header).click(); + ui.heading.findByText(header).should('have.attr', 'aria-sort', sortOrder); cy.get('[data-qa="alert-table"]').within(() => { cy.get('[data-qa-alert-cell]').should(($cells) => { @@ -263,8 +261,8 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { ui.button .findByAttribute('aria-label', 'Clear') .should('be.visible') - .scrollIntoView() - .click(); + .scrollIntoView(); + ui.button.findByAttribute('aria-label', 'Clear').click(); }); // Filter by alert status and validate the results @@ -296,8 +294,8 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { cy.findByPlaceholderText('Search for Alerts') .should('be.visible') .and('not.be.disabled') - .clear() - .type(alertName); + .clear(); + cy.findByPlaceholderText('Search for Alerts').type(alertName); cy.focused().click(); }; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index 63d47b8b555..ea61bc06d6e 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -246,7 +246,8 @@ describe('Integration Tests for DBaaS Dashboard ', () => { ).should('not.exist'); }); - ui.regionSelect.find().click().clear(); + ui.regionSelect.find().click(); + ui.regionSelect.find().clear(); ui.regionSelect .findItemByRegionId(mockRegion.id, [mockRegion]) .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts index a67d1fbdf98..86665684993 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts @@ -61,14 +61,14 @@ const pages = [1, 2]; describe('Integration Tests for Edit Alert', () => { /* - * - Confirms navigation from the Alert Definitions List page to the Edit Alert page. - * - Confirms alert creation is successful using mock API data. - * - Confirms that UI handles API interactions and displays correct data. - * - Confirms that UI redirects back to the Alert Definitions List page after saving updates. - * - Confirms that a toast notification appears upon successful alert update. - * - Confirms that UI redirects to the alert listing page after creating an alert. - * - Confirms that after submitting, the data matches with the API response. - */ + * - Confirms navigation from the Alert Definitions List page to the Edit Alert page. + * - Confirms alert creation is successful using mock API data. + * - Confirms that UI handles API interactions and displays correct data. + * - Confirms that UI redirects back to the Alert Definitions List page after saving updates. + * - Confirms that a toast notification appears upon successful alert update. + * - Confirms that UI redirects to the alert listing page after creating an alert. + * - Confirms that after submitting, the data matches with the API response. + */ beforeEach(() => { mockAppendFeatureFlags(flags); mockGetAccount(mockAccount); @@ -127,10 +127,7 @@ describe('Integration Tests for Edit Alert', () => { ); // Select all resources cy.get('[data-qa-notice="true"]').within(() => { - ui.button - .findByTitle('Select All') - .should('be.visible') - .click(); + ui.button.findByTitle('Select All').should('be.visible').click(); // Unselect button should be visible after clicking on Select All button ui.button diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index 647a8cc3c18..074053af664 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -221,14 +221,16 @@ describe('Integration Tests for Linode Dashboard ', () => { }); // Select a region from the dropdown. - ui.regionSelect.find().click().clear().type(`${region}{enter}`); + ui.regionSelect.find().click(); + ui.regionSelect.find().clear(); + ui.regionSelect.find().type(`${region}{enter}`); // Select a resource from the autocomplete input. ui.autocomplete .findByLabel('Resources') .should('be.visible') - .type(`${resource}{enter}`) - .click(); + .type(`${resource}{enter}`); + ui.autocomplete.findByLabel('Resources').click(); cy.findByText(resource).should('be.visible'); diff --git a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts index 251a08690bd..987af195cda 100644 --- a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts @@ -86,17 +86,16 @@ describe('create a database cluster, mocked data', () => { cy.findByText('Create').should('be.visible'); }); - cy.findByText('Cluster Label') - .should('be.visible') - .click() - .type(configuration.label); + cy.findByText('Cluster Label').should('be.visible').click(); + cy.focused().type(configuration.label); - cy.findByText('Database Engine') - .should('be.visible') - .click() - .type(`${configuration.engine} v${configuration.version}{enter}`); + cy.findByText('Database Engine').should('be.visible').click(); + cy.focused().type( + `${configuration.engine} v${configuration.version}{enter}` + ); - ui.regionSelect.find().click().type(`${databaseRegionLabel}{enter}`); + ui.regionSelect.find().click(); + cy.focused().type(`${databaseRegionLabel}{enter}`); // Click either the "Dedicated CPU" or "Shared CPU" tab, according // to the type of cluster being created. diff --git a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts index 453e50248c7..e055dd40302 100644 --- a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts @@ -61,7 +61,8 @@ describe('Delete database clusters', () => { .findByTitle(`Delete Database Cluster ${database.label}`) .should('be.visible') .within(() => { - cy.findByLabelText('Cluster Name').click().type(database.label); + cy.findByLabelText('Cluster Name').click(); + cy.focused().type(database.label); ui.buttonGroup .findButtonByTitle('Delete Cluster') @@ -123,7 +124,8 @@ describe('Delete database clusters', () => { .findByTitle(`Delete Database Cluster ${database.label}`) .should('be.visible') .within(() => { - cy.findByLabelText('Cluster Name').click().type(database.label); + cy.findByLabelText('Cluster Name').click(); + cy.focused().type(database.label); ui.buttonGroup .findButtonByTitle('Delete Cluster') diff --git a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts index 303aac90f97..0ebcbae629e 100644 --- a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts @@ -39,7 +39,8 @@ const resizeDatabase = (initialLabel: string) => { .findByTitle(`Resize Database Cluster ${initialLabel}?`) .should('be.visible') .within(() => { - cy.findByLabelText('Cluster Name').click().type(initialLabel); + cy.findByLabelText('Cluster Name').click(); + cy.focused().type(initialLabel); ui.buttonGroup .findButtonByTitle('Resize Cluster') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts index 44167766719..639d29e791e 100644 --- a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts @@ -46,11 +46,9 @@ const updateDatabaseLabel = (originalLabel: string, newLabel: string) => { cy.get('[data-qa-edit-field="true"]') .should('be.visible') .within(() => { - cy.get('[data-testid="textfield-input"]') - .should('be.visible') - .click() - .clear() - .type(newLabel); + cy.get('[data-testid="textfield-input"]').should('be.visible').click(); + cy.focused().clear(); + cy.focused().type(newLabel); cy.get('[data-qa-save-edit="true"]').should('be.visible').click(); }); @@ -108,9 +106,8 @@ const manageAccessControl = (allowedIps: string[], existingIps: number = 0) => { } cy.findByLabelText( `Allowed IP Addresses or Ranges ip-address-${index + existingIps}` - ) - .click() - .type(allowedIp); + ).click(); + cy.focused().type(allowedIp); }); ui.buttonGroup From 50f63604f1d999232730ce13242ec63bffff671e Mon Sep 17 00:00:00 2001 From: agorthi-akamai Date: Thu, 20 Feb 2025 21:05:28 +0530 Subject: [PATCH 005/219] test: [DI-23274] - E2E Automations Cypress - ACLP Create alerts (#11670) * DI-23274:E2E Automations Cypress - ACLP Create alerts * test[DI-23274]:Add test for ACLP Create Alerts. Improve test coverage and ensure validations are in place * Adding data-qa-ids to Metric and Dimension Filter fields * Update Metric.tsx * [DI-23274] -E2E Automations Cypress - ACLP Create alerts * [DI-23274] -E2E Automations Cypress - ACLP Create alerts * DI-23274:small improvements to the alert spec --------- Co-authored-by: santoshp210-akamai <159890961+santoshp210-akamai@users.noreply.github.com> --- .../pr-11670-tests-1739857190104.md | 5 + .../core/cloudpulse/create-user-alert.spec.ts | 424 ++++++++++++++++++ .../cypress/support/constants/alert.ts | 6 + .../src/factories/cloudpulse/alerts.ts | 96 +++- packages/manager/src/factories/dashboards.ts | 17 +- .../CreateAlert/Criteria/DimensionFilter.tsx | 1 + .../Criteria/DimensionFilterField.tsx | 3 + .../Alerts/CreateAlert/Criteria/Metric.tsx | 5 + .../Criteria/TriggerConditions.tsx | 1 + .../AddChannelListing.tsx | 1 + .../AddNotificationChannelDrawer.tsx | 2 +- 11 files changed, 557 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-11670-tests-1739857190104.md create mode 100644 packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts diff --git a/packages/manager/.changeset/pr-11670-tests-1739857190104.md b/packages/manager/.changeset/pr-11670-tests-1739857190104.md new file mode 100644 index 00000000000..d45f99e92af --- /dev/null +++ b/packages/manager/.changeset/pr-11670-tests-1739857190104.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add test for ACLP Create Alerts. Improve test coverage and ensure validations are in place ([#11670](https://github.com/linode/manager/pull/11670)) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts new file mode 100644 index 00000000000..75687630202 --- /dev/null +++ b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts @@ -0,0 +1,424 @@ +/** + * @fileoverview Cypress test suite for the "Create Alert" functionality. + */ + +import { Flags } from 'src/featureFlags'; +import { + mockCreateAlertDefinition, + mockGetAlertChannels, + mockGetAllAlertDefinitions, + mockGetCloudPulseMetricDefinitions, + mockGetCloudPulseServices, +} from 'support/intercepts/cloudpulse'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { ui } from 'support/ui'; +import { mockGetAccount } from 'support/intercepts/account'; +import { + accountFactory, + alertDefinitionFactory, + alertFactory, + cpuRulesFactory, + dashboardMetricFactory, + databaseFactory, + memoryRulesFactory, + notificationChannelFactory, + regionFactory, + triggerConditionFactory, +} from 'src/factories'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { widgetDetails } from 'support/constants/widgets'; +import { mockGetDatabases } from 'support/intercepts/databases'; +import { statusMap } from 'support/constants/alert'; +import { formatDate } from 'src/utilities/formatDate'; + +export interface MetricDetails { + ruleIndex: number; + dataField: string; + aggregationType: string; + operator: string; + threshold: string; +} + +const flags: Partial = { aclp: { enabled: true, beta: true } }; + +// Create mock data +const mockAccount = accountFactory.build(); +const mockRegion = regionFactory.build({ + capabilities: ['Managed Databases'], + id: 'us-ord', + label: 'Chicago, IL', +}); +const { metrics, serviceType } = widgetDetails.dbaas; +const databaseMock = databaseFactory.buildList(10, { + region: 'us-ord', + engine: 'mysql', + cluster_size: 3, +}); + +const notificationChannels = notificationChannelFactory.build({ + channel_type: 'email', + type: 'custom', + label: 'channel-1', + id: 1, +}); + +const customAlertDefinition = alertDefinitionFactory.build({ + channel_ids: [1], + label: 'Alert-1', + severity: 0, + description: 'My Custom Description', + entity_ids: ['2'], + tags: [''], + rule_criteria: { + rules: [cpuRulesFactory.build(), memoryRulesFactory.build()], + }, + trigger_conditions: triggerConditionFactory.build(), +}); + +const metricDefinitions = metrics.map(({ title, name, unit }) => + dashboardMetricFactory.build({ + label: title, + metric: name, + unit, + }) +); +const mockAlerts = alertFactory.build({ + service_type: 'dbaas', + alert_channels: [{ id: 1 }], + label: 'Alert-1', + severity: 0, + description: 'My Custom Description', + entity_ids: ['2'], + updated: new Date().toISOString(), + created_by: 'user1', + rule_criteria: { + rules: [cpuRulesFactory.build(), memoryRulesFactory.build()], + }, + trigger_conditions: triggerConditionFactory.build(), + tags: [''], +}); + +/** + * Fills metric details in the form. + * @param ruleIndex - The index of the rule to fill. + * @param dataField - The metric's data field (e.g., "CPU Utilization"). + * @param aggregationType - The aggregation type (e.g., "Average"). + * @param operator - The operator (e.g., ">=", "=="). + * @param threshold - The threshold value for the metric. + */ +const fillMetricDetailsForSpecificRule = ({ + ruleIndex, + dataField, + aggregationType, + operator, + threshold, +}: MetricDetails) => { + cy.get(`[data-testid="rule_criteria.rules.${ruleIndex}-id"]`).within(() => { + // Fill Data Field + ui.autocomplete + .findByLabel('Data Field') + .should('be.visible') + .type(dataField); + + ui.autocompletePopper.findByTitle(dataField).should('be.visible').click(); + + // Validate Aggregation Type + ui.autocomplete + .findByLabel('Aggregation Type') + .should('be.visible') + .type(aggregationType); + + ui.autocompletePopper + .findByTitle(aggregationType) + .should('be.visible') + .click(); + + // Fill Operator + ui.autocomplete.findByLabel('Operator').should('be.visible').type(operator); + + ui.autocompletePopper.findByTitle(operator).should('be.visible').click(); + + // Fill Threshold + cy.get('[data-qa-threshold]').should('be.visible').clear().type(threshold); + }); +}; + +describe('Create Alert', () => { + /* + * - Confirms that users can navigate from the Alert Listings page to the Create Alert page. + * - Confirms that users can enter alert details, select resources, and configure conditions. + * - Confirms that the UI allows adding notification channels and setting thresholds. + * - Confirms client-side validation when entering invalid metric values. + * - Confirms that API interactions work correctly and return the expected responses. + * - Confirms that the UI displays a success message after creating an alert. + */ + beforeEach(() => { + mockAppendFeatureFlags(flags); + mockGetAccount(mockAccount); + mockGetCloudPulseServices([serviceType]); + mockGetRegions([mockRegion]); + mockGetCloudPulseMetricDefinitions(serviceType, metricDefinitions); + mockGetDatabases(databaseMock); + mockGetAllAlertDefinitions([mockAlerts]).as('getAlertDefinitionsList'); + mockGetAlertChannels([notificationChannels]); + mockCreateAlertDefinition(serviceType, customAlertDefinition).as( + 'createAlertDefinition' + ); + }); + + it('should navigate to the Create Alert page from the Alert Listings page', () => { + // Navigate to the alert definitions list page with login + cy.visitWithLogin('/monitor/alerts/definitions'); + + // Wait for the alert definitions list API call to complete + cy.wait('@getAlertDefinitionsList'); + + ui.buttonGroup + .findButtonByTitle('Create Alert') + .should('be.visible') + .should('be.enabled') + .click(); + + // Verify the URL ends with the expected details page path + cy.url().should('endWith', 'monitor/alerts/definitions/create'); + }); + + it('should successfully create a new alert', () => { + cy.visitWithLogin('monitor/alerts/definitions/create'); + + // Enter Name and Description + cy.findByPlaceholderText('Enter Name') + .should('be.visible') + .type(customAlertDefinition.label); + + cy.findByPlaceholderText('Enter Description') + .should('be.visible') + .type(customAlertDefinition.description ?? ''); + + // Select Service + ui.autocomplete + .findByLabel('Service') + .should('be.visible') + .type('Databases'); + ui.autocompletePopper.findByTitle('Databases').should('be.visible').click(); + // Select Severity + ui.autocomplete.findByLabel('Severity').should('be.visible').type('Severe'); + ui.autocompletePopper.findByTitle('Severe').should('be.visible').click(); + + // Search for Resource + cy.findByPlaceholderText('Search for a Region or Resource') + .should('be.visible') + .type('database-2'); + + // Find the table and locate the resource cell containing 'database-2', then check the corresponding checkbox + cy.get('[data-qa-alert-table="true"]') // Find the table + .contains('[data-qa-alert-cell*="resource"]', 'database-2') // Find resource cell + .parents('tr') + .find('[type="checkbox"]') + .check(); + + // Assert resource selection notice + cy.get('[data-qa-notice="true"]') + .find('p') + .should('have.text', '1 of 10 resources are selected.'); + + cy.get('[data-qa-notice="true"]').should('be.visible').should('be.enabled'); + + // Fill metric details for the first rule + const cpuUsageMetricDetails = { + ruleIndex: 0, + dataField: 'CPU Utilization', + aggregationType: 'Average', + operator: '==', + threshold: '1000', + }; + + fillMetricDetailsForSpecificRule(cpuUsageMetricDetails); + + // Add metrics + cy.findByRole('button', { name: 'Add metric' }) + .should('be.visible') + .click(); + + ui.buttonGroup + .findButtonByTitle('Add dimension filter') + .should('be.visible') + .click(); + + ui.autocomplete + .findByLabel('Data Field') + .eq(1) + .should('be.visible') + .clear() + .type('State of CPU'); + + cy.findByText('State of CPU').should('be.visible').click(); + + ui.autocomplete + .findByLabel('Operator') + .eq(1) + .should('be.visible') + .clear() + .type('Equal'); + + cy.findByText('Equal').should('be.visible').click(); + + ui.autocomplete.findByLabel('Value').should('be.visible').type('User'); + + cy.findByText('User').should('be.visible').click(); + + // Fill metric details for the second rule + + const memoryUsageMetricDetails = { + ruleIndex: 1, + dataField: 'Memory Usage', + aggregationType: 'Average', + operator: '==', + threshold: '1000', + }; + + fillMetricDetailsForSpecificRule(memoryUsageMetricDetails); + // Set evaluation period + ui.autocomplete + .findByLabel('Evaluation Period') + .should('be.visible') + .type('5 min'); + ui.autocompletePopper.findByTitle('5 min').should('be.visible').click(); + + // Set polling interval + ui.autocomplete + .findByLabel('Polling Interval') + .should('be.visible') + .type('5 min'); + ui.autocompletePopper.findByTitle('5 min').should('be.visible').click(); + + // Set trigger occurrences + cy.get('[data-qa-trigger-occurrences]') + .should('be.visible') + .clear() + .type('5'); + + // Add notification channel + ui.buttonGroup.find().contains('Add notification channel').click(); + + ui.autocomplete.findByLabel('Type').should('be.visible').type('Email'); + ui.autocompletePopper.findByTitle('Email').should('be.visible').click(); + + ui.autocomplete + .findByLabel('Channel') + .should('be.visible') + .type('channel-1'); + + ui.autocompletePopper.findByTitle('channel-1').should('be.visible').click(); + + // Add channel + ui.drawer + .findByTitle('Add Notification Channel') + .should('be.visible') + .within(() => { + ui.buttonGroup + .findButtonByTitle('Add channel') + .should('be.visible') + .click(); + }); + // Click on submit button + ui.buttonGroup + .find() + .find('button') + .filter('[type="submit"]') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createAlertDefinition').then(({ request }) => { + const { + label, + description, + severity, + rule_criteria: { rules }, + trigger_conditions: { + trigger_occurrences, + evaluation_period_seconds, + polling_interval_seconds, + criteria_condition, + }, + } = customAlertDefinition; + + const { created_by, updated, status } = mockAlerts; + + // Validate top-level properties + expect(request.body.label).to.equal(label); + expect(request.body.description).to.equal(description); + expect(request.body.severity).to.equal(severity); + + // Validate rule criteria + expect(request.body.rule_criteria).to.have.property('rules'); + expect(request.body.rule_criteria.rules) + .to.be.an('array') + .with.length(rules.length); + + // Validate first rule + const firstRule = request.body.rule_criteria.rules[0]; + const firstCustomRule = rules[0]; + expect(firstRule.aggregate_function).to.equal( + firstCustomRule.aggregate_function + ); + expect(firstRule.metric).to.equal(firstCustomRule.metric); + expect(firstRule.operator).to.equal(firstCustomRule.operator); + expect(firstRule.threshold).to.equal(firstCustomRule.threshold); + expect(firstRule.dimension_filters[0]?.dimension_label ?? '').to.equal( + firstCustomRule.dimension_filters?.[0]?.dimension_label ?? '' + ); + expect(firstRule.dimension_filters[0]?.operator ?? '').to.equal( + firstCustomRule.dimension_filters?.[0]?.operator ?? '' + ); + expect(firstRule.dimension_filters[0]?.value ?? '').to.equal( + firstCustomRule.dimension_filters?.[0]?.value ?? '' + ); + + // Validate second rule + const secondRule = request.body.rule_criteria.rules[1]; + const secondCustomRule = rules[1]; + expect(secondRule.aggregate_function).to.equal( + secondCustomRule.aggregate_function + ); + expect(secondRule.metric).to.equal(secondCustomRule.metric); + expect(secondRule.operator).to.equal(secondCustomRule.operator); + expect(secondRule.threshold).to.equal(secondCustomRule.threshold); + + // Validate trigger conditions + const triggerConditions = request.body.trigger_conditions; + expect(triggerConditions.trigger_occurrences).to.equal( + trigger_occurrences + ); + expect(triggerConditions.evaluation_period_seconds).to.equal( + evaluation_period_seconds + ); + expect(triggerConditions.polling_interval_seconds).to.equal( + polling_interval_seconds + ); + expect(triggerConditions.criteria_condition).to.equal(criteria_condition); + + // Validate entity IDs and channels + expect(request.body.entity_ids).to.include('2'); + expect(request.body.channel_ids).to.include(1); + + // Verify URL redirection and toast notification + cy.url().should('endWith', 'monitor/alerts/definitions'); + ui.toast.assertMessage('Alert successfully created'); + + // Confirm that Alert is listed on landing page with expected configuration. + cy.findByText(label) + .closest('tr') + .within(() => { + cy.findByText(label).should('be.visible'); + cy.findByText(statusMap[status]).should('be.visible'); + cy.findByText('Databases').should('be.visible'); + cy.findByText(created_by).should('be.visible'); + cy.findByText( + formatDate(updated, { format: 'MMM dd, yyyy, h:mm a' }) + ); + }); + }); + }); +}); diff --git a/packages/manager/cypress/support/constants/alert.ts b/packages/manager/cypress/support/constants/alert.ts index 56cb2210a32..2783c154f5c 100644 --- a/packages/manager/cypress/support/constants/alert.ts +++ b/packages/manager/cypress/support/constants/alert.ts @@ -1,5 +1,6 @@ import type { AlertSeverityType, + AlertStatusType, DimensionFilterOperatorType, MetricAggregationType, MetricOperatorType, @@ -36,3 +37,8 @@ export const aggregationTypeMap: Record = { min: 'Minimum', sum: 'Sum', }; + +export const statusMap: Record = { + disabled: 'Disabled', + enabled: 'Enabled', +}; diff --git a/packages/manager/src/factories/cloudpulse/alerts.ts b/packages/manager/src/factories/cloudpulse/alerts.ts index 6f9a157efe7..f28b77b4e79 100644 --- a/packages/manager/src/factories/cloudpulse/alerts.ts +++ b/packages/manager/src/factories/cloudpulse/alerts.ts @@ -3,6 +3,9 @@ import Factory from 'src/factories/factoryProxy'; import type { AlertDefinitionDimensionFilter, AlertDefinitionMetricCriteria, + CreateAlertDefinitionPayload, + MetricCriteria, + TriggerCondition, } from '@linode/api-v4'; import type { Alert } from '@linode/api-v4'; @@ -27,6 +30,51 @@ export const alertRulesFactory = Factory.Sync.makeFactory( + { + criteria_condition: 'ALL', + evaluation_period_seconds: 300, + polling_interval_seconds: 300, + trigger_occurrences: 5, + } +); +export const cpuRulesFactory = Factory.Sync.makeFactory({ + aggregate_function: 'avg', + dimension_filters: [ + { + dimension_label: 'state', + operator: 'eq', + value: 'user', + }, + ], + metric: 'system_cpu_utilization_percent', + operator: 'eq', + threshold: 1000, +}); + +export const memoryRulesFactory = Factory.Sync.makeFactory({ + aggregate_function: 'avg', + dimension_filters: [], + metric: 'system_memory_usage_by_resource', + operator: 'eq', + threshold: 1000, +}); + +export const alertDefinitionFactory = Factory.Sync.makeFactory( + { + channel_ids: [1, 2, 3], + description: 'This is a default alert description.', + entity_ids: ['1', '2', '3', '4', '5'], + label: 'Default Alert Label', + rule_criteria: { + rules: [cpuRulesFactory.build(), memoryRulesFactory.build()], + }, + severity: 1, + tags: ['tag1', 'tag2'], + trigger_conditions: triggerConditionFactory.build(), + } +); + export const alertFactory = Factory.Sync.makeFactory({ alert_channels: [ { @@ -51,7 +99,52 @@ export const alertFactory = Factory.Sync.makeFactory({ id: Factory.each((i) => i), label: Factory.each((id) => `Alert-${id}`), rule_criteria: { - rules: [], + rules: [ + { + aggregate_function: 'avg', + dimension_filters: [ + { + dimension_label: 'Test', + label: 'Test', + operator: 'eq', + value: '40', + }, + ], + label: 'CPU Usage', + metric: 'CPU Usage', + operator: 'gt', + threshold: 60, + unit: 'Bytes', + }, + { + aggregate_function: 'avg', + dimension_filters: [ + { + dimension_label: 'OperatingSystem', + label: 'OperatingSystem', + operator: 'eq', + value: 'MacOS', + }, + { + dimension_label: 'OperatingSystem', + label: 'OperatingSystem', + operator: 'eq', + value: 'Windows', + }, + { + dimension_label: 'Test', + label: 'Test', + operator: 'neq', + value: '40', + }, + ], + label: 'CPU Usage', + metric: 'CPU Usage', + operator: 'gt', + threshold: 50, + unit: 'Percentage', + }, + ], }, service_type: 'linode', severity: 0, @@ -63,7 +156,6 @@ export const alertFactory = Factory.Sync.makeFactory({ polling_interval_seconds: 120, trigger_occurrences: 3, }, - type: 'system', updated: new Date().toISOString(), updated_by: 'system', diff --git a/packages/manager/src/factories/dashboards.ts b/packages/manager/src/factories/dashboards.ts index 3b1b4504f82..60b86374705 100644 --- a/packages/manager/src/factories/dashboards.ts +++ b/packages/manager/src/factories/dashboards.ts @@ -55,7 +55,22 @@ export const widgetFactory = Factory.Sync.makeFactory({ export const dashboardMetricFactory = Factory.Sync.makeFactory( { available_aggregate_functions: ['min', 'max', 'avg', 'sum'], - dimensions: [], + dimensions: [ + { + dimension_label: 'state', + label: 'State of CPU', + values: [ + 'user', + 'system', + 'idle', + 'interrupt', + 'nice', + 'softirq', + 'steal', + 'wait', + ], + }, + ], is_alertable: true, label: Factory.each((i) => `widget_label_${i}`), metric: Factory.each((i) => `widget_metric_${i}`), diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx index a9a05303102..dd80aa017b2 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx @@ -60,6 +60,7 @@ export const DimensionFilters = (props: DimensionFilterProps) => { } buttonType="secondary" compactX + data-qa-buttons="true" size="small" sx={{ justifyContent: 'start' }} > 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 9d44b5a760c..16eb4fafa10 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterField.tsx @@ -103,6 +103,7 @@ export const DimensionFilterField = (props: DimensionFilterFieldProps) => { (option) => option.value === field.value ) ?? null } + data-qa-dimension-filter={`${name}-data-field`} data-testid="data-field" disabled={dataFieldDisabled} errorText={fieldState.error?.message} @@ -134,6 +135,7 @@ export const DimensionFilterField = (props: DimensionFilterFieldProps) => { (option) => option.value === field.value ) ?? null } + data-qa-dimension-filter={`${name}-operator`} data-testid="operator" disabled={!dimensionFieldWatcher} errorText={fieldState.error?.message} @@ -168,6 +170,7 @@ export const DimensionFilterField = (props: DimensionFilterFieldProps) => { (option) => option.value === field.value ) ?? null } + data-qa-dimension-filter={`${name}-value`} data-testid="value" disabled={!dimensionFieldWatcher} errorText={fieldState.error?.message} 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 c5fabddffaa..40c5f460626 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/Metric.tsx @@ -159,6 +159,7 @@ export const Metric = (props: MetricCriteriaProps) => { (option) => option.value === field.value ) ?? null } + data-qa-metric-threshold={`${name}-data-field`} data-testid="data-field" disabled={!serviceWatcher} label="Data Field" @@ -191,6 +192,7 @@ export const Metric = (props: MetricCriteriaProps) => { aggOptions.find((option) => option.value === field.value) ?? null } + data-qa-metric-threshold={`${name}-aggregation-type`} data-testid="aggregation-type" disabled={aggOptions.length === 0} errorText={fieldState.error?.message} @@ -227,6 +229,7 @@ export const Metric = (props: MetricCriteriaProps) => { ) : null } + data-qa-metric-threshold={`${name}-operator`} data-testid="operator" disabled={!metricWatcher} errorText={fieldState.error?.message} @@ -251,6 +254,8 @@ export const Metric = (props: MetricCriteriaProps) => { onWheel={(event: React.SyntheticEvent) => event.target instanceof HTMLElement && event.target.blur() } + data-qa-metric-threshold={`${name}-threshold`} + data-qa-threshold="threshold" data-testid="threshold" errorText={fieldState.error?.message} label="Threshold" diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/TriggerConditions.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/TriggerConditions.tsx index 5c4c5ed381e..e51d7b8f51a 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/TriggerConditions.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/TriggerConditions.tsx @@ -152,6 +152,7 @@ export const TriggerConditions = (props: TriggerConditionProps) => { height: '30px', width: '30px', }} + data-qa-trigger-occurrences data-testid="trigger-occurences" errorText={fieldState.error?.message} label="" diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx index 9345c44a2fa..876a31a645a 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx @@ -151,6 +151,7 @@ export const AddChannelListing = React.memo((props: AddChannelListingProps) => { ); diff --git a/packages/manager/src/components/LandingHeader/LandingHeader.tsx b/packages/manager/src/components/LandingHeader/LandingHeader.tsx index 4560d7e00ca..34bbdf06bd8 100644 --- a/packages/manager/src/components/LandingHeader/LandingHeader.tsx +++ b/packages/manager/src/components/LandingHeader/LandingHeader.tsx @@ -1,6 +1,6 @@ import { Button } from '@linode/ui'; import { styled, useTheme } from '@mui/material/styles'; -import Grid from '@mui/material/Unstable_Grid2'; +import Grid from '@mui/material/Grid2'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; @@ -76,11 +76,13 @@ export const LandingHeader = ({ return ( {betaFeedbackLink && ( should highlight text consistently 1`] = `

Some markdown diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 0a3f717a970..ee1489870ff 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -7,7 +7,7 @@ import { Typography, } from '@linode/ui'; import Close from '@mui/icons-material/Close'; -import Grid from '@mui/material/Unstable_Grid2'; +import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; @@ -252,12 +252,14 @@ export const MultipleIPInput = React.memo((props: MultipeIPInputProps) => { container data-testid="domain-transfer-input" direction="row" - justifyContent="center" key={`domain-transfer-ip-${idx}`} - maxWidth={forVPCIPv4Ranges ? '415px' : undefined} spacing={2} + sx={{ + justifyContent: 'center', + maxWidth: forVPCIPv4Ranges ? '415px' : undefined, + }} > - + { {/** 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) && ( + + + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfacesTable.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfacesTable.tsx new file mode 100644 index 00000000000..80a214e5f4e --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfacesTable.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; + +import { LinodeInterfacesTableContent } from './LinodeInterfacesTableContent'; + +interface Props { + linodeId: number; +} + +export const LinodeInterfacesTable = ({ linodeId }: Props) => { + return ( + + + + ID + Type + MAC Address + Version + Firewall + Updated + Created + + + + + + +
+ ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfacesTableContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfacesTableContent.tsx new file mode 100644 index 00000000000..b78b16560e5 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfacesTableContent.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; +import { TableRowError } from 'src/components/TableRowError/TableRowError'; +import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; +import { useLinodeInterfacesQuery } from 'src/queries/linodes/interfaces'; + +import { LinodeInterfaceTableRow } from './LinodeInterfaceTableRow'; + +interface Props { + linodeId: number; +} + +export const LinodeInterfacesTableContent = ({ linodeId }: Props) => { + const { data, error, isPending } = useLinodeInterfacesQuery(linodeId); + + if (isPending) { + return ; + } + + if (error) { + return ; + } + + if (data.interfaces.length === 0) { + return ( + + ); + } + + return data.interfaces.map((networkInterface) => ( + + )); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities.test.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities.test.ts new file mode 100644 index 00000000000..ee1c103dd77 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities.test.ts @@ -0,0 +1,27 @@ +import { + linodeInterfaceFactoryPublic, + linodeInterfaceFactoryVPC, + linodeInterfaceFactoryVlan, +} from 'src/factories/linodeInterface'; + +import { getLinodeInterfaceType } from './utilities'; + +describe('getLinodeInterfaceType', () => { + it("returns 'public' if the given interface defines a public interface", () => { + const networkInterface = linodeInterfaceFactoryPublic.build(); + + expect(getLinodeInterfaceType(networkInterface)).toBe('public'); + }); + + it("returns 'vpc' if the given interface defines a VPC interface", () => { + const networkInterface = linodeInterfaceFactoryVPC.build(); + + expect(getLinodeInterfaceType(networkInterface)).toBe('vpc'); + }); + + it("returns 'vlan' if the given interface defines a VLAN interface", () => { + const networkInterface = linodeInterfaceFactoryVlan.build(); + + expect(getLinodeInterfaceType(networkInterface)).toBe('vlan'); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities.ts new file mode 100644 index 00000000000..68c5fa36ac0 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/utilities.ts @@ -0,0 +1,13 @@ +import type { LinodeInterface } from '@linode/api-v4'; + +export const getLinodeInterfaceType = (networkInterface: LinodeInterface) => { + if (networkInterface.vpc) { + return 'vpc'; + } + if (networkInterface.vlan) { + return 'vlan'; + } + return 'public'; +}; + +export type LinodeInterfaceType = ReturnType; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetwork.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetwork.tsx deleted file mode 100644 index 486ca340ba1..00000000000 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetwork.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Stack } from '@linode/ui'; -import * as React from 'react'; -import { useParams } from 'react-router-dom'; - -import { LinodeFirewalls } from './LinodeFirewalls/LinodeFirewalls'; -import { LinodeIPAddresses } from './LinodeIPAddresses'; -import { LinodeNetworkingSummaryPanel } from './NetworkingSummaryPanel/NetworkingSummaryPanel'; - -export const LinodeStorage = () => { - const { linodeId } = useParams<{ linodeId: string }>(); - const _linodeId = Number(linodeId); - - return ( - - - - - - ); -}; - -export default LinodeStorage; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx new file mode 100644 index 00000000000..6b75168df78 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworking.tsx @@ -0,0 +1,44 @@ +import { CircleProgress, Stack } from '@linode/ui'; +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { ErrorState } from 'src/components/ErrorState/ErrorState'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + +import { LinodeFirewalls } from './LinodeFirewalls/LinodeFirewalls'; +import { LinodeInterfaces } from './LinodeInterfaces/LinodeInterfaces'; +import { LinodeIPAddresses } from './LinodeIPAddresses'; +import { LinodeNetworkingSummaryPanel } from './NetworkingSummaryPanel/NetworkingSummaryPanel'; + +export const LinodeNetworking = () => { + const { isLinodeInterfaceEnabled } = useIsLinodeInterfacesEnabled(); + const { linodeId } = useParams<{ linodeId: string }>(); + const id = Number(linodeId); + + const { data: linode, error, isPending } = useLinodeQuery(id); + + if (isPending) { + return ; + } + + if (error) { + return ; + } + + const showInterfacesTable = + isLinodeInterfaceEnabled && linode.interface_generation === 'linode'; + + const showFirewallsTable = + !linode.interface_generation || + linode.interface_generation === 'legacy_config'; + + return ( + + + {showFirewallsTable && } + {showInterfacesTable && } + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx index 17f7783ffce..c6c1b58ee8e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx @@ -21,8 +21,10 @@ import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useTypeQuery } from 'src/queries/types'; const LinodeSummary = React.lazy(() => import('./LinodeSummary/LinodeSummary')); -const LinodeNetwork = React.lazy( - () => import('./LinodeNetworking/LinodeNetwork') +const LinodeNetworking = React.lazy(() => + import('./LinodeNetworking/LinodeNetworking').then((module) => ({ + default: module.LinodeNetworking, + })) ); const LinodeStorage = React.lazy(() => import('./LinodeStorage/LinodeStorage')); const LinodeConfigurations = React.lazy( @@ -142,7 +144,7 @@ const LinodesDetailNavigation = () => { - + {isBareMetalInstance ? null : ( <> diff --git a/packages/manager/src/queries/linodes/interfaces.ts b/packages/manager/src/queries/linodes/interfaces.ts new file mode 100644 index 00000000000..7e5b536a618 --- /dev/null +++ b/packages/manager/src/queries/linodes/interfaces.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query'; + +import { linodeQueries } from './linodes'; + +import type { + APIError, + Firewall, + LinodeInterfaces, + ResourcePage, +} from '@linode/api-v4'; + +export const useLinodeInterfacesQuery = (linodeId: number) => { + return useQuery( + linodeQueries.linode(linodeId)._ctx.interfaces._ctx.interfaces + ); +}; + +export const useLinodeInterfaceFirewallsQuery = ( + linodeId: number, + interfaceId: number +) => { + return useQuery, APIError[]>( + linodeQueries.linode(linodeId)._ctx.interfaces._ctx.interface(interfaceId) + ._ctx.firewalls + ); +}; diff --git a/packages/manager/src/queries/linodes/linodes.ts b/packages/manager/src/queries/linodes/linodes.ts index 7271c869a38..77a7e1b6335 100644 --- a/packages/manager/src/queries/linodes/linodes.ts +++ b/packages/manager/src/queries/linodes/linodes.ts @@ -7,6 +7,9 @@ import { getLinodeBackups, getLinodeFirewalls, getLinodeIPs, + getLinodeInterface, + getLinodeInterfaceFirewalls, + getLinodeInterfaces, getLinodeKernel, getLinodeLish, getLinodeStats, @@ -96,6 +99,26 @@ export const linodeQueries = createQueryKeys('linodes', { queryFn: () => getLinodeFirewalls(id), queryKey: null, }, + interfaces: { + contextQueries: { + interface: (interfaceId: number) => ({ + contextQueries: { + firewalls: { + queryFn: () => getLinodeInterfaceFirewalls(id, interfaceId), + queryKey: null, + }, + queryKey: null, + }, + queryFn: () => getLinodeInterface(id, interfaceId), + queryKey: [interfaceId], + }), + interfaces: { + queryFn: () => getLinodeInterfaces(id), + queryKey: null, + }, + }, + queryKey: null, + }, ips: { queryFn: () => getLinodeIPs(id), queryKey: null, diff --git a/packages/manager/src/utilities/linodes.test.ts b/packages/manager/src/utilities/linodes.test.ts index 3517cc0e5ec..ce98d051643 100644 --- a/packages/manager/src/utilities/linodes.test.ts +++ b/packages/manager/src/utilities/linodes.test.ts @@ -29,7 +29,7 @@ describe('useIsLinodeInterfacesEnabled', () => { wrapper: (ui) => wrapWithTheme(ui, options), }); - expect(result.current?.enabled).toBe(true); + expect(result.current?.isLinodeInterfaceEnabled).toBe(true); }); it('returns enabled: false if the feature is NOT enabled', () => { @@ -39,6 +39,6 @@ describe('useIsLinodeInterfacesEnabled', () => { wrapper: (ui) => wrapWithTheme(ui, options), }); - expect(result.current?.enabled).toBe(false); + expect(result.current?.isLinodeInterfaceEnabled).toBe(false); }); }); diff --git a/packages/manager/src/utilities/linodes.ts b/packages/manager/src/utilities/linodes.ts index 9e351e35e5a..a933f8d9003 100644 --- a/packages/manager/src/utilities/linodes.ts +++ b/packages/manager/src/utilities/linodes.ts @@ -45,5 +45,5 @@ export const useIsLinodeInterfacesEnabled = () => { // @TODO Linode Interfaces - check for customer tag when it exists - return flags.linodeInterfaces; + return { isLinodeInterfaceEnabled: flags.linodeInterfaces?.enabled ?? false }; }; diff --git a/packages/manager/src/utilities/testHelpers.tsx b/packages/manager/src/utilities/testHelpers.tsx index 8634e0459f8..783827fc1ed 100644 --- a/packages/manager/src/utilities/testHelpers.tsx +++ b/packages/manager/src/utilities/testHelpers.tsx @@ -32,7 +32,7 @@ import type { AnyRootRoute, AnyRouter } from '@tanstack/react-router'; import type { MatcherFunction, RenderResult } from '@testing-library/react'; import type { FormikConfig, FormikValues } from 'formik'; import type { FieldValues, UseFormProps } from 'react-hook-form'; -import type { MemoryRouterProps} from 'react-router-dom'; +import type { MemoryRouterProps } from 'react-router-dom'; import type { DeepPartial } from 'redux'; import type { FlagSet } from 'src/featureFlags'; import type { ApplicationState, ApplicationStore } from 'src/store'; diff --git a/packages/manager/vite.config.ts b/packages/manager/vite.config.ts index a0ed3f20a15..ddc7d2f3dfd 100644 --- a/packages/manager/vite.config.ts +++ b/packages/manager/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ }, }, server: { + allowedHosts: ['cloud.lindev.local'], port: 3000, }, test: { From b3f0d0186008b148dd87310a744f600f7023064b Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:12:25 -0500 Subject: [PATCH 053/219] fix: [M3-9413] - Authentication Provider Selection Card UI Issue (#11732) * use selectioncard, clean up, and fix tests * changeset --------- Co-authored-by: Banks Nussman --- .../pr-11732-fixed-1740533390539.md | 5 + .../SelectionCard/SelectionCard.tsx | 17 ++- .../TPAProviders.styles.ts | 88 -------------- .../TPAProviders.test.tsx | 86 ++++++------- .../AuthenticationSettings/TPAProviders.tsx | 114 ++++++------------ 5 files changed, 103 insertions(+), 207 deletions(-) create mode 100644 packages/manager/.changeset/pr-11732-fixed-1740533390539.md delete mode 100644 packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.styles.ts diff --git a/packages/manager/.changeset/pr-11732-fixed-1740533390539.md b/packages/manager/.changeset/pr-11732-fixed-1740533390539.md new file mode 100644 index 00000000000..c4f2289f3c7 --- /dev/null +++ b/packages/manager/.changeset/pr-11732-fixed-1740533390539.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Authentication Provider Selection Card UI regression ([#11732](https://github.com/linode/manager/pull/11732)) diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index 8e91d4a54ee..c85f9a0af8e 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -1,6 +1,6 @@ import { Tooltip } from '@linode/ui'; import { styled } from '@mui/material/styles'; -import Grid from '@mui/material/Grid2'; +import Grid, { Grid2Props } from '@mui/material/Grid2'; import * as React from 'react'; import { CardBase } from './CardBase'; @@ -18,11 +18,21 @@ export interface SelectionCardProps { * Additional CSS classes to apply to the root element. */ className?: string; + /** + * An optional custom data-testid + * @default selection-card + */ + 'data-testid'?: string; /** * If true, the card will be disabled and will be displayed in a disabled state. * @default false */ disabled?: boolean; + /** + * Optionally override the grid item's size + * @default { lg: 4, sm: 6, xl: 3, xs: 12 } + */ + gridSize?: Grid2Props['size']; /** * The heading of the card. * @example Linode 1GB @@ -106,6 +116,7 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => { checked, className, disabled, + gridSize, heading, headingDecoration, id, @@ -156,12 +167,12 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => { className={className} data-qa-selection-card data-qa-selection-card-checked={checked} - data-testid="selection-card" + data-testid={props['data-testid'] ?? 'selection-card'} disabled={disabled} id={id} onClick={handleClick} onKeyPress={handleKeyPress} - size={{ lg: 4, sm: 6, xl: 3, xs: 12 }} + size={gridSize ?? { lg: 4, sm: 6, xl: 3, xs: 12 }} sx={sxGrid} tabIndex={0} > diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.styles.ts b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.styles.ts deleted file mode 100644 index 68c52926131..00000000000 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.styles.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Button, Notice, Paper, Typography } from '@linode/ui'; -import { styled } from '@mui/material/styles'; -import Grid from '@mui/material/Grid2'; - -export const StyledRootContainer = styled(Paper, { - label: 'StyledRootContainer', -})(({ theme }) => ({ - marginBottom: theme.spacing(3), - padding: theme.spacing(3), - paddingTop: 17, -})); - -export const StyledCopy = styled(Typography, { - label: 'StyledCopy', -})(({ theme }) => ({ - lineHeight: '1.25rem', - marginBottom: theme.spacing(2), - marginTop: theme.spacing(), - maxWidth: 960, -})); - -export const StyledProvidersListGrid = styled(Grid, { - label: 'StyledProvidersListGrid', -})(({ theme }) => ({ - '& .MuiGrid-item': { - [theme.breakpoints.down('sm')]: { - flexBasis: '100%', - maxWidth: '100%', - }, - [theme.breakpoints.down(1100)]: { - flexBasis: '50%', - maxWidth: '50%', - }, - }, - marginBottom: 0, - [theme.breakpoints.down('sm')]: { - marginTop: theme.spacing(), - }, - width: 'calc(100% + 24px)', -})); - -export const StyledButton = styled(Button, { - label: 'StyledButton', - shouldForwardProp: (propName) => propName !== 'isButtonEnabled', -})<{ isButtonEnabled: boolean }>(({ isButtonEnabled, theme }) => ({ - '& > span': { - color: theme.color.headline, - display: 'inline-block', - width: '100%', - }, - '&:hover': { - backgroundColor: theme.color.grey6, - }, - backgroundColor: - theme.name === 'light' - ? theme.tokens.color.Ultramarine[5] - : theme.tokens.color.Ultramarine[80], - borderRadius: 1, - marginTop: theme.spacing(), - minHeight: 70, - paddingLeft: `calc(${theme.spacing(3)} - 4px)`, - paddingRight: `calc(${theme.spacing(3)} - 4px)`, - [theme.breakpoints.down('md')]: { - marginLeft: 0, - }, - [theme.breakpoints.down('sm')]: { - marginLeft: 0, - marginTop: 0, - }, - width: 'calc(100% - 8px)', - ...(isButtonEnabled && { - border: `1px solid ${theme.palette.primary.main} !important`, - }), -})); - -export const StyledEnabledText = styled('span', { - label: 'StyledEnabledText', -})(({ theme }) => ({ - font: theme.font.normal, - marginLeft: 4, -})); - -export const StyledNotice = styled(Notice, { - label: 'StyledNotice', -})(({ theme }) => ({ - font: theme.font.bold, - fontSize: '0.875rem', -})); diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.test.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.test.tsx index d954fb07540..f4e969b4b57 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.test.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.test.tsx @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -6,64 +6,68 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { TPAProviders } from './TPAProviders'; -vi.mock('src/hooks/useFlags', () => ({ - __esModule: true, - useFlags: vi.fn().mockReturnValue({ - tpaProviders: [ - { - displayName: 'Google', - href: 'https://google.com', - icon: 'GoogleIcon', - name: 'google', - }, - { - displayName: 'GitHub', - href: 'https://github.com', - icon: 'GitHubIcon', - name: 'github', - }, - ], - }), -})); +import type { Provider } from 'src/featureFlags'; + +const providers: Provider[] = [ + { + displayName: 'Google', + href: 'https://google.com', + icon: 'GoogleIcon', + name: 'google', + }, + { + displayName: 'GitHub', + href: 'https://github.com', + icon: 'GitHubIcon', + name: 'github', + }, +]; + +const flags = { tpaProviders: providers }; describe('TPAProviders component', () => { it('Should render login method with Linode button', () => { - const authType = 'password'; - renderWithTheme(); + renderWithTheme(, { flags }); + const linodeButton = screen.getByTestId('Button-Cloud Manager'); - const LinodeButtonEnabled = screen.getByTestId('Enabled-Cloud Manager'); expect(linodeButton).toBeInTheDocument(); - expect(linodeButton).toHaveAttribute('aria-disabled', 'true'); - expect(LinodeButtonEnabled).toBeInTheDocument(); + expect(linodeButton).toBeDisabled(); + + const enabledText = within(linodeButton).getByText('Enabled'); + expect(enabledText).toBeVisible(); }); it('Should render login method with Google button', () => { - const authType = 'google'; - renderWithTheme(); + renderWithTheme(, { flags }); + const googleButton = screen.getByTestId('Button-Google'); - const googleButtonEnabled = screen.getByTestId('Enabled-Google'); - const noticeElement = screen.getByTestId('Notice-Google'); expect(googleButton).toBeInTheDocument(); - expect(googleButton).toHaveAttribute('aria-disabled', 'true'); - expect(googleButtonEnabled).toBeInTheDocument(); + expect(googleButton).toBeDisabled(); + + const enabledText = within(googleButton).getByText('Enabled'); + expect(enabledText).toBeVisible(); + + const noticeElement = screen.getByTestId('Notice-Google'); expect(noticeElement).toBeInTheDocument(); }); it('Should render login method with GitHub button', () => { - const authType = 'github'; - renderWithTheme(); + renderWithTheme(, { flags }); + const githubButton = screen.getByTestId('Button-GitHub'); - const githubButtonEnabled = screen.getByTestId('Enabled-GitHub'); - const noticeElement = screen.getByTestId('Notice-GitHub'); expect(githubButton).toBeInTheDocument(); - expect(githubButton).toHaveAttribute('aria-disabled', 'true'); - expect(githubButtonEnabled).toBeInTheDocument(); + expect(githubButton).toBeDisabled(); + + const enabledText = within(githubButton).getByText('Enabled'); + expect(enabledText).toBeInTheDocument(); + + const noticeElement = screen.getByTestId('Notice-GitHub'); expect(noticeElement).toBeInTheDocument(); }); test('Should open the dialog when the button is clicked', async () => { - const authType = 'password'; - const { getByTestId } = renderWithTheme( - - ); + const { + getByTestId, + } = renderWithTheme(, { flags }); + const button = getByTestId('Button-Google'); await userEvent.click(button); const dialog = getByTestId('drawer'); diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx index 5a7348837d3..8413c886654 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TPAProviders.tsx @@ -1,5 +1,4 @@ -import { Box, Divider, Typography } from '@linode/ui'; -import { useTheme } from '@mui/material/styles'; +import { Box, Divider, Notice, Paper, Stack, Typography } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; @@ -8,17 +7,10 @@ import AkamaiWaveOnlyIcon from 'src/assets/icons/providers/akamai-logo-rgb-waveO import GitHubIcon from 'src/assets/icons/providers/github-logo.svg'; import GoogleIcon from 'src/assets/icons/providers/google-logo.svg'; import { Link } from 'src/components/Link'; +import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { useFlags } from 'src/hooks/useFlags'; import { TPADialog } from './TPADialog'; -import { - StyledButton, - StyledCopy, - StyledEnabledText, - StyledNotice, - StyledProvidersListGrid, - StyledRootContainer, -} from './TPAProviders.styles'; import type { TPAProvider } from '@linode/api-v4/lib/profile'; @@ -40,7 +32,6 @@ const linode = { }; export const TPAProviders = (props: Props) => { - const theme = useTheme(); const flags = useFlags(); // Get list of providers from LaunchDarkly @@ -61,10 +52,10 @@ export const TPAProviders = (props: Props) => { }; return ( - <> - + + Login Method - + You can use your Cloud Manager credentials or another provider such as Google or GitHub to log in to your Cloud Manager account. More information is available in{' '} @@ -72,99 +63,72 @@ export const TPAProviders = (props: Props) => { How to Enable Third Party Authentication on Your User Account . We strongly recommend setting up Two-Factor Authentication (2FA). - - + + {providersIncludingLinode.map((thisProvider) => { const ProviderIcon = icons[thisProvider.name]; const isProviderEnabled = props.authType === thisProvider.name; return ( - - { - handleProviderChange(thisProvider.name); - }} - data-testid={`Button-${thisProvider.displayName}`} - disabled={isProviderEnabled} - isButtonEnabled={isProviderEnabled} - > - - - - {thisProvider.displayName} - {isProviderEnabled ? ( - - (Enabled) - - ) : null} - - {isProviderEnabled ? : null} - - - + renderVariant={ + isProviderEnabled ? () => : undefined + } + tooltip={ + isProviderEnabled + ? `${thisProvider.displayName} is your current authentication provider.` + : undefined + } + data-testid={`Button-${thisProvider.displayName}`} + disabled={isProviderEnabled} + heading={thisProvider.displayName} + key={thisProvider.displayName} + onClick={() => handleProviderChange(thisProvider.name)} + renderIcon={() => } + subheadings={isProviderEnabled ? ['Enabled'] : []} + tooltipPlacement="bottom" + /> ); })} - - {isThirdPartyAuthEnabled ? ( -
+ + {isThirdPartyAuthEnabled && ( + {currentProvider.displayName} Authentication - + Your login credentials are currently managed via{' '} {currentProvider.displayName}. - - + + If you need to reset your password or set up Two-Factor Authentication (2FA), please visit the{' '} {`${currentProvider.displayName}` + ` website`} . - - + + To disable {currentProvider.displayName} authentication and log in using your Cloud Manager credentials, click the Cloud Manager button above. We’ll send you an e-mail with instructions on how to reset your password. - -
- ) : null} -
+ + + )} + setDialogOpen(false)} open={isDialogOpen} /> - + ); }; From 95302257aef517eb0cbbd5ac07f209d591f52aca Mon Sep 17 00:00:00 2001 From: dmcintyr-akamai Date: Thu, 27 Feb 2025 09:35:20 -0500 Subject: [PATCH 054/219] test [M3-8070]: eslint formatting of files i previously removed errors from (#11722) * M3-8070 eslint formatting of files i previously removed errors from * Added changeset: Linting cypress files * revert change to src file * revert change to src file --- .../pr-11722-tests-1740498549527.md | 5 ++ .../core/account/account-cancellation.spec.ts | 28 +++---- .../account/account-login-history.spec.ts | 31 ++++---- .../core/account/account-maintenance.spec.ts | 19 ++--- .../cypress/e2e/core/account/betas.spec.ts | 2 +- .../e2e/core/account/display-settings.spec.ts | 18 +++-- .../e2e/core/account/email-bounce.spec.ts | 14 ++-- .../e2e/core/account/oauth-apps.spec.ts | 5 +- .../account/personal-access-tokens.spec.ts | 10 ++- .../core/account/security-questions.spec.ts | 5 +- .../e2e/core/account/service-transfer.spec.ts | 27 +++---- .../core/account/smoke-enroll-beta.spec.ts | 12 +-- .../e2e/core/account/sms-verification.spec.ts | 15 ++-- .../cypress/e2e/core/account/ssh-keys.spec.ts | 11 +-- .../account/third-party-access-tokens.spec.ts | 10 ++- .../e2e/core/account/two-factor-auth.spec.ts | 26 ++++--- .../e2e/core/account/user-permissions.spec.ts | 19 ++--- .../e2e/core/account/user-profile.spec.ts | 9 ++- .../account/user-verification-banner.spec.ts | 16 ++-- .../core/account/users-landing-page.spec.ts | 40 +++++----- .../e2e/core/billing/billing-contact.spec.ts | 32 ++++---- .../e2e/core/billing/billing-invoices.spec.ts | 25 +++--- .../credit-card-expired-banner.spec.ts | 3 +- .../billing/default-payment-method.spec.ts | 17 +++-- .../e2e/core/billing/google-pay.spec.ts | 31 ++++---- .../billing/restricted-user-billing.spec.ts | 21 ++--- .../billing/smoke-billing-activity.spec.ts | 32 ++++---- .../cloudpulse/alert-show-details.spec.ts | 63 +++++++-------- .../cloudpulse/alerts-listing-page.spec.ts | 52 +++++++------ .../cloudpulse-dashboard-errors.spec.ts | 64 ++++++++-------- .../cloudpulse/cloudpulse-navigation.spec.ts | 5 +- .../dbaas-widgets-verification.spec.ts | 76 ++++++++++--------- .../core/cloudpulse/edit-system-alert.spec.ts | 44 ++++++----- .../linode-widget-verification.spec.ts | 60 ++++++++------- .../core/databases/create-database.spec.ts | 28 +++---- .../core/databases/delete-database.spec.ts | 34 +++++---- .../core/databases/resize-database.spec.ts | 52 +++++++------ .../core/databases/update-database.spec.ts | 50 ++++++------ 38 files changed, 534 insertions(+), 477 deletions(-) create mode 100644 packages/manager/.changeset/pr-11722-tests-1740498549527.md diff --git a/packages/manager/.changeset/pr-11722-tests-1740498549527.md b/packages/manager/.changeset/pr-11722-tests-1740498549527.md new file mode 100644 index 00000000000..50c1e23f7c9 --- /dev/null +++ b/packages/manager/.changeset/pr-11722-tests-1740498549527.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Linting cypress files ([#11722](https://github.com/linode/manager/pull/11722)) diff --git a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts index dfe7a8ac1b8..728ea5695d7 100644 --- a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts @@ -2,23 +2,17 @@ * @file Integration tests for Cloud Manager account cancellation flows. */ -import { profileFactory } from 'src/factories/profile'; -import { accountFactory } from 'src/factories/account'; -import { - mockGetAccount, - mockCancelAccount, - mockCancelAccountError, -} from 'support/intercepts/account'; import { cancellationDataLossWarning, - cancellationPaymentErrorMessage, cancellationDialogTitle, + cancellationPaymentErrorMessage, } from 'support/constants/account'; import { - CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, - PARENT_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, - PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, -} from 'src/features/Account/constants'; + mockCancelAccount, + mockCancelAccountError, + mockGetAccount, +} from 'support/intercepts/account'; +import { mockWebpageUrl } from 'support/intercepts/general'; import { mockGetProfile } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { @@ -26,8 +20,16 @@ import { randomPhrase, randomString, } from 'support/util/random'; + +import { accountFactory } from 'src/factories/account'; +import { profileFactory } from 'src/factories/profile'; +import { + CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, + PARENT_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, + PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, +} from 'src/features/Account/constants'; + import type { CancelAccount } from '@linode/api-v4'; -import { mockWebpageUrl } from 'support/intercepts/general'; describe('Account cancellation', () => { /* diff --git a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts index 60bd4626fee..b53a18ef7d0 100644 --- a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts @@ -2,16 +2,17 @@ * @file Integration tests for Cloud Manager account login history flows. */ -import { profileFactory } from 'src/factories'; -import { accountLoginFactory } from 'src/factories/accountLogin'; -import { formatDate } from 'src/utilities/formatDate'; -import { mockGetAccountLogins } from 'support/intercepts/account'; -import { mockGetProfile } from 'support/intercepts/profile'; import { - loginHelperText, loginEmptyStateMessageText, + loginHelperText, } from 'support/constants/account'; +import { mockGetAccountLogins } from 'support/intercepts/account'; +import { mockGetProfile } from 'support/intercepts/profile'; + +import { profileFactory } from 'src/factories'; +import { accountLoginFactory } from 'src/factories/accountLogin'; import { PARENT_USER } from 'src/features/Account/constants'; +import { formatDate } from 'src/utilities/formatDate'; describe('Account login history', () => { /* @@ -22,18 +23,18 @@ describe('Account login history', () => { */ it('users can view the login history table', () => { const mockProfile = profileFactory.build({ - username: 'mock-user', restricted: false, user_type: 'default', + username: 'mock-user', }); const mockFailedLogin = accountLoginFactory.build({ + restricted: true, status: 'failed', username: 'mock-restricted-user', - restricted: true, }); const mockSuccessfulLogin = accountLoginFactory.build({ - status: 'successful', restricted: false, + status: 'successful', }); mockGetProfile(mockProfile).as('getProfile'); @@ -95,9 +96,9 @@ describe('Account login history', () => { */ it('restricted child users cannot view login history', () => { const mockProfile = profileFactory.build({ - username: 'mock-child-user', restricted: true, user_type: 'child', + username: 'mock-child-user', }); mockGetProfile(mockProfile).as('getProfile'); @@ -121,9 +122,9 @@ describe('Account login history', () => { */ it('unrestricted child users can view login history', () => { const mockProfile = profileFactory.build({ - username: 'mock-child-user', restricted: false, user_type: 'child', + username: 'mock-child-user', }); mockGetProfile(mockProfile).as('getProfile'); @@ -144,9 +145,9 @@ describe('Account login history', () => { */ it('restricted users cannot view login history', () => { const mockProfile = profileFactory.build({ - username: 'mock-restricted-user', restricted: true, user_type: 'default', + username: 'mock-restricted-user', }); mockGetProfile(mockProfile).as('getProfile'); @@ -172,19 +173,19 @@ describe('Account login history', () => { */ it('shows each login in the Login History landing page as expected', () => { const mockProfile = profileFactory.build({ - username: 'mock-user', restricted: false, user_type: 'default', + username: 'mock-user', }); const mockFailedLogin = accountLoginFactory.build({ + restricted: false, status: 'failed', username: 'mock-user-failed', - restricted: false, }); const mockSuccessfulLogin = accountLoginFactory.build({ + restricted: false, status: 'successful', username: 'mock-user-successful', - restricted: false, }); mockGetProfile(mockProfile).as('getProfile'); diff --git a/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts b/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts index ba00f881332..9029596acb4 100644 --- a/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts @@ -1,7 +1,8 @@ import { mockGetMaintenance } from 'support/intercepts/account'; -import { accountMaintenanceFactory } from 'src/factories'; import { parseCsv } from 'support/util/csv'; +import { accountMaintenanceFactory } from 'src/factories'; + describe('Maintenance', () => { /* * - Confirm user can navigate to account maintenance page via user menu. @@ -152,9 +153,9 @@ describe('Maintenance', () => { (maintenance) => ({ entity_label: maintenance.entity.label, entity_type: maintenance.entity.type, - type: maintenance.type, - status: maintenance.status, reason: maintenance.reason, + status: maintenance.status, + type: maintenance.type, }) ); @@ -172,9 +173,9 @@ describe('Maintenance', () => { (entry: any) => ({ entity_label: entry['Entity Label'], entity_type: entry['Entity Type'], - type: entry['Type'], - status: entry['Status'], reason: entry['Reason'], + status: entry['Status'], + type: entry['Type'], }) ); @@ -202,9 +203,9 @@ describe('Maintenance', () => { (maintenance) => ({ entity_label: maintenance.entity.label, entity_type: maintenance.entity.type, - type: maintenance.type, - status: maintenance.status, reason: maintenance.reason, + status: maintenance.status, + type: maintenance.type, }) ); @@ -224,9 +225,9 @@ describe('Maintenance', () => { (entry: any) => ({ entity_label: entry['Entity Label'], entity_type: entry['Entity Type'], - type: entry['Type'], - status: entry['Status'], reason: entry['Reason'], + status: entry['Status'], + type: entry['Type'], }) ); diff --git a/packages/manager/cypress/e2e/core/account/betas.spec.ts b/packages/manager/cypress/e2e/core/account/betas.spec.ts index f47545cf792..292449a4cab 100644 --- a/packages/manager/cypress/e2e/core/account/betas.spec.ts +++ b/packages/manager/cypress/e2e/core/account/betas.spec.ts @@ -3,8 +3,8 @@ */ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { ui } from 'support/ui'; import { mockGetUserPreferences } from 'support/intercepts/profile'; +import { ui } from 'support/ui'; // TODO Delete feature flag mocks when feature flag is removed. beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/account/display-settings.spec.ts b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts index b9691c3e8cd..64f950dd149 100644 --- a/packages/manager/cypress/e2e/core/account/display-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts @@ -1,13 +1,15 @@ -import { Profile } from '@linode/api-v4'; import { profileFactory } from '@src/factories'; -import { mockGetProfile } from 'support/intercepts/profile'; import { getProfile } from 'support/api/account'; -import { interceptGetProfile } from 'support/intercepts/profile'; import { mockUpdateUsername } from 'support/intercepts/account'; +import { interceptGetProfile } from 'support/intercepts/profile'; +import { mockGetProfile } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { randomString } from 'support/util/random'; + import { RESTRICTED_FIELD_TOOLTIP } from 'src/features/Account/constants'; +import type { Profile } from '@linode/api-v4'; + const verifyUsernameAndEmail = ( mockRestrictedProxyProfile: Profile, tooltip: string, @@ -89,9 +91,9 @@ describe('Display Settings', () => { it('disables username/email fields for restricted proxy user', () => { const mockRestrictedProxyProfile = profileFactory.build({ - username: 'restricted-proxy-user', - user_type: 'proxy', restricted: true, + user_type: 'proxy', + username: 'restricted-proxy-user', }); verifyUsernameAndEmail( @@ -103,8 +105,8 @@ describe('Display Settings', () => { it('disables username/email fields for unrestricted proxy user', () => { const mockUnrestrictedProxyProfile = profileFactory.build({ - username: 'unrestricted-proxy-user', user_type: 'proxy', + username: 'unrestricted-proxy-user', }); verifyUsernameAndEmail( @@ -116,9 +118,9 @@ describe('Display Settings', () => { it('disables username/email fields for regular restricted user', () => { const mockRegularRestrictedProfile = profileFactory.build({ - username: 'regular-restricted-user', - user_type: 'default', restricted: true, + user_type: 'default', + username: 'regular-restricted-user', }); verifyUsernameAndEmail( diff --git a/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts b/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts index 792471191c8..668d8726631 100644 --- a/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts +++ b/packages/manager/cypress/e2e/core/account/email-bounce.spec.ts @@ -2,27 +2,29 @@ * @file Integration tests for Cloud Manager email bounce banners. */ -import { Notification } from '@linode/api-v4'; import { notificationFactory } from '@src/factories/notification'; -import { mockGetNotifications } from 'support/intercepts/events'; import { getProfile } from 'support/api/account'; -import { ui } from 'support/ui'; +import { mockGetAccount, mockUpdateAccount } from 'support/intercepts/account'; +import { mockGetNotifications } from 'support/intercepts/events'; import { mockUpdateProfile } from 'support/intercepts/profile'; +import { ui } from 'support/ui'; import { randomString } from 'support/util/random'; + import { accountFactory } from 'src/factories/account'; -import { mockGetAccount, mockUpdateAccount } from 'support/intercepts/account'; + +import type { Notification } from '@linode/api-v4'; const notifications_billing_email_bounce: Notification[] = [ notificationFactory.build({ - type: 'billing_email_bounce', severity: 'major', + type: 'billing_email_bounce', }), ]; const notifications_user_email_bounce: Notification[] = [ notificationFactory.build({ - type: 'user_email_bounce', severity: 'major', + type: 'user_email_bounce', }), ]; diff --git a/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts b/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts index 633eeb19ec4..f18d980d3ea 100644 --- a/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/account/oauth-apps.spec.ts @@ -7,7 +7,8 @@ import { mockUpdateOAuthApps, } from 'support/intercepts/profile'; import { ui } from 'support/ui'; -import { randomLabel, randomHex } from 'support/util/random'; +import { randomHex, randomLabel } from 'support/util/random'; + import type { OAuthClient } from '@linode/api-v4'; /** @@ -149,8 +150,8 @@ describe('OAuth Apps', () => { }), oauthClientFactory.build({ label: randomLabel(), - secret: randomHex(64), public: true, + secret: randomHex(64), }), ]; diff --git a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts index 8e2a17e63c3..81d1f357097 100644 --- a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts +++ b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts @@ -2,9 +2,6 @@ * @file Integration tests for personal access token CRUD operations. */ -import { Token } from '@linode/api-v4'; -import { appTokenFactory } from 'src/factories/oauth'; -import { profileFactory } from 'src/factories/profile'; import { mockCreatePersonalAccessToken, mockGetAppTokens, @@ -13,10 +10,15 @@ import { mockRevokePersonalAccessToken, mockUpdatePersonalAccessToken, } from 'support/intercepts/profile'; -import { randomLabel, randomString } from 'support/util/random'; import { ui } from 'support/ui'; +import { randomLabel, randomString } from 'support/util/random'; + +import { appTokenFactory } from 'src/factories/oauth'; +import { profileFactory } from 'src/factories/profile'; import { PROXY_USER_RESTRICTED_TOOLTIP_TEXT } from 'src/features/Account/constants'; +import type { Token } from '@linode/api-v4'; + describe('Personal access tokens', () => { /* * - Uses mocked API requests to confirm UI flow to create a personal access token diff --git a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts index f0a11d32b51..3d161c672b4 100644 --- a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts @@ -2,8 +2,6 @@ * @file Integration tests for account security questions. */ -import { profileFactory } from 'src/factories/profile'; -import { securityQuestionsFactory } from 'src/factories/profile'; import { mockGetProfile, mockGetSecurityQuestions, @@ -11,6 +9,9 @@ import { } from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { securityQuestionsFactory } from 'src/factories/profile'; +import { profileFactory } from 'src/factories/profile'; + /** * Finds the "Security Questions" section on the profile auth page. * 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 e9a35369a38..5f6e29425bf 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -3,30 +3,31 @@ */ import { getProfile } from '@linode/api-v4/lib/profile'; -import { EntityTransfer, Linode, Profile } from '@linode/api-v4'; -import { entityTransferFactory } from 'src/factories/entityTransfers'; -import { linodeFactory } from 'src/factories'; -import { createLinodeRequestFactory } from 'src/factories/linodes'; -import { formatDate } from 'src/utilities/formatDate'; import { authenticate } from 'support/api/authentication'; +import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { interceptInitiateEntityTransfer, mockAcceptEntityTransfer, mockGetEntityTransfers, - mockReceiveEntityTransfer, mockInitiateEntityTransferError, + mockReceiveEntityTransfer, mockGetEntityTransfersError, } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; +import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomUuid } from 'support/util/random'; -import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { chooseRegion } from 'support/util/regions'; -import { cleanUp } from 'support/util/cleanup'; + +import { linodeFactory } from 'src/factories'; +import { entityTransferFactory } from 'src/factories/entityTransfers'; +import { createLinodeRequestFactory } from 'src/factories/linodes'; +import { formatDate } from 'src/utilities/formatDate'; import type { EntityTransferStatus } from '@linode/api-v4'; +import type { EntityTransfer, Linode, Profile } from '@linode/api-v4'; // Service transfer empty state message. const serviceTransferEmptyState = 'No data to display.'; @@ -190,25 +191,25 @@ describe('Account service transfers', () => { */ it('lists service transfers on landing page', () => { const pendingTransfers = entityTransferFactory.buildList(3, { - status: 'pending', entities: { linodes: [0, 1, 2, 3, 4], }, + status: 'pending', }); const receivedTransfers = entityTransferFactory.buildList(4, { - is_sender: false, entities: { linodes: [0], }, + is_sender: false, }); const sentTransfers = serviceTransferStatuses.map((status) => { return entityTransferFactory.build({ - is_sender: true, entities: { linodes: [0, 1], }, + is_sender: true, status, }); }); @@ -434,12 +435,12 @@ describe('Account service transfers', () => { it('can receive a service transfer', () => { const token = randomUuid(); const transfer = entityTransferFactory.build({ - token, entities: { linodes: [0], }, - status: 'pending', is_sender: false, + status: 'pending', + token, }); mockGetEntityTransfers([], [], []).as('getTransfers'); diff --git a/packages/manager/cypress/e2e/core/account/smoke-enroll-beta.spec.ts b/packages/manager/cypress/e2e/core/account/smoke-enroll-beta.spec.ts index b341be1c977..17bfa535985 100644 --- a/packages/manager/cypress/e2e/core/account/smoke-enroll-beta.spec.ts +++ b/packages/manager/cypress/e2e/core/account/smoke-enroll-beta.spec.ts @@ -1,13 +1,13 @@ import { accountBetaFactory, betaFactory } from '@src/factories'; -import { authenticate } from 'support/api/authentication'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { DateTime } from 'luxon'; +import { authenticate } from 'support/api/authentication'; import { mockGetAccountBetas, - mockGetBetas, mockGetBeta, + mockGetBetas, mockPostBeta, } from 'support/intercepts/betas'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; authenticate(); @@ -21,17 +21,17 @@ describe('Enroll in a Beta Program', () => { selfServeBetas: true, }).as('getFeatureFlags'); const currentlyEnrolledBeta = accountBetaFactory.build({ - id: '12345', enrolled: DateTime.now().minus({ days: 10 }).toISO(), + id: '12345', started: DateTime.now().minus({ days: 11 }).toISO(), }); const availableBetas = betaFactory.buildList(2); const historicalBetas = accountBetaFactory.buildList(2, { + ended: DateTime.now().minus({ days: 5 }).toISO(), + enrolled: DateTime.now().minus({ days: 10 }).toISO(), id: '1234', label: 'Historical Beta', started: DateTime.now().minus({ days: 15 }).toISO(), - enrolled: DateTime.now().minus({ days: 10 }).toISO(), - ended: DateTime.now().minus({ days: 5 }).toISO(), }); const accountBetas = [currentlyEnrolledBeta, ...historicalBetas]; diff --git a/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts b/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts index 8c7fe9700ac..6a5ed1aed43 100644 --- a/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts @@ -2,13 +2,6 @@ * @file Integration tests for SMS phone verification. */ -import { getFormattedNumber } from 'src/features/Profile/AuthenticationSettings/PhoneVerification/helpers'; -import { profileFactory } from 'src/factories/profile'; -import { - randomLabel, - randomNumber, - randomPhoneNumber, -} from 'support/util/random'; import { mockGetProfile, mockSendVerificationCode, @@ -16,6 +9,14 @@ import { mockVerifyVerificationCode, } from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { + randomLabel, + randomNumber, + randomPhoneNumber, +} from 'support/util/random'; + +import { profileFactory } from 'src/factories/profile'; +import { getFormattedNumber } from 'src/features/Profile/AuthenticationSettings/PhoneVerification/helpers'; describe('SMS phone verification', () => { /* diff --git a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts index b88ade1096a..5afeac8936c 100644 --- a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts +++ b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts @@ -1,4 +1,4 @@ -import { sshKeyFactory } from 'src/factories'; +import { sshFormatErrorMessage } from 'support/constants/account'; import { mockCreateSSHKey, mockCreateSSHKeyError, @@ -8,7 +8,8 @@ import { } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; -import { sshFormatErrorMessage } from 'support/constants/account'; + +import { sshKeyFactory } from 'src/factories'; describe('SSH keys', () => { /* @@ -21,11 +22,11 @@ describe('SSH keys', () => { */ it('adds an SSH key via Profile page as expected', () => { const randomKey = randomString(400, { - uppercase: true, lowercase: true, numbers: true, spaces: false, symbols: false, + uppercase: true, }); const mockSSHKey = sshKeyFactory.build({ label: randomLabel(), @@ -132,11 +133,11 @@ describe('SSH keys', () => { const errorMessage = 'failed to add an SSH key.'; const sshKeyLabel = randomLabel(); const randomKey = randomString(400, { - uppercase: true, lowercase: true, numbers: true, spaces: false, symbols: false, + uppercase: true, }); const sshPublicKey = `ssh-rsa e2etestkey${randomKey} e2etest@linode`; @@ -187,11 +188,11 @@ describe('SSH keys', () => { */ it('updates an SSH key via Profile page as expected', () => { const randomKey = randomString(400, { - uppercase: true, lowercase: true, numbers: true, spaces: false, symbols: false, + uppercase: true, }); const mockSSHKey = sshKeyFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts b/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts index 0f020d73aa0..3f6a4761a6f 100644 --- a/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts +++ b/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts @@ -1,16 +1,18 @@ +import { getProfile } from '@linode/api-v4/lib/profile'; import { accessFactory, appTokenFactory } from '@src/factories'; import 'cypress-file-upload'; +import { authenticate } from 'support/api/authentication'; import { - mockGetPersonalAccessTokens, mockGetAppTokens, + mockGetPersonalAccessTokens, mockRevokeAppToken, } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; -import { Token, Profile } from '@linode/api-v4'; -import { getProfile } from '@linode/api-v4/lib/profile'; + import { formatDate } from 'src/utilities/formatDate'; -import { authenticate } from 'support/api/authentication'; + +import type { Profile, Token } from '@linode/api-v4'; authenticate(); describe('Third party access tokens', () => { diff --git a/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts b/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts index f9fd7080754..9f1e945cb5a 100644 --- a/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts +++ b/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts @@ -2,11 +2,6 @@ * @file Integration tests for account two-factor authentication functionality. */ -import { SecurityQuestionsData } from '@linode/api-v4'; -import { - profileFactory, - securityQuestionsFactory, -} from 'src/factories/profile'; import { mockConfirmTwoFactorAuth, mockDisableTwoFactorAuth, @@ -16,12 +11,19 @@ import { } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { - randomNumber, + randomHex, randomLabel, + randomNumber, randomString, - randomHex, } from 'support/util/random'; +import { + profileFactory, + securityQuestionsFactory, +} from 'src/factories/profile'; + +import type { SecurityQuestionsData } from '@linode/api-v4'; + /** * Returns a Cypress chainable for the "Two-Factor Authentication". * @@ -39,10 +41,10 @@ const getTwoFactorSection = (): Cypress.Chainable => { const randomScratchCode = (): string => { const randomScratchCodeOptions = { lowercase: true, - uppercase: false, - symbols: false, numbers: false, spaces: false, + symbols: false, + uppercase: false, }; const segmentA = randomString(5, randomScratchCodeOptions); @@ -61,10 +63,10 @@ const randomScratchCode = (): string => { const randomToken = (): string => { const randomTokenOptions = { lowercase: false, - uppercase: false, numbers: true, - symbols: false, spaces: false, + symbols: false, + uppercase: false, }; return randomString(6, randomTokenOptions); @@ -103,10 +105,10 @@ const getAnsweredSecurityQuestions = (): SecurityQuestionsData => { // User profile with 2FA disabled. const userProfile = profileFactory.build({ + two_factor_auth: false, uid: randomNumber(1000, 9999), username: randomLabel(), verified_phone_number: undefined, - two_factor_auth: false, }); // User profile with 2FA enabled. diff --git a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts index f2d902a8d08..ccaa315dfa9 100644 --- a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts @@ -1,4 +1,3 @@ -import type { Grant, Grants } from '@linode/api-v4'; import { profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; @@ -16,6 +15,8 @@ import { ui } from 'support/ui'; import { shuffleArray } from 'support/util/arrays'; import { randomLabel } from 'support/util/random'; +import type { Grant, Grants } from '@linode/api-v4'; + // Message shown when user has unrestricted account access. const unrestrictedAccessMessage = 'This user has unrestricted access to the account.'; @@ -175,8 +176,8 @@ describe('User permission management', () => { */ it('can toggle full account access', () => { const mockUser = accountUserFactory.build({ - username: randomLabel(), restricted: false, + username: randomLabel(), }); const mockUserUpdated = { @@ -266,8 +267,8 @@ describe('User permission management', () => { */ it('can update global and specific permissions', () => { const mockUser = accountUserFactory.build({ - username: randomLabel(), restricted: true, + username: randomLabel(), }); const mockUserGrants = { ...userPermissionsGrants }; @@ -278,11 +279,9 @@ describe('User permission management', () => { ...mockUserGrants, global: { account_access: 'read_only', - cancel_account: true, - child_account_access: true, add_buckets: true, - add_domains: true, add_databases: true, + add_domains: true, add_firewalls: true, add_images: true, add_kubernetes: true, @@ -292,6 +291,8 @@ describe('User permission management', () => { add_stackscripts: true, add_volumes: true, add_vpcs: true, + cancel_account: true, + child_account_access: true, longview_subscription: true, }, }; @@ -387,8 +388,8 @@ describe('User permission management', () => { */ it('can reset user permissions changes', () => { const mockUser = accountUserFactory.build({ - username: randomLabel(), restricted: true, + username: randomLabel(), }); const mockUserGrants = { ...userPermissionsGrants }; @@ -487,9 +488,9 @@ describe('User permission management', () => { }); const mockActiveUser = accountUserFactory.build({ - username: 'unrestricted-child-user', restricted: false, user_type: 'child', + username: 'unrestricted-child-user', }); const mockRestrictedUser = { @@ -545,8 +546,8 @@ describe('User permission management', () => { */ it('tests the user permissions for a child account viewing a proxy user', () => { const mockChildProfile = profileFactory.build({ - username: 'proxy-user', user_type: 'child', + username: 'proxy-user', }); const mockChildUser = accountUserFactory.build({ diff --git a/packages/manager/cypress/e2e/core/account/user-profile.spec.ts b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts index 98b9dc65be4..b288cdd14ea 100644 --- a/packages/manager/cypress/e2e/core/account/user-profile.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts @@ -1,4 +1,3 @@ -import { accountUserFactory } from 'src/factories/accountUsers'; import { getProfile } from 'support/api/account'; import { interceptGetUser, @@ -6,9 +5,11 @@ import { mockGetUsers, mockUpdateUsername, } from 'support/intercepts/account'; -import { randomString } from 'support/util/random'; -import { ui } from 'support/ui'; import { mockUpdateProfile } from 'support/intercepts/profile'; +import { ui } from 'support/ui'; +import { randomString } from 'support/util/random'; + +import { accountUserFactory } from 'src/factories/accountUsers'; import { PARENT_USER, RESTRICTED_FIELD_TOOLTIP, @@ -199,8 +200,8 @@ describe('User Profile', () => { getProfile().then((profile) => { const proxyUsername = 'proxy_user'; const mockAccountUsers = accountUserFactory.buildList(1, { - username: proxyUsername, user_type: 'proxy', + username: proxyUsername, }); mockGetUsers(mockAccountUsers).as('getUsers'); diff --git a/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts b/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts index 2b2fd767d4f..d2547970203 100644 --- a/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts @@ -1,15 +1,15 @@ import { profileFactory, securityQuestionsFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; +import { verificationBannerNotice } from 'support/constants/user'; import { mockGetUser, mockGetUserGrants, mockGetUsers, } from 'support/intercepts/account'; import { mockGetSecurityQuestions } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; import { mockGetProfile } from 'support/intercepts/profile'; -import { verificationBannerNotice } from 'support/constants/user'; +import { ui } from 'support/ui'; describe('User verification banner', () => { /* @@ -18,15 +18,15 @@ describe('User verification banner', () => { */ it('can show up when a child user has not associated a phone number or set up security questions for their account', () => { const mockChildProfile = profileFactory.build({ - username: 'child-user', user_type: 'child', + username: 'child-user', verified_phone_number: null, }); const mockChildUser = accountUserFactory.build({ restricted: false, - username: 'child-user', user_type: 'child', + username: 'child-user', verified_phone_number: null, }); @@ -85,15 +85,15 @@ describe('User verification banner', () => { */ it('can show up when a child user has set up security questions but not a phone number for their account', () => { const mockChildProfile = profileFactory.build({ - username: 'child-user', user_type: 'child', + username: 'child-user', verified_phone_number: null, }); const mockChildUser = accountUserFactory.build({ restricted: false, - username: 'child-user', user_type: 'child', + username: 'child-user', verified_phone_number: null, }); @@ -162,15 +162,15 @@ describe('User verification banner', () => { */ it('does not show up when a child user adds a phone number and sets up security questions', () => { const mockChildProfile = profileFactory.build({ - username: 'child-user', user_type: 'child', + username: 'child-user', verified_phone_number: '+15555555555', }); const mockChildUser = accountUserFactory.build({ restricted: false, - username: 'child-user', user_type: 'child', + username: 'child-user', verified_phone_number: '+15555555555', }); diff --git a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts index bac3a3d11fc..af4179b74c5 100644 --- a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts @@ -1,14 +1,13 @@ import { profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; -import type { Profile } from '@linode/api-v4'; import { mockAddUser, + mockDeleteUser, mockGetUser, mockGetUserGrants, mockGetUserGrantsUnrestrictedAccess, mockGetUsers, - mockDeleteUser, } from 'support/intercepts/account'; import { mockGetProfile, @@ -16,8 +15,11 @@ import { } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; + import { PARENT_USER } from 'src/features/Account/constants'; +import type { Profile } from '@linode/api-v4'; + /** * Initialize test users before tests * @@ -31,16 +33,16 @@ const initTestUsers = (profile: Profile, enableChildAccountAccess: boolean) => { const mockRestrictedParentWithoutChildAccountAccess = accountUserFactory.build( { - username: 'restricted-parent-user-without-child-account-access', restricted: true, user_type: 'parent', + username: 'restricted-parent-user-without-child-account-access', } ); const mockRestrictedParentWithChildAccountAccess = accountUserFactory.build({ - username: 'restricted-parent-user-with-child-account-access', restricted: true, user_type: 'parent', + username: 'restricted-parent-user-with-child-account-access', }); const mockUsers = [ @@ -89,9 +91,9 @@ describe('Users landing page', () => { */ it('shows "Child account access" column for unrestricted parent users and shows restricted parent users who have the correct grant status', () => { const mockProfile = profileFactory.build({ - username: 'unrestricted-parent-user', restricted: false, user_type: 'parent', + username: 'unrestricted-parent-user', }); const mockUsers = initTestUsers(mockProfile, true); @@ -122,9 +124,9 @@ describe('Users landing page', () => { it('shows "Child account access" column for restricted parent users with child_account_access grant set to true', () => { const mockProfile = profileFactory.build({ - username: 'restricted-parent-user', restricted: true, user_type: 'parent', + username: 'restricted-parent-user', }); initTestUsers(mockProfile, true); @@ -138,9 +140,9 @@ describe('Users landing page', () => { it('hides "Child account access" column for restricted parent users with child_account_access grant set to false', () => { const mockProfile = profileFactory.build({ - username: 'restricted-parent-user', restricted: true, user_type: 'parent', + username: 'restricted-parent-user', }); initTestUsers(mockProfile, false); @@ -154,8 +156,8 @@ describe('Users landing page', () => { it('hides "Child account access" column for default users', () => { const mockProfile = profileFactory.build({ - username: 'default-user', restricted: false, + username: 'default-user', }); initTestUsers(mockProfile, false); @@ -170,9 +172,9 @@ describe('Users landing page', () => { it('hides "Child account access" column for proxy users', () => { const mockProfile = profileFactory.build({ - username: 'proxy-user', restricted: false, user_type: 'proxy', + username: 'proxy-user', }); initTestUsers(mockProfile, false); @@ -187,9 +189,9 @@ describe('Users landing page', () => { it('hides "Child account access" column for child users', () => { const mockProfile = profileFactory.build({ - username: 'child-user', restricted: false, user_type: 'child', + username: 'child-user', }); initTestUsers(mockProfile, false); @@ -207,14 +209,14 @@ describe('Users landing page', () => { */ it('hides "Parent User Settings" section for parent users', () => { const mockProfile = profileFactory.build({ - username: 'unrestricted-parent-user', restricted: false, user_type: 'parent', + username: 'unrestricted-parent-user', }); const mockUser = accountUserFactory.build({ - username: 'unrestricted-user', restricted: false, + username: 'unrestricted-user', }); // Initially mock user with unrestricted account access. @@ -240,8 +242,8 @@ describe('Users landing page', () => { */ it('tests the users landing flow for a child account viewing a proxy user', () => { const mockChildProfile = profileFactory.build({ - username: 'child-user', user_type: 'child', + username: 'child-user', }); const mockChildUser = accountUserFactory.build({ @@ -297,15 +299,15 @@ describe('Users landing page', () => { it('can add users with full access', () => { const mockUser = accountUserFactory.build({ - username: randomLabel(), restricted: false, + username: randomLabel(), }); const username = randomLabel(); const newUser = accountUserFactory.build({ - username: username, email: `${username}@test.com`, restricted: false, + username, }); mockGetUsers([mockUser]).as('getUsers'); @@ -433,15 +435,15 @@ describe('Users landing page', () => { it('can add users with restricted access', () => { const mockUser = accountUserFactory.build({ - username: randomLabel(), restricted: false, + username: randomLabel(), }); const username = randomLabel(); const newUser = accountUserFactory.build({ - username: username, email: `${username}@test.com`, restricted: true, + username, }); mockGetUsers([mockUser]).as('getUsers'); @@ -547,15 +549,15 @@ describe('Users landing page', () => { it('can delete users', () => { const mockUser = accountUserFactory.build({ - username: randomLabel(), restricted: false, + username: randomLabel(), }); const username = randomLabel(); const additionalUser = accountUserFactory.build({ - username: username, email: `${username}@test.com`, restricted: false, + username, }); mockGetUsers([mockUser, additionalUser]).as('getUsers'); diff --git a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts index dd706091247..f2f94a3085d 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts @@ -3,23 +3,22 @@ import { mockUpdateAccount, mockUpdateAccountAgreements, } from 'support/intercepts/account'; -import { accountFactory } from 'src/factories/account'; -import type { Account } from '@linode/api-v4'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetUserPreferences } from 'support/intercepts/profile'; import { ui } from 'support/ui'; + +import { accountAgreementsFactory } from 'src/factories'; +import { accountFactory } from 'src/factories/account'; import { TAX_ID_AGREEMENT_TEXT, TAX_ID_HELPER_TEXT, } from 'src/features/Billing/constants'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { mockGetUserPreferences } from 'support/intercepts/profile'; -import { accountAgreementsFactory } from 'src/factories'; + +import type { Account } from '@linode/api-v4'; /* eslint-disable sonarjs/no-duplicate-string */ const accountData = accountFactory.build({ - company: 'company_name', - email: 'test_email@linode.com', - first_name: 'First name', - last_name: 'Last Name', + active_promotions: [], address_1: 'terrible address address for test', address_2: 'Very long address for test Very long address for test Ve ', balance: 0, @@ -32,25 +31,28 @@ const accountData = accountFactory.build({ 'Kubernetes', ], city: 'philadelphia', + company: 'company_name', country: 'US', - credit_card: { last_four: '4000', expiry: '01/2090' }, + credit_card: { expiry: '01/2090', last_four: '4000' }, + email: 'test_email@linode.com', euuid: '7C1E3EE8-2F65-418A-95EF12E477XXXXXX', + first_name: 'First name', + last_name: 'Last Name', phone: '2154444444', state: 'Pennsylvania', tax_id: '1234567890', zip: '19109', - active_promotions: [], }); const newAccountData = accountFactory.build({ - company: 'New company_name', - email: 'new_test_email@linode.com', - first_name: 'NewFirstName', - last_name: 'New Last Name', address_1: 'new terrible address address for test', address_2: 'new Very long address for test Very long address for test Ve ', city: 'New Philadelphia', + company: 'New company_name', country: 'FR', + email: 'new_test_email@linode.com', + first_name: 'NewFirstName', + last_name: 'New Last Name', phone: '6104444444', state: 'Pennsylvania', tax_id: '9234567890', diff --git a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts index 2f33d308bc2..b00ce254e9e 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts @@ -2,7 +2,6 @@ * @file Integration tests for account invoice functionality. */ -import type { InvoiceItem, TaxSummary } from '@linode/api-v4'; import { invoiceFactory, invoiceItemFactory } from '@src/factories'; import { DateTime } from 'luxon'; import { MAGIC_DATE_THAT_DC_SPECIFIC_PRICING_WAS_IMPLEMENTED } from 'support/constants/dc-specific-pricing'; @@ -16,6 +15,8 @@ import { formatUsd } from 'support/util/currency'; import { randomItem, randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion, getRegionById } from 'support/util/regions'; +import type { InvoiceItem, TaxSummary } from '@linode/api-v4'; + /** * Returns a string representation of a region, as shown on the invoice details page. * @@ -56,17 +57,17 @@ describe('Account invoices', () => { return invoiceItemFactory.build({ amount: subtotal, - tax, - total: subtotal + tax, from: DateTime.now().minus({ days: i }).toISO(), - to: DateTime.now().minus({ days: i }).plus({ hours }).toISO(), - quantity, - region: chooseRegion().id, - unit_price: `${randomNumber(5, 300) / 10000}`, label: `${itemType} ${randomNumber( 1, 24 )}GB - ${randomLabel()} (${randomNumber(10000, 99999)})`, + quantity, + region: chooseRegion().id, + tax, + to: DateTime.now().minus({ days: i }).plus({ hours }).toISO(), + total: subtotal + tax, + unit_price: `${randomNumber(5, 300) / 10000}`, }); }); @@ -75,9 +76,9 @@ describe('Account invoices', () => { ...mockInvoiceItemsWithRegions, invoiceItemFactory.build({ amount: 5, - total: 6, region: null, tax: 1, + total: 6, }), ]; @@ -111,10 +112,10 @@ describe('Account invoices', () => { // Create an Invoice object to correspond with the Invoice Items and their // charges. const mockInvoice = invoiceFactory.build({ + date: MAGIC_DATE_THAT_DC_SPECIFIC_PRICING_WAS_IMPLEMENTED, id: randomNumber(10000, 99999), - tax: sumTax, subtotal: sumSubtotal, - total: sumTax + sumSubtotal, + tax: sumTax, tax_summary: [ { name: 'PA STATE TAX', @@ -125,7 +126,7 @@ describe('Account invoices', () => { tax: Math.ceil(sumTax / 2), }, ], - date: MAGIC_DATE_THAT_DC_SPECIFIC_PRICING_WAS_IMPLEMENTED, + total: sumTax + sumSubtotal, }); // All mocked invoice items. @@ -250,8 +251,8 @@ describe('Account invoices', () => { it('does not list the region on past invoices', () => { const mockInvoice = invoiceFactory.build({ - id: randomNumber(), date: '2023-09-30 00:00:00Z', + id: randomNumber(), }); // Regular invoice items. diff --git a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts b/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts index 148d8c270bd..2ed6fa23839 100644 --- a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts @@ -1,8 +1,9 @@ -import { accountFactory } from 'src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { accountFactory } from 'src/factories'; + const creditCardExpiredBannerNotice = 'Your credit card has expired! Please update your payment details.'; diff --git a/packages/manager/cypress/e2e/core/billing/default-payment-method.spec.ts b/packages/manager/cypress/e2e/core/billing/default-payment-method.spec.ts index 5378475269f..a1722991cd1 100644 --- a/packages/manager/cypress/e2e/core/billing/default-payment-method.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/default-payment-method.spec.ts @@ -1,30 +1,31 @@ -import type { CreditCardData } from '@linode/api-v4'; import { paymentMethodFactory } from '@src/factories/accountPayment'; import { - mockSetDefaultPaymentMethod, mockGetPaymentMethods, + mockSetDefaultPaymentMethod, } from 'support/intercepts/account'; import { ui } from 'support/ui'; +import type { CreditCardData } from '@linode/api-v4'; + const paymentMethodGpay = (isDefault: boolean) => { return paymentMethodFactory.build({ + data: { card_type: 'Visa', expiry: '07/2025', last_four: '2045' }, id: 434357, - type: 'google_pay', is_default: isDefault, - data: { card_type: 'Visa', last_four: '2045', expiry: '07/2025' }, + type: 'google_pay', }); }; const paymentMethodCC = (isDefault: boolean) => { return paymentMethodFactory.build({ - id: 420330, - type: 'credit_card', - is_default: isDefault, data: { card_type: 'American Express', - last_four: '2222', expiry: '07/2025', + last_four: '2222', }, + id: 420330, + is_default: isDefault, + type: 'credit_card', }); }; diff --git a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts index 743cc24e784..f4d90998420 100644 --- a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts @@ -1,25 +1,26 @@ import { mockGetPaymentMethods } from 'support/intercepts/account'; -import { PaymentMethod, CreditCardData } from '@linode/api-v4'; import { ui } from 'support/ui'; +import type { CreditCardData, PaymentMethod } from '@linode/api-v4'; + const mockPaymentMethods: PaymentMethod[] = [ { - id: 420330, - type: 'credit_card', - is_default: true, created: '2021-07-27T14:37:43', data: { card_type: 'American Express', - last_four: '2222', expiry: '07/2025', + last_four: '2222', }, + id: 420330, + is_default: true, + type: 'credit_card', }, { + created: '2021-08-04T18:29:01', + data: { card_type: 'Visa', expiry: '07/2025', last_four: '2045' }, id: 434357, - type: 'google_pay', is_default: false, - created: '2021-08-04T18:29:01', - data: { card_type: 'Visa', last_four: '2045', expiry: '07/2025' }, + type: 'google_pay', }, ]; @@ -31,22 +32,22 @@ const mockPaymentMethodsData = mockPaymentMethods.map( const mockPaymentMethodsExpired: PaymentMethod[] = [ { - id: 420330, - type: 'credit_card', - is_default: true, created: '2021-07-27T14:37:43', data: { card_type: 'American Express', - last_four: '2222', expiry: '07/2025', + last_four: '2222', }, + id: 420330, + is_default: true, + type: 'credit_card', }, { + created: '2021-08-04T18:29:01', + data: { card_type: 'Visa', expiry: '07/2020', last_four: '2045' }, id: 434357, - type: 'google_pay', is_default: false, - created: '2021-08-04T18:29:01', - data: { card_type: 'Visa', last_four: '2045', expiry: '07/2020' }, + type: 'google_pay', }, ]; diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index 6869a54a91b..4c55cefadfa 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -5,7 +5,6 @@ import { paymentMethodFactory, profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; -import { ADMINISTRATOR, PARENT_USER } from 'src/features/Account/constants'; import { mockGetPaymentMethods, mockGetUser } from 'support/intercepts/account'; import { mockGetProfile, @@ -14,6 +13,8 @@ import { import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; +import { ADMINISTRATOR, PARENT_USER } from 'src/features/Account/constants'; + // Tooltip message that appears on disabled billing action buttons for restricted // and child users. const restrictedUserTooltip = @@ -234,14 +235,14 @@ describe('restricted user billing flows', () => { */ it('cannot edit billing information with read-only account access', () => { const mockProfile = profileFactory.build({ - username: randomLabel(), restricted: true, + username: randomLabel(), }); const mockUser = accountUserFactory.build({ - username: mockProfile.username, restricted: true, user_type: 'default', + username: mockProfile.username, }); const mockGrants = grantsFactory.build({ @@ -273,8 +274,8 @@ describe('restricted user billing flows', () => { */ it('cannot edit billing information as child account', () => { const mockProfile = profileFactory.build({ - username: randomLabel(), user_type: 'child', + username: randomLabel(), }); const mockUser = accountUserFactory.build({ @@ -299,25 +300,25 @@ describe('restricted user billing flows', () => { */ it('can edit billing information as a regular user and as a parent user', () => { const mockProfileRegular = profileFactory.build({ - username: randomLabel(), restricted: false, + username: randomLabel(), }); const mockUserRegular = accountUserFactory.build({ - username: mockProfileRegular.username, - user_type: 'default', restricted: false, + user_type: 'default', + username: mockProfileRegular.username, }); const mockProfileParent = profileFactory.build({ - username: randomLabel(), restricted: false, + username: randomLabel(), }); const mockUserParent = accountUserFactory.build({ - username: mockProfileParent.username, - user_type: 'parent', restricted: false, + user_type: 'parent', + username: mockProfileParent.username, }); // Confirm button behavior for regular users. diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index c4536e153fa..9189a234d17 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -1,19 +1,21 @@ -import { DateTime } from 'luxon'; import { getProfile } from '@linode/api-v4'; -import type { Invoice, Profile, Payment } from '@linode/api-v4'; -import { invoiceFactory, paymentFactory } from 'src/factories/billing'; +import { profileFactory } from '@src/factories'; +import { formatDate } from '@src/utilities/formatDate'; +import { DateTime } from 'luxon'; import { authenticate } from 'support/api/authentication'; import { mockGetInvoices, - mockGetPayments, mockGetPaymentMethods, + mockGetPayments, } from 'support/intercepts/account'; -import { formatDate } from '@src/utilities/formatDate'; -import { randomNumber } from 'support/util/random'; -import { ui } from 'support/ui'; -import { profileFactory } from '@src/factories'; import { mockGetProfile, mockUpdateProfile } from 'support/intercepts/profile'; +import { ui } from 'support/ui'; import { buildArray } from 'support/util/arrays'; +import { randomNumber } from 'support/util/random'; + +import { invoiceFactory, paymentFactory } from 'src/factories/billing'; + +import type { Invoice, Payment, Profile } from '@linode/api-v4'; /** * Uses the user menu to navigate to the Profile Display page. @@ -67,8 +69,8 @@ const navigateToBilling = () => { */ const assertInvoiceInfo = (invoice: Invoice, timezone: string) => { const invoiceDate = formatDate(invoice.date, { - timezone, displayTime: true, + timezone, }); cy.findByText(invoice.label) .should('be.visible') @@ -97,8 +99,8 @@ const assertInvoiceInfo = (invoice: Invoice, timezone: string) => { */ const assertPaymentInfo = (payment: Payment, timezone: string) => { const paymentDate = formatDate(payment.date, { - timezone, displayTime: true, + timezone, }); cy.findByText(`Payment #${payment.id}`) .should('be.visible') @@ -132,9 +134,9 @@ describe('Billing Activity Feed', () => { const tax = randomNumber(5, 50); return invoiceFactory.build({ + date, id, label: `Invoice #${id}`, - date, subtotal, tax, total: subtotal + tax, @@ -148,8 +150,8 @@ describe('Billing Activity Feed', () => { const date = DateTime.now().minus({ months: i }).toISO(); return paymentFactory.build({ - id, date, + id, usd: invoice.total, }); } @@ -332,9 +334,9 @@ describe('Billing Activity Feed', () => { it('displays correct timezone for invoice and payment dates', () => { // Time zones against which to verify invoice and payment dates. const timeZonesList = [ - { key: 'America/New_York', human: 'Eastern Time - New York' }, - { key: 'UTC', human: 'Coordinated Universal Time' }, - { key: 'Asia/Hong_Kong', human: 'Hong Kong Standard Time' }, + { human: 'Eastern Time - New York', key: 'America/New_York' }, + { human: 'Coordinated Universal Time', key: 'UTC' }, + { human: 'Hong Kong Standard Time', key: 'Asia/Hong_Kong' }, ]; const mockProfile = profileFactory.build({ diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts index a6834c7721e..5e921b0a002 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts @@ -4,7 +4,24 @@ * This file contains Cypress tests that validate the display and content of the Alerts Show Detail Page in the CloudPulse application. * It ensures that all alert details, criteria, and resource information are displayed correctly. */ +import { capitalize } from '@linode/utilities'; +import { + aggregationTypeMap, + dimensionOperatorTypeMap, + metricOperatorTypeMap, + severityMap, +} from 'support/constants/alert'; +import { mockGetAccount } from 'support/intercepts/account'; +import { + mockGetAlertChannels, + mockGetAlertDefinitions, + mockGetAllAlertDefinitions, +} from 'support/intercepts/cloudpulse'; +import { mockGetDatabases } from 'support/intercepts/databases'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; + import { accountFactory, alertFactory, @@ -13,67 +30,51 @@ import { notificationChannelFactory, regionFactory, } from 'src/factories'; -import { mockGetAccount } from 'support/intercepts/account'; -import type { Flags } from 'src/featureFlags'; - -import { - mockGetAlertChannels, - mockGetAlertDefinitions, - mockGetAllAlertDefinitions, -} from 'support/intercepts/cloudpulse'; -import { mockGetRegions } from 'support/intercepts/regions'; import { formatDate } from 'src/utilities/formatDate'; -import { - metricOperatorTypeMap, - dimensionOperatorTypeMap, - severityMap, - aggregationTypeMap, -} from 'support/constants/alert'; -import { ui } from 'support/ui'; -import { Database } from '@linode/api-v4'; -import { mockGetDatabases } from 'support/intercepts/databases'; -import { capitalize } from '@linode/utilities'; -const flags: Partial = { aclp: { enabled: true, beta: true } }; +import type { Database } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; + +const flags: Partial = { aclp: { beta: true, enabled: true } }; const mockAccount = accountFactory.build(); const regions = [ regionFactory.build({ capabilities: ['Managed Databases'], + country: 'us', id: 'us-ord', label: 'Chicago, IL', - country: 'us', }), regionFactory.build({ capabilities: ['Managed Databases'], + country: 'us', id: 'us-east', label: 'Newark', - country: 'us', }), ]; const databases: Database[] = databaseFactory.buildList(5).map((db, index) => ({ ...db, - type: 'MySQL', - region: regions[index % regions.length].id, engine: 'mysql', + region: regions[index % regions.length].id, + type: 'MySQL', })); const alertDetails = alertFactory.build({ + entity_ids: databases.slice(0, 4).map((db) => db.id.toString()), + rule_criteria: { rules: alertRulesFactory.buildList(2) }, service_type: 'dbaas', severity: 1, status: 'enabled', type: 'system', - entity_ids: databases.slice(0, 4).map((db) => db.id.toString()), - rule_criteria: { rules: alertRulesFactory.buildList(2) }, }); const { - service_type, - severity, - rule_criteria, + created_by, + description, id, label, - description, - created_by, + rule_criteria, + service_type, + severity, updated, } = alertDetails; const { rules } = rule_criteria; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index 3917af76e95..ceb7c10d904 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -2,60 +2,62 @@ * @file Integration Tests for the CloudPulse Alerts Listing Page. * This file verifies the UI, functionality, and sorting/filtering of the CloudPulse Alerts Listing Page. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { accountFactory, alertFactory } from 'src/factories'; +import { cloudPulseServiceMap } from 'support/constants/cloudpulse'; import { mockGetAccount } from 'support/intercepts/account'; -import type { Flags } from 'src/featureFlags'; import { mockGetAllAlertDefinitions, mockGetCloudPulseServices, mockUpdateAlertDefinitions, } from 'support/intercepts/cloudpulse'; -import { formatDate } from 'src/utilities/formatDate'; -import { Alert } from '@linode/api-v4'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; + +import { accountFactory, alertFactory } from 'src/factories'; import { alertStatuses } from 'src/features/CloudPulse/Alerts/constants'; -import { cloudPulseServiceMap } from 'support/constants/cloudpulse'; +import { formatDate } from 'src/utilities/formatDate'; + +import type { Alert } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; -const flags: Partial = { aclp: { enabled: true, beta: true } }; +const flags: Partial = { aclp: { beta: true, enabled: true } }; const mockAccount = accountFactory.build(); const now = new Date(); const mockAlerts = [ alertFactory.build({ + created_by: 'user1', + label: 'Alert-1', service_type: 'dbaas', severity: 1, status: 'enabled', type: 'user', - created_by: 'user1', updated: new Date(now.getTime() - 86400).toISOString(), - label: 'Alert-1', }), alertFactory.build({ + created_by: 'user4', + label: 'Alert-2', service_type: 'dbaas', - type: 'user', severity: 0, status: 'disabled', + type: 'user', updated: new Date(now.getTime() - 10 * 86400).toISOString(), - created_by: 'user4', - label: 'Alert-2', }), alertFactory.build({ + created_by: 'user2', + label: 'Alert-3', service_type: 'linode', - type: 'user', severity: 2, status: 'enabled', + type: 'user', updated: new Date(now.getTime() - 6 * 86400).toISOString(), - created_by: 'user2', - label: 'Alert-3', }), alertFactory.build({ + created_by: 'user3', + label: 'Alert-4', service_type: 'linode', severity: 3, status: 'disabled', type: 'user', updated: new Date(now.getTime() - 4 * 86400).toISOString(), - created_by: 'user3', - label: 'Alert-4', }), ]; @@ -114,7 +116,7 @@ const verifyTableSorting = ( * @param {Alert} alert - The alert object to validate. */ const validateAlertDetails = (alert: Alert) => { - const { id, service_type, status, label, updated, created_by } = alert; + const { created_by, id, label, service_type, status, updated } = alert; cy.get(`[data-qa-alert-cell="${id}"]`).within(() => { cy.findByText(cloudPulseServiceMap[service_type]) @@ -168,22 +170,22 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { it('should verify sorting functionality for multiple columns in ascending and descending order', () => { const sortCases = [ - { column: 'label', descending: [4, 3, 2, 1], ascending: [1, 2, 3, 4] }, - { column: 'status', descending: [1, 3, 2, 4], ascending: [2, 4, 1, 3] }, + { ascending: [1, 2, 3, 4], column: 'label', descending: [4, 3, 2, 1] }, + { ascending: [2, 4, 1, 3], column: 'status', descending: [1, 3, 2, 4] }, { + ascending: [2, 1, 4, 3], column: 'service_type', descending: [4, 3, 2, 1], - ascending: [2, 1, 4, 3], }, { + ascending: [1, 3, 4, 2], column: 'created_by', descending: [2, 4, 3, 1], - ascending: [1, 3, 4, 2], }, - { column: 'updated', descending: [1, 4, 3, 2], ascending: [2, 3, 4, 1] }, + { ascending: [2, 3, 4, 1], column: 'updated', descending: [1, 4, 3, 2] }, ]; - sortCases.forEach(({ column, descending, ascending }) => { + sortCases.forEach(({ ascending, column, descending }) => { // Verify descending order verifyTableSorting(column, 'descending', descending); @@ -303,7 +305,7 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { // Function to toggle an alert's status const toggleAlertStatus = ( alertName: string, - action: 'Enable' | 'Disable', + action: 'Disable' | 'Enable', alias: string, successMessage: string ) => { diff --git a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts index c3efa4d24e7..66aeae2d68b 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts @@ -1,21 +1,32 @@ /** * @file Error Handling Tests for CloudPulse Dashboard. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { widgetDetails } from 'support/constants/widgets'; +import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateCloudPulseJWEToken, mockGetCloudPulseDashboard, - mockGetCloudPulseDashboards, - mockGetCloudPulseMetricDefinitions, - mockGetCloudPulseServices, mockGetCloudPulseDashboardByIdError, + mockGetCloudPulseDashboards, mockGetCloudPulseDashboardsError, + mockGetCloudPulseMetricDefinitions, mockGetCloudPulseMetricDefinitionsError, + mockGetCloudPulseServices, mockGetCloudPulseServicesError, mockGetCloudPulseTokenError, } from 'support/intercepts/cloudpulse'; +import { + mockGetDatabases, + mockGetDatabasesError, +} from 'support/intercepts/databases'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetUserPreferences } from 'support/intercepts/profile'; +import { + mockGetRegions, + mockGetRegionsError, +} from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { widgetDetails } from 'support/constants/widgets'; + import { accountFactory, dashboardFactory, @@ -24,18 +35,9 @@ import { regionFactory, widgetFactory, } from 'src/factories'; -import { mockGetUserPreferences } from 'support/intercepts/profile'; -import { - mockGetRegions, - mockGetRegionsError, -} from 'support/intercepts/regions'; -import { - mockGetDatabases, - mockGetDatabasesError, -} from 'support/intercepts/databases'; -import { Database } from '@linode/api-v4'; -import { mockGetAccount } from 'support/intercepts/account'; -import { Flags } from 'src/featureFlags'; + +import type { Database } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; /** * Verifies the presence and values of specific properties within the aclpPreference object @@ -47,7 +49,7 @@ import { Flags } from 'src/featureFlags'; */ const flags: Partial = { - aclp: { enabled: true, beta: true }, + aclp: { beta: true, enabled: true }, aclpResourceTypeMap: [ { dimensionKey: 'LINODE_ID', @@ -64,29 +66,29 @@ const flags: Partial = { ], }; const { - metrics, - id, - serviceType, + clusterName, dashboardName, engine, - clusterName, + id, + metrics, nodeType, + serviceType, } = widgetDetails.dbaas; const dashboard = dashboardFactory.build({ label: dashboardName, service_type: serviceType, - widgets: metrics.map(({ title, yLabel, name, unit }) => { + widgets: metrics.map(({ name, title, unit, yLabel }) => { return widgetFactory.build({ label: title, - y_label: yLabel, metric: name, unit, + y_label: yLabel, }); }), }); -const metricDefinitions = metrics.map(({ title, name, unit }) => +const metricDefinitions = metrics.map(({ name, title, unit }) => dashboardMetricFactory.build({ label: title, metric: name, @@ -101,13 +103,13 @@ const mockRegion = regionFactory.build({ }); const databaseMock: Database = databaseFactory.build({ + cluster_size: 3, + engine: 'mysql', label: clusterName, - type: engine, region: mockRegion.id, - version: '1', status: 'provisioning', - cluster_size: 3, - engine: 'mysql', + type: engine, + version: '1', }); const mockAccount = accountFactory.build(); @@ -148,7 +150,7 @@ describe('Tests for API error handling', () => { .should('be.visible') .click(); - //Select a Database Engine from the autocomplete input. + // Select a Database Engine from the autocomplete input. ui.autocomplete .findByLabel('Database Engine') .should('be.visible') @@ -410,7 +412,7 @@ describe('Tests for API error handling', () => { .should('be.visible') .click(); - //Select a Database Engine from the autocomplete input. + // Select a Database Engine from the autocomplete input. ui.autocomplete .findByLabel('Database Engine') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts index f83b057626e..89d0c23ca31 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts @@ -2,11 +2,12 @@ * @file Integration tests for CloudPulse navigation. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetAccount } from 'support/intercepts/account'; -import { accountFactory } from 'src/factories'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; +import { accountFactory } from 'src/factories'; + const mockAccount = accountFactory.build(); describe('CloudPulse navigation', () => { diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index ea61bc06d6e..7e2b870bace 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -1,17 +1,24 @@ /** * @file Integration Tests for CloudPulse Dbass Dashboard. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { widgetDetails } from 'support/constants/widgets'; +import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateCloudPulseJWEToken, - mockGetCloudPulseDashboard, mockCreateCloudPulseMetrics, + mockGetCloudPulseDashboard, mockGetCloudPulseDashboards, mockGetCloudPulseMetricDefinitions, mockGetCloudPulseServices, } from 'support/intercepts/cloudpulse'; +import { mockGetDatabases } from 'support/intercepts/databases'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetLinodes } from 'support/intercepts/linodes'; +import { mockGetUserPreferences } from 'support/intercepts/profile'; +import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { widgetDetails } from 'support/constants/widgets'; +import { generateRandomMetricsData } from 'support/util/cloudpulse'; + import { accountFactory, cloudPulseMetricsResponseFactory, @@ -23,16 +30,11 @@ import { regionFactory, widgetFactory, } from 'src/factories'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetLinodes } from 'support/intercepts/linodes'; -import { mockGetUserPreferences } from 'support/intercepts/profile'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { CloudPulseMetricsResponse, Database } from '@linode/api-v4'; -import { generateRandomMetricsData } from 'support/util/cloudpulse'; -import { mockGetDatabases } from 'support/intercepts/databases'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; -import type { Flags } from 'src/featureFlags'; import { formatToolTip } from 'src/features/CloudPulse/Utils/unitConversion'; + +import type { CloudPulseMetricsResponse, Database } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; import type { Interception } from 'support/cypress-exports'; /** @@ -49,7 +51,7 @@ const expectedGranularityArray = ['Auto', '1 day', '1 hr', '5 min']; const timeDurationToSelect = 'Last 24 Hours'; const flags: Partial = { - aclp: { enabled: true, beta: true }, + aclp: { beta: true, enabled: true }, aclpResourceTypeMap: [ { dimensionKey: 'LINODE_ID', @@ -67,29 +69,29 @@ const flags: Partial = { }; const { - metrics, - id, - serviceType, + clusterName, dashboardName, engine, - clusterName, + id, + metrics, nodeType, + serviceType, } = widgetDetails.dbaas; const dashboard = dashboardFactory.build({ label: dashboardName, service_type: serviceType, - widgets: metrics.map(({ title, yLabel, name, unit }) => { + widgets: metrics.map(({ name, title, unit, yLabel }) => { return widgetFactory.build({ label: title, - y_label: yLabel, metric: name, unit, + y_label: yLabel, }); }), }); -const metricDefinitions = metrics.map(({ title, name, unit }) => +const metricDefinitions = metrics.map(({ name, title, unit }) => dashboardMetricFactory.build({ label: title, metric: name, @@ -98,8 +100,8 @@ const metricDefinitions = metrics.map(({ title, name, unit }) => ); const mockLinode = linodeFactory.build({ - label: clusterName, id: kubeLinodeFactory.build().instance_id ?? undefined, + label: clusterName, }); const mockAccount = accountFactory.build(); @@ -141,7 +143,7 @@ const getWidgetLegendRowValuesFromResponse = ( // Generate graph data using the provided parameters const graphData = generateGraphData({ flags, - label: label, + label, metricsList: responsePayload, resources: [ { @@ -150,9 +152,9 @@ const getWidgetLegendRowValuesFromResponse = ( region: 'us-ord', }, ], - serviceType: serviceType, + serviceType, status: 'success', - unit: unit, + unit, }); // Destructure metrics data from the first legend row @@ -167,17 +169,17 @@ const getWidgetLegendRowValuesFromResponse = ( }; const databaseMock: Database = databaseFactory.build({ - label: clusterName, - type: engine, - region: mockRegion.label, - version: '1', - status: 'provisioning', cluster_size: 2, engine: 'mysql', hosts: { primary: undefined, secondary: undefined, }, + label: clusterName, + region: mockRegion.label, + status: 'provisioning', + type: engine, + version: '1', }); describe('Integration Tests for DBaaS Dashboard ', () => { @@ -225,7 +227,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => { .should('be.visible') .click(); - //Select a Database Engine from the autocomplete input. + // Select a Database Engine from the autocomplete input. ui.autocomplete .findByLabel('Database Engine') .should('be.visible') @@ -302,13 +304,13 @@ describe('Integration Tests for DBaaS Dashboard ', () => { metricsAPIResponsePayload ).as('getGranularityMetrics'); - //find the interval component and select the expected granularity + // find the interval component and select the expected granularity ui.autocomplete .findByLabel('Select an Interval') .should('be.visible') - .type(`${testData.expectedGranularity}{enter}`); //type expected granularity + .type(`${testData.expectedGranularity}{enter}`); // type expected granularity - //check if the API call is made correctly with time granularity value selected + // check if the API call is made correctly with time granularity value selected cy.wait('@getGranularityMetrics').then((interception) => { expect(interception) .to.have.property('response') @@ -318,7 +320,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => { ); }); - //validate the widget areachart is present + // validate the widget areachart is present cy.get('.recharts-responsive-container').within(() => { const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( metricsAPIResponsePayload, @@ -356,13 +358,13 @@ describe('Integration Tests for DBaaS Dashboard ', () => { metricsAPIResponsePayload ).as('getAggregationMetrics'); - //find the interval component and select the expected granularity + // find the interval component and select the expected granularity ui.autocomplete .findByLabel('Select an Aggregate Function') .should('be.visible') - .type(`${testData.expectedAggregation}{enter}`); //type expected granularity + .type(`${testData.expectedAggregation}{enter}`); // type expected granularity - //check if the API call is made correctly with time granularity value selected + // check if the API call is made correctly with time granularity value selected cy.wait('@getAggregationMetrics').then((interception) => { expect(interception) .to.have.property('response') @@ -372,7 +374,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => { ); }); - //validate the widget areachart is present + // validate the widget areachart is present cy.get('.recharts-responsive-container').within(() => { const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( metricsAPIResponsePayload, diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts index f1faa4aca3d..d78e9ce7631 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts @@ -4,39 +4,41 @@ * This file contains Cypress tests for the Edit Alert page of the CloudPulse application. * It ensures that users can navigate to the Edit Alert Page and that alerts are correctly displayed and interactive on the Edit page. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { - accountFactory, - alertFactory, - databaseFactory, - regionFactory, -} from 'src/factories'; import { mockGetAccount } from 'support/intercepts/account'; -import type { Flags } from 'src/featureFlags'; import { mockGetAlertDefinitions, mockGetAllAlertDefinitions, mockUpdateAlertDefinitions, } from 'support/intercepts/cloudpulse'; +import { mockGetDatabases } from 'support/intercepts/databases'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { Alert, Database } from '@linode/api-v4'; -import { mockGetDatabases } from 'support/intercepts/databases'; -const flags: Partial = { aclp: { enabled: true, beta: true } }; +import { + accountFactory, + alertFactory, + databaseFactory, + regionFactory, +} from 'src/factories'; + +import type { Alert, Database } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; + +const flags: Partial = { aclp: { beta: true, enabled: true } }; const expectedResourceIds = Array.from({ length: 50 }, (_, i) => String(i + 1)); const mockAccount = accountFactory.build(); const alertDetails = alertFactory.build({ - label: 'Alert-1', description: 'Test description', + entity_ids: ['1', '2', '3'], + label: 'Alert-1', service_type: 'dbaas', severity: 1, status: 'enabled', type: 'system', - entity_ids: ['1', '2', '3'], }); -const { service_type, id, label } = alertDetails; +const { id, label, service_type } = alertDetails; const regions = [ regionFactory.build({ capabilities: ['Managed Databases'], @@ -53,10 +55,10 @@ const databases: Database[] = databaseFactory .buildList(50) .map((db, index) => ({ ...db, - type: 'MySQL', + engine: 'mysql', region: regions[index % regions.length].id, status: 'active', - engine: 'mysql', + type: 'MySQL', })); const pages = [1, 2]; @@ -193,11 +195,11 @@ describe('Integration Tests for Edit Alert', () => { cy.wait('@updateDefinitions').then(({ request, response }) => { const { - type, - status, - severity, - description, created_by, + description, + severity, + status, + type, updated_by, } = alertDetails; @@ -214,8 +216,8 @@ describe('Integration Tests for Edit Alert', () => { // Destructure alert_channels and trigger_conditions from alertResponse const { alert_channels, - trigger_conditions: responseTriggerConditions, tags, + trigger_conditions: responseTriggerConditions, } = alertResponse; const { criteria_condition: responseCriteriaCondition, diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index 074053af664..f3735d6413a 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -1,17 +1,23 @@ /** * @file Integration Tests for CloudPulse Linode Dashboard. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { widgetDetails } from 'support/constants/widgets'; +import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateCloudPulseJWEToken, - mockGetCloudPulseDashboard, mockCreateCloudPulseMetrics, + mockGetCloudPulseDashboard, mockGetCloudPulseDashboards, mockGetCloudPulseMetricDefinitions, mockGetCloudPulseServices, } from 'support/intercepts/cloudpulse'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetLinodes } from 'support/intercepts/linodes'; +import { mockGetUserPreferences } from 'support/intercepts/profile'; +import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { widgetDetails } from 'support/constants/widgets'; +import { generateRandomMetricsData } from 'support/util/cloudpulse'; + import { accountFactory, cloudPulseMetricsResponseFactory, @@ -22,15 +28,11 @@ import { regionFactory, widgetFactory, } from 'src/factories'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetLinodes } from 'support/intercepts/linodes'; -import { mockGetUserPreferences } from 'support/intercepts/profile'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { CloudPulseMetricsResponse } from '@linode/api-v4'; -import { generateRandomMetricsData } from 'support/util/cloudpulse'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; -import { Flags } from 'src/featureFlags'; import { formatToolTip } from 'src/features/CloudPulse/Utils/unitConversion'; + +import type { CloudPulseMetricsResponse } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; import type { Interception } from 'support/cypress-exports'; /** @@ -46,7 +48,7 @@ import type { Interception } from 'support/cypress-exports'; const expectedGranularityArray = ['Auto', '1 day', '1 hr', '5 min']; const timeDurationToSelect = 'Last 24 Hours'; const flags: Partial = { - aclp: { enabled: true, beta: true }, + aclp: { beta: true, enabled: true }, aclpResourceTypeMap: [ { dimensionKey: 'LINODE_ID', @@ -63,28 +65,28 @@ const flags: Partial = { ], }; const { - metrics, - id, - serviceType, dashboardName, + id, + metrics, region, resource, + serviceType, } = widgetDetails.linode; const dashboard = dashboardFactory.build({ label: dashboardName, service_type: serviceType, - widgets: metrics.map(({ title, yLabel, name, unit }) => { + widgets: metrics.map(({ name, title, unit, yLabel }) => { return widgetFactory.build({ label: title, - y_label: yLabel, metric: name, unit, + y_label: yLabel, }); }), }); -const metricDefinitions = metrics.map(({ title, name, unit }) => +const metricDefinitions = metrics.map(({ name, title, unit }) => dashboardMetricFactory.build({ label: title, metric: name, @@ -93,8 +95,8 @@ const metricDefinitions = metrics.map(({ title, name, unit }) => ); const mockLinode = linodeFactory.build({ - label: resource, id: kubeLinodeFactory.build().instance_id ?? undefined, + label: resource, }); const mockAccount = accountFactory.build(); @@ -136,7 +138,7 @@ const getWidgetLegendRowValuesFromResponse = ( // Generate graph data using the provided parameters const graphData = generateGraphData({ flags, - label: label, + label, metricsList: responsePayload, resources: [ { @@ -145,9 +147,9 @@ const getWidgetLegendRowValuesFromResponse = ( region: 'us-ord', }, ], - serviceType: serviceType, + serviceType, status: 'success', - unit: unit, + unit, }); // Destructure metrics data from the first legend row @@ -264,13 +266,13 @@ describe('Integration Tests for Linode Dashboard ', () => { metricsAPIResponsePayload ).as('getGranularityMetrics'); - //find the interval component and select the expected granularity + // find the interval component and select the expected granularity ui.autocomplete .findByLabel('Select an Interval') .should('be.visible') - .type(`${testData.expectedGranularity}{enter}`); //type expected granularity + .type(`${testData.expectedGranularity}{enter}`); // type expected granularity - //check if the API call is made correctly with time granularity value selected + // check if the API call is made correctly with time granularity value selected cy.wait('@getGranularityMetrics').then((interception) => { expect(interception) .to.have.property('response') @@ -280,7 +282,7 @@ describe('Integration Tests for Linode Dashboard ', () => { ); }); - //validate the widget areachart is present + // validate the widget areachart is present cy.get('.recharts-responsive-container').within(() => { const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( metricsAPIResponsePayload, @@ -321,13 +323,13 @@ describe('Integration Tests for Linode Dashboard ', () => { metricsAPIResponsePayload ).as('getAggregationMetrics'); - //find the interval component and select the expected granularity + // find the interval component and select the expected granularity ui.autocomplete .findByLabel('Select an Aggregate Function') .should('be.visible') - .type(`${testData.expectedAggregation}{enter}`); //type expected granularity + .type(`${testData.expectedAggregation}{enter}`); // type expected granularity - //check if the API call is made correctly with time granularity value selected + // check if the API call is made correctly with time granularity value selected cy.wait('@getAggregationMetrics').then((interception) => { expect(interception) .to.have.property('response') @@ -337,7 +339,7 @@ describe('Integration Tests for Linode Dashboard ', () => { ); }); - //validate the widget areachart is present + // validate the widget areachart is present cy.get('.recharts-responsive-container').within(() => { const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( metricsAPIResponsePayload, diff --git a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts index 987af195cda..db6a3dee064 100644 --- a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts @@ -1,21 +1,23 @@ -import { accountFactory, databaseFactory, eventFactory } from 'src/factories'; -import { mockGetAccount } from 'support/intercepts/account'; import { - databaseClusterConfiguration, databaseConfigurations, mockDatabaseEngineTypes, mockDatabaseNodeTypes, } from 'support/constants/databases'; +import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateDatabase, - mockGetDatabases, mockGetDatabaseEngines, mockGetDatabaseTypes, + mockGetDatabases, } from 'support/intercepts/databases'; import { mockGetEvents } from 'support/intercepts/events'; -import { getRegionById } from 'support/util/regions'; import { ui } from 'support/ui'; +import { getRegionById } from 'support/util/regions'; + +import { accountFactory, databaseFactory, eventFactory } from 'src/factories'; + import type { Database } from '@linode/api-v4'; +import type { databaseClusterConfiguration } from 'support/constants/databases'; describe('create a database cluster, mocked data', () => { databaseConfigurations.forEach( @@ -24,17 +26,17 @@ describe('create a database cluster, mocked data', () => { it(`creates a ${configuration.linodeType} ${configuration.engine} v${configuration.version}.x ${configuration.clusterSize}-node cluster`, () => { // Database mock immediately after instance has been created. const databaseMock: Database = databaseFactory.build({ - label: configuration.label, - type: configuration.linodeType, - region: configuration.region.id, - version: configuration.version, - status: 'provisioning', cluster_size: configuration.clusterSize, engine: configuration.dbType, hosts: { primary: undefined, secondary: undefined, }, + label: configuration.label, + region: configuration.region.id, + status: 'provisioning', + type: configuration.linodeType, + version: configuration.version, }); // Database mock once instance has been provisioned. @@ -47,16 +49,16 @@ describe('create a database cluster, mocked data', () => { // Event mock which will trigger Cloud to re-fetch DBaaS instance. const eventMock = eventFactory.build({ - status: 'finished', action: 'database_create', - percent_complete: 100, entity: { - label: databaseMock.label, id: databaseMock.id, + label: databaseMock.label, type: 'database', url: `/v4/databases/${configuration.dbType}/instances/${databaseMock.id}`, }, + percent_complete: 100, secondary_entity: undefined, + status: 'finished', }); const clusterSizeSelection = diff --git a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts index e055dd40302..9d6565873c4 100644 --- a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts @@ -2,8 +2,10 @@ * @file DBaaS integration tests for delete operations. */ -import { accountFactory, databaseFactory } from 'src/factories'; -import { randomNumber, randomIp } from 'support/util/random'; +import { + databaseConfigurations, + mockDatabaseNodeTypes, +} from 'support/constants/databases'; import { mockGetAccount } from 'support/intercepts/account'; import { mockDeleteDatabase, @@ -12,11 +14,11 @@ import { mockGetDatabaseTypes, } from 'support/intercepts/databases'; import { ui } from 'support/ui'; -import { - databaseClusterConfiguration, - databaseConfigurations, - mockDatabaseNodeTypes, -} from 'support/constants/databases'; +import { randomIp, randomNumber } from 'support/util/random'; + +import { accountFactory, databaseFactory } from 'src/factories'; + +import type { databaseClusterConfiguration } from 'support/constants/databases'; describe('Delete database clusters', () => { databaseConfigurations.forEach( @@ -30,13 +32,13 @@ describe('Delete database clusters', () => { it('Can delete active database clusters', () => { const allowedIp = randomIp(); const database = databaseFactory.build({ + allow_list: [allowedIp], + engine: configuration.dbType, id: randomNumber(1, 1000), - type: configuration.linodeType, label: configuration.label, region: configuration.region.id, - engine: configuration.dbType, status: 'active', - allow_list: [allowedIp], + type: configuration.linodeType, }); // Mock account to ensure 'Managed Databases' capability. @@ -83,17 +85,17 @@ describe('Delete database clusters', () => { */ it('Cannot delete provisioning database clusters', () => { const database = databaseFactory.build({ - id: randomNumber(1, 1000), - type: configuration.linodeType, - label: configuration.label, - region: configuration.region.id, - engine: configuration.dbType, - status: 'provisioning', allow_list: [], + engine: configuration.dbType, hosts: { primary: undefined, secondary: undefined, }, + id: randomNumber(1, 1000), + label: configuration.label, + region: configuration.region.id, + status: 'provisioning', + type: configuration.linodeType, }); const errorMessage = diff --git a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts index 0ebcbae629e..d3222977fb0 100644 --- a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts @@ -2,9 +2,11 @@ * @file DBaaS integration tests for resize operations. */ -import { randomNumber, randomIp, randomString } from 'support/util/random'; -import { databaseFactory, possibleStatuses } from 'src/factories/databases'; -import { ui } from 'support/ui'; +import { accountFactory } from '@src/factories'; +import { + databaseConfigurationsResize, + mockDatabaseNodeTypes, +} from 'support/constants/databases'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetDatabase, @@ -13,12 +15,12 @@ import { mockResize, mockResizeProvisioningDatabase, } from 'support/intercepts/databases'; -import { - databaseClusterConfiguration, - databaseConfigurationsResize, - mockDatabaseNodeTypes, -} from 'support/constants/databases'; -import { accountFactory } from '@src/factories'; +import { ui } from 'support/ui'; +import { randomIp, randomNumber, randomString } from 'support/util/random'; + +import { databaseFactory, possibleStatuses } from 'src/factories/databases'; + +import type { databaseClusterConfiguration } from 'support/constants/databases'; /** * Resizes a current database cluster to a larger plan size. @@ -63,15 +65,15 @@ describe('Resizing existing clusters', () => { const allowedIp = randomIp(); const initialPassword = randomString(16); const database = databaseFactory.build({ + allow_list: [allowedIp], + cluster_size: 3, + engine: configuration.dbType, id: randomNumber(1, 1000), - type: configuration.linodeType, label: initialLabel, + platform: 'rdbms-legacy', region: configuration.region.id, - engine: configuration.dbType, - cluster_size: 3, status: 'active', - allow_list: [allowedIp], - platform: 'rdbms-legacy', + type: configuration.linodeType, }); // Mock account to ensure 'Managed Databases' capability. @@ -212,14 +214,14 @@ describe('Resizing existing clusters', () => { const allowedIp = randomIp(); const initialPassword = randomString(16); const database = databaseFactory.build({ + allow_list: [allowedIp], + cluster_size: 3, + engine: configuration.dbType, id: randomNumber(1, 1000), - type: configuration.linodeType, label: initialLabel, region: configuration.region.id, - engine: configuration.dbType, - cluster_size: 3, status: 'active', - allow_list: [allowedIp], + type: configuration.linodeType, }); // Mock account to ensure 'Managed Databases' capability. @@ -291,18 +293,18 @@ describe('Resizing existing clusters', () => { const initialLabel = configuration.label; const allowedIp = randomIp(); const database = databaseFactory.build({ - id: randomNumber(1, 1000), - type: configuration.linodeType, - label: initialLabel, - region: configuration.region.id, - engine: configuration.dbType, - cluster_size: 3, - status: dbstatus, allow_list: [allowedIp], + cluster_size: 3, + engine: configuration.dbType, hosts: { primary: undefined, secondary: undefined, }, + id: randomNumber(1, 1000), + label: initialLabel, + region: configuration.region.id, + status: dbstatus, + type: configuration.linodeType, }); const errorMessage = `Your database is ${dbstatus}; please wait until it becomes active to perform this operation.`; diff --git a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts index 639d29e791e..fd998f69319 100644 --- a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts @@ -2,14 +2,11 @@ * @file DBaaS integration tests for update operations. */ +import { accountFactory } from '@src/factories'; import { - randomLabel, - randomNumber, - randomIp, - randomString, -} from 'support/util/random'; -import { databaseFactory } from 'src/factories/databases'; -import { ui } from 'support/ui'; + databaseConfigurations, + mockDatabaseNodeTypes, +} from 'support/constants/databases'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetDatabase, @@ -20,13 +17,18 @@ import { mockUpdateDatabase, mockUpdateProvisioningDatabase, } from 'support/intercepts/databases'; -import { - databaseClusterConfiguration, - databaseConfigurations, - mockDatabaseNodeTypes, -} from 'support/constants/databases'; -import { accountFactory } from '@src/factories'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { ui } from 'support/ui'; +import { + randomIp, + randomLabel, + randomNumber, + randomString, +} from 'support/util/random'; + +import { databaseFactory } from 'src/factories/databases'; + +import type { databaseClusterConfiguration } from 'support/constants/databases'; /** * Updates a database cluster's label. @@ -164,7 +166,7 @@ describe('Update database clusters', () => { ], }); mockAppendFeatureFlags({ - dbaasV2: { enabled: false, beta: false }, + dbaasV2: { beta: false, enabled: false }, }); mockGetAccount(mockAccount); }); @@ -186,14 +188,14 @@ describe('Update database clusters', () => { const newAllowedIp = randomIp(); const initialPassword = randomString(16); const database = databaseFactory.build({ + allow_list: [allowedIp], + engine: configuration.dbType, id: randomNumber(1, 1000), - type: configuration.linodeType, label: initialLabel, + platform: 'rdbms-legacy', region: configuration.region.id, - engine: configuration.dbType, status: 'active', - allow_list: [allowedIp], - platform: 'rdbms-legacy', + type: configuration.linodeType, }); mockGetDatabase(database).as('getDatabase'); @@ -298,18 +300,18 @@ describe('Update database clusters', () => { const updateAttemptLabel = randomLabel(); const allowedIp = randomIp(); const database = databaseFactory.build({ - id: randomNumber(1, 1000), - type: configuration.linodeType, - label: initialLabel, - region: configuration.region.id, - engine: configuration.dbType, - status: 'provisioning', allow_list: [allowedIp], + engine: configuration.dbType, hosts: { primary: undefined, secondary: undefined, }, + id: randomNumber(1, 1000), + label: initialLabel, platform: 'rdbms-legacy', + region: configuration.region.id, + status: 'provisioning', + type: configuration.linodeType, }); const errorMessage = From 63a250c06516fc16e8a62ebae1d82310022dd3dc Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:48:38 -0500 Subject: [PATCH 055/219] chore: [M3-9440] - Improve Banner Spacing (#11724) * improve-banner-spacing * update test * add changeset --------- Co-authored-by: Banks Nussman --- .../src/features/GlobalNotifications/TokensUpdateBanner.tsx | 2 +- packages/ui/.changeset/pr-11724-changed-1740590596984.md | 5 +++++ packages/ui/src/components/Notice/Notice.test.tsx | 2 +- packages/ui/src/components/Notice/Notice.tsx | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 packages/ui/.changeset/pr-11724-changed-1740590596984.md diff --git a/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx b/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx index 8ee2c177479..496a5f086ea 100644 --- a/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx +++ b/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx @@ -30,7 +30,7 @@ export const DesignUpdateBanner = () => { */ return ( - + We are improving the Cloud Manager experience for our users.{' '} Read more about recent updates. diff --git a/packages/ui/.changeset/pr-11724-changed-1740590596984.md b/packages/ui/.changeset/pr-11724-changed-1740590596984.md new file mode 100644 index 00000000000..b757bf0c247 --- /dev/null +++ b/packages/ui/.changeset/pr-11724-changed-1740590596984.md @@ -0,0 +1,5 @@ +--- +"@linode/ui": Changed +--- + +`Notice`'s default `marginBottom` from `24px` to `8px` ([#11724](https://github.com/linode/manager/pull/11724)) diff --git a/packages/ui/src/components/Notice/Notice.test.tsx b/packages/ui/src/components/Notice/Notice.test.tsx index 2e8675d54fa..f2777a5e3ef 100644 --- a/packages/ui/src/components/Notice/Notice.test.tsx +++ b/packages/ui/src/components/Notice/Notice.test.tsx @@ -10,7 +10,7 @@ describe('Notice Component', () => { const { container } = renderWithTheme(); const notice = container.firstChild; - expect(notice).toHaveStyle('margin-bottom: 24px'); + expect(notice).toHaveStyle('margin-bottom: 8px'); expect(notice).toHaveStyle('margin-left: 0'); expect(notice).toHaveStyle('margin-top: 0'); }); diff --git a/packages/ui/src/components/Notice/Notice.tsx b/packages/ui/src/components/Notice/Notice.tsx index a0e9b0443e3..7ee39f3f248 100644 --- a/packages/ui/src/components/Notice/Notice.tsx +++ b/packages/ui/src/components/Notice/Notice.tsx @@ -143,7 +143,7 @@ export const Notice = (props: NoticeProps) => { marginBottom: spacingBottom !== undefined ? `${spacingBottom}px` - : theme.spacing(3), + : theme.spacing(1), marginLeft: spacingLeft !== undefined ? `${spacingLeft}px` : 0, marginTop: spacingTop !== undefined ? `${spacingTop}px` : 0, }), From f7a0656184fb46885dff5a37660428c986118f45 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:47:40 -0800 Subject: [PATCH 056/219] upcoming: [M3-8844] - Add LKE-E final copy and update some existing LKE copy (#11664) * Update cluster tier copy * Update Region and Version copy and link * Update checkout bar * Make LKE details CTAs copy updates * Fix typo * Update test for new recycle copy * Move constants from utils to constants file * Update create flow test for docs links and checkout summary * Update autoscaler copy now rather than in drawer refactor * Improve responsive design of cluster tier panel * Update copy for resize drawer * Revert copy change to AddNodePoolDrawer; intended for Resize Drawer * Add changesets * Fix selection card styling * Attempt styled stack clean up * Improve Autoscale copy and validation error * Update copy in autoscale and resize integration tests * Update missed placeholder copy for nodes when provisioning * Add changeset for validation * Make kubernetes docs links external * Make more updates after UX/Prod/TR review --- .../pr-11664-changed-1739979366424.md | 5 ++ ...r-11664-upcoming-features-1739979306430.md | 5 ++ .../e2e/core/kubernetes/lke-create.spec.ts | 21 ++++-- .../e2e/core/kubernetes/lke-update.spec.ts | 64 +++++++++++-------- .../SelectionCard/SelectionCard.tsx | 2 +- ...sterTypePanel.tsx => ClusterTierPanel.tsx} | 38 +++++------ .../CreateCluster/CreateCluster.styles.ts | 4 +- .../CreateCluster/CreateCluster.tsx | 62 +++++++++++------- .../KubeCheckoutBar/KubeCheckoutBar.tsx | 5 +- .../NodePoolsDisplay/AddNodePoolDrawer.tsx | 2 +- .../NodePoolsDisplay/AutoscalePoolDialog.tsx | 15 +++-- .../LabelsAndTaints/LabelAndTaintDrawer.tsx | 16 +++-- .../NodePoolsDisplay/NodeTable.test.tsx | 2 +- .../NodePoolsDisplay/NodeTable.tsx | 4 +- .../NodePoolsDisplay/RecycleNodeDialog.tsx | 6 +- .../NodePoolsDisplay/ResizeNodePoolDrawer.tsx | 27 ++++---- .../RecycleClusterDialog.tsx | 9 +-- .../RecycleNodePoolDialog.tsx | 9 +-- .../UpgradeClusterDialog.tsx | 2 +- .../Kubernetes/UpgradeVersionModal.tsx | 3 +- .../src/features/Kubernetes/constants.ts | 10 +++ .../src/features/Kubernetes/kubeUtils.ts | 3 - .../pr-11664-added-1740008383592.md | 5 ++ packages/validation/src/kubernetes.schema.ts | 4 +- 24 files changed, 191 insertions(+), 132 deletions(-) create mode 100644 packages/manager/.changeset/pr-11664-changed-1739979366424.md create mode 100644 packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md rename packages/manager/src/features/Kubernetes/CreateCluster/{ClusterTypePanel.tsx => ClusterTierPanel.tsx} (65%) create mode 100644 packages/manager/src/features/Kubernetes/constants.ts create mode 100644 packages/validation/.changeset/pr-11664-added-1740008383592.md diff --git a/packages/manager/.changeset/pr-11664-changed-1739979366424.md b/packages/manager/.changeset/pr-11664-changed-1739979366424.md new file mode 100644 index 00000000000..6bd40d0fc99 --- /dev/null +++ b/packages/manager/.changeset/pr-11664-changed-1739979366424.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update copy in Node Pool resize, autoscale, and recycle CTAs ([#11664](https://github.com/linode/manager/pull/11664)) diff --git a/packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md b/packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md new file mode 100644 index 00000000000..3425af6a526 --- /dev/null +++ b/packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add final copy and docs links for LKE-E ([#11664](https://github.com/linode/manager/pull/11664)) 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 32434240d7d..2b4ee311605 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -48,6 +48,10 @@ import { mockGetLinodeTypes } from 'support/intercepts/linodes'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { chooseRegion } from 'support/util/regions'; import { getTotalClusterMemoryCPUAndStorage } from 'src/features/Kubernetes/kubeUtils'; +import { + CLUSTER_TIER_DOCS_LINK, + CLUSTER_VERSIONS_DOCS_LINK, +} from 'src/features/Kubernetes/constants'; import { getTotalClusterPrice } from 'src/utilities/pricing/kubernetes'; import type { ExtendedType } from 'src/utilities/extendType'; @@ -1178,6 +1182,10 @@ describe('LKE Cluster Creation with LKE-E', () => { cy.findByText('Cluster Tier').should('be.visible'); + cy.findByText('Compare Tiers') + .should('be.visible') + .should('have.attr', 'href', CLUSTER_TIER_DOCS_LINK); + // Confirm both Cluster Tiers exist and the LKE card is selected by default cy.get(`[data-qa-select-card-heading="LKE"]`) .closest('[data-qa-selection-card]') @@ -1210,7 +1218,7 @@ describe('LKE Cluster Creation with LKE-E', () => { // Confirm that there is a tooltip explanation for the region dropdown options ui.tooltip .findByText( - 'Only regions that support Kubernetes Enterprise are listed.' + 'Only regions that support LKE Enterprise clusters are listed.' ) .should('be.visible'); @@ -1220,6 +1228,10 @@ describe('LKE Cluster Creation with LKE-E', () => { .should('be.visible') .click(); + cy.findByText('Kubernetes Versions') + .should('be.visible') + .should('have.attr', 'href', CLUSTER_VERSIONS_DOCS_LINK); + ui.autocompletePopper .findByTitle(latestEnterpriseTierKubernetesVersion.id) .should('be.visible') @@ -1271,14 +1283,11 @@ describe('LKE Cluster Creation with LKE-E', () => { // Confirm LKE-E section is shown cy.findByText('LKE Enterprise').should('be.visible'); - cy.findByText('HA control plane, Dedicated control plane').should( - 'be.visible' - ); cy.findByText('$300.00/month').should('be.visible'); - cy.findByText(`Dedicated 4 GB Plan`).should('be.visible'); + cy.findByText('Dedicated 4 GB Plan').should('be.visible'); cy.findByText('$144.00').should('be.visible'); - cy.findByText(`Linode 2 GB Plan`).should('be.visible'); + cy.findByText('Linode 2 GB Plan').should('be.visible'); cy.findByText('$15.00').should('be.visible'); cy.findByText('$459.00').should('be.visible'); diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 7887107e5f5..eda579c7e42 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -390,8 +390,6 @@ describe('LKE cluster updates', () => { }); const recycleWarningSubstrings = [ - 'will be deleted', - 'will be created', 'local storage (such as ’hostPath’ volumes) will be erased', 'may take several minutes', ]; @@ -418,6 +416,9 @@ describe('LKE cluster updates', () => { .findByTitle(`Recycle ${mockKubeLinode.id}?`) .should('be.visible') .within(() => { + cy.findByText('Redeploy this node in the node pool.', { + exact: false, + }).should('be.visible'); recycleWarningSubstrings.forEach((warning: string) => { cy.findByText(warning, { exact: false }).should('be.visible'); }); @@ -445,6 +446,13 @@ describe('LKE cluster updates', () => { .findByTitle('Recycle node pool?') .should('be.visible') .within(() => { + cy.findByText('Redeploy all nodes in the node pool.', { + exact: false, + }).should('be.visible'); + recycleWarningSubstrings.forEach((warning: string) => { + cy.findByText(warning, { exact: false }).should('be.visible'); + }); + ui.button .findByTitle('Recycle Pool Nodes') .should('be.visible') @@ -468,6 +476,9 @@ describe('LKE cluster updates', () => { .findByTitle('Recycle all nodes in cluster?') .should('be.visible') .within(() => { + cy.findByText('Redeploy all nodes in the cluster.', { + exact: false, + }).should('be.visible'); recycleWarningSubstrings.forEach((warning: string) => { cy.findByText(warning, { exact: false }).should('be.visible'); }); @@ -541,7 +552,7 @@ describe('LKE cluster updates', () => { .findByTitle('Autoscale Pool') .should('be.visible') .within(() => { - cy.findByText('Autoscaler').should('be.visible').click(); + cy.findByText('Autoscale').should('be.visible').click(); cy.findByLabelText('Min') .should('be.visible') @@ -594,7 +605,7 @@ describe('LKE cluster updates', () => { .findByTitle('Autoscale Pool') .should('be.visible') .within(() => { - cy.findByText('Autoscaler').should('be.visible').click(); + cy.findByText('Autoscale').should('be.visible').click(); ui.button .findByTitle('Save Changes') @@ -705,9 +716,12 @@ describe('LKE cluster updates', () => { .should('be.visible') .should('be.disabled'); - cy.findByText('Resized pool: $12/month (1 node at $12/month)').should( - 'be.visible' - ); + cy.findByText( + 'Current price: $12/month (1 node at $12/month each)' + ).should('be.visible'); + cy.findByText( + 'Resized price: $12/month (1 node at $12/month each)' + ).should('be.visible'); cy.findByLabelText('Add 1') .should('be.visible') @@ -717,7 +731,7 @@ describe('LKE cluster updates', () => { cy.findByLabelText('Edit Quantity').should('have.value', '3'); cy.findByText( - 'Resized pool: $36/month (3 nodes at $12/month)' + 'Resized price: $36/month (3 nodes at $12/month each)' ).should('be.visible'); ui.button @@ -1985,10 +1999,10 @@ describe('LKE cluster updates', () => { .should('be.disabled'); cy.findByText( - 'Current pool: $14.40/month (1 node at $14.40/month)' + 'Current price: $14.40/month (1 node at $14.40/month each)' ).should('be.visible'); cy.findByText( - 'Resized pool: $14.40/month (1 node at $14.40/month)' + 'Resized price: $14.40/month (1 node at $14.40/month each)' ).should('be.visible'); cy.findByLabelText('Add 1') @@ -2000,10 +2014,10 @@ describe('LKE cluster updates', () => { cy.findByLabelText('Edit Quantity').should('have.value', '4'); cy.findByText( - 'Current pool: $14.40/month (1 node at $14.40/month)' + 'Current price: $14.40/month (1 node at $14.40/month each)' ).should('be.visible'); cy.findByText( - 'Resized pool: $57.60/month (4 nodes at $14.40/month)' + 'Resized price: $57.60/month (4 nodes at $14.40/month each)' ).should('be.visible'); cy.findByLabelText('Subtract 1') @@ -2013,7 +2027,7 @@ describe('LKE cluster updates', () => { cy.findByLabelText('Edit Quantity').should('have.value', '3'); cy.findByText( - 'Resized pool: $43.20/month (3 nodes at $14.40/month)' + 'Resized price: $43.20/month (3 nodes at $14.40/month each)' ).should('be.visible'); ui.button @@ -2234,12 +2248,12 @@ describe('LKE cluster updates', () => { .should('be.visible') .should('be.disabled'); - cy.findByText('Current pool: $0/month (1 node at $0/month)').should( - 'be.visible' - ); - cy.findByText('Resized pool: $0/month (1 node at $0/month)').should( - 'be.visible' - ); + cy.findByText( + 'Current price: $0/month (1 node at $0/month each)' + ).should('be.visible'); + cy.findByText( + 'Resized price: $0/month (1 node at $0/month each)' + ).should('be.visible'); cy.findByLabelText('Add 1') .should('be.visible') @@ -2249,12 +2263,12 @@ describe('LKE cluster updates', () => { .click(); cy.findByLabelText('Edit Quantity').should('have.value', '4'); - cy.findByText('Current pool: $0/month (1 node at $0/month)').should( - 'be.visible' - ); - cy.findByText('Resized pool: $0/month (4 nodes at $0/month)').should( - 'be.visible' - ); + cy.findByText( + 'Current price: $0/month (1 node at $0/month each)' + ).should('be.visible'); + cy.findByText( + 'Resized price: $0/month (4 nodes at $0/month each)' + ).should('be.visible'); ui.button .findByTitle('Save Changes') diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index c85f9a0af8e..4e92e47ea6b 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -211,7 +211,7 @@ const StyledGrid = styled(Grid, { cursor: 'pointer', }), ...(props.disabled && { - '& .cardSubheadingItem, & .cardSubheadingTitle': { + '& .cardSubheadingItem, & .cardSubheadingTitle, & p': { opacity: 0.3, }, cursor: 'not-allowed', diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTypePanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx similarity index 65% rename from packages/manager/src/features/Kubernetes/CreateCluster/ClusterTypePanel.tsx rename to packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx index 843420acd7c..8134b92c32a 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTypePanel.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx @@ -6,25 +6,26 @@ import { DocsLink } from 'src/components/DocsLink/DocsLink'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { useAccount } from 'src/queries/account/account'; -import { StyledDocsLinkContainer } from './CreateCluster.styles'; +import { CLUSTER_TIER_DOCS_LINK } from '../constants'; +import { + StyledDocsLinkContainer, + StyledStackWithTabletBreakpoint, +} from './CreateCluster.styles'; import type { KubernetesTier } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; interface Props { - handleClusterTypeSelection: (tier: KubernetesTier) => void; + handleClusterTierSelection: (tier: KubernetesTier) => void; isUserRestricted: boolean; selectedTier: KubernetesTier; } -export const ClusterTypePanel = (props: Props) => { - const { handleClusterTypeSelection, isUserRestricted, selectedTier } = props; +export const ClusterTierPanel = (props: Props) => { + const { handleClusterTierSelection, isUserRestricted, selectedTier } = props; const { data: account } = useAccount(); - const mdDownBreakpoint = useMediaQuery((theme: Theme) => - theme.breakpoints.down('md') - ); const smDownBreakpoint = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm') ); @@ -35,18 +36,19 @@ export const ClusterTypePanel = (props: Props) => { return ( - + Cluster Tier - Choose from a managed solution for smaller deployments or enterprise - grade clusters with enhanced ingress, networking, and security. + Select the cluster tier that’s appropriate for your intended + workloads. Choose LKE for smaller deployments or LKE Enterprise for + a more scalable, enterprise-grade solution. - + - + { > handleClusterTypeSelection('standard')} + onClick={() => handleClusterTierSelection('standard')} /> handleClusterTypeSelection('enterprise')} + onClick={() => handleClusterTierSelection('enterprise')} tooltipPlacement={smDownBreakpoint ? 'bottom' : 'right'} /> diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.styles.ts b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.styles.ts index 3c4227e1a8a..b37153379c4 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.styles.ts +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.styles.ts @@ -41,8 +41,8 @@ export const useStyles = makeStyles()((theme: Theme) => ({ }, })); -export const StyledFieldWithDocsStack = styled(Stack, { - label: 'StyledFieldWithDocsStack', +export const StyledStackWithTabletBreakpoint = styled(Stack, { + label: 'StyledStackWithTabletBreakpoint', })(({ theme }) => ({ flexDirection: 'row', [theme.breakpoints.down('md')]: { diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 7f3db1a31a0..36eecf284cf 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -40,18 +40,19 @@ import { extendType } from 'src/utilities/extendType'; import { filterCurrentTypes } from 'src/utilities/filterCurrentLinodeTypes'; import { stringToExtendedIP } from 'src/utilities/ipUtils'; import { plansNoticesUtils } from 'src/utilities/planNotices'; -import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants'; +import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing'; import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; +import { CLUSTER_VERSIONS_DOCS_LINK } from '../constants'; import KubeCheckoutBar from '../KubeCheckoutBar'; import { ApplicationPlatform } from './ApplicationPlatform'; -import { ClusterTypePanel } from './ClusterTypePanel'; +import { ClusterTierPanel } from './ClusterTierPanel'; import { ControlPlaneACLPane } from './ControlPlaneACLPane'; import { StyledDocsLinkContainer, - StyledFieldWithDocsStack, + StyledStackWithTabletBreakpoint, useStyles, } from './CreateCluster.styles'; import { HAControlPlane } from './HAControlPlane'; @@ -107,7 +108,7 @@ export const CreateCluster = () => { isLoading: isLoadingKubernetesTypes, } = useKubernetesTypesQuery(selectedTier === 'enterprise'); - const handleClusterTypeSelection = (tier: KubernetesTier) => { + const handleClusterTierSelection = (tier: KubernetesTier) => { setSelectedTier(tier); // HA is enabled by default for enterprise clusters @@ -363,15 +364,15 @@ export const CreateCluster = () => { {isLkeEnterpriseLAFlagEnabled && ( <> - )} - + { tooltipText={ isLkeEnterpriseLAFeatureEnabled && selectedTier === 'enterprise' - ? 'Only regions that support Kubernetes Enterprise are listed.' + ? 'Only regions that support LKE Enterprise clusters are listed.' : undefined } disableClearable @@ -406,32 +407,45 @@ export const CreateCluster = () => { label={DOCS_LINK_LABEL_DC_PRICING} /> - + - { - setVersion(selected?.value); - }} - disableClearable={!!version} - disabled={isCreateClusterRestricted} - errorText={errorMap.k8s_version} - label="Kubernetes Version" - loading={isLoadingVersions} - options={versions} - placeholder={' '} - value={versions.find((v) => v.value === version) ?? null} - /> + + + { + setVersion(selected?.value); + }} + disableClearable={!!version} + disabled={isCreateClusterRestricted} + errorText={errorMap.k8s_version} + label="Kubernetes Version" + loading={isLoadingVersions} + options={versions} + placeholder={' '} + sx={{ minWidth: 416 }} + value={versions.find((v) => v.value === version) ?? null} + /> + + ({ marginTop: theme.spacing(2) })} + > + + + {showAPL && ( <> - + - + )} diff --git a/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx b/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx index 7f7a139c291..99796fc2064 100644 --- a/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx +++ b/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.tsx @@ -20,7 +20,7 @@ import { getTotalClusterPrice, } from 'src/utilities/pricing/kubernetes'; -import { nodeWarning } from '../kubeUtils'; +import { nodeWarning } from '../constants'; import { StyledBox, StyledHeader } from './KubeCheckoutSummary.styles'; import { NodePoolSummaryItem } from './NodePoolSummaryItem'; @@ -141,9 +141,6 @@ export const KubeCheckoutBar = (props: Props) => { LKE Enterprise - - HA control plane, Dedicated control plane - {`$${enterprisePrice?.toFixed( 2 )}/month`} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx index f4813602ffb..7b42502a2a5 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx @@ -18,7 +18,7 @@ import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; import { KubernetesPlansPanel } from '../../KubernetesPlansPanel/KubernetesPlansPanel'; -import { nodeWarning } from '../../kubeUtils'; +import { nodeWarning } from '../../constants'; import { hasInvalidNodePoolPrice } from './utils'; import type { KubernetesTier, Region } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx index 28573361c8b..005b11e72a0 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AutoscalePoolDialog.tsx @@ -162,12 +162,12 @@ export const AutoscalePoolDialog = (props: Props) => { ) : null} - Set minimum and maximum node pool constraints for LKE to resize your - cluster automatically based on resource demand and overall usage. - Maximum limit is 100 nodes.{' '} - - Learn more. + Enable the built-in autoscaler to automatically add and remove nodes + based on resource demand and usage.{' '} + + Learn more + . { onChange={handleChange} /> } - label="Autoscaler" + label="Autoscale" style={{ marginTop: 12 }} /> + + Define the minimum and maximum node constraints: + { Labels are key-value pairs that are used as identifiers. Review the guidelines in the{' '} - - Kubernetes documentation + + Kubernetes documentation. - .
');r("
").addClass(t.section).attr("data-id",e).html(a({classes:t,labels:o})).appendTo("."+t.body),n.appendTo("."+i+" ."+t.sectionBody)}}function s(e){function n(){return""+(new Date).getTime()}function t(e,n){var t=r('[data-id="'+e+'"]');return t.length?t:null}function o(e,n,t){var o,i,a;"accordion"===pendo.guideWidget.data.templateName?(i="_pendo-launcher-accordion_ _pendo-launcher-"+e+"_",a="._pendo-launcher-body_",o=d.template("
')):(i=n.menuItem,a="."+n.menu,o=d.template(''));var s=r("
").addClass(i).attr("data-id",e).html(o({classes:n,labels:t})).appendTo(a);return r("button",s)}this.data=e,this.use="launcher",this.name=this.data.name,this.getName=function(){return this.name},this.getClasses=function(){return e.options.classes?{menu:e.options.classes.menu||"_pendo-launcher-menu_",menuItem:e.options.classes.menuItem||"_pendo-launcher-menu-item_",button:e.options.classes.button||"_pendo-launcher-menu-item-button_",buttonTitle:e.options.classes.buttonTitle||"_pendo-launcher-menu-item-title_",buttonDescription:e.options.classes.buttonDescription||"_pendo-launcher-menu-item-description_",body:e.options.classes.body||"_pendo-launcher-body_",section:e.options.classes.section||"_pendo-launcher-section-content_",sectionHeader:e.options.classes.sectionHeader||"_pendo-launcher-section-header_",sectionTitle:e.options.classes.sectionTitle||"_pendo-launcher-section-title_",sectionBody:e.options.classes.sectionDescription||"_pendo-launcher-section-body_",sectionBackButton:e.options.classes.sectionBackButton||"_pendo-launcher-section-back-button_"}:{menu:"_pendo-launcher-menu_",menuItem:"_pendo-launcher-menu-item_",button:"_pendo-launcher-menu-item-button_",buttonTitle:"_pendo-launcher-menu-item-title_",buttonDescription:"_pendo-launcher-menu-item-description_",body:"_pendo-launcher-body_",section:"_pendo-launcher-section-content_",sectionHeader:"_pendo-launcher-section-header_",sectionTitle:"_pendo-launcher-section-title_",sectionBody:"_pendo-launcher-section-body_",sectionBackButton:"_pendo-launcher-section-back-button_"}},this.getLabels=function(){return{title:this.data.options.labels.title||this.name,description:this.data.options.labels.description}},this.getConfig=function(){return this.data},this.getFrame=function(){if(!this.frame){var e=["allow-forms","allow-modals","allow-pointer-lock","allow-popups","allow-popups-to-escape-sandbox","allow-scripts","allow-same-origin","allow-top-navigation"],t=this.data.uri,o=new URL(t).hostname,i="chat.app.pendo.io";o===i&&"app.eu.pendo.io"===new URL(window.location).hostname&&(t=t.replace(i,"chat.app.eu.pendo.io")),this.frame=r("