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

Tech Story [M3-6276]: Nodebalancers MUI Migration #9139

Merged
merged 10 commits into from
May 23, 2023
158 changes: 69 additions & 89 deletions packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import { Linode } from '@linode/api-v4/lib/linodes';
import * as React from 'react';
import { compose } from 'recompose';

import { createStyles, withStyles, WithStyles } from '@mui/styles';

import { Props as TextFieldProps } from 'src/components/TextField';
import LinodeSelect from 'src/features/linodes/LinodeSelect';
import { Linode } from '@linode/api-v4/lib/linodes';
import { privateIPRegex } from 'src/utilities/ipUtils';
import type { Props as TextFieldProps } from 'src/components/TextField';

type ClassNames = 'labelOuter';

const styles = () =>
createStyles({
labelOuter: {
display: 'block',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not replacing this, display: block is a default for a div and it was not overridden anywhere else

},
});

interface Props {
interface ConfigNodeIPSelectProps {
selectedRegion?: string;
handleChange: (nodeIndex: number, ipAddress: string) => void;
nodeIndex: number;
Expand All @@ -28,79 +15,72 @@ interface Props {
textfieldProps: Omit<TextFieldProps, 'label'>;
}

type CombinedProps = WithStyles<ClassNames> & Props;

const ConfigNodeIPSelect: React.FC<CombinedProps> = (props) => {
const [selectedLinode, setSelectedLinode] = React.useState<number | null>(
null
);
const { classes, handleChange: _handleChange, inputId } = props;

const handleChange = (linode: Linode) => {
setSelectedLinode(linode.id);
const thisLinodesPrivateIP = linode.ipv4.find((ipv4) =>
ipv4.match(privateIPRegex)
export const ConfigNodeIPSelect = React.memo(
(props: ConfigNodeIPSelectProps) => {
const [selectedLinode, setSelectedLinode] = React.useState<number | null>(
null
);
/**
* we can be sure the selection has a private IP because of the
* filterCondition prop in the render method below
*/
_handleChange(props.nodeIndex, thisLinodesPrivateIP!);
};

return (
<LinodeSelect
noMarginTop
inputId={inputId}
textFieldProps={props.textfieldProps}
value={
props.nodeAddress
? {
value: props.nodeAddress,
label: props.nodeAddress,
}
: null
}
selectedLinode={selectedLinode}
noOptionsMessage={`No options - please ensure you have at least 1 Linode
with a private IP located in the selected region.`}
generalError={props.errorText}
handleChange={handleChange}
label="IP Address"
disabled={props.disabled}
small
placeholder="Enter IP Address"
valueOverride={(linode) => {
return linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex));
}}
labelOverride={(linode) => {
return (
<div>
<strong>
{linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex))}
</strong>
<div className={classes.labelOuter}>{` ${linode.label}`}</div>
</div>
);
}}
filterCondition={(linode) => {
/**
* if the Linode doesn't have an private IP OR if the Linode
* is in a different region that the NodeBalancer, don't show it
* in the select dropdown
*/
return (
!!linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex)) &&
linode.region === props.selectedRegion
);
}}
/>
);
};
const { handleChange: _handleChange, inputId } = props;

const styled = withStyles(styles);
const handleChange = (linode: Linode) => {
setSelectedLinode(linode.id);
const thisLinodesPrivateIP = linode.ipv4.find((ipv4) =>
ipv4.match(privateIPRegex)
);
/**
* we can be sure the selection has a private IP because of the
* filterCondition prop in the render method below
*/
_handleChange(props.nodeIndex, thisLinodesPrivateIP!);
};

export default compose<CombinedProps, Props>(
React.memo,
styled
)(ConfigNodeIPSelect);
return (
<LinodeSelect
noMarginTop
inputId={inputId}
textFieldProps={props.textfieldProps}
value={
props.nodeAddress
? {
value: props.nodeAddress,
label: props.nodeAddress,
}
: null
}
selectedLinode={selectedLinode}
noOptionsMessage={`No options - please ensure you have at least 1 Linode
with a private IP located in the selected region.`}
generalError={props.errorText}
handleChange={handleChange}
label="IP Address"
disabled={props.disabled}
small
placeholder="Enter IP Address"
valueOverride={(linode) => {
return linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex));
}}
labelOverride={(linode) => {
return (
<div>
<strong>
{linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex))}
</strong>
<div>{` ${linode.label}`}</div>
</div>
);
}}
filterCondition={(linode) => {
/**
* if the Linode doesn't have an private IP OR if the Linode
* is in a different region that the NodeBalancer, don't show it
* in the select dropdown
*/
return (
!!linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex)) &&
linode.region === props.selectedRegion
);
}}
/>
);
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import * as React from 'react';
import FormHelperText from 'src/components/core/FormHelperText';
import Grid from '@mui/material/Unstable_Grid2';
import InputAdornment from 'src/components/core/InputAdornment';
import Select from 'src/components/EnhancedSelect/Select';
import TextField from 'src/components/TextField';
import Typography from 'src/components/core/Typography';
import { setErrorMap } from './utils';
import type { Item } from 'src/components/EnhancedSelect';
import type { NodeBalancerConfigPanelProps } from './types';

interface ActiveCheckProps extends NodeBalancerConfigPanelProps {
errorMap: Record<string, string | undefined>;
}

const displayProtocolText = (p: string) => {
if (p === 'tcp') {
return `'TCP Connection' requires a successful TCP handshake.`;
}
if (p === 'http' || p === 'https') {
return `'HTTP Valid Status' requires a 2xx or 3xx response from the backend node. 'HTTP Body Regex' uses a regex to match against an expected result body.`;
}
return undefined;
};

export const ActiveCheck = (props: ActiveCheckProps) => {
const {
checkBody,
checkPath,
configIdx,
disabled,
errors,
forEdit,
healthCheckAttempts,
healthCheckInterval,
healthCheckTimeout,
healthCheckType,
protocol,
} = props;

const errorMap = setErrorMap(errors || []);

const onCheckBodyChange = (e: React.ChangeEvent<HTMLInputElement>) =>
props.onCheckBodyChange(e.target.value);

const onCheckPathChange = (e: React.ChangeEvent<HTMLInputElement>) =>
props.onCheckPathChange(e.target.value);

const onHealthCheckAttemptsChange = (
e: React.ChangeEvent<HTMLInputElement>
) => props.onHealthCheckAttemptsChange(e.target.value);

const onHealthCheckIntervalChange = (
e: React.ChangeEvent<HTMLInputElement>
) => props.onHealthCheckIntervalChange(e.target.value);

const onHealthCheckTimeoutChange = (e: React.ChangeEvent<HTMLInputElement>) =>
props.onHealthCheckTimeoutChange(e.target.value);

const onHealthCheckTypeChange = (e: Item<string>) =>
props.onHealthCheckTypeChange(e.value);

const conditionalText = displayProtocolText(protocol);

const typeOptions = [
{
label: 'None',
value: 'none',
},
{
label: 'TCP Connection',
value: 'connection',
},
{
label: 'HTTP Status',
value: 'http',
disabled: protocol === 'tcp',
},
{
label: 'HTTP Body',
value: 'http_body',
disabled: protocol === 'tcp',
},
];

const defaultType = typeOptions.find((eachType) => {
return eachType.value === healthCheckType;
});

return (
<Grid xs={12} md={6} sx={{ padding: 0 }}>
<Grid container spacing={2}>
<Grid xs={12}>
<Typography variant="h2" data-qa-active-checks-header>
Active Health Checks
</Typography>
</Grid>
<Grid xs={12}>
<Select
options={typeOptions}
label="Type"
inputId={`type-${configIdx}`}
value={defaultType || typeOptions[0]}
onChange={onHealthCheckTypeChange}
errorText={errorMap.check}
errorGroup={forEdit ? `${configIdx}` : undefined}
textFieldProps={{
dataAttrs: {
'data-qa-active-check-select': true,
},
}}
small
disabled={disabled}
isClearable={false}
noMarginTop
/>
<FormHelperText>
Active health checks proactively check the health of back-end nodes.{' '}
{conditionalText}
</FormHelperText>
</Grid>
{healthCheckType !== 'none' && (
<React.Fragment>
<Grid xs={12}>
<TextField
type="number"
label="Interval"
InputProps={{
'aria-label': 'Active Health Check Interval',
endAdornment: (
<InputAdornment position="end">seconds</InputAdornment>
),
}}
value={healthCheckInterval}
onChange={onHealthCheckIntervalChange}
errorText={errorMap.check_interval}
errorGroup={forEdit ? `${configIdx}` : undefined}
data-qa-active-check-interval
disabled={disabled}
/>
<FormHelperText>
Seconds between health check probes
</FormHelperText>
</Grid>
<Grid xs={12}>
<TextField
type="number"
label="Timeout"
InputProps={{
'aria-label': 'Active Health Check Timeout',
endAdornment: (
<InputAdornment position="end">seconds</InputAdornment>
),
}}
value={healthCheckTimeout}
onChange={onHealthCheckTimeoutChange}
errorText={errorMap.check_timeout}
errorGroup={forEdit ? `${configIdx}` : undefined}
data-qa-active-check-timeout
disabled={disabled}
/>
<FormHelperText>
Seconds to wait before considering the probe a failure. 1-30.
Must be less than check_interval.
</FormHelperText>
</Grid>
<Grid xs={12} lg={6}>
<TextField
type="number"
label="Attempts"
value={healthCheckAttempts}
onChange={onHealthCheckAttemptsChange}
errorText={errorMap.check_attempts}
errorGroup={forEdit ? `${configIdx}` : undefined}
InputProps={{
'aria-label': 'Active Health Check Attempts',
}}
data-qa-active-check-attempts
disabled={disabled}
/>
<FormHelperText>
Number of failed probes before taking a node out of rotation.
1-30
</FormHelperText>
</Grid>
{['http', 'http_body'].includes(healthCheckType) && (
<Grid xs={12} lg={6}>
<TextField
label="Check HTTP Path"
value={checkPath || ''}
onChange={onCheckPathChange}
required={['http', 'http_body'].includes(healthCheckType)}
errorText={errorMap.check_path}
errorGroup={forEdit ? `${configIdx}` : undefined}
disabled={disabled}
/>
</Grid>
)}
{healthCheckType === 'http_body' && (
<Grid xs={12} md={4}>
<TextField
label="Expected HTTP Body"
value={checkBody}
onChange={onCheckBodyChange}
required={healthCheckType === 'http_body'}
errorText={errorMap.check_body}
errorGroup={forEdit ? `${configIdx}` : undefined}
disabled={disabled}
/>
</Grid>
)}
</React.Fragment>
)}
</Grid>
</Grid>
);
};
Copy link
Contributor Author

@abailly-akamai abailly-akamai May 18, 2023

Choose a reason for hiding this comment

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

Extracting to a new component cause NodeBalancerConfigPanel was a huge class component. No code was modified in the process. (nothing new)

Loading