Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-7254] - Add AGLB Routes - Rule Edit Drawer #9778

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add AGLB Routes - Rule Edit Drawer ([#9778](https://github.com/linode/manager/pull/9778))
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,78 @@ describe('Akamai Global Load Balancer routes page', () => {

cy.wait('@updateRoute');
});
it('surfaces API errors in the Add Rule Drawer', () => {
it('can edit a HTTP rule', () => {
const loadbalancer = loadbalancerFactory.build();
const routes = routeFactory.buildList(1, { protocol: 'http' });
const serviceTargets = serviceTargetFactory.buildList(3);

mockAppendFeatureFlags({
aglb: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');
mockGetLoadBalancer(loadbalancer).as('getLoadBalancer');
mockGetLoadBalancerRoutes(loadbalancer.id, routes).as('getRoutes');
mockGetLoadBalancerServiceTargets(loadbalancer.id, serviceTargets).as(
'getServiceTargets'
);

cy.visitWithLogin(`/loadbalancers/${loadbalancer.id}/routes`);
cy.wait([
'@getFeatureFlags',
'@getClientStream',
'@getLoadBalancer',
'@getRoutes',
]);

cy.findByLabelText(`route-${routes[0].id} expand row`).click();

ui.actionMenu.findByTitle('Action Menu for Rule 0').click();

ui.actionMenuItem.findByTitle('Edit').click();

mockUpdateRoute(loadbalancer, routes[0]).as('updateRoute');

ui.drawer
.findByTitle('Edit Rule')
.should('be.visible')
.within(() => {
cy.findByLabelText('Hostname')
.should('have.value', routes[0].rules[0].match_condition.hostname)
.clear()
.type('example.com');

cy.findByLabelText('Match Type')
.should('be.visible')
.click()
.clear()
.type('Header');

ui.autocompletePopper
.findByTitle('HTTP Header')
.should('be.visible')
.click();

cy.findByLabelText('Match Value')
.should('have.value', routes[0].rules[0].match_condition.match_value)
.clear()
.type('x-header=my-header-value');

ui.buttonGroup
.findButtonByTitle('Save')
.should('be.visible')
.should('be.enabled')
.click();
});

cy.wait('@updateRoute');

// Verify the table updates after the drawer saves and closes
cy.findByLabelText('Rule 0').within(() => {
cy.findByText('x-header=my-header-value');
cy.findByText('HTTP Header');
});
});
it('surfaces API errors in the Add Rule Drawer for an HTTP route', () => {
const loadbalancer = loadbalancerFactory.build();
const routes = routeFactory.buildList(1, { protocol: 'http' });
const serviceTargets = serviceTargetFactory.buildList(3);
Expand Down
5 changes: 1 addition & 4 deletions packages/manager/src/components/Notice/Notice.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const useStyles = makeStyles<
[`& .${classes.noticeText}`]: {
fontFamily: theme.font.normal,
},
backgroundColor: theme.bg.bgPaper,
},
info: {
[`&.${classes.important}`]: {
Expand Down Expand Up @@ -56,11 +57,7 @@ export const useStyles = makeStyles<
[`& .${classes.error}`]: {
borderLeftColor: theme.color.red,
},
[`& .${classes.important}`]: {
backgroundColor: theme.bg.bgPaper,
},
alignItems: 'center',
backgroundColor: theme.bg.bgPaper,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused a UI regression. I don't think we want notices to have a background unless they are important

Screenshot 2023-10-10 at 2 17 36 PM

borderRadius: 1,
display: 'flex',
fontSize: '1rem',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import { useOrder } from 'src/hooks/useOrder';
import { usePagination } from 'src/hooks/usePagination';
import { useLoadBalancerRoutesQuery } from 'src/queries/aglb/routes';

import { AddRuleDrawer } from './Routes/AddRuleDrawer';
import { DeleteRouteDialog } from './Routes/DeleteRouteDialog';
import { RuleDrawer } from './Routes/RuleDrawer';
import { RulesTable } from './RulesTable';

import type { Filter, Route } from '@linode/api-v4';
Expand All @@ -39,6 +39,7 @@ export const LoadBalancerRoutes = () => {
const [query, setQuery] = useState<string>();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedRouteId, setSelectedRouteId] = useState<number>();
const [selectedRuleIndex, setSelectedRuleIndex] = useState<number>();

const pagination = usePagination(1, PREFERENCE_KEY);

Expand Down Expand Up @@ -78,6 +79,12 @@ export const LoadBalancerRoutes = () => {
setSelectedRouteId(route.id);
};

const onEditRule = (route: Route, ruleIndex: number) => {
setIsAddRuleDrawerOpen(true);
setSelectedRouteId(route.id);
setSelectedRuleIndex(ruleIndex);
};

const onDeleteRoute = (route: Route) => {
setIsDeleteDialogOpen(true);
setSelectedRouteId(route.id);
Expand Down Expand Up @@ -124,7 +131,11 @@ export const LoadBalancerRoutes = () => {
);

const InnerTable = (
<RulesTable loadbalancerId={Number(loadbalancerId)} route={route} />
<RulesTable
loadbalancerId={Number(loadbalancerId)}
onEditRule={(index) => onEditRule(route, index)}
route={route}
/>
);

return {
Expand Down Expand Up @@ -216,11 +227,15 @@ export const LoadBalancerRoutes = () => {
page={pagination.page}
pageSize={pagination.pageSize}
/>
<AddRuleDrawer
<RuleDrawer
onClose={() => {
setIsAddRuleDrawerOpen(false);
setSelectedRuleIndex(undefined);
}}
loadbalancerId={Number(loadbalancerId)}
onClose={() => setIsAddRuleDrawerOpen(false)}
open={isAddRuleDrawerOpen}
route={selectedRoute}
ruleIndexToEdit={selectedRuleIndex}
/>
<DeleteRouteDialog
loadbalancerId={Number(loadbalancerId)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,31 @@ interface Props {
onClose: () => void;
open: boolean;
route: Route | undefined;
ruleIndexToEdit: number | undefined;
}

export const AddRuleDrawer = (props: Props) => {
const { loadbalancerId, onClose: _onClose, open, route } = props;
/**
* Drawer used for *adding* and *editing* AGLB rules
*/
export const RuleDrawer = (props: Props) => {
const {
loadbalancerId,
onClose: _onClose,
open,
route,
ruleIndexToEdit,
} = props;

const ruleIndex = ruleIndexToEdit ?? route?.rules.length ?? 0;

const ruleIndex = route?.rules.length ?? 0;
const isEditMode = ruleIndexToEdit !== undefined;

const validationSchema =
route?.protocol === 'tcp' ? TCPRuleSchema : HTTPRuleSchema;

const {
error,
isLoading,
mutateAsync: updateRule,
reset,
} = useLoadBalancerRouteUpdateMutation(loadbalancerId, route?.id ?? -1);
Expand All @@ -62,13 +75,23 @@ export const AddRuleDrawer = (props: Props) => {

const formik = useFormik<RulePayload>({
enableReinitialize: true,
initialValues,
initialValues: isEditMode
? route?.rules[ruleIndexToEdit] ?? initialValues
: initialValues,
async onSubmit(rule) {
try {
const existingRules = route?.rules ?? [];

// If we are editing, update the rule with the form data.
if (isEditMode) {
existingRules[ruleIndexToEdit] = rule;
}

await updateRule({
protocol: route?.protocol,
rules: [...existingRules, rule],
// If we are editing, send the updated rules, otherwise
// append a new rule to the end.
rules: isEditMode ? existingRules : [...existingRules, rule],
});
onClose();
} catch (errors) {
Expand Down Expand Up @@ -167,7 +190,12 @@ export const AddRuleDrawer = (props: Props) => {
.join(', ');

return (
<Drawer onClose={onClose} open={open} title="Add Rule" wide>
<Drawer
onClose={onClose}
open={open}
title={`${isEditMode ? 'Edit' : 'Add'} Rule`}
wide
>
<form onSubmit={formik.handleSubmit}>
{/**
* @todo: AGLB update copy
Expand Down Expand Up @@ -464,8 +492,8 @@ export const AddRuleDrawer = (props: Props) => {
</Stack>
<ActionsPanel
primaryButtonProps={{
label: 'Add Rule',
loading: formik.isSubmitting,
label: isEditMode ? 'Save' : 'Add Rule',
loading: formik.isSubmitting || isLoading,
type: 'submit',
}}
secondaryButtonProps={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const mockRoute = routeFactory.build({
describe('RulesTable', () => {
it('renders table headers', () => {
const { getByText } = renderWithTheme(
<RulesTable loadbalancerId={1} route={mockRoute} />
<RulesTable loadbalancerId={1} onEditRule={jest.fn()} route={mockRoute} />
);
expect(getByText('Execution')).toBeInTheDocument();
expect(getByText('Match Value')).toBeInTheDocument();
Expand All @@ -34,6 +34,7 @@ describe('RulesTable', () => {
const { getByText } = renderWithTheme(
<RulesTable
loadbalancerId={1}
onEditRule={jest.fn()}
route={{ id: 0, label: 'test', protocol: 'http', rules: [] }}
/>
);
Expand All @@ -42,7 +43,7 @@ describe('RulesTable', () => {

it('renders rules correctly', () => {
const { getByText } = renderWithTheme(
<RulesTable loadbalancerId={1} route={mockRoute} />
<RulesTable loadbalancerId={1} onEditRule={jest.fn()} route={mockRoute} />
);

expect(getByText('First')).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { MatchField, Route } from '@linode/api-v4';

interface Props {
loadbalancerId: number;
onEditRule: (ruleIndex: number) => void;
route: Route;
}

Expand All @@ -42,7 +43,7 @@ const matchFieldMap: Record<MatchField, string> = {
const screenReaderMessage =
'Some screen readers may require you to enter focus mode to interact with Loadbalancer rule list items. In focus mode, press spacebar to begin a drag or tab to access item actions.';

export const RulesTable = ({ loadbalancerId, route }: Props) => {
export const RulesTable = ({ loadbalancerId, onEditRule, route }: Props) => {
const { label, protocol, rules } = route;
const theme = useTheme();
const { enqueueSnackbar } = useSnackbar();
Expand Down Expand Up @@ -146,10 +147,7 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => {
>
{(provided) => (
<li
aria-label={
rule.match_condition.hostname ??
`Rule ${rule.match_condition.hostname}`
}
aria-label={`Rule ${index}`}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change okay for accessibility? Rules don't have a solid way to uniquely identify them so I think index is best here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since order matters and hostname isn't unique, then I think the change makes sense.

aria-roledescription={screenReaderMessage}
aria-selected={false}
ref={provided.innerRef}
Expand All @@ -158,7 +156,6 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => {
{...provided.dragHandleProps}
>
<StyledRuleBox
aria-label={`Rule ${rule.match_condition.hostname}`}
key={index}
sx={{ backgroundColor: theme.bg.bgPaper }}
>
Expand Down Expand Up @@ -219,22 +216,26 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => {
}}
aria-label={`Service Targets: ${rule.service_targets.length}`}
>
<TextTooltip
displayText={String(
rule.service_targets.length
)}
tooltipText={
<>
{rule.service_targets.map(
({ id, label }) => (
<div key={label}>
{label}:{id}
</div>
)
)}
</>
}
/>
{rule.service_targets.length > 0 ? (
<TextTooltip
displayText={String(
rule.service_targets.length
)}
tooltipText={
<>
{rule.service_targets.map(
({ id, label }) => (
<div key={label}>
{label}:{id}
</div>
)
)}
</>
}
/>
) : (
'None'
)}
</Box>
</Hidden>
<Hidden smDown>
Expand All @@ -253,7 +254,7 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => {
}}
>
{rule.match_condition
.session_stickiness_cookie &&
.session_stickiness_cookie ||
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes incorrect logic

rule.match_condition.session_stickiness_ttl
? 'Yes'
: 'No'}
Expand All @@ -270,7 +271,10 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => {
{/** TODO: AGLB: The Edit and Delete Action menu behavior should be implemented in future AGLB tickets. */}
<ActionMenu
actionsList={[
{ onClick: () => null, title: 'Edit' },
{
onClick: () => onEditRule(index),
title: 'Edit',
},
{
disabled: index === 0,
onClick: () => handleMoveUp(index),
Expand All @@ -283,7 +287,7 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => {
},
{ onClick: () => null, title: 'Remove' },
]}
ariaLabel={`Action Menu for Rule ${rule.match_condition.match_value}`}
ariaLabel={`Action Menu for Rule ${index}`}
/>
</Box>
</StyledRuleBox>
Expand Down
Loading