diff --git a/portal-ui/src/icons/DeleteIcon.tsx b/portal-ui/src/icons/DeleteIcon.tsx index 9ca333e7d4..95a217ad4a 100644 --- a/portal-ui/src/icons/DeleteIcon.tsx +++ b/portal-ui/src/icons/DeleteIcon.tsx @@ -25,22 +25,18 @@ const DeleteIcon = (props: SVGProps) => ( viewBox="0 0 256 256" {...props} > - - - - - - - - - - - + + + ); - export default DeleteIcon; diff --git a/portal-ui/src/icons/HistoryIcon.tsx b/portal-ui/src/icons/HistoryIcon.tsx index a5b270d4f4..a95b2c44cb 100644 --- a/portal-ui/src/icons/HistoryIcon.tsx +++ b/portal-ui/src/icons/HistoryIcon.tsx @@ -25,22 +25,20 @@ const HistoryIcon = (props: SVGProps) => ( viewBox="0 0 256 256" {...props} > - - - - - - - - + diff --git a/portal-ui/src/icons/PreviewIcon.tsx b/portal-ui/src/icons/PreviewIcon.tsx index ecbaf17736..46850595fb 100644 --- a/portal-ui/src/icons/PreviewIcon.tsx +++ b/portal-ui/src/icons/PreviewIcon.tsx @@ -19,28 +19,30 @@ import { SVGProps } from "react"; const PreviewIcon = (props: SVGProps) => ( - - - - - - - - - - + + + + + + + + ); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index 3f5429e193..c39defe8ff 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -89,8 +89,7 @@ import SearchBox from "../../../../Common/SearchBox"; import withSuspense from "../../../../Common/Components/withSuspense"; import { displayName } from "./utils"; -import { DownloadIcon } from "../../../../../../icons"; -import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton"; +import { DownloadIcon, PreviewIcon, ShareIcon } from "../../../../../../icons"; import UploadFilesButton from "../../UploadFilesButton"; const AddFolderIcon = React.lazy( @@ -119,7 +118,6 @@ const ShareFile = withSuspense( React.lazy(() => import("../ObjectDetails/ShareFile")) ); const RewindEnable = withSuspense(React.lazy(() => import("./RewindEnable"))); -const DeleteObject = withSuspense(React.lazy(() => import("./DeleteObject"))); const PreviewFileModal = withSuspense( React.lazy(() => import("../Preview/PreviewFileModal")) ); @@ -140,8 +138,11 @@ const styles = (theme: Theme) => badgeOverlap: { "& .MuiBadge-badge": { - top: 35, - right: 10, + top: 10, + right: 1, + width: 5, + height: 5, + minWidth: 5 }, }, screenTitle: { @@ -233,7 +234,6 @@ const ListObjects = ({ classes, match, history, - downloadingFiles, rewindEnabled, rewindDate, bucketToRewind, @@ -254,10 +254,8 @@ const ListObjects = ({ const [loading, setLoading] = useState(true); const [rewind, setRewind] = useState([]); const [loadingRewind, setLoadingRewind] = useState(false); - const [deleteOpen, setDeleteOpen] = useState(false); const [deleteMultipleOpen, setDeleteMultipleOpen] = useState(false); const [createFolderOpen, setCreateFolderOpen] = useState(false); - const [selectedObject, setSelectedObject] = useState(""); const [filterObjects, setFilterObjects] = useState(""); const [loadingStartTime, setLoadingStartTime] = useState(0); const [loadingMessage, setLoadingMessage] = @@ -276,6 +274,8 @@ const ListObjects = ({ >("ASC"); const [currentSortField, setCurrentSortField] = useState("name"); const [iniLoad, setIniLoad] = useState(false); + const [canShareFile, setCanShareFile] = useState(false); + const [canPreviewFile, setCanPreviewFile] = useState(false); const internalPaths = get(match.params, "subpaths", ""); const bucketName = match.params["bucketName"]; @@ -290,6 +290,27 @@ const ListObjects = ({ } }, [folderUpload]); + useEffect(() => { + if (selectedObjects.length === 1) { + const objectName = selectedObjects[0]; + + if (extensionPreview(objectName) !== "none") { + setCanPreviewFile(true); + } else { + setCanPreviewFile(false); + } + + if (objectName.endsWith("/")) { + setCanShareFile(false); + } else { + setCanShareFile(true); + } + } else { + setCanShareFile(false); + setCanPreviewFile(false); + } + }, [selectedObjects]); + const displayDeleteObject = hasPermission(bucketName, [ IAM_SCOPES.S3_DELETE_OBJECT, ]); @@ -575,15 +596,6 @@ const ListObjects = ({ setErrorSnackMessage, ]); - const closeDeleteModalAndRefresh = (refresh: boolean) => { - setDeleteOpen(false); - - if (refresh) { - setSnackBarMessage(`Object '${selectedObject}' deleted successfully.`); - setLoading(true); - } - }; - const closeDeleteMultipleModalAndRefresh = (refresh: boolean) => { setDeleteMultipleOpen(false); @@ -630,11 +642,6 @@ const ListObjects = ({ return niceBytes(String(object.size)); }; - const confirmDeleteObject = (object: BucketObject) => { - setDeleteOpen(true); - setSelectedObject(object.name); - }; - const displayDeleteFlag = (state: boolean) => { return state ? "Yes" : "No"; }; @@ -856,14 +863,36 @@ const ListObjects = ({ [isDragActive, isDragAccept] ); - const openPreview = (fileObject: BucketObject) => { - setSelectedPreview(fileObject); - setPreviewOpen(true); + const openPreview = () => { + if (selectedObjects.length === 1) { + let fileObject: BucketObject | undefined; + + const findFunction = (currValue: BucketObject | RewindObject) => + selectedObjects.includes(currValue.name); + + fileObject = filteredRecords.find(findFunction); + + if (fileObject) { + setSelectedPreview(fileObject); + setPreviewOpen(true); + } + } }; - const openShare = (fileObject: BucketObject) => { - setSelectedPreview(fileObject); - setShareFileModalOpen(true); + const openShare = () => { + if (selectedObjects.length === 1) { + let fileObject: BucketObject | undefined; + + const findFunction = (currValue: BucketObject | RewindObject) => + selectedObjects.includes(currValue.name); + + fileObject = filteredRecords.find(findFunction); + + if (fileObject) { + setSelectedPreview(fileObject); + setShareFileModalOpen(true); + } + } }; const closeShareModal = () => { @@ -878,50 +907,8 @@ const ListObjects = ({ onClick: openPath, sendOnlyId: true, }, - { - type: "preview", - label: "Preview", - onClick: openPreview, - disableButtonFunction: (item: string) => - extensionPreview(item) === "none", - }, - { - type: "share", - label: "Share", - onClick: openShare, - disableButtonFunction: (item: string) => item.endsWith("/"), - }, - { - type: "download", - label: "Download", - onClick: downloadObject, - showLoaderFunction: (item: string) => - downloadingFiles.includes(`${match.params["bucket"]}/${item}`), - disableButtonFunction: (item: string) => { - if (rewindEnabled) { - const element = rewind.find((elm) => elm.name === item); - - if (element && element.delete_flag) { - return true; - } - } - return false; - }, - sendOnlyId: false, - }, ]; - if (displayDeleteObject) { - tableActions.push({ - type: "delete", - label: "Delete", - onClick: confirmDeleteObject, - disableButtonFunction: () => { - return rewindEnabled; - }, - }); - } - const filteredRecords = records.filter((b: BucketObject) => { if (filterObjects === "") { return true; @@ -1089,15 +1076,6 @@ const ListObjects = ({ }} /> )} - {deleteOpen && ( - - )} {deleteMultipleOpen && ( - -
- - { - setDeleteMultipleOpen(true); - }} - text={"Delete Selected"} - icon={} - color="secondary" - disabled={selectedObjects.length === 0} - variant={"outlined"} - /> - - } - color="primary" - disabled={selectedObjects.length === 0} - variant={"contained"} - /> -
- - - - - { - setCreateFolderOpen(true); - }} - text={""} - icon={} - color="primary" - disabled={rewindEnabled} - variant={"outlined"} - /> - - - - { - setRewindSelect(true); - }} - text={""} - icon={} - color="primary" - disabled={!isVersioned} - variant={"outlined"} - /> - - - - { - setLoading(true); - }} - text={""} - icon={} - color="primary" - disabled={rewindEnabled} - variant={"contained"} - /> - -
@@ -1332,6 +1218,88 @@ const ListObjects = ({ triggerSort: sortChange, }} onSelectAll={selectAllItems} + actionButtons={[ + { + action: downloadSelected, + label: "Download", + disabled: selectedObjects.length === 0, + icon: , + tooltip: "Download Selected", + }, + { + action: openShare, + label: "Share", + disabled: selectedObjects.length !== 1 || !canShareFile, + icon: , + tooltip: "Share Selected File", + }, + { + action: openPreview, + label: "Preview", + disabled: selectedObjects.length !== 1 || !canPreviewFile, + icon: , + tooltip: "Preview Selected File", + }, + { + action: () => { + setDeleteMultipleOpen(true); + }, + label: "Delete", + icon: , + 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: ( + + + + ), + tooltip: "Rewind Bucket", + }, + { + action: () => { + setCreateFolderOpen(true); + }, + label: "New Path", + icon: , + disabled: + rewindEnabled || + !hasPermission(bucketName, [IAM_SCOPES.S3_PUT_OBJECT]), + tooltip: "Choose or create a new path", + }, + ]} + globalActions={[ + { + action: () => { + setLoading(true); + }, + label: "Reload", + icon: , + disabled: + !hasPermission(bucketName, [IAM_SCOPES.S3_LIST_BUCKET]) || + rewindEnabled, + tooltip: "Reload List", + }, + ]} />
diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/UploadFilesButton.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/UploadFilesButton.tsx index 35af2c3c76..67016ce04f 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/UploadFilesButton.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/UploadFilesButton.tsx @@ -34,6 +34,7 @@ interface IUploadFilesButton { const styles = (theme: Theme) => createStyles({ listUploadIcons: { + height: 20, "& .min-icon": { width: 18, fill: "rgba(0,0,0,0.87)", diff --git a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx index 673e16f25d..116a1bfb04 100644 --- a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx @@ -34,6 +34,7 @@ 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,6 +82,14 @@ interface ISortConfig { currentDirection: "ASC" | "DESC" | undefined; } +interface IActionButton { + label: string; + icon?: React.ReactNode; + action: () => void; + disabled: boolean; + tooltip?: string; +} + interface TableWrapperProps { itemActions?: ItemActions[] | null; columns: IColumns[]; @@ -110,6 +119,10 @@ interface TableWrapperProps { }: { index: number; }) => "deleted" | "" | React.CSSProperties; + generalTableActions?: () => void; + actionButtons?: IActionButton[]; + subActions?: React.ReactNode; + globalActions?: IActionButton[]; } const borderColor = "#9c9c9c80"; @@ -128,9 +141,19 @@ const styles = () => overflowY: "scroll", position: "relative", "&::-webkit-scrollbar": { - width: 3, + 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", @@ -167,6 +190,59 @@ 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}`, @@ -465,6 +541,9 @@ const TableWrapper = ({ disabled = false, onSelectAll, rowStyle, + actionButtons, + subActions, + globalActions, }: TableWrapperProps) => { const [columnSelectorOpen, setColumnSelectorOpen] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -563,6 +642,10 @@ const TableWrapper = ({ customPaperHeight !== "" ? customPaperHeight : classes.defaultPaperHeight + } ${ + onSelect || actionButtons || subActions || globalActions + ? "actionsBar" + : "" }`} > {isLoading && ( @@ -575,6 +658,71 @@ const TableWrapper = ({ )} + {!isLoading && ( + + + {onSelect && ( + + {selectedItems?.length || 0} Objects selected + + )} + {actionButtons && ( + + {actionButtons.map((button, index) => { + return ( + + {button.label} + + ); + })} + + )} + {subActions && ( + + {subActions} + + )} + {globalActions && ( + + {globalActions.map((button, index) => { + return ( + + {button.label} + + ); + })} + + )} + + + )} {columnsSelector && !isLoading && records.length > 0 && (
{columnsSelection(columns)} diff --git a/portal-ui/src/screens/Console/Common/TableWrapper/TopActionButton.tsx b/portal-ui/src/screens/Console/Common/TableWrapper/TopActionButton.tsx new file mode 100644 index 0000000000..7be53c810c --- /dev/null +++ b/portal-ui/src/screens/Console/Common/TableWrapper/TopActionButton.tsx @@ -0,0 +1,132 @@ +// 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 . + +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, + 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 ( + + + + ); +}; + +export default withStyles(styles)(TopActionButton);