From 9a14b92621e281c65dfe0d312c586cc8ffc72549 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:12:45 -0500 Subject: [PATCH] upcoming: [M3-7294] - AGLB Full Create Flow - Add Rule Support to Routes (#10035) * add collapseable table * remove old routes table and setup drawer handlers * wire up add rule drawer * wow this is gonna be so broken * improve form experence * yikes... * add create mutation * small fixes and tweaks * Added changeset: Add Rule support to AGLB Full Create Flow * remove crazy code that we won't need when the POST changes --------- Co-authored-by: Banks Nussman --- ...r-10035-upcoming-features-1705441915646.md | 5 + .../LoadBalancerConfiguration.test.tsx | 3 + .../LoadBalancerConfigurations.tsx | 44 +- .../LoadBalancerCreate/Routes.tsx | 148 ++++-- .../LoadBalancerCreate/RuleDrawer.tsx | 480 ++++++++++++++++++ .../LoadBalancerCreate/RulesTable.tsx | 159 ++++++ .../LoadBalancerDetail/Routes/utils.ts | 5 +- .../LoadBalancerDetail/RuleRow.tsx | 4 +- .../manager/src/queries/aglb/loadbalancers.ts | 13 + .../validation/src/loadbalancers.schema.ts | 10 +- 10 files changed, 811 insertions(+), 60 deletions(-) create mode 100644 packages/manager/.changeset/pr-10035-upcoming-features-1705441915646.md create mode 100644 packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RuleDrawer.tsx create mode 100644 packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RulesTable.tsx diff --git a/packages/manager/.changeset/pr-10035-upcoming-features-1705441915646.md b/packages/manager/.changeset/pr-10035-upcoming-features-1705441915646.md new file mode 100644 index 00000000000..f72924ea004 --- /dev/null +++ b/packages/manager/.changeset/pr-10035-upcoming-features-1705441915646.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Rule support to AGLB Full Create Flow ([#10035](https://github.com/linode/manager/pull/10035)) diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx index b62a7357233..585e7e109ab 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx @@ -14,9 +14,12 @@ import type { Handlers } from './LoadBalancerConfigurations'; export const handlers: Handlers = { handleAddRoute: vi.fn(), + handleAddRule: vi.fn(), handleAddServiceTarget: vi.fn(), + handleCloseRuleDrawer: vi.fn(), handleCloseServiceTargetDrawer: vi.fn(), handleEditRoute: vi.fn(), + handleEditRule: vi.fn(), handleEditServiceTarget: vi.fn(), }; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfigurations.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfigurations.tsx index 170aef9e9ed..f624d76c462 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfigurations.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfigurations.tsx @@ -1,19 +1,27 @@ import { useFormikContext } from 'formik'; -import * as React from 'react'; import { useState } from 'react'; +import * as React from 'react'; import { AddRouteDrawer } from './AddRouteDrawer'; import { EditRouteDrawer } from './EditRouteDrawer'; import { LoadBalancerConfiguration } from './LoadBalancerConfiguration'; +import { RuleDrawer } from './RuleDrawer'; import { ServiceTargetDrawer } from './ServiceTargetDrawer'; import type { CreateLoadbalancerPayload } from '@linode/api-v4'; export interface Handlers { handleAddRoute: (configurationIndex: number) => void; + handleAddRule: (configurationIndex: number, routeIndex: number) => void; handleAddServiceTarget: (configurationIndex: number) => void; + handleCloseRuleDrawer: () => void; handleCloseServiceTargetDrawer: () => void; handleEditRoute: (index: number, configurationIndex: number) => void; + handleEditRule: ( + configurationIndex: number, + routeIndex: number, + ruleIndex: number + ) => void; handleEditServiceTarget: (index: number, configurationIndex: number) => void; } @@ -25,12 +33,14 @@ export const LoadBalancerConfigurations = () => { ); const [isAddRouteDrawerOpen, setIsAddRouteDrawerOpen] = useState(false); const [isEditRouteDrawerOpen, setIsEditRouteDrawerOpen] = useState(false); + const [isRuleDrawerOpen, setIsRuleDrawerOpen] = useState(false); const [ selectedServiceTargetIndex, setSelectedServiceTargetIndex, ] = useState(); const [selectedRouteIndex, setSelectedRouteIndex] = useState(); + const [selectedRuleIndex, setSelectedRuleIndex] = useState(); const [ selectedConfigurationIndex, setSelectedConfigurationIndex, @@ -66,11 +76,36 @@ export const LoadBalancerConfigurations = () => { setIsServiceTargetDrawerOpen(false); }; + const handleEditRule = ( + configurationIndex: number, + routeIndex: number, + ruleIndex: number + ) => { + setSelectedConfigurationIndex(configurationIndex); + setSelectedRouteIndex(routeIndex); + setSelectedRuleIndex(ruleIndex); + setIsRuleDrawerOpen(true); + }; + + const handleAddRule = (configurationIndex: number, routeIndex: number) => { + setSelectedConfigurationIndex(configurationIndex); + setSelectedRouteIndex(routeIndex); + setIsRuleDrawerOpen(true); + }; + + const handleCloseRuleDrawer = () => { + setSelectedRuleIndex(undefined); + setIsRuleDrawerOpen(false); + }; + const handlers: Handlers = { handleAddRoute, + handleAddRule, handleAddServiceTarget, + handleCloseRuleDrawer, handleCloseServiceTargetDrawer, handleEditRoute, + handleEditRule, handleEditServiceTarget, }; @@ -106,6 +141,13 @@ export const LoadBalancerConfigurations = () => { open={isEditRouteDrawerOpen} routeIndex={selectedRouteIndex} /> + ); }; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/Routes.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/Routes.tsx index 50169861ebb..69b91524123 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/Routes.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/Routes.tsx @@ -3,19 +3,25 @@ import { useFormikContext } from 'formik'; import React, { useState } from 'react'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; +import { + CollapsibleTable, + TableItem, +} from 'src/components/CollapsibleTable/CollapsibleTable'; +import { Hidden } from 'src/components/Hidden'; import { IconButton } from 'src/components/IconButton'; +import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; import { InputAdornment } from 'src/components/InputAdornment'; import { Stack } from 'src/components/Stack'; -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 { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; +import { RulesTable } from './RulesTable'; + import type { Handlers } from './LoadBalancerConfigurations'; import type { LoadBalancerCreateFormData } from './LoadBalancerCreateFormWrapper'; @@ -35,13 +41,89 @@ export const Routes = ({ configurationIndex, handlers }: Props) => { const configuration = values.configurations![configurationIndex]; const handleRemoveRoute = (index: number) => { - configuration.routes!.splice(index, 1); - setFieldValue( - `configurations[${configurationIndex}].routes`, - configuration.routes - ); + const newRoutes = [...configuration.routes!]; + newRoutes.splice(index, 1); + setFieldValue(`configurations[${configurationIndex}].routes`, newRoutes); + }; + + const getTableItems = (): TableItem[] => { + if (configuration.routes?.length === 0) { + return []; + } + + return configuration + .routes!.filter((route) => { + if (query) { + return route.label.includes(query); + } + return true; + }) + .map((route, index) => { + const OuterTableCells = ( + <> + + {route.rules.length} + + + {route.protocol.toLocaleUpperCase()} + + + + handlers.handleAddRule(configurationIndex, index) + } + actionText="Add Rule" + /> + + handlers.handleEditRoute(index, configurationIndex), + title: 'Edit Label', + }, + { + onClick: () => handleRemoveRoute(index), + title: 'Remove', + }, + ]} + ariaLabel={`Action Menu for Route ${route.label}`} + /> + + + ); + + const InnerTable = ( + + handlers.handleEditRule(configurationIndex, index, ruleIndex) + } + configurationIndex={configurationIndex} + routeIndex={index} + /> + ); + + return { + InnerTable, + OuterTableCells, + id: index, + label: route.label, + }; + }); }; + const RoutesTableRowHead = ( + + Route Label + + Rules + + + Protocol + + + + ); + return ( Routes @@ -80,49 +162,13 @@ export const Routes = ({ configurationIndex, handlers }: Props) => { value={query} /> - - - - Route Label - Rules - - - - - {configuration.routes!.length === 0 && ( - - )} - {configuration.routes - ?.filter((route) => { - if (query) { - return route.label.includes(query); - } - return true; - }) - .map((route, index) => ( - - {route.label} - {route.rules.length} - - - handlers.handleEditRoute(index, configurationIndex), - title: 'Edit Label', - }, - { - onClick: () => handleRemoveRoute(index), - title: 'Remove', - }, - ]} - ariaLabel={`Action Menu for Route ${route.label}`} - /> - - - ))} - -
+ + } + TableRowHead={RoutesTableRowHead} + /> + ); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RuleDrawer.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RuleDrawer.tsx new file mode 100644 index 00000000000..584ed8ce740 --- /dev/null +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RuleDrawer.tsx @@ -0,0 +1,480 @@ +import { CreateLoadBalancerRuleSchema } from '@linode/validation'; +import CloseIcon from '@mui/icons-material/Close'; +import { IconButton } from '@mui/material'; +import { getIn, useFormik, useFormikContext } from 'formik'; +import React, { useState } from 'react'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { Box } from 'src/components/Box'; +import { Divider } from 'src/components/Divider'; +import { Drawer } from 'src/components/Drawer'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { InputAdornment } from 'src/components/InputAdornment'; +import { LinkButton } from 'src/components/LinkButton'; +import { Notice } from 'src/components/Notice/Notice'; +import { Stack } from 'src/components/Stack'; +import { TextField } from 'src/components/TextField'; +import { Toggle } from 'src/components/Toggle/Toggle'; +import { Typography } from 'src/components/Typography'; + +import { MatchTypeInfo } from '../LoadBalancerDetail/Routes/MatchTypeInfo'; +import { ROUTE_COPY } from '../LoadBalancerDetail/Routes/constants'; +import { + TimeUnit, + defaultServiceTarget, + defaultTTL, + defaultTTLUnit, + getIsSessionStickinessEnabled, + matchTypeOptions, + matchValuePlaceholder, + stickyOptions, + timeUnitFactorMap, + timeUnitOptions, +} from '../LoadBalancerDetail/Routes/utils'; + +import type { LoadBalancerCreateFormData } from './LoadBalancerCreateFormWrapper'; +import type { RuleCreatePayload, ServiceTargetPayload } from '@linode/api-v4'; + +interface Props { + configurationIndex: number | undefined; + onClose: () => void; + open: boolean; + routeIndex: number | undefined; + ruleIndexToEdit: number | undefined; +} + +const initialValues: RuleCreatePayload = { + match_condition: { + hostname: '', + match_field: 'path_prefix' as const, + match_value: '', + session_stickiness_cookie: null, + session_stickiness_ttl: null, + }, + service_targets: [ + { + certificate_id: null, + endpoints: [], + healthcheck: { + healthy_threshold: 0, + interval: 0, + protocol: 'http', + timeout: 0, + unhealthy_threshold: 0, + }, + label: '', + load_balancing_policy: 'round_robin', + percentage: 100, + protocol: 'http', + }, + ], +}; + +export const RuleDrawer = (props: Props) => { + const { + configurationIndex, + onClose: _onClose, + open, + routeIndex, + ruleIndexToEdit, + } = props; + + const { + values, + setFieldValue, + } = useFormikContext(); + + const configuration = values.configurations![configurationIndex ?? 0]; + + const route = configuration.routes![routeIndex ?? 0]; + + const isEditMode = ruleIndexToEdit !== undefined; + + const protocol = route?.protocol ?? 'tcp'; + + const [ttlUnit, setTTLUnit] = useState(defaultTTLUnit); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: isEditMode + ? route?.rules[ruleIndexToEdit] ?? initialValues + : initialValues, + async onSubmit(rule) { + if (isEditMode) { + setFieldValue( + `configurations[${configurationIndex}].routes[${routeIndex}].rules[${ruleIndexToEdit}]`, + rule + ); + } else { + setFieldValue( + `configurations[${configurationIndex}].routes[${routeIndex}].rules`, + [...route.rules, rule] + ); + } + onClose(); + }, + validationSchema: CreateLoadBalancerRuleSchema, + }); + + const allServiceTargets = values.configurations.reduce< + ServiceTargetPayload[] + >((acc, configuration) => { + return acc.concat(configuration.service_targets); + }, []); + + const onClose = () => { + _onClose(); + formik.resetForm(); + setTTLUnit(defaultTTLUnit); + }; + + const onAddServiceTarget = () => { + formik.setFieldValue('service_targets', [ + ...formik.values.service_targets, + defaultServiceTarget, + ]); + }; + + const onRemoveServiceTarget = (index: number) => { + formik.values.service_targets.splice(index, 1); + formik.setFieldValue('service_targets', formik.values.service_targets); + }; + + const onStickinessChange = ( + _: React.ChangeEvent, + checked: boolean + ) => { + if (checked) { + formik.setFieldValue( + 'match_condition.session_stickiness_ttl', + defaultTTL + ); + } else { + formik.setFieldValue('match_condition.session_stickiness_ttl', null); + formik.setFieldValue('match_condition.session_stickiness_cookie', null); + } + }; + + const isStickinessEnabled = getIsSessionStickinessEnabled(formik.values); + + const cookieType = + formik.values.match_condition.session_stickiness_ttl === null + ? stickyOptions[1] + : stickyOptions[0]; + + return ( + +
+ + {ROUTE_COPY.Rule.Description[protocol]} + + + theme.bg.app} p={2.5} spacing={1.5}> + Match Rule + + {ROUTE_COPY.Rule.MatchRule[route?.protocol ?? 'tcp']} + + {route?.protocol !== 'tcp' && ( + <> + + + + formik.setFieldTouched('match_condition.match_field') + } + onChange={(_, option) => + formik.setFieldValue( + 'match_condition.match_field', + option?.value ?? null + ) + } + value={ + matchTypeOptions.find( + (option) => + option.value === + formik.values.match_condition.match_field + ) ?? matchTypeOptions[0] + } + disableClearable + label="Match Type" + options={matchTypeOptions} + sx={{ minWidth: 200 }} + textFieldProps={{ labelTooltipText: }} + /> + + + + + Routes to + + + + + )} + {typeof formik.errors.service_targets === 'string' && ( + + )} + {formik.values.service_targets.map((serviceTargt, index) => ( + + % + ), + }} + errorText={ + formik.touched.service_targets?.[index]?.percentage + ? getIn( + formik.errors, + `service_targets[${index}].percentage` + ) + : undefined + } + hideLabel={index !== 0} + label="Percent" + max={100} + min={0} + name={`service_targets[${index}].percentage`} + noMarginTop + onBlur={formik.handleBlur} + onChange={formik.handleChange} + type="number" + value={formik.values.service_targets[index].percentage} + /> + + option.label === value.label + } + onChange={(_, value) => + formik.setFieldValue( + `service_targets[${index}]`, + { + ...value, + percentage: + formik.values.service_targets[index].percentage, + }, + true + ) + } + textFieldProps={{ + hideLabel: index !== 0, + }} + value={allServiceTargets.find( + (st) => + st.label === formik.values.service_targets[index].label + )} + fullWidth + label="Service Target" + noMarginTop={index === 0} + options={allServiceTargets} + /> + onRemoveServiceTarget(index)} + > + + + + ))} + + + Add Service Target + + + + {route?.protocol !== 'tcp' && ( + + Session Stickiness + {ROUTE_COPY.Rule.Stickiness.Description} + + } + label="Use Session Stickiness" + /> + {isStickinessEnabled && ( + <> + { + formik.setFieldValue( + 'match_condition.session_stickiness_ttl', + option?.label === 'Load Balancer Generated' + ? defaultTTL + : null + ); + formik.setFieldValue( + 'match_condition.session_stickiness_cookie', + option?.label === 'Load Balancer Generated' ? null : '' + ); + }} + textFieldProps={{ + labelTooltipText: ROUTE_COPY.Rule.Stickiness.CookieType, + }} + disableClearable + label="Cookie type" + options={stickyOptions} + value={cookieType} + /> + + {cookieType.label === 'Load Balancer Generated' && ( + + + formik.setFieldValue( + 'match_condition.session_stickiness_ttl', + (e.target as HTMLInputElement).valueAsNumber * + timeUnitFactorMap[ttlUnit] + ) + } + value={ + (formik.values.match_condition + .session_stickiness_ttl ?? 0) / + timeUnitFactorMap[ttlUnit] + } + label="Stickiness TTL" + labelTooltipText={ROUTE_COPY.Rule.Stickiness.TTL} + name="match_condition.session_stickiness_ttl" + onBlur={formik.handleBlur} + type="number" + /> + { + const factor = + timeUnitFactorMap[option.key] / + timeUnitFactorMap[ttlUnit]; + + setTTLUnit(option.key); + + if ( + formik.values.match_condition.session_stickiness_ttl + ) { + const oldValue = + formik.values.match_condition + .session_stickiness_ttl; + + formik.setFieldValue( + 'match_condition.session_stickiness_ttl', + oldValue * factor + ); + } + }} + value={timeUnitOptions.find( + (option) => option.key === ttlUnit + )} + disableClearable + label="test" + options={timeUnitOptions} + sx={{ marginTop: '45px !important', minWidth: '140px' }} + textFieldProps={{ hideLabel: true }} + /> + + )} + + )} + + )} + + + +
+ ); +}; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RulesTable.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RulesTable.tsx new file mode 100644 index 00000000000..41db729157f --- /dev/null +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/RulesTable.tsx @@ -0,0 +1,159 @@ +import { Hidden } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { useFormikContext } from 'formik'; +import React from 'react'; +import { DragDropContext, DropResult, Droppable } from 'react-beautiful-dnd'; + +import { Box } from 'src/components/Box'; + +import { RuleRow } from '../LoadBalancerDetail/RuleRow'; +import { + StyledInnerBox, + StyledUl, + sxBox, + sxItemSpacing, +} from '../LoadBalancerDetail/RulesTable.styles'; + +import type { LoadBalancerCreateFormData } from './LoadBalancerCreateFormWrapper'; + +interface Props { + configurationIndex: number; + onEditRule: (ruleIndex: number) => void; + routeIndex: number; +} + +export const RulesTable = (props: Props) => { + const { configurationIndex, onEditRule, routeIndex } = props; + const { + setFieldValue, + values, + } = useFormikContext(); + const theme = useTheme(); + + const configuration = values.configurations![configurationIndex ?? 0]; + + const route = configuration.routes![routeIndex ?? 0]; + + const onDeleteRule = (ruleIndex: number) => { + route.rules.splice(ruleIndex, 1); + setFieldValue( + `configurations[${configurationIndex}].routes[${routeIndex}].rules`, + route.rules + ); + }; + + const handleRulesReorder = async ( + sourceIndex: number, + destinationIndex: number + ) => { + const reorderedRules = [...route.rules]; + const [removed] = reorderedRules.splice(sourceIndex, 1); + reorderedRules.splice(destinationIndex, 0, removed); + + setFieldValue( + `configurations[${configurationIndex}].routes[${routeIndex}].rules`, + reorderedRules + ); + }; + + const handleMoveUp = (sourceIndex: number) => { + handleRulesReorder(sourceIndex, sourceIndex - 1); + }; + + const handleMoveDown = (sourceIndex: number) => { + handleRulesReorder(sourceIndex, sourceIndex + 1); + }; + + const onDragEnd = (result: DropResult) => { + if ( + !result.destination || + result.destination.index === result.source.index + ) { + return; + } + + if (result.destination) { + handleRulesReorder(result.source.index, result.destination!.index); + } + }; + + const xsDown = useMediaQuery(theme.breakpoints.down('sm')); + + return ( + <> + + + + Execution + + + Match Value + + + + Match Type + + Service Targets + + Session Stickiness + + + + + + + + + + {(provided) => ( + + {route.rules.length > 0 ? ( + route.rules.map((rule, index) => ( + onDeleteRule(index)} + onEditRule={() => onEditRule(index)} + onMoveDown={() => handleMoveDown(index)} + onMoveUp={() => handleMoveUp(index)} + rule={rule} + totalRules={route.rules.length} + /> + )) + ) : ( + ({ + bgcolor: theme.bg.bgPaper, + display: 'flex', + justifyContent: 'center', + padding: 1.5, + })} + > + No Rules + + )} + {provided.placeholder} + + )} + + + + + ); +}; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts index fbe3cf5f49a..6e65c4e01a6 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/utils.ts @@ -6,6 +6,7 @@ import type { MatchField, Route, Rule, + RuleCreatePayload, RulePayload, } from '@linode/api-v4'; @@ -59,7 +60,9 @@ export const initialValues = { service_targets: [defaultServiceTarget], }; -export const getIsSessionStickinessEnabled = (rule: Rule | RulePayload) => { +export const getIsSessionStickinessEnabled = ( + rule: Rule | RulePayload | RuleCreatePayload +) => { return ( rule.match_condition.session_stickiness_cookie !== null || rule.match_condition.session_stickiness_ttl !== null diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx index 6d904fb490e..83469e32cf4 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RuleRow.tsx @@ -14,7 +14,7 @@ import { sxItemSpacing, } from './RulesTable.styles'; -import type { Rule } from '@linode/api-v4'; +import type { Rule, RuleCreatePayload } from '@linode/api-v4'; import type { Theme } from '@mui/material'; const screenReaderMessage = @@ -26,7 +26,7 @@ interface RuleRowProps { onEditRule: () => void; onMoveDown: () => void; onMoveUp: () => void; - rule: Rule; + rule: Rule | RuleCreatePayload; totalRules: number; } diff --git a/packages/manager/src/queries/aglb/loadbalancers.ts b/packages/manager/src/queries/aglb/loadbalancers.ts index 4e86e6a913c..9b419fdea78 100644 --- a/packages/manager/src/queries/aglb/loadbalancers.ts +++ b/packages/manager/src/queries/aglb/loadbalancers.ts @@ -1,5 +1,6 @@ import { createBasicLoadbalancer, + createLoadbalancer, deleteLoadbalancer, getLoadbalancer, getLoadbalancerEndpointHealth, @@ -11,6 +12,7 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'; import type { APIError, CreateBasicLoadbalancerPayload, + CreateLoadbalancerPayload, Filter, LoadBalancerEndpointHealth, Loadbalancer, @@ -71,6 +73,17 @@ export const useLoadBalancerBasicCreateMutation = () => { ); }; +export const useLoadBalancerCreateMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createLoadbalancer, + onSuccess(data) { + queryClient.setQueryData([QUERY_KEY, 'aglb', data.id], data); + queryClient.invalidateQueries([QUERY_KEY, 'paginated']); + }, + }); +}; + export const useLoadBalancerDeleteMutation = (id: number) => { const queryClient = useQueryClient(); return useMutation<{}, APIError[]>(() => deleteLoadbalancer(id), { diff --git a/packages/validation/src/loadbalancers.schema.ts b/packages/validation/src/loadbalancers.schema.ts index b4ae7315a0a..1cce4bd9861 100644 --- a/packages/validation/src/loadbalancers.schema.ts +++ b/packages/validation/src/loadbalancers.schema.ts @@ -230,7 +230,7 @@ const CreateLoadBalancerServiceTargetSchema = object({ percentage: number().integer().required(), label: string().required(), endpoints: array().of(CreateLoadBalancerEndpointSchema).required(), - certificate_id: number().integer(), + certificate_id: number().integer().nullable(), load_balancing_policy: string() .oneOf(['round_robin', 'least_request', 'ring_hash', 'random', 'maglev']) .required(), @@ -238,13 +238,13 @@ const CreateLoadBalancerServiceTargetSchema = object({ }); // Rule Schema -const CreateLoadBalancerRuleSchema = object({ +export const CreateLoadBalancerRuleSchema = object({ match_condition: object().shape({ - hostname: string().required(), + hostname: string().nullable(), match_field: string().oneOf(matchFieldOptions).required(), match_value: string().required(), - session_stickiness_cookie: string(), - session_stickiness_ttl: number().integer(), + session_stickiness_cookie: string().nullable(), + session_stickiness_ttl: number().integer().nullable(), }), service_targets: array().of(CreateLoadBalancerServiceTargetSchema).required(), });