diff --git a/packages/api-v4/.changeset/pr-10264-changed-1709750042330.md b/packages/api-v4/.changeset/pr-10264-changed-1709750042330.md new file mode 100644 index 00000000000..dbec42a1174 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10264-changed-1709750042330.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Made `match_condition` optional in Rule types to support TCP rules ([#10264](https://github.com/linode/manager/pull/10264)) diff --git a/packages/api-v4/src/aclb/types.ts b/packages/api-v4/src/aclb/types.ts index e8c1a692ddf..4eb00ce4b00 100644 --- a/packages/api-v4/src/aclb/types.ts +++ b/packages/api-v4/src/aclb/types.ts @@ -61,7 +61,7 @@ export interface Route { label: string; protocol: RouteProtocol; rules: { - match_condition: MatchCondition; + match_condition?: MatchCondition; service_targets: { id: number; label: string; @@ -83,7 +83,7 @@ export interface CreateRoutePayload { } export interface Rule { - match_condition: MatchCondition; + match_condition?: MatchCondition; service_targets: { id: number; label: string; @@ -92,7 +92,7 @@ export interface Rule { } export interface RulePayload { - match_condition: MatchCondition; + match_condition?: MatchCondition; service_targets: { id: number; label: string; diff --git a/packages/manager/.changeset/pr-10264-fixed-1709749998087.md b/packages/manager/.changeset/pr-10264-fixed-1709749998087.md new file mode 100644 index 00000000000..148c25e2055 --- /dev/null +++ b/packages/manager/.changeset/pr-10264-fixed-1709749998087.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +ACLB TCP rule creation ([#10264](https://github.com/linode/manager/pull/10264)) diff --git a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts index bcf952ca27b..35336e7b045 100644 --- a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts +++ b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts @@ -540,7 +540,7 @@ describe('Akamai Cloud Load Balancer routes page', () => { .should('be.visible') .within(() => { cy.findByLabelText('Hostname Match (optional)') - .should('have.value', routes[0].rules[0].match_condition.hostname) + .should('have.value', routes[0].rules[0].match_condition!.hostname) .clear() .type('example.com'); @@ -558,7 +558,7 @@ describe('Akamai Cloud Load Balancer routes page', () => { cy.findByLabelText('Match Value') .should( 'have.value', - routes[0].rules[0].match_condition.match_value + routes[0].rules[0].match_condition!.match_value ) .clear() .type('x-header=my-header-value'); @@ -604,7 +604,7 @@ describe('Akamai Cloud Load Balancer routes page', () => { // Verify all rules are shown for (const rule of routes[0].rules) { - cy.findByText(rule.match_condition.match_value).should('be.visible'); + cy.findByText(rule.match_condition!.match_value).should('be.visible'); } const indexOfRuleToDelete = 1; @@ -625,7 +625,7 @@ describe('Akamai Cloud Load Balancer routes page', () => { // Verify the deleted rule no longer shows cy.findByText( - routes[0].rules[indexOfRuleToDelete].match_condition.match_value + routes[0].rules[indexOfRuleToDelete].match_condition!.match_value ).should('not.exist'); }); }); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.test.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.test.tsx index dfe4b9509ec..1e60a18b722 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.test.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.test.tsx @@ -54,7 +54,7 @@ describe('RuleDrawer', () => { const hostnameField = getByLabelText('Hostname Match (optional)'); expect(hostnameField).toHaveDisplayValue( - props.route.rules[0].match_condition.hostname! + props.route.rules[0].match_condition!.hostname! ); const matchTypeField = getByLabelText('Match Type'); @@ -62,17 +62,17 @@ describe('RuleDrawer', () => { const matchValueField = getByLabelText('Match Value'); expect(matchValueField).toHaveDisplayValue( - props.route.rules[0].match_condition.match_value + props.route.rules[0].match_condition!.match_value ); const cookieField = getByLabelText('Cookie Key'); expect(cookieField).toHaveDisplayValue( - props.route.rules[0].match_condition.session_stickiness_cookie! + props.route.rules[0].match_condition!.session_stickiness_cookie! ); const ttlField = getByLabelText('Stickiness TTL'); expect(ttlField).toHaveDisplayValue( - String(props.route.rules[0].match_condition.session_stickiness_ttl) + String(props.route.rules[0].match_condition!.session_stickiness_ttl) ); }); }); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.tsx index a9c544a9d93..a38ff5da44f 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/RuleDrawer.tsx @@ -21,16 +21,16 @@ import { useLoadBalancerRouteUpdateMutation } from 'src/queries/aclb/routes'; import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils'; import { ServiceTargetSelect } from '../ServiceTargets/ServiceTargetSelect'; -import { MatchTypeInfo } from './MatchTypeInfo'; import { ROUTE_COPY } from './constants'; +import { MatchTypeInfo } from './MatchTypeInfo'; import { TimeUnit, defaultServiceTarget, defaultTTL, defaultTTLUnit, + getInitialValues, getIsSessionStickinessEnabled, getNormalizedRulePayload, - initialValues, matchTypeOptions, matchValuePlaceholder, stickyOptions, @@ -77,6 +77,8 @@ export const RuleDrawer = (props: Props) => { const [ttlUnit, setTTLUnit] = useState(defaultTTLUnit); + const initialValues = getInitialValues(protocol); + const formik = useFormik({ enableReinitialize: true, initialValues: isEditMode @@ -155,7 +157,7 @@ export const RuleDrawer = (props: Props) => { const isStickinessEnabled = getIsSessionStickinessEnabled(formik.values); const cookieType = - formik.values.match_condition.session_stickiness_ttl === null + formik.values.match_condition?.session_stickiness_ttl === null ? stickyOptions[1] : stickyOptions[0]; @@ -219,11 +221,7 @@ export const RuleDrawer = (props: Props) => { {route?.protocol !== 'tcp' && ( <> { onChange={formik.handleChange} optional placeholder="www.example.org" - value={formik.values.match_condition.hostname} + value={formik.values.match_condition?.hostname} /> formik.setFieldTouched('match_condition.match_field') } @@ -253,7 +250,7 @@ export const RuleDrawer = (props: Props) => { matchTypeOptions.find( (option) => option.value === - formik.values.match_condition.match_field + formik.values.match_condition?.match_field ) ?? matchTypeOptions[0] } disableClearable @@ -264,18 +261,20 @@ export const RuleDrawer = (props: Props) => { /> { name="match_condition.match_value" onBlur={formik.handleBlur} onChange={formik.handleChange} - value={formik.values.match_condition.match_value} + value={formik.values.match_condition?.match_value} /> @@ -419,14 +418,19 @@ export const RuleDrawer = (props: Props) => { /> { @@ -453,7 +462,7 @@ export const RuleDrawer = (props: Props) => { } value={ (formik.values.match_condition - .session_stickiness_ttl ?? 0) / + ?.session_stickiness_ttl ?? 0) / timeUnitFactorMap[ttlUnit] } label="Stickiness TTL" @@ -471,11 +480,12 @@ export const RuleDrawer = (props: Props) => { setTTLUnit(option.key); if ( - formik.values.match_condition.session_stickiness_ttl + formik.values.match_condition + ?.session_stickiness_ttl ) { const oldValue = formik.values.match_condition - .session_stickiness_ttl; + ?.session_stickiness_ttl; formik.setFieldValue( 'match_condition.session_stickiness_ttl', @@ -489,7 +499,7 @@ export const RuleDrawer = (props: Props) => { disableClearable label="test" options={timeUnitOptions} - sx={{ marginTop: '45px !important', minWidth: '140px' }} + sx={{ marginTop: '52px !important', minWidth: '140px' }} textFieldProps={{ hideLabel: true }} /> diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts index 454b1e9f551..b1664b4d4f8 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts @@ -51,23 +51,28 @@ export const defaultServiceTarget = { export const defaultTTLUnit = 'second'; -export const initialValues = { - match_condition: { - hostname: '', - match_field: 'path_prefix' as const, - match_value: '', - session_stickiness_cookie: null, - session_stickiness_ttl: null, - }, - service_targets: [defaultServiceTarget], +export const getInitialValues = (protocol: Route['protocol']) => { + if (protocol === 'tcp') { + return { service_targets: [defaultServiceTarget] }; + } + return { + match_condition: { + hostname: '', + match_field: 'path_prefix' as const, + match_value: '', + session_stickiness_cookie: null, + session_stickiness_ttl: null, + }, + service_targets: [defaultServiceTarget], + }; }; export const getIsSessionStickinessEnabled = ( rule: Rule | RulePayload | RuleCreatePayload ) => { return ( - rule.match_condition.session_stickiness_cookie !== null || - rule.match_condition.session_stickiness_ttl !== null + rule.match_condition?.session_stickiness_cookie !== null || + rule.match_condition?.session_stickiness_ttl !== null ); }; @@ -76,12 +81,14 @@ export const getIsSessionStickinessEnabled = ( * so that the API accepts the payload. */ export const getNormalizedRulePayload = (rule: RulePayload) => ({ - match_condition: { - ...rule.match_condition, - hostname: rule.match_condition.hostname - ? rule.match_condition.hostname - : null, - }, + match_condition: rule.match_condition + ? { + ...rule.match_condition, + hostname: rule.match_condition.hostname + ? rule.match_condition.hostname + : null, + } + : undefined, service_targets: rule.service_targets, }); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx index 83469e32cf4..8905359f67a 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx @@ -75,9 +75,9 @@ export const RuleRow = (props: RuleRowProps) => { ...sxItemSpacing, width: xsDown ? '45%' : '20%', }} - aria-label={`Match value: ${rule.match_condition.match_value}`} + aria-label={`Match value: ${rule.match_condition?.match_value}`} > - {rule.match_condition.match_value + {rule.match_condition?.match_value ? rule.match_condition.match_value : 'None'} @@ -85,11 +85,15 @@ export const RuleRow = (props: RuleRowProps) => { - {matchFieldMap[rule.match_condition.match_field]} + {rule.match_condition + ? matchFieldMap[rule.match_condition.match_field] + : 'None'} @@ -105,12 +109,15 @@ export const RuleRow = (props: RuleRowProps) => { - {rule.service_targets.map(({ label }) => ( -
{label}
+ {rule.service_targets.map(({ label, percentage }) => ( +
+ {label} ({percentage}%) +
))} } displayText={String(rule.service_targets.length)} + minWidth={100} /> ) : ( 'None' @@ -120,8 +127,8 @@ export const RuleRow = (props: RuleRowProps) => { { width: '20%', }} > - {rule.match_condition.session_stickiness_cookie || - rule.match_condition.session_stickiness_ttl + {rule.match_condition?.session_stickiness_cookie || + rule.match_condition?.session_stickiness_ttl ? 'Yes' : 'No'} diff --git a/packages/validation/.changeset/pr-10264-changed-1709750194085.md b/packages/validation/.changeset/pr-10264-changed-1709750194085.md new file mode 100644 index 00000000000..c75c910da83 --- /dev/null +++ b/packages/validation/.changeset/pr-10264-changed-1709750194085.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Changed +--- + +Updated TCP rules to not include a `match_condition` ([#10264](https://github.com/linode/manager/pull/10264)) diff --git a/packages/validation/src/loadbalancers.schema.ts b/packages/validation/src/loadbalancers.schema.ts index f051e975454..e1773071e59 100644 --- a/packages/validation/src/loadbalancers.schema.ts +++ b/packages/validation/src/loadbalancers.schema.ts @@ -117,25 +117,20 @@ const RouteServiceTargetSchema = object({ .required('Percent is required.'), }); -const TCPMatchConditionSchema = object({ +const MatchConditionSchema = object({ hostname: string().nullable(), + match_field: string() + .oneOf(matchFieldOptions) + .required('Match field is required.'), + match_value: string().required('Match value is required.'), + session_stickiness_cookie: string().nullable(), + session_stickiness_ttl: number() + .min(0, 'TTL must be greater than or equal to 0.') + .typeError('TTL must be a number.') + .nullable(), }); -const HTTPMatchConditionSchema = TCPMatchConditionSchema.concat( - object({ - match_field: string() - .oneOf(matchFieldOptions) - .required('Match field is required.'), - match_value: string().required('Match value is required.'), - session_stickiness_cookie: string().nullable(), - session_stickiness_ttl: number() - .min(0, 'TTL must be greater than or equal to 0.') - .typeError('TTL must be a number.') - .nullable(), - }) -); - -const BaseRuleSchema = object({ +export const TCPRuleSchema = object({ service_targets: array(RouteServiceTargetSchema) .test( 'sum-of-percentage', @@ -155,15 +150,9 @@ const BaseRuleSchema = object({ .required(), }); -export const HTTPRuleSchema = BaseRuleSchema.concat( - object({ - match_condition: HTTPMatchConditionSchema, - }) -); - -export const TCPRuleSchema = BaseRuleSchema.concat( +export const HTTPRuleSchema = TCPRuleSchema.concat( object({ - match_condition: TCPMatchConditionSchema, + match_condition: MatchConditionSchema, }) );