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

Adds Pagination to Data Table #1367

Merged
merged 11 commits into from
Feb 2, 2022
8 changes: 5 additions & 3 deletions ui/components/CommitsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,20 @@ function CommitsTable({
fields={[
{
label: "SHA",
displayLabel: "SHA",
value: (row: Commit) => (
<Link newTab href={row.url}>
{row.hash}
</Link>
),
},
{
label: "Date",
label: "date",
displayLabel: "Date",
value: (row: Commit) => timestamp(row.date),
},
{ label: "Message", value: "message" },
{ label: "Author", value: "author" },
{ label: "message", displayLabel: "Message", value: "message" },
{ label: "author", displayLabel: "Author", value: "author" },
]}
rows={commits.commits}
/>
Expand Down
8 changes: 4 additions & 4 deletions ui/components/ConditionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ function ConditionsTable({ className, conditions }: Props) {
return (
<DataTable
fields={[
{ label: "Type", value: "type" },
{ label: "Status", value: "status" },
{ label: "Reason", value: "reason" },
{ label: "Message", value: "message" },
{ label: "type", displayLabel: "Type", value: "type" },
Copy link
Contributor

Choose a reason for hiding this comment

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

There shouldn't be a label and displayLabel prop here; label is already mean to give control over what gets displayed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the _.sort function to work the labels have to be all lowercase to match the fields on the rows which meant of lot of string.toLowerCase()-ing which I h8ed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would go with string.toLowerCase() myself. As it is in this PR, it is a leaky abstraction, and you are asking the user to think about how the sorting logic works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only other thing that's coming to mind is if someone wants to sort columns with a label that won't match when it's shifted to lowercase. Like "Last Commit" with last_commit as the key. It makes those columns not sortable right?

Copy link
Contributor

Choose a reason for hiding this comment

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

You can do stuff like _.lowerCase(_.snakeCase(string)) if you want to normalize the values. I would have to see the full filtering implementation to say for sure.

{ label: "status", displayLabel: "Status", value: "status" },
{ label: "reason", displayLabel: "Reason", value: "reason" },
{ label: "message", displayLabel: "Message", value: "message" },
]}
rows={conditions}
sortFields={[""]}
Expand Down
58 changes: 40 additions & 18 deletions ui/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface Props {
/** A list of objects with two fields: `label`, which is a string representing the column header, and `value`, which can be a string, or a function that extracts the data needed to fill the table cell. */
fields: {
label: string;
displayLabel: string;
value: string | ((k: any) => string | JSX.Element);
}[];
/** A list of data that will be iterated through to create the columns described in `fields`. */
Expand All @@ -30,10 +31,11 @@ export interface Props {
sortFields: string[];
/** an optional list of string widths for each field/column. */
widths?: string[];
/** for passing pagination */
children?: any;
}

const EmptyRow = styled(TableRow)<{ colSpan: number }>`
font-style: italic;
td {
text-align: center;
}
Expand All @@ -46,15 +48,14 @@ const TableButton = styled(Button)`
text-transform: none;
}
&.MuiButton-text {
color: ${(props) => props.theme.colors.neutral30};
min-width: 0px;
.selected {
color: ${(props) => props.theme.colors.neutral40};
}
}
&.arrow {
min-width: 0px;
}
&.selected {
color: ${(props) => props.theme.colors.neutral40};
}
`;

/** Form DataTable */
Expand All @@ -64,6 +65,7 @@ function UnstyledDataTable({
rows,
sortFields,
widths,
children,
}: Props) {
const [sort, setSort] = React.useState(sortFields[0]);
const [reverseSort, setReverseSort] = React.useState(false);
Expand All @@ -73,22 +75,22 @@ function UnstyledDataTable({
sorted.reverse();
}

type labelProps = { label: string };
function SortableLabel({ label }: labelProps) {
type labelProps = { label: string; displayLabel: string };
function SortableLabel({ label, displayLabel }: labelProps) {
return (
<Flex align start>
<TableButton
color="inherit"
variant="text"
onClick={() => {
setReverseSort(sort === label.toLowerCase() ? !reverseSort : false);
setSort(label.toLowerCase());
setReverseSort(sort === label ? !reverseSort : false);
setSort(label);
}}
>
<h2>{label}</h2>
<h2 className={sort === label ? "selected" : ""}>{displayLabel}</h2>
</TableButton>
<Spacer padding="xxs" />
{sort === label.toLowerCase() ? (
{sort === label ? (
<Icon
type={IconType.ArrowUpwardIcon}
size="base"
Expand Down Expand Up @@ -119,10 +121,13 @@ function UnstyledDataTable({
<TableRow>
{_.map(fields, (f, i) => (
<TableCell style={widths && { width: widths[i] }} key={f.label}>
{sortFields.includes(f.label.toLowerCase()) ? (
<SortableLabel label={f.label} />
{sortFields.includes(f.label) ? (
<SortableLabel
label={f.label}
displayLabel={f.displayLabel}
/>
) : (
<h2 className="thead">{f.label}</h2>
<h2>{f.displayLabel}</h2>
)}
</TableCell>
))}
Expand All @@ -134,24 +139,41 @@ function UnstyledDataTable({
) : (
<EmptyRow colSpan={fields.length}>
<TableCell colSpan={fields.length}>
<span style={{ fontStyle: "italic" }}>No rows</span>
<Flex center align>
<Icon
color="neutral20"
type={IconType.RemoveCircleIcon}
size="base"
/>
<Spacer padding="xxs" />
<Text color="neutral30">No data</Text>
</Flex>
</TableCell>
</EmptyRow>
)}
</TableBody>
</Table>
</TableContainer>
<Spacer padding="xs" />
{/* optional pagination component */}
{children}
</div>
);
}

export const DataTable = styled(UnstyledDataTable)`
h2 {
font-size: 14px;
font-weight: 600;
Copy link

Choose a reason for hiding this comment

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

I have played around with this in the inspector and for me this only renders bold (> 600) or regular (<600) – is semi-bold Proxima Nova not getting imported somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmmm...I feel like I've seen that before, but I just tested it here's the screenshots:

font-weight 800:
image

font-weight 600:
image

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 DEFINITELY changes but it is subtle for sure

color: ${(props) => props.theme.colors.neutral30};
margin: 0px;
}
.thead {
color: ${(props) => props.theme.colors.neutral30};
font-weight: 800;
.MuiTableRow-root {
transition: background 0.5s ease-in-out;
}
.MuiTableRow-root:not(.MuiTableRow-head):hover {
background: ${(props) => props.theme.colors.neutral10};
transition: background 0.5s ease-in-out;
}
`;

Expand Down
28 changes: 24 additions & 4 deletions ui/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import DeleteIcon from "@material-ui/icons/Delete";
import ErrorIcon from "@material-ui/icons/Error";
import HourglassFullIcon from "@material-ui/icons/HourglassFull";
import LaunchIcon from "@material-ui/icons/Launch";
import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore";
import NavigateNextIcon from "@material-ui/icons/NavigateNext";
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 * as React from "react";
import styled from "styled-components";
import { colors, spacing } from "../typedefs/styled";
Expand All @@ -21,11 +25,15 @@ export enum IconType {
AddIcon,
ArrowUpwardIcon,
DeleteIcon,
NavigateNextIcon,
SaveAltIcon,
ErrorIcon,
CheckCircleIcon,
HourglassFullIcon,
NavigateNextIcon,
NavigateBeforeIcon,
SkipNextIcon,
SkipPreviousIcon,
RemoveCircleIcon,
}

type Props = {
Expand Down Expand Up @@ -56,9 +64,6 @@ function getIcon(i: IconType) {
case IconType.DeleteIcon:
return DeleteIcon;

case IconType.NavigateNextIcon:
return NavigateNextIcon;

case IconType.SaveAltIcon:
return SaveAltIcon;

Expand All @@ -71,6 +76,21 @@ function getIcon(i: IconType) {
case IconType.ErrorIcon:
return ErrorIcon;

case IconType.NavigateNextIcon:
return NavigateNextIcon;

case IconType.NavigateBeforeIcon:
return NavigateBeforeIcon;

case IconType.SkipNextIcon:
return SkipNextIcon;

case IconType.SkipPreviousIcon:
return SkipPreviousIcon;

case IconType.RemoveCircleIcon:
return RemoveCircleIcon;

default:
break;
}
Expand Down
6 changes: 5 additions & 1 deletion ui/components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ type Props = {
};

function Link({ children, href, className, to = "", newTab, ...props }: Props) {
const txt = <Text color="primary">{children}</Text>;
const txt = (
<Text size="small" color="primary">
Copy link
Contributor

Choose a reason for hiding this comment

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

We want to change all <Link /> components to have small text?

{children}
</Text>
);

if (href) {
return (
Expand Down
118 changes: 118 additions & 0 deletions ui/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { FormControl, MenuItem, Select } from "@material-ui/core";
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";

export interface Props {
/** CSS MUI Overrides or other styling. */
className?: string;
/** func for forward one page button */
onForward: () => void;
/** func for skip to last page button */
onSkipForward: () => void;
/** func for back one page button */
onBack: () => void;
/** func for skip to start button */
onSkipBack: () => void;
/** onChange func for perPage select */
onSelect: (value) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's give this value a type. Is it a string? Number?

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's an event, and it needs to always be an event right? Is there a way to express that in Typescript?

Copy link
Contributor

Choose a reason for hiding this comment

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

Something like React.SyntheticEvent or some such:

Suggested change
onSelect: (value) => void;
onSelect: (ev: React.SyntheticEvent) => void;

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/14d95eb0fe90f5e0579c49df136cccdfe89b2855/types/react/index.d.ts#L1167

You can also just leave it blank if you don't expect the user to do anything with the arg.

/** options for perPage select */
perPageOptions?: number[];
/** starting index */
index: number;
/** total rows */
length: number;
/** all objects */
totalObjects: number;
}

function unstyledPagination({
className,
onForward,
onSkipForward,
onBack,
onSkipBack,
onSelect,
perPageOptions = [25, 50, 75, 100],
index,
length,
totalObjects,
}: Props) {
return (
<Flex wide align end className={className}>
<FormControl>
<Flex align>
<label htmlFor="pagination">Rows Per Page: </label>
<Spacer padding="xxs" />
<Select
id="pagination"
variant="outlined"
defaultValue={perPageOptions[0]}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
onSelect(e.target.value);
}}
>
{perPageOptions.map((option, index) => {
return (
<MenuItem key={index} value={option}>
{option}
</MenuItem>
);
})}
</Select>
</Flex>
</FormControl>
<Spacer padding="base" />
<Text>
{index + 1} - {index + length} out of {totalObjects}
</Text>
<Spacer padding="base" />
<Flex>
<Button
color="inherit"
variant="text"
aria-label="skip to first page"
disabled={index === 0}
onClick={() => onSkipBack()}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
onClick={() => onSkipBack()}
onClick={onSkipBack}

>
<Icon type={IconType.SkipPreviousIcon} size="medium" />
</Button>
<Button
color="inherit"
variant="text"
aria-label="back one page"
disabled={index === 0}
onClick={() => onBack()}
>
<Icon type={IconType.NavigateBeforeIcon} size="medium" />
</Button>
<Button
color="inherit"
variant="text"
aria-label="forward one page"
disabled={index + length >= totalObjects}
onClick={() => onForward()}
>
<Icon type={IconType.NavigateNextIcon} size="medium" />
</Button>
<Button
color="inherit"
variant="text"
aria-label="skip to last page"
disabled={index + length >= totalObjects}
onClick={() => onSkipForward()}
>
<Icon type={IconType.SkipNextIcon} size="medium" />
</Button>
</Flex>
</Flex>
);
}

export const Pagination = styled(unstyledPagination)``;

export default Pagination;
9 changes: 6 additions & 3 deletions ui/components/__tests__/DataTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ describe("DataTable", () => {

const fields = [
{
label: "Name",
label: "name",
displayLabel: "Name",
value: ({ name }) => <a href="/some_url">{name}</a>,
},
{
label: "Status",
label: "status",
displayLabel: "Status",
value: (v) => v.status,
},
{
label: "Last Updated",
label: "lastUpdate",
displayLabel: "Last Updated",
value: "lastUpdate",
},
];
Expand Down