First set of redesign objects listing (#1602)

- Removed table action buttons & global actions

- Changed Upload Files label

- Added reload, new path & rewind buttons in top bars

- Added multi-select objects panel & table behaviors for single selection & multiselection

- Fixed disabled action button styles

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-02-18 11:18:09 -07:00
committed by GitHub
parent 1cdc719405
commit 78e4e3fd08
8 changed files with 187 additions and 374 deletions

View File

@@ -99,6 +99,8 @@ import {
import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton";
import MultiSelectionPanel from "./MultiSelectionPanel";
const AddFolderIcon = React.lazy(
() => import("../../../../../../icons/AddFolderIcon")
@@ -346,6 +348,17 @@ const ListObjects = ({
}
}, [quota, bucketName]);
useEffect(() => {
if (selectedObjects.length > 0) {
setDetailsOpen(true);
return;
}
if (selectedObjects.length === 0 && selectedInternalPaths === null) {
setDetailsOpen(false);
}
}, [selectedObjects, selectedInternalPaths]);
const displayDeleteObject = hasPermission(bucketName, [
IAM_SCOPES.S3_DELETE_OBJECT,
]);
@@ -713,6 +726,7 @@ const ListObjects = ({
};
const openPath = (idElement: string) => {
setSelectedObjects([]);
if (idElement.endsWith("/")) {
const newPath = `/buckets/${bucketName}/browse${
idElement ? `/${encodeFileName(idElement)}` : ``
@@ -995,6 +1009,7 @@ const ListObjects = ({
elements = elements.filter((element) => element !== value);
}
setSelectedObjects(elements);
setSelectedInternalPaths(null);
return elements;
};
@@ -1115,6 +1130,42 @@ const ListObjects = ({
uploadPath = uploadPath.concat(currentPath);
}
const multiActionButtons = [
{
action: downloadSelected,
label: "Download",
disabled: selectedObjects.length === 0,
icon: <DownloadIcon />,
tooltip: "Download Selected",
},
{
action: openShare,
label: "Share",
disabled: selectedObjects.length !== 1 || !canShareFile,
icon: <ShareIcon />,
tooltip: "Share Selected File",
},
{
action: openPreview,
label: "Preview",
disabled: selectedObjects.length !== 1 || !canPreviewFile,
icon: <PreviewIcon />,
tooltip: "Preview Selected File",
},
{
action: () => {
setDeleteMultipleOpen(true);
},
label: "Delete",
icon: <DeleteIcon />,
disabled:
!hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]) ||
selectedObjects.length === 0 ||
!displayDeleteObject,
tooltip: "Delete Selected Files",
},
];
return (
<React.Fragment>
{shareFileModalOpen && selectedPreview && (
@@ -1215,6 +1266,61 @@ const ListObjects = ({
}
actions={
<Fragment>
<RBIconButton
id={"new-path"}
tooltip={"Choose or create a new path"}
text={"New Path"}
icon={<AddFolderIcon />}
color="primary"
variant={"outlined"}
onClick={() => {
setCreateFolderOpen(true);
}}
disabled={
rewindEnabled ||
!hasPermission(bucketName, [IAM_SCOPES.S3_PUT_OBJECT])
}
/>
<RBIconButton
id={"rewind-objects-list"}
tooltip={"Rewind Bucket"}
text={"Rewind"}
icon={
<Badge
badgeContent=" "
color="secondary"
variant="dot"
invisible={!rewindEnabled}
className={classes.badgeOverlap}
>
<HistoryIcon />
</Badge>
}
color="primary"
variant={"outlined"}
onClick={() => {
setRewindSelect(true);
}}
disabled={
!isVersioned ||
!hasPermission(bucketName, [IAM_SCOPES.S3_PUT_OBJECT])
}
/>
<RBIconButton
id={"refresh-objects-list"}
tooltip={"Reload List"}
text={"Reload"}
icon={<RefreshIcon />}
color="primary"
variant={"outlined"}
onClick={() => {
setLoading(true);
}}
disabled={
!hasPermission(bucketName, [IAM_SCOPES.S3_LIST_BUCKET]) ||
rewindEnabled
}
/>
<input
type="file"
multiple
@@ -1282,96 +1388,21 @@ const ListObjects = ({
triggerSort: sortChange,
}}
onSelectAll={selectAllItems}
actionButtons={[
{
action: downloadSelected,
label: "Download",
disabled: selectedObjects.length === 0,
icon: <DownloadIcon />,
tooltip: "Download Selected",
},
{
action: openShare,
label: "Share",
disabled: selectedObjects.length !== 1 || !canShareFile,
icon: <ShareIcon />,
tooltip: "Share Selected File",
},
{
action: openPreview,
label: "Preview",
disabled: selectedObjects.length !== 1 || !canPreviewFile,
icon: <PreviewIcon />,
tooltip: "Preview Selected File",
},
{
action: () => {
setDeleteMultipleOpen(true);
},
label: "Delete",
icon: <DeleteIcon />,
disabled:
!hasPermission(bucketName, [
IAM_SCOPES.S3_DELETE_OBJECT,
]) ||
selectedObjects.length === 0 ||
!displayDeleteObject,
tooltip: "Delete Selected Files",
},
{
action: () => {
setRewindSelect(true);
},
label: "Rewind",
disabled:
!isVersioned ||
!hasPermission(bucketName, [IAM_SCOPES.S3_PUT_OBJECT]),
icon: (
<Badge
badgeContent=" "
color="secondary"
variant="dot"
invisible={!rewindEnabled}
className={classes.badgeOverlap}
>
<HistoryIcon />
</Badge>
),
tooltip: "Rewind Bucket",
},
{
action: () => {
setCreateFolderOpen(true);
},
label: "New Path",
icon: <AddFolderIcon />,
disabled:
rewindEnabled ||
!hasPermission(bucketName, [IAM_SCOPES.S3_PUT_OBJECT]),
tooltip: "Choose or create a new path",
},
]}
globalActions={[
{
action: () => {
setLoading(true);
},
label: "Reload",
icon: <RefreshIcon />,
disabled:
!hasPermission(bucketName, [IAM_SCOPES.S3_LIST_BUCKET]) ||
rewindEnabled,
tooltip: "Reload List",
},
]}
/>
<DetailsListPanel
open={detailsOpen}
closePanel={() => {
setDetailsOpen(false);
setSelectedInternalPaths(null);
setSelectedObjects([]);
}}
>
{selectedObjects.length > 0 && (
<MultiSelectionPanel
items={multiActionButtons}
title={"Selected Objects:"}
/>
)}
{selectedInternalPaths !== null && (
<ObjectDetailPanel
internalPaths={selectedInternalPaths}

View File

@@ -0,0 +1,69 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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, { Fragment } from "react";
import ObjectActionButton from "./ObjectActionButton";
import { withStyles } from "@mui/styles";
import createStyles from "@mui/styles/createStyles";
import { detailsPanel } from "../../../../Common/FormComponents/common/styleLibrary";
const styles = () =>
createStyles({
...detailsPanel,
});
export interface MultiSelectionItem {
action: () => void;
label: string;
disabled: boolean;
icon: React.ReactNode;
tooltip: string;
}
interface IMultiSelectionPanelProps {
items: MultiSelectionItem[];
title: string;
classes: any;
}
const MultiSelectionPanel = ({
items,
classes,
title,
}: IMultiSelectionPanelProps) => {
return (
<Fragment>
<div className={classes.titleLabel}>{title}</div>
<ul className={classes.objectActions}>
<li>Actions:</li>
{items.map((actionItem) => {
return (
<li>
<ObjectActionButton
label={actionItem.label}
icon={actionItem.icon}
onClick={actionItem.action}
disabled={actionItem.disabled}
/>
</li>
);
})}
</ul>
</Fragment>
);
};
export default withStyles(styles)(MultiSelectionPanel);

View File

@@ -65,6 +65,7 @@ const ObjectActionButton = ({
return (
<Button
{...restProps}
disabled={disabled}
onClick={onClick}
className={classes.root}
startIcon={icon}

View File

@@ -435,7 +435,7 @@ const ObjectDetailPanel = ({
</div>
<ul className={classes.objectActions}>
<li>Object Actions:</li>
<li>Actions:</li>
<li>
<ObjectActionButton
label={"Download"}

View File

@@ -84,7 +84,7 @@ const UploadFilesButton = ({
aria-haspopup="true"
aria-expanded={openUploadMenu ? "true" : undefined}
onClick={handleClick}
text={"Upload Files"}
text={"Upload"}
icon={<UploadIcon />}
color="primary"
variant={"contained"}

View File

@@ -49,7 +49,7 @@ const styles = (theme: Theme) =>
...deleteDialogStyles,
root: {
"& .MuiPaper-root": {
padding: "1rem 2rem 2rem 1rem",
padding: "0 2rem 2rem 1rem",
},
},
content: {

View File

@@ -34,7 +34,6 @@ import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
import TableActionButton from "./TableActionButton";
import CheckboxWrapper from "../FormComponents/CheckboxWrapper/CheckboxWrapper";
import TopActionButton from "./TopActionButton";
import history from "../../../../history";
import {
checkboxIcons,
@@ -81,15 +80,6 @@ interface ISortConfig {
currentSort: string;
currentDirection: "ASC" | "DESC" | undefined;
}
interface IActionButton {
label: string;
icon?: React.ReactNode;
action: () => void;
disabled: boolean;
tooltip?: string;
}
interface TableWrapperProps {
itemActions?: ItemActions[] | null;
columns: IColumns[];
@@ -119,10 +109,6 @@ interface TableWrapperProps {
}: {
index: number;
}) => "deleted" | "" | React.CSSProperties;
generalTableActions?: () => void;
actionButtons?: IActionButton[];
subActions?: React.ReactNode;
globalActions?: IActionButton[];
}
const borderColor = "#9c9c9c80";
@@ -133,7 +119,7 @@ const styles = () =>
display: "flex",
overflow: "auto",
flexDirection: "column",
padding: "8px 16px",
padding: "0 16px 8px",
boxShadow: "none",
border: "#EAEDEE 1px solid",
borderRadius: 3,
@@ -144,16 +130,6 @@ const styles = () =>
width: 0,
height: 3,
},
"&.actionsBar": {
padding: "45px 16px 8px",
},
},
topHelpers: {
position: "absolute",
top: 0,
left: 0,
backgroundColor: "#F8F8F8",
borderBottom: "#EAEDEE 1px solid",
},
noBackground: {
backgroundColor: "transparent",
@@ -190,59 +166,6 @@ const styles = () =>
checkAllWrapper: {
marginTop: -16,
},
actionsScrollable: {
display: "flex",
overflowX: "auto",
overflowY: "hidden",
height: 36,
alignItems: "center",
"&::-webkit-scrollbar": {
height: 2,
minHeight: 2,
borderRadius: 0,
},
"&::-webkit-scrollbar-track": {
background: "#F0F0F0",
borderRadius: 0,
boxShadow: "inset 0px 0px 0px 0px transparent",
height: 2,
},
"&::-webkit-scrollbar-thumb": {
background: "#5E5E5E",
borderRadius: 0,
},
"&::-webkit-scrollbar-thumb:hover": {
background: "#4C4C4C",
},
},
objectsSelected: {
display: "flex",
flexGrow: 0,
whiteSpace: "nowrap",
backgroundColor: "#F4F2F2",
color: "#000",
fontWeight: "bold",
fontSize: 14,
height: 37,
maxHeight: 37,
padding: "0 25px",
alignItems: "center",
position: "relative",
"&::after": {
content: "' '",
borderRight: "#eaeaea 1px solid",
width: 1,
height: 22,
display: "block",
position: "absolute",
right: 0,
},
},
globalActions: {
display: "flex",
flexGrow: 0,
justifyContent: "flex-end",
},
"@global": {
".rowLine": {
borderBottom: `1px solid ${borderColor}`,
@@ -541,9 +464,6 @@ const TableWrapper = ({
disabled = false,
onSelectAll,
rowStyle,
actionButtons,
subActions,
globalActions,
}: TableWrapperProps) => {
const [columnSelectorOpen, setColumnSelectorOpen] = useState<boolean>(false);
const [anchorEl, setAnchorEl] = React.useState<any>(null);
@@ -642,10 +562,6 @@ const TableWrapper = ({
customPaperHeight !== ""
? customPaperHeight
: classes.defaultPaperHeight
} ${
onSelect || actionButtons || subActions || globalActions
? "actionsBar"
: ""
}`}
>
{isLoading && (
@@ -658,75 +574,6 @@ const TableWrapper = ({
</Grid>
</Grid>
)}
{!isLoading && (
<Fragment>
<Grid
container
direction={"row"}
alignItems={"center"}
flexGrow={"1"}
className={classes.topHelpers}
>
{onSelect && (
<Grid item xs className={classes.objectsSelected}>
{selectedItems?.length || 0} Item
{selectedItems?.length
? selectedItems?.length !== 1 && "s"
: "s"}{" "}
selected
</Grid>
)}
{actionButtons && (
<Grid
item
xs
className={classes.actionsScrollable}
flexWrap={"nowrap"}
>
{actionButtons.map((button, index) => {
return (
<TopActionButton
variant={"text"}
onClick={button.action}
disabled={button.disabled}
id={`button-option-${button.label}`}
key={`tableActon-${index.toString()}`}
startIcon={button.icon}
tooltip={button.tooltip}
>
{button.label}
</TopActionButton>
);
})}
</Grid>
)}
{subActions && (
<Grid item xs>
{subActions}
</Grid>
)}
{globalActions && (
<Grid item xs className={classes.globalActions}>
{globalActions.map((button, index) => {
return (
<TopActionButton
variant={"contained"}
onClick={button.action}
disabled={button.disabled}
id={`button-option-${button.label}`}
key={`tableActon-${index.toString()}`}
endIcon={button.icon}
tooltip={button.tooltip}
>
{button.label}
</TopActionButton>
);
})}
</Grid>
)}
</Grid>
</Fragment>
)}
{columnsSelector && !isLoading && records.length > 0 && (
<div className={classes.overlayColumnSelection}>
{columnsSelection(columns)}

View File

@@ -1,135 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Button, ButtonProps, Tooltip } from "@mui/material";
import clsx from "clsx";
const styles = (theme: Theme) =>
createStyles({
root: {
padding: "0 15px",
height: 22,
margin: 0,
color: "#5E5E5E",
fontWeight: "normal",
fontSize: 14,
whiteSpace: "nowrap",
borderRight: "#E5E5E5 1px solid",
borderStyle: "solid",
borderRadius: 0,
"&:hover": {
backgroundColor: "transparent",
color: "#000",
},
"& .min-icon": {
width: 11,
},
"&:disabled": {
color: "#EBEBEB",
borderColor: "#EBEBEB",
},
"&": {
"@media (max-width: 1279px)": {
padding: 0,
display: "flex",
justifyContent: "center",
"& .min-icon": {
width: 15,
},
},
},
},
contained: {
borderColor: "#5E5E5E",
background: "#5E5E5E",
color: "white",
borderRadius: 0,
height: 37,
fontWeight: "bold",
padding: "15px 25px",
"& .MuiTouchRipple-root span": {
backgroundColor: "#4c4c4c",
borderRadius: 3,
opacity: 0.3,
},
"&:hover": {
backgroundColor: "#4c4c4c",
color: "#FFF",
},
"& .min-icon": {
width: 12,
marginTop: -3,
},
"&": {
"@media (max-width: 1279px)": {
padding: 0,
display: "flex",
justifyContent: "center",
"& .min-icon": {
width: 15,
},
},
},
},
});
interface ITopActionButton extends ButtonProps {
classes: any;
children: any;
variant?: "text" | "contained";
tooltip?: string;
}
const TopActionButton = ({
classes,
children,
variant = "text",
tooltip,
...rest
}: ITopActionButton) => {
return (
<Tooltip title={tooltip || ""}>
<span>
<Button
{...rest}
className={clsx(classes.root, {
[classes.contained]: variant === "contained",
})}
sx={{
"& span.buttonItem": {
"@media (max-width: 1279px)": {
display: "none",
},
},
"& span": {
"@media (max-width: 1279px)": {
margin: 0,
},
},
}}
>
<span className={"buttonItem"}>{children}</span>
</Button>
</span>
</Tooltip>
);
};
export default withStyles(styles)(TopActionButton);