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-7016] - Add Basic Filtering for Firewall Devices #9626

Merged
merged 18 commits into from
Sep 12, 2023
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
---

Added Basic Filtering for Firewall Devices ([#9626](https://github.com/linode/manager/pull/9626))
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { ActionPanelProps, ActionsPanel } from './ActionsPanel';

const meta: Meta<typeof ActionsPanel> = {
component: ActionsPanel,
title: 'Components/ActionsPanel',
};

type Story = StoryObj<typeof ActionsPanel>;

const primaryButtonProps = {
label: 'Confirm',
};

const secondaryButtonProps = {
label: 'Cancel',
};

export const StandardActions: Story = {
args: {
primaryButtonProps,
secondaryButtonProps,
},
render: (args: ActionPanelProps) => {
return <ActionsPanel sx={{ justifyContent: 'flex-start' }} {...args} />;
},
};

export default meta;
14 changes: 6 additions & 8 deletions packages/manager/src/components/ActionsPanel/ActionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import cx from 'classnames';
import * as React from 'react';

import { Button, ButtonProps } from 'src/components/Button/Button';
import { RenderGuard } from 'src/components/RenderGuard';

import { Box, BoxProps } from '../Box';

Expand All @@ -13,19 +12,22 @@ interface ActionButtonsProps extends ButtonProps {
label?: string;
}

interface ActionPanelProps extends BoxProps {
export interface ActionPanelProps extends BoxProps {
/**
* primary type actionable button custom aria descripton.
*/
primaryButtonProps?: ActionButtonsProps;

/**
* secondary type actionable button custom aria descripton.
*/
secondaryButtonProps?: ActionButtonsProps;
}

const ActionsPanelComponent = (props: ActionPanelProps) => {
/**
* `ActionPanel` is a container for primary and secondary actions (ex: "Cancel" & "Save")
* It can also be used to render a single action within modals or drawers for styling and layout consistency.
*/
export const ActionsPanel = (props: ActionPanelProps) => {
const {
className,
primaryButtonProps,
Expand Down Expand Up @@ -83,7 +85,3 @@ const StyledBox = styled(Box)(({ theme: { spacing } }) => ({
paddingBottom: spacing(1),
paddingTop: spacing(2),
}));

const ActionsPanel = RenderGuard(ActionsPanelComponent);

export { ActionsPanel };
2 changes: 1 addition & 1 deletion packages/manager/src/components/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown';
import { Box } from 'src/components/Box';
import {
default as _TextField,
StandardTextFieldProps,
Expand All @@ -9,6 +8,7 @@ import { clamp } from 'ramda';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';

import { Box } from 'src/components/Box';
import { CircleProgress } from 'src/components/CircleProgress';
import { FormHelperText } from 'src/components/FormHelperText';
import { InputAdornment } from 'src/components/InputAdornment';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Grid from '@mui/material/Unstable_Grid2';
import { useTheme } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import * as React from 'react';

import { Button } from 'src/components/Button/Button';
import { Link } from 'src/components/Link';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
import { Notice } from 'src/components/Notice/Notice';
import { Typography } from 'src/components/Typography';
import { useAllFirewallDevicesQuery } from 'src/queries/firewalls';
Expand All @@ -13,7 +14,7 @@ import { AddNodebalancerDrawer } from './AddNodebalancerDrawer';
import { FirewallDeviceTable } from './FirewallDeviceTable';
import { RemoveDeviceDialog } from './RemoveDeviceDialog';

import type { FirewallDeviceEntityType } from '@linode/api-v4';
import type { FirewallDevice, FirewallDeviceEntityType } from '@linode/api-v4';

export interface FirewallDeviceLandingProps {
disabled: boolean;
Expand All @@ -38,8 +39,17 @@ export const FirewallDeviceLanding = React.memo(
firewallID
);

const devices =
allDevices?.filter((device) => device.entity.type === type) || [];
const theme = useTheme();

const [filteredDevices, setFilteredDevices] = React.useState<
FirewallDevice[]
>([]);

React.useEffect(() => {
setFilteredDevices(
allDevices?.filter((device) => device.entity.type === type) || []
);
}, [allDevices, type]);

const [
isRemoveDeviceDialogOpen,
Expand All @@ -48,7 +58,7 @@ export const FirewallDeviceLanding = React.memo(

const [selectedDeviceId, setSelectedDeviceId] = React.useState<number>(-1);

const selectedDevice = devices?.find(
const selectedDevice = filteredDevices?.find(
(device) => device.id === selectedDeviceId
);

Expand All @@ -60,6 +70,16 @@ export const FirewallDeviceLanding = React.memo(
setDeviceDrawerOpen(false);
};

const [searchText, setSearchText] = React.useState('');

const filter = (value: string) => {
setSearchText(value);
const filtered = filteredDevices.filter((device) => {
return device.entity.label.toLowerCase().includes(value.toLowerCase());
});
setFilteredDevices(filtered);
};

const formattedType = formattedTypes[type];

return (
Expand All @@ -73,36 +93,53 @@ export const FirewallDeviceLanding = React.memo(
variant="error"
/>
) : null}
<Grid container direction="column">
<Grid sx={{ paddingBottom: 0, width: 'calc(100% - 300px)' }}>
<StyledTypography>
The following {formattedType}s have been assigned to this
Firewall. A {formattedType} can only be assigned to a single
Firewall.
<Link to="#">
Learn about how Firewall rules apply to {formattedType}s.
</Link>
{/* @todo add documentation link */}
</StyledTypography>
<Grid
container
direction="column"
sx={{ marginBottom: theme.spacing(2) }}
>
<StyledTypography>
The following {formattedType}s have been assigned to this Firewall.
A {formattedType} can only be assigned to a single Firewall.
</StyledTypography>
<Grid
alignItems="center"
container
direction="row"
justifyContent="space-between"
>
<Grid sx={{ width: '30%' }}>
<DebouncedSearchTextField
onSearch={(val) => {
filter(val);
}}
debounceTime={250}
expand={true}
hideLabel
label=""
placeholder={`Search ${formattedType}s`}
value={searchText}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

you may want to set a lower debounceTime than the default (250 instead of 400)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay! is there a reason its at 400? Should we change the default to something lower?

Copy link
Contributor

Choose a reason for hiding this comment

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

We may want to discuss this for a later PR, but see how the experience feels with 400. I find it to feel laggy

</Grid>
<Grid>
<Button
buttonType="primary"
data-testid="add-device-button"
disabled={disabled}
onClick={() => setDeviceDrawerOpen(true)}
>
Add {formattedType}s to Firewall
</Button>
Comment on lines +125 to +132
Copy link
Contributor

Choose a reason for hiding this comment

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

We could use ActionsPanel here to avoid variations for primary actions button.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it seems like the ActionsPanel is also dependent on the primary actions button. Let me know if I misinterpreted your comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tyler-akamai Yes, you are right. My intention is to use the ActionsPanel component to render the Primary button instead of having isolated references to the primary button, unless the ActionsPanel doesn't meet our requirements. This approach would make future maintenance easier.

Copy link
Contributor Author

@tyler-akamai tyler-akamai Sep 8, 2023

Choose a reason for hiding this comment

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

This is a good callout. I checked with the team on this. They said that for one off selections like "Add a Nodebalancer/Linode" we should use the Button component. We should only use the ActionsPanel component if there are multiple actions to select from, or we are in a modal or drawer.

Copy link
Contributor Author

@tyler-akamai tyler-akamai Sep 8, 2023

Choose a reason for hiding this comment

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

I added an ActionsPanel storybook component and added this clarification to the documentation page.

</Grid>
</Grid>
<StyledGrid>
<Button
buttonType="primary"
data-testid="add-device-button"
disabled={disabled}
onClick={() => setDeviceDrawerOpen(true)}
>
Add {formattedType}s to Firewall
</Button>
</StyledGrid>
</Grid>
<FirewallDeviceTable
triggerRemoveDevice={(id) => {
setSelectedDeviceId(id);
setIsRemoveDeviceDialogOpen(true);
}}
deviceType={type}
devices={devices ?? []}
devices={filteredDevices ?? []}
disabled={disabled}
error={error ?? undefined}
loading={isLoading}
Expand Down Expand Up @@ -136,22 +173,7 @@ export const FirewallDeviceLanding = React.memo(
const StyledTypography = styled(Typography, { label: 'StyledTypography' })(
({ theme }) => ({
fontSize: '0.875rem',
marginBottom: theme.spacing(2),
marginTop: theme.spacing(),
[theme.breakpoints.down('lg')]: {
marginLeft: theme.spacing(),
marginRight: theme.spacing(),
},
})
);

const StyledGrid = styled(Grid, { label: 'StyledGrid' })(({ theme }) => ({
'&.MuiGrid-item': {
paddingTop: 0,
},
display: 'flex',
justifyContent: 'flex-end',
marginBottom: theme.spacing(),
[theme.breakpoints.only('sm')]: {
marginRight: theme.spacing(),
},
}));
8 changes: 0 additions & 8 deletions packages/manager/src/features/Images/ImagesDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -397,14 +397,6 @@ export const ImagesDrawer = (props: CombinedProps) => {
label: 'Cancel',
onClick: close,
}}
updateFor={[
requirementsMet,
classes,
submitting,
mode,
label,
description,
]}
style={{ marginTop: 16 }}
/>
</Drawer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export const ListeningServices = (props: TableProps) => {
const { services, servicesError, servicesLoading } = props;

return (
<Grid
md={8}
xs={12}
>
<Grid md={8} xs={12}>
<Typography
sx={(theme) => ({
[theme.breakpoints.down('lg')]: {
Expand Down