Table component implementation (#71)

Implementation of a new table wrapper component that will be used across the application.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2020-04-18 10:16:17 -05:00
committed by GitHub
parent 5c137a8678
commit 0bcf88eb7c
7 changed files with 476 additions and 89 deletions

View File

@@ -0,0 +1,60 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { IconButton } from "@material-ui/core";
import ViewIcon from "./TableActionIcons/ViewIcon";
import PencilIcon from "./TableActionIcons/PencilIcon";
import DeleteIcon from "./TableActionIcons/DeleteIcon";
interface IActionButton {
type: string;
onClick: (id: string) => any;
valueToSend: any;
selected: boolean;
}
const defineIcon = (type: string, selected: boolean) => {
switch (type) {
case "view":
return <ViewIcon active={selected} />;
case "edit":
return <PencilIcon active={selected} />;
case "delete":
return <DeleteIcon active={selected} />;
}
return null;
};
const TableActionButton = ({
type,
onClick,
valueToSend,
selected
}: IActionButton) => {
return (
<IconButton
aria-label={type}
onClick={() => {
onClick(valueToSend);
}}
>
{defineIcon(type, selected)}
</IconButton>
);
};
export default TableActionButton;

View File

@@ -0,0 +1,23 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const DeleteIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<g transform="translate(-1225 -657)">
<path
fill={active ? selected : unSelected}
d="M0,8H0a8,8,0,0,0,8,8H8a8,8,0,0,0,8-8h0A8,8,0,0,0,8,0H8A8,8,0,0,0,0,8Zm10.007,3.489L8,9.482,5.993,11.489,4.511,10.007,6.518,8,4.511,5.993,5.993,4.511,8,6.518l2.007-2.007,1.482,1.482L9.482,8l2.007,2.007Z"
transform="translate(1225 657)"
/>
</g>
</svg>
);
};
export default DeleteIcon;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const PencilIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 13.833 13.833"
>
<path
fill={active ? selected : unSelected}
d="M2.934,16H0V13.066L10.607,2.459a1,1,0,0,1,1.414,0l1.52,1.52a1,1,0,0,1,0,1.414Z"
transform="translate(0 -2.167)"
/>
</svg>
);
};
export default PencilIcon;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const ViewIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 11.856"
>
<path
fill={active ? selected : unSelected}
d="M-54,8l1.764,2.614A7.52,7.52,0,0,0-46,13.928h0a7.52,7.52,0,0,0,6.234-3.314L-38,8l-1.764-2.614A7.52,7.52,0,0,0-46,2.072h0a7.52,7.52,0,0,0-6.234,3.314Zm10.286,0A2.285,2.285,0,0,1-46,10.286,2.285,2.285,0,0,1-48.286,8,2.285,2.285,0,0,1-46,5.714,2.285,2.285,0,0,1-43.714,8Zm1.3,0A3.59,3.59,0,0,1-46,11.59,3.59,3.59,0,0,1-49.59,8,3.59,3.59,0,0,1-46,4.41,3.59,3.59,0,0,1-42.41,8Z"
transform="translate(54 -2.072)"
/>
</svg>
);
};
export default ViewIcon;

View File

@@ -0,0 +1,6 @@
export interface IIcon {
active: boolean;
}
export const unSelected = "#adadad";
export const selected = "#201763";

View File

@@ -0,0 +1,301 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import {
LinearProgress,
TablePagination,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Paper,
Grid,
Checkbox
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
interface ItemActions {
type: string;
onClick(valueToSend: any): any;
}
interface IColumns {
label: string;
elementKey: string;
sortable?: boolean;
}
interface IPaginatorConfig {
rowsPerPageOptions: number[];
colSpan: number;
count: number;
rowsPerPage: number;
page: number;
SelectProps: any;
onChangePage: (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number
) => void;
onChangeRowsPerPage?: React.ChangeEventHandler<
HTMLTextAreaElement | HTMLInputElement
>;
ActionsComponent?: React.ElementType<TablePaginationActionsProps>;
}
interface TableWrapperProps {
itemActions?: ItemActions[] | null;
columns: IColumns[];
onSelect?: (e: React.ChangeEvent<HTMLInputElement>) => any;
idField: string;
isLoading: boolean;
records: any[];
classes: any;
entityName: string;
selectedItems?: string[];
stickyHeader?: boolean;
paginatorConfig?: IPaginatorConfig;
}
const borderColor = "#eaeaea";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
padding: "19px 38px"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor
}
}
},
rowUnselected: {
...rowText
},
rowSelected: {
...rowText,
color: "#201763"
},
paginatorContainer: {
display: "flex",
justifyContent: "flex-end",
padding: "5px 38px"
},
checkBoxHeader: {
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9
}
},
actionsContainer: {
width: 120,
borderColor: borderColor
},
paginatorComponent: {
borderBottom: 0
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763"
},
checkBoxRow: {
borderColor: borderColor
}
});
const titleColumnsMap = (columns: IColumns[]) => {
const columnsList = columns.map((column: IColumns, index: number) => {
return (
<TableCell key={`tbCT-${column.elementKey}-${index}`}>
{column.label}
</TableCell>
);
});
return columnsList;
};
const rowColumnsMap = (
columns: IColumns[],
itemData: any,
classes: any,
isSelected: boolean
) => {
const rowElements = columns.map((column: IColumns, index: number) => {
const itemElement = get(itemData, column.elementKey, null);
return (
<TableCell
key={`tbRE-${column.elementKey}-${index}`}
className={isSelected ? classes.rowSelected : classes.rowUnselected}
>
{itemElement}
</TableCell>
);
});
return rowElements;
};
const elementActions = (
actions: ItemActions[],
valueToSend: any,
selected: boolean
) => {
const actionsElements = actions.map((action: ItemActions, index: number) => {
return (
<TableActionButton
type={action.type}
onClick={action.onClick}
valueToSend={valueToSend}
selected={selected}
key={`actions-${action.type}-${index.toString()}`}
/>
);
});
return actionsElements;
};
const TableWrapper = ({
itemActions,
columns,
onSelect,
records,
isLoading,
entityName,
selectedItems,
idField,
classes,
stickyHeader = false,
paginatorConfig
}: TableWrapperProps) => {
return (
<Grid item xs={12}>
<Paper className={classes.paper}>
{isLoading && <LinearProgress />}
{records && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
<TableRow>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxHeader}
>
Select
</TableCell>
)}
{titleColumnsMap(columns)}
{itemActions && itemActions.length > 0 && (
<TableCell
align="center"
className={classes.actionsContainer}
>
Actions
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{records.map((record: any, index: number) => {
const isSelected = selectedItems
? selectedItems.includes(record[idField])
: false;
return (
<TableRow key={`tb-${entityName}-${index.toString()}`}>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxRow}
>
<Checkbox
value={record[idField]}
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={isSelected}
onChange={onSelect}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
/>
</TableCell>
)}
{rowColumnsMap(columns, record, classes, isSelected)}
{itemActions && itemActions.length > 0 && (
<TableCell
align="right"
className={classes.actionsContainer}
>
{elementActions(itemActions, record, isSelected)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
) : (
<div>{`There are no ${entityName} yet.`}</div>
)}
</Paper>
{paginatorConfig && (
<Grid item xs={12} className={classes.paginatorContainer}>
<Table>
<TableBody>
<TableRow>
<TablePagination
{...paginatorConfig}
className={classes.paginatorComponent}
/>
</TableRow>
</TableBody>
</Table>
</Grid>
)}
</Grid>
);
};
export default withStyles(styles)(TableWrapper);

View File

@@ -19,25 +19,12 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Paper,
Grid,
Typography,
TextField,
InputAdornment
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import Checkbox from "@material-ui/core/Checkbox";
import DeleteIcon from "@material-ui/icons/Delete";
import ViewIcon from "@material-ui/icons/Visibility";
import GroupIcon from "@material-ui/icons/Group";
import { User, UsersList } from "./types";
import { usersSort } from "../../../utils/sortFunctions";
@@ -46,6 +33,7 @@ import { CreateIcon } from "../../../icons";
import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -251,6 +239,25 @@ class Users extends React.Component<IUsersProps, IUsersState> {
return elements;
};
const viewAction = (selectionElement: any): void => {
this.setState({
addScreenOpen: true,
selectedUser: selectionElement
});
};
const deleteAction = (selectionElement: any): void => {
this.setState({
deleteOpen: true,
selectedUser: selectionElement
});
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
];
return (
<React.Fragment>
{addScreenOpen && (
@@ -340,82 +347,30 @@ class Users extends React.Component<IUsersProps, IUsersState> {
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Access Key</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRecords.map(row => (
<TableRow key={`user-${row.accessKey}`}>
<TableCell padding="checkbox">
<Checkbox
value={row.accessKey}
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={checkedUsers.includes(row.accessKey)}
onChange={selectionChanged}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.accessKey}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="view"
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: row
});
}}
>
<ViewIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
this.setState({
deleteOpen: true,
selectedUser: row
});
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={3}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Users</div>
)}
</Paper>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
onSelect={selectionChanged}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
entityName="Users"
idField="accessKey"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
}}
/>
</Grid>
</Grid>
</React.Fragment>