Skip to content

Commit

Permalink
Add Filtering Structure for Data Table (#1436)
Browse files Browse the repository at this point in the history
* added outside parts, now working on inside parts

* filter bar v1

* updated tests

* hitting pause on this awaiting design changes

* separated out chips into chip group, addressed pr comments, changing styling to new design

* styles and tests

* readded icons
  • Loading branch information
joshri authored and jpellizzari committed Feb 25, 2022
1 parent 986f4ef commit 91b7f44
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 6 deletions.
42 changes: 42 additions & 0 deletions ui/components/ChipGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Chip } from "@material-ui/core";
import _ from "lodash";
import * as React from "react";
import styled from "styled-components";
import Flex from "./Flex";
import Spacer from "./Spacer";

export interface Props {
className?: string;
/** currently checked filter options. Part of a `useState` with `setActiveChips` */
activeChips: string[];
/** the setState function for `activeChips` */
setActiveChips: (newChips: string[]) => void;
}

function ChipGroup({ className, activeChips, setActiveChips }: Props) {
return (
<Flex className={className} align start>
{_.map(activeChips, (filter, index) => {
return (
<Flex key={index}>
<Spacer padding="xxs" />
<Chip
label={filter}
onDelete={() =>
setActiveChips(
activeChips.filter((filterCheck) => filterCheck !== filter)
)
}
/>
<Spacer padding="xxs" />
</Flex>
);
})}
{activeChips.length > 0 && (
<Chip label="Clear All" onDelete={() => setActiveChips([])} />
)}
</Flex>
);
}

export default styled(ChipGroup).attrs({ className: ChipGroup.name })``;
130 changes: 130 additions & 0 deletions ui/components/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
Checkbox,
List,
ListItem,
ListItemIcon,
Popover,
} from "@material-ui/core";
import _ from "lodash";
import * as React from "react";
import styled from "styled-components";
import Button from "./Button";
import Flex from "./Flex";
import Icon, { IconType } from "./Icon";
import Spacer from "./Spacer";
import Text from "./Text";

const StyledPopover = styled(Popover)`
.MuiPopover-paper {
min-width: 450px;
border-left: 2px solid ${(props) => props.theme.colors.neutral30};
padding-left: ${(props) => props.theme.spacing.medium};
}
.MuiListItem-gutters {
padding-left: 0px;
}
.MuiCheckbox-root {
padding: 0px;
}
.MuiCheckbox-colorSecondary {
&.Mui-checked {
color: ${(props) => props.theme.colors.primary};
}
}
`;

/** Filter Bar Properties */
export interface Props {
className?: string;
/** currently checked filter options. Part of a `useState` with `setActiveFilters` */
activeFilters: string[];
/** the setState function for `activeFilters` */
setActiveFilters: (newFilters: string[]) => void;
/** Object containing column headers + corresponding filter options */
filterList: { [header: string]: string[] };
}

/** Form Filter Bar */
function UnstyledFilterBar({
className,
activeFilters,
setActiveFilters,
filterList,
}: Props) {
/** why isn't this a ref? It doesn't work. In The MUI Popover docs they do it with setState too... :( https://mui.com/components/popover/ */
const [anchorEl, setAnchorEl] = React.useState(null);
const [showFilters, setShowFilters] = React.useState(false);

const onCheck = (e: React.ChangeEvent<HTMLInputElement>, option: string) => {
e.target.checked
? setActiveFilters([...activeFilters, option])
: setActiveFilters(
activeFilters.filter((filterCheck) => filterCheck !== option)
);
};

const onClose = () => setShowFilters(false);

return (
<Flex className={className + " filter-bar"} align start>
<Button
variant="text"
color="inherit"
onClick={(e) => {
if (!anchorEl) setAnchorEl(e.currentTarget);
setShowFilters(!showFilters);
}}
>
<Icon type={IconType.FilterIcon} size="medium" color="neutral30" />
</Button>
<StyledPopover
PaperProps={{ square: true }}
elevation={0}
open={showFilters}
anchorEl={anchorEl}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
onClose={onClose}
>
<Spacer padding="medium">
<Flex wide align between>
<Text size="extraLarge" color="neutral30">
Filters
</Text>
<Button variant="text" color="inherit" onClick={onClose}>
<Icon type={IconType.ClearIcon} size="large" color="neutral30" />
</Button>
</Flex>
<List>
{_.map(filterList, (options: string[], header: string) => {
return (
<ListItem key={header}>
<Flex column>
<Text size="large" color="neutral30">
{header}
</Text>
<List>
{options.map((option: string, index: number) => {
return (
<ListItem key={index}>
<ListItemIcon>
<Checkbox onChange={(e) => onCheck(e, option)} />
</ListItemIcon>
<Text color="neutral30">{option}</Text>
</ListItem>
);
})}
</List>
</Flex>
</ListItem>
);
})}
</List>
</Spacer>
</StyledPopover>
</Flex>
);
}

export const FilterBar = styled(UnstyledFilterBar)``;

export default FilterBar;
12 changes: 11 additions & 1 deletion ui/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import AddIcon from "@material-ui/icons/Add";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
import ClearIcon from "@material-ui/icons/Clear";
import DeleteIcon from "@material-ui/icons/Delete";
import ErrorIcon from "@material-ui/icons/Error";
import LogoutIcon from "@material-ui/icons/ExitToApp";
import FilterIcon from "@material-ui/icons/FilterList";
import HourglassFullIcon from "@material-ui/icons/HourglassFull";
import LaunchIcon from "@material-ui/icons/Launch";
import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore";
Expand All @@ -12,7 +15,6 @@ import RemoveCircleIcon from "@material-ui/icons/RemoveCircle";
import SaveAltIcon from "@material-ui/icons/SaveAlt";
import SkipNextIcon from "@material-ui/icons/SkipNext";
import SkipPreviousIcon from "@material-ui/icons/SkipPrevious";
import LogoutIcon from "@material-ui/icons/ExitToApp";
import * as React from "react";
import styled from "styled-components";
// eslint-disable-next-line
Expand All @@ -37,6 +39,8 @@ export enum IconType {
SkipPreviousIcon,
RemoveCircleIcon,
LogoutIcon,
FilterIcon,
ClearIcon,
}

type Props = {
Expand Down Expand Up @@ -97,6 +101,12 @@ function getIcon(i: IconType) {
case IconType.LogoutIcon:
return LogoutIcon;

case IconType.FilterIcon:
return FilterIcon;

case IconType.ClearIcon:
return ClearIcon;

default:
break;
}
Expand Down
7 changes: 6 additions & 1 deletion ui/components/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Props = {
className?: string;
size?: keyof typeof fontSizes;
bold?: boolean;
semiBold?: boolean;
capitalize?: boolean;
italic?: boolean;
color?: keyof typeof colors;
Expand All @@ -14,7 +15,11 @@ type Props = {
const Text = styled.span<Props>`
font-family: ${(props) => props.theme.fontFamilies.regular};
font-size: ${(props) => props.theme.fontSizes[props.size]};
font-weight: ${(props) => (props.bold ? "600" : "400")};
font-weight: ${(props) => {
if (props.bold) return "800";
else if (props.semiBold) return "600";
else return "400";
}};
text-transform: ${(props) => (props.capitalize ? "uppercase" : "none")};
font-style: ${(props) => (props.italic ? "italic" : "normal")};
color: ${(props) => props.theme.colors[props.color as any]};
Expand Down
21 changes: 21 additions & 0 deletions ui/components/__tests__/ChipGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render, screen } from "@testing-library/react";
import "jest-styled-components";
import React from "react";
import { withTheme } from "../../lib/test-utils";
import ChipGroup from "../ChipGroup";

describe("ChipGroup", () => {
const setActiveChips = jest.fn();
const chipList = ["app", "app2", "app3"];

it("should render chips", () => {
render(
withTheme(
<ChipGroup activeChips={chipList} setActiveChips={setActiveChips} />
)
);
expect(screen.queryByText("app")).toBeTruthy();
expect(screen.queryByText("app3")).toBeTruthy();
expect(screen.queryByText("Clear All")).toBeTruthy();
});
});
41 changes: 41 additions & 0 deletions ui/components/__tests__/FilterBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { fireEvent, render, screen } from "@testing-library/react";
import "jest-styled-components";
import React from "react";
import { withTheme } from "../../lib/test-utils";
import FilterBar from "../FilterBar";

describe("FilterBar", () => {
const setActiveFilters = jest.fn();
const filterList = {
Name: ["app", "app2", "app3"],
Status: ["Ready", "Failed"],
Type: ["Application", "Helm Release"],
};
it("should initially render button with filter list closed", () => {
render(
withTheme(
<FilterBar
filterList={filterList}
activeFilters={[]}
setActiveFilters={setActiveFilters}
/>
)
);
expect(screen.getByRole("button")).toBeTruthy();
expect(screen.queryByText("Name")).toBeNull();
});
it("should reveal filter list on icon click", () => {
render(
withTheme(
<FilterBar
filterList={filterList}
activeFilters={[]}
setActiveFilters={setActiveFilters}
/>
)
);
const icon = screen.getAllByRole("button")[0];
fireEvent.click(icon);
expect(screen.queryByText("Name")).toBeTruthy();
});
});
2 changes: 1 addition & 1 deletion ui/components/__tests__/__snapshots__/Text.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`Text snapshots bold 1`] = `
.c0 {
font-family: 'proxima-nova',Helvetica,Arial,sans-serif;
font-size: 16px;
font-weight: 600;
font-weight: 800;
text-transform: none;
font-style: normal;
}
Expand Down
5 changes: 3 additions & 2 deletions ui/lib/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export const theme: DefaultTheme = {
white: "#fff",
primary: "#00b3ec",
primaryLight: "#98E0F7",
primaryDark: "#009CCC",
primary10: "#009CCC",
primary20: "#006B8E",
success: "#27AE60",
alert: "#BC381D",
neutral00: "#ffffff",
Expand Down Expand Up @@ -103,7 +104,7 @@ export const muiTheme = createTheme({
palette: {
primary: {
//Main - Primary Color Dark - 10
main: theme.colors.primaryDark,
main: theme.colors.primary10,
},
secondary: {
//Feedback - Alert - Original
Expand Down
35 changes: 35 additions & 0 deletions ui/stories/ChipGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Meta, Story } from "@storybook/react";
import React from "react";
import ChipGroup, { Props } from "../components/ChipGroup";

export default {
title: "ChipGroup",
component: ChipGroup,
parameters: {
docs: {
description: {
component:
"Series of deletable MUI Chip components: https://mui.com/components/chips/",
},
},
},
} as Meta;

const Template: Story<Props> = (args) => {
const [storyChips, setStoryChips] = React.useState([
"chip",
"chippy",
"another one",
]);

return (
<ChipGroup
{...args}
activeChips={storyChips}
setActiveChips={setStoryChips}
/>
);
};

export const Default = Template.bind({});
Default.args = {};

0 comments on commit 91b7f44

Please sign in to comment.