Object browser migrated into bucket details (#1017)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2021-09-09 12:39:03 -05:00
committed by GitHub
parent 33acf45264
commit 605f4d4a62
24 changed files with 593 additions and 1082 deletions

View File

@@ -32,7 +32,11 @@ var (
metrics = "/metrics"
profiling = "/profiling"
buckets = "/buckets"
bucketsDetail = "/buckets/*"
bucketsGeneral = "/buckets/*"
bucketsAdmin = "/buckets/:bucketName/admin/*"
bucketsAdminMain = "/buckets/:bucketName/admin"
bucketsBrowser = "/buckets/:bucketName/browse/*"
bucketsBrowserMain = "/buckets/:bucketName/browse"
serviceAccounts = "/account"
changePassword = "/account/change-password"
tenants = "/tenants"
@@ -50,9 +54,6 @@ var (
storageDrives = "/storage/drives"
remoteBuckets = "/remote-buckets"
replication = "/replication"
objectBrowser = "/object-browser/:bucket/*"
objectBrowserBucket = "/object-browser/:bucket"
mainObjectBrowser = "/object-browser"
license = "/license"
watch = "/watch"
heal = "/heal"
@@ -281,30 +282,31 @@ var displayRules = map[string]func() bool{
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
usersDetail: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
policiesDetail: iamPoliciesActionSet,
dashboard: dashboardActionSet,
metrics: dashboardActionSet,
profiling: profilingActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
changePassword: changePasswordActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
objectBrowserBucket: objectBrowserActionSet,
license: licenseActionSet,
watch: watchActionSet,
heal: healActionSet,
trace: traceActionSet,
logs: logsActionSet,
healthInfo: healthInfoActionSet,
configuration: configurationActionSet,
users: usersActionSet,
usersDetail: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
policiesDetail: iamPoliciesActionSet,
dashboard: dashboardActionSet,
metrics: dashboardActionSet,
profiling: profilingActionSet,
buckets: bucketsActionSet,
bucketsGeneral: bucketsActionSet,
bucketsAdmin: bucketsActionSet,
bucketsAdminMain: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
changePassword: changePasswordActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
bucketsBrowser: objectBrowserActionSet,
bucketsBrowserMain: objectBrowserActionSet,
license: licenseActionSet,
watch: watchActionSet,
heal: healActionSet,
trace: traceActionSet,
logs: logsActionSet,
healthInfo: healthInfoActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -50,7 +50,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
args: args{
[]string{"admin:ServerInfo"},
},
want: 8,
want: 7,
},
{
name: "policies endpoint",
@@ -63,7 +63,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 8,
want: 7,
},
{
name: "all admin endpoints",
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 22,
want: 21,
},
{
name: "all s3 endpoints",
@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 8,
want: 9,
},
{
name: "all admin and s3 endpoints",
@@ -91,14 +91,14 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 24,
want: 25,
},
{
name: "Console User - default endpoints",
args: args{
[]string{},
},
want: 6,
want: 5,
},
}

View File

@@ -0,0 +1,107 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useEffect } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Link } from "react-router-dom";
import { Grid, IconButton, Tooltip } from "@material-ui/core";
import get from "lodash/get";
import { AppState } from "../../../../store";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import { setFileModeEnabled } from "../../ObjectBrowser/actions";
import ObjectDetails from "../ListBuckets/Objects/ObjectDetails/ObjectDetails";
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
import PageHeader from "../../Common/PageHeader/PageHeader";
import { SettingsIcon } from "../../../../icons";
interface IBrowserHandlerProps {
fileMode: boolean;
match: any;
history: any;
classes: any;
setFileModeEnabled: typeof setFileModeEnabled;
}
const styles = (theme: Theme) =>
createStyles({
breadcrumLink: {
textDecoration: "none",
color: "black",
},
...containerForHeader(theme.spacing(4)),
});
const BrowserHandler = ({
fileMode,
match,
history,
classes,
setFileModeEnabled,
}: IBrowserHandlerProps) => {
const bucketName = match.params["bucketName"];
const internalPaths = get(match.params, "subpaths", "");
useEffect(() => {
setFileModeEnabled(false);
}, [internalPaths, setFileModeEnabled]);
const openBucketConfiguration = () => {
history.push(`/buckets/${bucketName}/admin`);
};
return (
<Fragment>
<PageHeader
label={
<Fragment>
<Link to={"/buckets"} className={classes.breadcrumLink}>Buckets</Link> &gt; {bucketName}
</Fragment>
}
actions={
<Fragment>
<Tooltip title={"Configure Bucket"}>
<IconButton
color="primary"
aria-label="Configure Bucket"
component="span"
onClick={openBucketConfiguration}
>
<SettingsIcon />
</IconButton>
</Tooltip>
</Fragment>
}
/>
<Grid container className={classes.container}>
{fileMode ? <ObjectDetails /> : <ListObjects />}
</Grid>
</Fragment>
);
};
const mapStateToProps = ({ objectBrowser }: AppState) => ({
fileMode: get(objectBrowser, "fileMode", false),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
});
const mapDispatchToProps = {
setFileModeEnabled,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStyles(styles)(connector(BrowserHandler));

View File

@@ -47,7 +47,7 @@ import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle";
import { IconButton, Tooltip } from "@material-ui/core";
import { BucketsIcon, DeleteIcon } from "../../../../icons";
import { BucketsIcon, DeleteIcon, FolderIcon } from "../../../../icons";
import DeleteBucket from "../ListBuckets/DeleteBucket";
import AccessRulePanel from "./AccessRulePanel";
import RefreshIcon from "../../../../icons/RefreshIcon";
@@ -226,7 +226,7 @@ const BucketDetails = ({
]);
useEffect(() => {
let matchURL = match.params ? match.params["0"] : "summary";
let matchURL = match.params ? match.params["0"] : "browse";
if (!matchURL) {
matchURL = "";
@@ -283,22 +283,22 @@ const BucketDetails = ({
switch (newTab) {
case "events":
mainRoute += "/events";
mainRoute += "/admin/events";
break;
case "replication":
mainRoute += "/replication";
mainRoute += "/admin/replication";
break;
case "lifecycle":
mainRoute += "/lifecycle";
mainRoute += "/admin/lifecycle";
break;
case "access":
mainRoute += "/access";
mainRoute += "/admin/access";
break;
case "prefix":
mainRoute += "/prefix";
mainRoute += "/admin/prefix";
break;
default:
mainRoute += "/summary";
mainRoute += "/admin/summary";
}
setBucketDetailsTab(newTab);
@@ -312,6 +312,10 @@ const BucketDetails = ({
}
};
const openBucketBrowser = () => {
history.push(`/buckets/${bucketName}/browse`);
};
return (
<Fragment>
{deleteOpen && (
@@ -331,6 +335,20 @@ const BucketDetails = ({
</Link>
</Fragment>
}
actions={
<Fragment>
<Tooltip title={"Browse Bucket"}>
<IconButton
color="primary"
aria-label="Browse Bucket"
component="span"
onClick={openBucketBrowser}
>
<FolderIcon />
</IconButton>
</Tooltip>
</Fragment>
}
/>
<Grid container className={classes.container}>
<Grid item xs={12}>
@@ -443,38 +461,38 @@ const BucketDetails = ({
<Router history={history}>
<Switch>
<Route
path="/buckets/:bucketName/summary"
path="/buckets/:bucketName/admin/summary"
component={BucketSummaryPanel}
/>
<Route
path="/buckets/:bucketName/events"
path="/buckets/:bucketName/admin/events"
component={BucketEventsPanel}
/>
{distributedSetup && (
<Route
path="/buckets/:bucketName/replication"
path="/buckets/:bucketName/admin/replication"
component={BucketReplicationPanel}
/>
)}
{distributedSetup && (
<Route
path="/buckets/:bucketName/lifecycle"
path="/buckets/:bucketName/admin/lifecycle"
component={BucketLifecyclePanel}
/>
)}
<Route
path="/buckets/:bucketName/access"
path="/buckets/:bucketName/admin/access"
component={AccessDetailsPanel}
/>
<Route
path="/buckets/:bucketName/prefix"
path="/buckets/:bucketName/admin/prefix"
component={AccessRulePanel}
/>
<Route
path="/buckets/:bucketName"
component={() => (
<Redirect to={`/buckets/${bucketName}/summary`} />
<Redirect to={`/buckets/${bucketName}/admin/summary`} />
)}
/>
</Switch>

View File

@@ -20,7 +20,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
import { AddIcon, CreateIcon } from "../../../../icons";
import { AddIcon } from "../../../../icons";
import { BucketEvent, BucketEventList } from "../types";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";

View File

@@ -22,7 +22,7 @@ import get from "lodash/get";
import * as reactMoment from "react-moment";
import Grid from "@material-ui/core/Grid";
import { LifeCycleItem } from "../types";
import { AddIcon, CreateIcon } from "../../../../icons";
import { AddIcon } from "../../../../icons";
import {
actionsTray,
searchField,

View File

@@ -19,7 +19,7 @@ import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { AddIcon, CreateIcon } from "../../../../icons";
import { AddIcon } from "../../../../icons";
import { setErrorSnackMessage } from "../../../../actions";
import {
actionsTray,

View File

@@ -16,13 +16,14 @@
import React from "react";
import history from "../../../history";
import { Route, Router, Switch, withRouter } from "react-router-dom";
import { Route, Router, Switch, withRouter, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../../store";
import { setMenuOpen } from "../../../actions";
import NotFoundPage from "../../NotFoundPage";
import ListBuckets from "./ListBuckets/ListBuckets";
import BucketDetails from "./BucketDetails/BucketDetails";
import BrowserHandler from "./BucketDetails/BrowserHandler";
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
@@ -34,8 +35,17 @@ const Buckets = () => {
return (
<Router history={history}>
<Switch>
<Route path="/buckets/:bucketName/*" component={BucketDetails} />
<Route path="/buckets/:bucketName" component={BucketDetails} />
<Route path="/buckets/:bucketName/admin/*" component={BucketDetails} />
<Route path="/buckets/:bucketName/admin" component={BucketDetails} />
<Route
path="/buckets/:bucketName/browse/:subpaths+"
component={BrowserHandler}
/>
<Route path="/buckets/:bucketName/browse" component={BrowserHandler} />
<Route
path="/buckets/:bucketName"
component={() => <Redirect to={`/buckets`} />}
/>
<Route path="/" component={ListBuckets} />
<Route component={NotFoundPage} />
</Switch>

View File

@@ -0,0 +1,155 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { Link } from "react-router-dom";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { BucketsIcon } from "../../../../icons";
import DeleteIcon from "@material-ui/icons/Delete";
import { Bucket } from "../types";
import { niceBytes } from "../../../../common/utils";
import { IconButton, Tooltip } from "@material-ui/core";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
const styles = (theme: Theme) =>
createStyles({
linkContainer: {
textDecoration: "none",
position: "relative",
color: "initial",
textAlign: "center",
border: "#EAEDEE 1px solid",
borderRadius: 3,
padding: 15,
backgroundColor: "#fff",
width: 200,
margin: 10,
zIndex: 200,
"&:hover": {
backgroundColor: "#EAEAEA",
"& .innerElement": {
visibility: "visible",
},
},
"& .innerElement": {
visibility: "hidden",
},
},
bucketName: {
fontSize: 14,
fontWeight: "bold",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
width: "100%",
display: "block",
},
bucketsIcon: {
width: 40,
height: 40,
color: "#393939",
},
bucketSize: {
color: "#777",
fontSize: 10,
},
deleteButton: {
position: "absolute",
zIndex: 300,
top: 0,
right: 0,
},
checkBoxElement: {
zIndex: 500,
position: "absolute",
display: "block",
bottom: 0,
left: 0,
},
});
interface IBucketListItem {
bucket: Bucket;
classes: any;
onDelete: (bucketName: string) => void;
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
selected: boolean;
}
const BucketListItem = ({
classes,
bucket,
onDelete,
onSelect,
selected,
}: IBucketListItem) => {
const onDeleteClick = (e: any) => {
e.preventDefault();
e.stopPropagation();
onDelete(bucket.name);
};
const onCheckboxClick = (e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
e.preventDefault();
onSelect(e);
};
return (
<Fragment>
<Link
to={`/buckets/${bucket.name}/browse`}
className={classes.linkContainer}
>
<Tooltip title={`Delete ${bucket.name} bucket`}>
<IconButton
color="secondary"
aria-label="Delete Bucket"
component="span"
onClick={onDeleteClick}
className={`innerElement ${classes.deleteButton}`}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<BucketsIcon width={40} height={40} className={classes.bucketsIcon} />
<br />
<Tooltip title={bucket.name}>
<span className={classes.bucketName}>{bucket.name}</span>
</Tooltip>
<span className={classes.bucketSize}>
<strong>Used Space:</strong> {niceBytes(bucket.size || "0")}
</span>
<span
className={classes.checkBoxElement}
onClick={(e) => {
e.stopPropagation();
}}
>
<CheckboxWrapper
checked={selected}
id={`select-${bucket.name}`}
label={""}
name={`select-${bucket.name}`}
onChange={onCheckboxClick}
value={bucket.name}
/>
</span>
</Link>
</Fragment>
);
};
export default withStyles(styles)(BucketListItem);

View File

@@ -22,10 +22,8 @@ import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import FileCopyIcon from "@material-ui/icons/FileCopy";
import Moment from "react-moment";
import { Bucket, BucketList, HasPermissionResponse } from "../types";
import { AddIcon, CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
import { AddIcon } from "../../../../icons";
import { AppState } from "../../../../store";
import { addBucketOpen, addBucketReset } from "../actions";
import { setErrorSnackMessage } from "../../../../actions";
@@ -36,12 +34,12 @@ import {
} from "../../Common/FormComponents/common/styleLibrary";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import PageHeader from "../../Common/PageHeader/PageHeader";
import BulkReplicationModal from "./BulkReplicationModal";
import SearchIcon from "../../../../icons/SearchIcon";
import BucketListItem from "./BucketListItem";
const styles = (theme: Theme) =>
createStyles({
@@ -70,6 +68,12 @@ const styles = (theme: Theme) =>
},
},
},
bucketsIconsContainer: {
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "flex-start",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
@@ -77,6 +81,7 @@ const styles = (theme: Theme) =>
interface IListBucketsProps {
classes: any;
history: any;
addBucketOpen: typeof addBucketOpen;
addBucketModalOpen: boolean;
addBucketReset: typeof addBucketReset;
@@ -85,6 +90,7 @@ interface IListBucketsProps {
const ListBuckets = ({
classes,
history,
addBucketOpen,
addBucketModalOpen,
addBucketReset,
@@ -178,15 +184,6 @@ const ListBuckets = ({
setSelectedBucket(bucket);
};
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
];
const displayParsedDate = (date: string) => {
return <Moment>{date}</Moment>;
};
const filteredRecords = records.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
@@ -226,6 +223,27 @@ const ListBuckets = ({
}
};
/*
[
{ label: "Name", elementKey: "name" },
{
label: "Creation Date",
elementKey: "creation_date",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
width: 60,
contentTextAlign: "right",
},
]
*/
return (
<Fragment>
{addBucketModalOpen && (
@@ -298,31 +316,18 @@ const ListBuckets = ({
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Creation Date",
elementKey: "creation_date",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
width: 60,
contentTextAlign: "right",
},
]}
isLoading={loading}
records={filteredRecords}
entityName="Buckets"
idField="name"
selectedItems={selectedBuckets}
onSelect={selectListBuckets}
/>
<Grid item xs={12} className={classes.bucketsIconsContainer}>
{filteredRecords.map((bucket, index) => {
return (
<BucketListItem
bucket={bucket}
key={`bucketListItem-${index.toString()}`}
onDelete={confirmDeleteBucket}
onSelect={selectListBuckets}
selected={selectedBuckets.includes(bucket.name)}
/>
);
})}
</Grid>
</Grid>
</Grid>

View File

@@ -21,13 +21,15 @@ import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/I
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../../../Common/FormComponents/common/styleLibrary";
import { connect } from "react-redux";
import { createFolder } from "../../../../ObjectBrowser/actions";
import { setFileModeEnabled } from "../../../../ObjectBrowser/actions";
import history from "../../../../../../history";
interface ICreateFolder {
classes: any;
modalOpen: boolean;
bucketName: string;
folderName: string;
createFolder: (newFolder: string) => any;
setFileModeEnabled: typeof setFileModeEnabled;
onClose: () => any;
}
@@ -46,23 +48,28 @@ const styles = (theme: Theme) =>
const CreateFolderModal = ({
modalOpen,
folderName,
bucketName,
onClose,
createFolder,
setFileModeEnabled,
classes,
}: ICreateFolder) => {
const [pathUrl, setPathUrl] = useState("");
const currentPath = `${bucketName}/${folderName}`;
const resetForm = () => {
setPathUrl("");
};
const createProcess = () => {
createFolder(pathUrl);
const newPath = `/buckets/${bucketName}/browse/${folderName !== "" ? `${folderName}/` : ""}${pathUrl}`;
history.push(newPath);
setFileModeEnabled(false);
onClose();
};
const folderTruncated = folderName.split("/").slice(2).join("/");
return (
<React.Fragment>
<ModalWrapper
@@ -72,7 +79,7 @@ const CreateFolderModal = ({
>
<Grid container>
<h3 className={classes.pathLabel}>
Current Path: {folderTruncated}/
Current Path: {currentPath}
</h3>
<Grid item xs={12}>
<InputBoxWrapper
@@ -112,7 +119,7 @@ const CreateFolderModal = ({
};
const mapDispatchToProps = {
createFolder,
setFileModeEnabled,
};
const connector = connect(null, mapDispatchToProps);

View File

@@ -39,7 +39,6 @@ import {
objectBrowserCommon,
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import {
Badge,
Button,
@@ -50,12 +49,8 @@ import {
import * as reactMoment from "react-moment";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import {
addRoute,
fileDownloadStarted,
fileIsBeingPrepared,
resetRewind,
setAllRoutes,
setLastAsFile,
setFileModeEnabled,
} from "../../../../ObjectBrowser/actions";
import {
ObjectBrowserReducer,
@@ -179,20 +174,17 @@ const styles = (theme: Theme) =>
interface IListObjectsProps {
classes: any;
match: any;
addRoute: (param1: string, param2: string, param3: string) => any;
setAllRoutes: (path: string) => any;
history: any;
routesList: Route[];
downloadingFiles: string[];
setLastAsFile: () => any;
rewindEnabled: boolean;
rewindDate: any;
bucketToRewind: string;
setLoadingProgress: typeof setLoadingProgress;
setSnackBarMessage: typeof setSnackBarMessage;
setErrorSnackMessage: typeof setErrorSnackMessage;
fileIsBeingPrepared: typeof fileIsBeingPrepared;
fileDownloadStarted: typeof fileDownloadStarted;
resetRewind: typeof resetRewind;
setFileModeEnabled: typeof setFileModeEnabled;
}
function useInterval(callback: any, delay: number) {
@@ -223,30 +215,25 @@ const defLoading = <Typography component="h3">Loading...</Typography>;
const ListObjects = ({
classes,
match,
addRoute,
setAllRoutes,
routesList,
history,
downloadingFiles,
rewindEnabled,
rewindDate,
bucketToRewind,
setLastAsFile,
setLoadingProgress,
setSnackBarMessage,
setErrorSnackMessage,
fileIsBeingPrepared,
fileDownloadStarted,
resetRewind,
setFileModeEnabled,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [rewind, setRewind] = useState<RewindObject[]>([]);
const [loadingRewind, setLoadingRewind] = useState<boolean>(true);
const [loadingRewind, setLoadingRewind] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
const [selectedObject, setSelectedObject] = useState<string>("");
const [selectedBucket, setSelectedBucket] = useState<string>("");
const [filterObjects, setFilterObjects] = useState<string>("");
const [loadingStartTime, setLoadingStartTime] = useState<number>(0);
const [loadingMessage, setLoadingMessage] =
@@ -260,9 +247,8 @@ const ListObjects = ({
null
);
const internalPaths = match.params[0];
const bucketName = match.params["bucket"];
const internalPaths = get(match.params, "subpaths", "");
const bucketName = match.params["bucketName"];
const fileUpload = useRef<HTMLInputElement>(null);
@@ -354,55 +340,10 @@ const ListObjects = ({
]);
useEffect(() => {
const internalPaths = match.params[0];
const verifyIfIsFile = () => {
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}?prefix=${
internalPaths ? `${internalPaths}/` : ""
}`
)
.then((res: RewindObjectList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects === null) {
setLastAsFile();
} else {
// It is a folder, we remove loader
setLoadingRewind(false);
setLoading(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoadingRewind(false);
setLoading(false);
setErrorSnackMessage(err);
});
} else {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects !== null) {
setLastAsFile();
} else {
// It is a folder, we remove loader
setLoading(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
}
};
setLoading(true);
}, [internalPaths]);
useEffect(() => {
if (loading) {
let extraPath = "";
if (internalPaths) {
@@ -416,8 +357,6 @@ const ListObjects = ({
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`)
.then((res: BucketObjectsList) => {
setSelectedBucket(bucketName);
const records: BucketObject[] = res.objects || [];
const folders: BucketObject[] = [];
const files: BucketObject[] = [];
@@ -437,10 +376,68 @@ const ListObjects = ({
setRecords(recordsInElement);
// In case no objects were retrieved, We check if item is a file
if (!res.objects && extraPath !== "") {
verifyIfIsFile();
return;
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}?prefix=${
internalPaths ? `${internalPaths}/` : ""
}`
)
.then((res: RewindObjectList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects === null) {
setFileModeEnabled(true);
setLoadingRewind(false);
setLoading(false);
} else {
// It is a folder, we remove loader
setLoadingRewind(false);
setLoading(false);
setFileModeEnabled(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoadingRewind(false);
setLoading(false);
setErrorSnackMessage(err);
});
} else {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (!res.objects) {
// It is a folder, we remove loader
setFileModeEnabled(false);
setLoading(false);
} else {
// This is an empty folder.
if (
res.objects.length === 1 &&
res.objects[0].name.endsWith("/")
) {
setFileModeEnabled(false);
} else {
setFileModeEnabled(true);
}
setLoading(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
}
} else {
setFileModeEnabled(false);
setLoading(false);
}
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
@@ -450,24 +447,14 @@ const ListObjects = ({
}, [
loading,
match,
setLastAsFile,
setErrorSnackMessage,
bucketName,
rewindEnabled,
rewindDate,
internalPaths,
setFileModeEnabled,
]);
useEffect(() => {
const url = get(match, "url", "/object-browser");
if (url !== routesList[routesList.length - 1].route) {
setAllRoutes(url);
}
}, [match, routesList, setAllRoutes]);
useEffect(() => {
setLoading(true);
}, [routesList, setLoading]);
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
@@ -580,10 +567,6 @@ const ListObjects = ({
setSelectedObject(object);
};
const removeDownloadAnimation = (path: string) => {
fileDownloadStarted(path);
};
const displayDeleteFlag = (state: boolean) => {
return state ? "Yes" : "No";
};
@@ -596,16 +579,11 @@ const ListObjects = ({
);
}
download(
selectedBucket,
object.name,
object.version_id,
removeDownloadAnimation
);
download(bucketName, object.name, object.version_id);
};
const openPath = (idElement: string) => {
const currentPath = get(match, "url", "/object-browser");
const currentPath = get(match, "url", `/buckets/${bucketName}`);
// Element is a folder, we redirect to it
if (idElement.endsWith("/")) {
@@ -615,7 +593,7 @@ const ListObjects = ({
const lastIndex = idElementClean.length - 1;
const newPath = `${currentPath}/${idElementClean[lastIndex]}`;
addRoute(newPath, idElementClean[lastIndex], "path");
history.push(newPath);
return;
}
// Element is a file. we open details here
@@ -623,24 +601,12 @@ const ListObjects = ({
const fileName = pathInArray[pathInArray.length - 1];
const newPath = `${currentPath}/${fileName}`;
addRoute(newPath, fileName, "file");
history.push(newPath);
return;
};
const uploadObject = (e: any): void => {
// Handle of deeper routes.
const currentPath = routesList[routesList.length - 1].route;
const splitPaths = currentPath
.split("/")
.filter((item) => item.trim() !== "");
let path = "";
if (splitPaths.length > 2) {
path = `${splitPaths.slice(2).join("/")}/`;
}
upload(e, selectedBucket, path);
upload(e, bucketName, `${internalPaths}/`);
};
const openPreview = (fileObject: BucketObject) => {
@@ -798,23 +764,16 @@ const ListObjects = ({
},
];
let pageTitle = "Folder";
const ccPath = internalPaths.split("/").pop();
if (match) {
if ("bucket" in match.params) {
pageTitle = match.params["bucket"];
}
if ("0" in match.params) {
pageTitle = match.params["0"].split("/").pop();
}
}
const pageTitle = ccPath !== "" ? ccPath : "/";
return (
<React.Fragment>
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
selectedBucket={bucketName}
selectedObject={selectedObject}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
@@ -822,7 +781,7 @@ const ListObjects = ({
{deleteMultipleOpen && (
<DeleteMultipleObjects
deleteOpen={deleteMultipleOpen}
selectedBucket={selectedBucket}
selectedBucket={bucketName}
selectedObjects={selectedObjects}
closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh}
/>
@@ -830,7 +789,8 @@ const ListObjects = ({
{createFolderOpen && (
<CreateFolderModal
modalOpen={createFolderOpen}
folderName={routesList[routesList.length - 1].route}
bucketName={bucketName}
folderName={internalPaths}
onClose={closeAddFolderModal}
/>
)}
@@ -850,8 +810,7 @@ const ListObjects = ({
/>
)}
<PageHeader label="Object Browser" />
<Grid container className={classes.container}>
<Grid container>
<Grid item xs={12}>
<ScreenTitle
icon={
@@ -862,7 +821,10 @@ const ListObjects = ({
title={pageTitle}
subTitle={
<Fragment>
<BrowserBreadcrumbs title={false} />
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={internalPaths}
/>
</Fragment>
}
actions={
@@ -983,12 +945,13 @@ const ListObjects = ({
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={rewindEnabled ? loadingRewind : loading}
loadingMessage={loadingMessage}
entityName="Rewind Objects"
entityName="Objects"
idField="name"
records={rewindEnabled ? rewind : filteredRecords}
customPaperHeight={classes.browsePaper}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={`This location is empty${!rewindEnabled ? ", please try uploading a new file" : ""}`}
/>
</Grid>
</Grid>
@@ -1005,14 +968,10 @@ const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
});
const mapDispatchToProps = {
addRoute,
setAllRoutes,
setLastAsFile,
setLoadingProgress,
setSnackBarMessage,
setErrorSnackMessage,
fileIsBeingPrepared,
fileDownloadStarted,
setFileModeEnabled,
resetRewind,
};

View File

@@ -1,68 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { useEffect } from "react";
import ListObjects from "./ListObjects";
import ObjectDetails from "../ObjectDetails/ObjectDetails";
import get from "lodash/get";
import { setAllRoutes } from "../../../../ObjectBrowser/actions";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { ObjectBrowserState, Route } from "../../../../ObjectBrowser/reducers";
interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
interface ObjectRoutingProps {
routesList: Route[];
setAllRoutes: (path: string) => any;
match: any;
}
const ObjectRouting = ({
routesList,
match,
setAllRoutes,
}: ObjectRoutingProps) => {
const currentItem = routesList[routesList.length - 1];
useEffect(() => {
const url = get(match, "url", "/object-browser");
if (url !== routesList[routesList.length - 1].route) {
setAllRoutes(url);
}
}, [match, routesList, setAllRoutes]);
return currentItem.type === "path" ? (
<ListObjects />
) : (
<ObjectDetails routesList={routesList} />
);
};
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
routesList: get(objectBrowser, "routesList", []),
});
const mapDispatchToProps = {
setAllRoutes,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withRouter(connector(ObjectRouting));

View File

@@ -16,6 +16,7 @@
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import get from "lodash/get";
import * as reactMoment from "react-moment";
import clsx from "clsx";
@@ -48,17 +49,10 @@ import {
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { FileInfoResponse, IFileInfo } from "./types";
import {
fileDownloadStarted,
fileIsBeingPrepared,
removeRouteLevel,
} from "../../../../ObjectBrowser/actions";
import { Route } from "../../../../ObjectBrowser/reducers";
import { download, extensionPreview } from "../utils";
import { TabPanel } from "../../../../../shared/tabs";
import history from "../../../../../../history";
import api from "../../../../../../common/api";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import ShareIcon from "../../../../../../icons/ShareIcon";
import DownloadIcon from "../../../../../../icons/DownloadIcon";
import DeleteIcon from "../../../../../../icons/DeleteIcon";
@@ -86,7 +80,7 @@ import { BucketObject } from "../ListObjects/types";
const styles = (theme: Theme) =>
createStyles({
objectNameContainer: {
currentItemContainer: {
marginBottom: 8,
},
objectPathContainer: {
@@ -98,7 +92,7 @@ const styles = (theme: Theme) =>
color: "#000",
},
},
objectName: {
currentItem: {
fontSize: 24,
},
propertiesContainer: {
@@ -212,17 +206,14 @@ const styles = (theme: Theme) =>
interface IObjectDetailsProps {
classes: any;
routesList: Route[];
downloadingFiles: string[];
rewindEnabled: boolean;
rewindDate: any;
match: any;
bucketToRewind: string;
distributedSetup: boolean;
removeRouteLevel: (newRoute: string) => any;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
fileIsBeingPrepared: typeof fileIsBeingPrepared;
fileDownloadStarted: typeof fileDownloadStarted;
}
const emptyFile: IFileInfo = {
@@ -239,17 +230,14 @@ const emptyFile: IFileInfo = {
const ObjectDetails = ({
classes,
routesList,
downloadingFiles,
rewindEnabled,
rewindDate,
distributedSetup,
match,
bucketToRewind,
removeRouteLevel,
setErrorSnackMessage,
setSnackBarMessage,
fileIsBeingPrepared,
fileDownloadStarted,
}: IObjectDetailsProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
@@ -266,11 +254,10 @@ const ObjectDetails = ({
const [metadata, setMetadata] = useState<any>({});
const [selectedTab, setSelectedTab] = useState<number>(0);
const currentItem = routesList[routesList.length - 1];
const allPathData = currentItem.route.split("/");
const objectName = allPathData[allPathData.length - 1];
const bucketName = allPathData[2];
const pathInBucket = allPathData.slice(3).join("/");
const internalPaths = get(match.params, "subpaths", "");
const bucketName = match.params["bucketName"];
const allPathData = internalPaths.split("/");
const currentItem = allPathData.pop();
const previewObject: BucketObject = {
name: actualInfo.name,
@@ -282,7 +269,7 @@ const ObjectDetails = ({
useEffect(() => {
if (loadObjectData) {
const encodedPath = encodeURIComponent(pathInBucket);
const encodedPath = encodeURIComponent(internalPaths);
api
.invoke(
"GET",
@@ -313,14 +300,14 @@ const ObjectDetails = ({
}, [
loadObjectData,
bucketName,
pathInBucket,
internalPaths,
setErrorSnackMessage,
distributedSetup,
]);
useEffect(() => {
if (metadataLoad) {
const encodedPath = encodeURIComponent(pathInBucket);
const encodedPath = encodeURIComponent(internalPaths);
api
.invoke(
"GET",
@@ -337,7 +324,7 @@ const ObjectDetails = ({
setMetadataLoad(false);
});
}
}, [bucketName, metadataLoad, pathInBucket]);
}, [bucketName, metadataLoad, internalPaths]);
let tagKeys: string[] = [];
@@ -369,10 +356,6 @@ const ObjectDetails = ({
setDeleteTagModalOpen(true);
};
const removeDownloadAnimation = (path: string) => {
fileDownloadStarted(path);
};
const downloadObject = (object: IFileInfo, includeVersion?: boolean) => {
if (object.size && parseInt(object.size) > 104857600) {
// If file is bigger than 100MB we show a notification
@@ -382,9 +365,9 @@ const ObjectDetails = ({
}
download(
bucketName,
pathInBucket,
internalPaths,
object.version_id,
removeDownloadAnimation,
() => {},
includeVersion
);
};
@@ -432,10 +415,8 @@ const ObjectDetails = ({
setDeleteOpen(false);
if (redirectBack) {
const newPath = allPathData.slice(0, -1).join("/");
removeRouteLevel(newPath);
history.push(newPath);
const newPath = allPathData.join("/");
history.push(`/buckets/${bucketName}/browse${newPath === "" ? "" : `/${newPath}`}`);
}
};
@@ -477,7 +458,7 @@ const ObjectDetails = ({
<SetRetention
open={retentionModalOpen}
closeModalAndRefresh={closeRetentionModal}
objectName={objectName}
objectName={currentItem}
objectInfo={actualInfo}
bucketName={bucketName}
/>
@@ -486,7 +467,7 @@ const ObjectDetails = ({
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={bucketName}
selectedObject={pathInBucket}
selectedObject={internalPaths}
closeDeleteModalAndRefresh={closeDeleteModal}
/>
)}
@@ -494,7 +475,7 @@ const ObjectDetails = ({
<AddTagModal
modalOpen={tagModalOpen}
currentTags={actualInfo.tags}
selectedObject={pathInBucket}
selectedObject={internalPaths}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
@@ -504,7 +485,7 @@ const ObjectDetails = ({
<DeleteTagModal
deleteOpen={deleteTagModalOpen}
currentTags={actualInfo.tags}
selectedObject={pathInBucket}
selectedObject={internalPaths}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
@@ -515,14 +496,13 @@ const ObjectDetails = ({
<SetLegalHoldModal
open={legalholdOpen}
closeModalAndRefresh={closeLegalholdModal}
objectName={pathInBucket}
objectName={internalPaths}
bucketName={bucketName}
actualInfo={actualInfo}
/>
)}
<PageHeader label={"Object Browser"} />
<Grid container className={classes.container}>
<Grid container>
<Grid item xs={12}>
<ScreenTitle
icon={
@@ -530,10 +510,13 @@ const ObjectDetails = ({
<ObjectBrowserIcon width={40} />
</Fragment>
}
title={objectName}
title={currentItem}
subTitle={
<Fragment>
<BrowserBreadcrumbs title={false} />
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={internalPaths}
/>
</Fragment>
}
actions={
@@ -621,7 +604,7 @@ const ObjectDetails = ({
onClick={() => {
setSelectedTab(2);
}}
disabled={extensionPreview(objectName) === "none"}
disabled={extensionPreview(currentItem) === "none"}
>
<ListItemText primary="Preview" />
</ListItem>
@@ -742,9 +725,9 @@ const ObjectDetails = ({
<Grid item xs={12}>
<Table className={classes.table} aria-label="simple table">
<TableBody>
{Object.keys(metadata).map((element) => {
{Object.keys(metadata).map((element, index) => {
return (
<TableRow>
<TableRow key={`tRow-${index.toString()}`}>
<TableCell
component="th"
scope="row"
@@ -773,7 +756,7 @@ const ObjectDetails = ({
<Grid item xs={12} className={classes.actionsTray}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<TextField
placeholder={`Search ${objectName}`}
placeholder={`Search ${currentItem}`}
className={clsx(classes.search, classes.searchField)}
id="search-resource"
label=""
@@ -805,6 +788,7 @@ const ObjectDetails = ({
versions.length - versions.indexOf(r);
return `v${versOrd}`;
},
elementKey: "version_id"
},
{ label: "Version ID", elementKey: "version_id" },
{
@@ -817,6 +801,7 @@ const ObjectDetails = ({
width: 60,
contentTextAlign: "center",
renderFullObject: true,
elementKey: "is_delete_marker",
renderFunction: (r) => {
const versOrd = r.is_delete_marker ? "Yes" : "No";
return `${versOrd}`;
@@ -857,13 +842,10 @@ const mapStateToProps = ({ objectBrowser, system }: AppState) => ({
});
const mapDispatchToProps = {
removeRouteLevel,
setErrorSnackMessage,
fileIsBeingPrepared,
fileDownloadStarted,
setSnackBarMessage,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(withStyles(styles)(ObjectDetails));
export default withRouter(connector(withStyles(styles)(ObjectDetails)));

View File

@@ -17,6 +17,7 @@
export interface Bucket {
name: string;
creation_date: Date;
size?: string;
}
export interface BucketEncryptionInfo {

View File

@@ -48,8 +48,6 @@ import ConfigurationMain from "./Configurations/ConfigurationMain";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import TenantsMain from "./Tenants/TenantsMain";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
import ObjectRouting from "./Buckets/ListBuckets/Objects/ListObjects/ObjectRouting";
import License from "./License/License";
import Trace from "./Trace/Trace";
import LogsMain from "./Logs/LogsMain";
@@ -238,18 +236,6 @@ const Console = ({
component: Buckets,
path: "/buckets/*",
},
{
component: ObjectBrowser,
path: "/object-browser",
},
{
component: ObjectRouting,
path: "/object-browser/:bucket",
},
{
component: ObjectRouting,
path: "/object-browser/:bucket/*",
},
{
component: Watch,
path: "/watch",

View File

@@ -87,14 +87,14 @@ interface ICardProps {
}
const DriveInfoCard = ({ classes, drive }: ICardProps) => {
console.log(drive);
const driveStatusToClass = (health_status: string) => {
switch (health_status) {
case "offline":
return classes.redState;
case "ok":
return classes.greenState;
deefault: return classes.greyState;
default:
return classes.greyState;
}
};

View File

@@ -52,7 +52,6 @@ const download = (filename: string, text: string) => {
"href",
"data:application/json;charset=utf-8," + encodeURIComponent(text)
);
console.log(filename);
element.setAttribute("download", filename);
element.style.display = "none";

View File

@@ -199,14 +199,6 @@ const Menu = ({
name: "Dashboard",
icon: <DashboardIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/object-browser",
name: "Object Browser",
icon: <ObjectBrowserIcon />,
},
{
group: "User",
type: "item",

View File

@@ -1,338 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { useState, useEffect, Fragment } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import { IconButton, Tooltip } from "@material-ui/core";
import { AddIcon, BucketsIcon, CreateIcon } from "../../../icons";
import { niceBytes } from "../../../common/utils";
import { Bucket, BucketList, HasPermissionResponse } from "../Buckets/types";
import {
actionsTray,
objectBrowserCommon,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { addRoute, resetRoutesList } from "./actions";
import { setErrorSnackMessage } from "../../../actions";
import { ErrorResponseHandler } from "../../../common/types";
import BrowserBreadcrumbs from "./BrowserBreadcrumbs";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddBucket from "../Buckets/ListBuckets/AddBucket";
import api from "../../../common/api";
import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
import RefreshIcon from "../../../icons/RefreshIcon";
import SearchIcon from "../../../icons/SearchIcon";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
usedSpaceCol: {
width: 150,
textAlign: "right",
},
subTitleLabel: {
alignItems: "center",
display: "flex",
},
bucketName: {
display: "flex",
alignItems: "center",
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
marginRight: 4,
},
},
iconBucket: {
backgroundImage: "url(/images/ob_bucket_clear.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
width: 16,
height: 40,
marginRight: 10,
},
"@global": {
".rowLine:hover .iconBucketElm": {
backgroundImage: "url(/images/ob_bucket_filled.svg)",
},
},
browsePaper: {
height: "calc(100vh - 280px)",
},
...actionsTray,
...searchField,
...objectBrowserCommon,
});
interface IBrowseBucketsProps {
classes: any;
addRoute: (path: string, label: string, type: string) => any;
resetRoutesList: (doVar: boolean) => any;
displayErrorMessage: typeof setErrorSnackMessage;
match: any;
}
const BrowseBuckets = ({
classes,
match,
addRoute,
resetRoutesList,
displayErrorMessage,
}: IBrowseBucketsProps) => {
const [loading, setLoading] = useState<boolean>(true);
const [records, setRecords] = useState<Bucket[]>([]);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [filterBuckets, setFilterBuckets] = useState<string>("");
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canCreateBucket, setCanCreateBucket] = useState<boolean>(false);
// check the permissions for creating bucket
useEffect(() => {
if (loadingPerms) {
api
.invoke("POST", `/api/v1/has-permission`, {
actions: [
{
id: "createBucket",
action: "s3:CreateBucket",
},
],
})
.then((res: HasPermissionResponse) => {
const canCreate = res.permissions
.filter((s) => s.id === "createBucket")
.pop();
if (canCreate && canCreate.can) {
setCanCreateBucket(true);
} else {
setCanCreateBucket(false);
}
setLoadingPerms(false);
// setRecords(res.buckets || []);
})
.catch((err: ErrorResponseHandler) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [loadingPerms]);
useEffect(() => {
resetRoutesList(true);
}, [match, resetRoutesList]);
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
setLoading(false);
setRecords(res.buckets || []);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
displayErrorMessage(err);
});
}
}, [loading, displayErrorMessage]);
const closeAddModalAndRefresh = (refresh: boolean) => {
setAddScreenOpen(false);
if (refresh) {
setLoading(true);
}
};
const filteredRecords = records.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
}
return b.name.indexOf(filterBuckets) >= 0;
});
const handleViewChange = (idElement: string) => {
const currentPath = get(match, "url", "/object-browser");
const newPath = `${currentPath}/${idElement}`;
addRoute(newPath, idElement, "path");
};
const renderBucket = (bucketName: string) => {
return (
<div className={classes.bucketName}>
<BucketsIcon />
<span>{bucketName}</span>
</div>
);
};
return (
<Fragment>
{addScreenOpen && (
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<ScreenTitle
icon={
<Fragment>
<BucketsIcon width={40} />
</Fragment>
}
title={"All Buckets"}
subTitle={
<Fragment>
<BrowserBreadcrumbs title={false} />
</Fragment>
}
actions={
<Fragment>
{canCreateBucket && (
<Fragment>
<Tooltip title={"Create Bucket"}>
<IconButton
color="primary"
aria-label="Create Bucket"
component="span"
onClick={() => {
setAddScreenOpen(true);
}}
>
<AddIcon />
</IconButton>
</Tooltip>
</Fragment>
)}
<Tooltip title={"Refresh List"}>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
setLoading(true);
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</Fragment>
}
/>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter Buckets"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterBuckets(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={[
{
type: "view",
sendOnlyId: true,
onClick: handleViewChange,
},
]}
columns={[
{
label: "Name",
elementKey: "name",
renderFunction: renderBucket,
},
{
label: "Used Space",
elementKey: "size",
renderFunction: niceBytes,
globalClass: classes.usedSpaceCol,
rowClass: classes.usedSpaceCol,
width: 100,
contentTextAlign: "right",
headerTextAlign: "right",
},
]}
isLoading={loading}
records={filteredRecords}
entityName="Buckets"
idField="name"
customPaperHeight={classes.browsePaper}
/>
</Grid>
</Grid>
</Fragment>
);
};
const mapDispatchToProps = {
addRoute,
resetRoutesList,
displayErrorMessage: setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(BrowseBuckets)));

View File

@@ -21,8 +21,7 @@ import Moment from "react-moment";
import { connect } from "react-redux";
import { withStyles } from "@material-ui/core";
import { createStyles, Theme } from "@material-ui/core/styles";
import { removeRouteLevel } from "./actions";
import { ObjectBrowserState, Route } from "./reducers";
import { ObjectBrowserState } from "./reducers";
import { objectBrowserCommon } from "../Common/FormComponents/common/styleLibrary";
import { Link } from "react-router-dom";
@@ -32,11 +31,10 @@ interface ObjectBrowserReducer {
interface IObjectBrowser {
classes: any;
objectsList: Route[];
rewindEnabled: boolean;
rewindDate: any;
removeRouteLevel: (path: string) => any;
title?: boolean;
bucketName: string;
internalPaths: string;
rewindEnabled?: boolean;
rewindDate?: any;
}
const styles = (theme: Theme) =>
@@ -46,36 +44,44 @@ const styles = (theme: Theme) =>
const BrowserBreadcrumbs = ({
classes,
objectsList,
bucketName,
internalPaths,
rewindEnabled,
rewindDate,
removeRouteLevel,
title = true,
}: IObjectBrowser) => {
const listBreadcrumbs = objectsList.map((objectItem, index) => {
return (
<React.Fragment key={`breadcrumbs-${index.toString()}`}>
<Link
to={objectItem.route}
onClick={() => {
removeRouteLevel(objectItem.route);
}}
>
{objectItem.label}
</Link>
{index < objectsList.length - 1 && <span> / </span>}
</React.Fragment>
);
});
let paths = internalPaths;
if (internalPaths !== "") {
paths = `/${internalPaths}`;
}
const splitPaths = paths.split("/");
const listBreadcrumbs = splitPaths.map(
(objectItem: string, index: number) => {
const subSplit = splitPaths.slice(1, index + 1).join("/");
const route = `/buckets/${bucketName}/browse${
objectItem !== "" ? `/${subSplit}` : ""}`;
const label = objectItem === "" ? bucketName : objectItem;
return (
<React.Fragment key={`breadcrumbs-${index.toString()}`}>
<Link to={route}>{label}</Link>
{index < splitPaths.length - 1 && <span> / </span>}
</React.Fragment>
);
}
);
const title = false;
return (
<React.Fragment>
{title && (
<Grid item xs={12}>
<div className={classes.sectionTitle}>
{objectsList && objectsList.length > 0
? objectsList.slice(-1)[0].label
: ""}
{rewindEnabled && objectsList.length > 1 && (
{splitPaths && splitPaths.length > 0 ? splitPaths[splitPaths.length - 1] : ""}
{rewindEnabled && splitPaths.length > 1 && (
<small className={classes.smallLabel}>
&nbsp;(Rewind:{" "}
<Moment date={rewindDate} format="MMMM Do YYYY, h:mm a" /> )
@@ -93,15 +99,10 @@ const BrowserBreadcrumbs = ({
};
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
objectsList: get(objectBrowser, "routesList", []),
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
});
const mapDispatchToProps = {
removeRouteLevel,
};
const connector = connect(mapStateToProps, null);
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(withStyles(styles)(BrowserBreadcrumbs));
export default withStyles(styles)(connector(BrowserBreadcrumbs));

View File

@@ -1,90 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid } from "@material-ui/core";
import BrowseBuckets from "./BrowseBuckets";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
interface IObjectBrowserProps {
match: any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
watchList: {
background: "white",
maxHeight: "400",
overflow: "auto",
"& ul": {
margin: "4",
padding: "0",
},
"& ul li": {
listStyle: "none",
margin: "0",
padding: "0",
borderBottom: "1px solid #dedede",
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012",
},
fieldContainer: {
background: "#FFFFFF",
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206",
boxShadow: "0px 3px 6px #00000012",
},
lastElementWPadding: {
paddingRight: "78",
},
...containerForHeader(theme.spacing(4)),
});
const ObjectBrowser = ({ match, classes }: IObjectBrowserProps) => {
const pathIn = get(match, "url", "");
return (
<React.Fragment>
<PageHeader label={"Object Browser"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
{pathIn === "/object-browser" && <BrowseBuckets />}
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ObjectBrowser);

View File

@@ -14,61 +14,10 @@
// 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/>.
export const OBJECT_BROWSER_ADD_ROUTE = "OBJECT_BROWSER/ADD_ROUTE";
export const OBJECT_BROWSER_RESET_ROUTES_LIST =
"OBJECT_BROWSER/RESET_ROUTES_LIST";
export const OBJECT_BROWSER_REMOVE_ROUTE_LEVEL =
"OBJECT_BROWSER/REMOVE_ROUTE_LEVEL";
export const OBJECT_BROWSER_SET_ALL_ROUTES = "OBJECT_BROWSER/SET_ALL_ROUTES";
export const OBJECT_BROWSER_CREATE_FOLDER = "OBJECT_BROWSER/CREATE_FOLDER";
export const OBJECT_BROWSER_SET_LAST_AS_FILE =
"OBJECT_BROWSER/SET_LAST_AS_FILE";
export const OBJECT_BROWSER_DOWNLOAD_FILE_LOADER =
"OBJECT_BROWSER/DOWNLOAD_FILE_LOADER";
export const OBJECT_BROWSER_DOWNLOADED_FILE = "OBJECT_BROWSER/DOWNLOADED_FILE";
export const REWIND_SET_ENABLE = "REWIND/SET_ENABLE";
export const REWIND_RESET_REWIND = "REWIND/RESET_REWIND";
interface AddRouteAction {
type: typeof OBJECT_BROWSER_ADD_ROUTE;
route: string;
label: string;
routeType: string;
}
interface ResetRoutesList {
type: typeof OBJECT_BROWSER_RESET_ROUTES_LIST;
reset: boolean;
}
interface RemoveRouteLevel {
type: typeof OBJECT_BROWSER_REMOVE_ROUTE_LEVEL;
toRoute: string;
}
interface SetAllRoutes {
type: typeof OBJECT_BROWSER_SET_ALL_ROUTES;
currentRoute: string;
}
interface CreateFolder {
type: typeof OBJECT_BROWSER_CREATE_FOLDER;
newRoute: string;
}
interface SetLastAsFile {
type: typeof OBJECT_BROWSER_SET_LAST_AS_FILE;
}
interface SetFileDownload {
type: typeof OBJECT_BROWSER_DOWNLOAD_FILE_LOADER;
path: string;
}
interface FileDownloaded {
type: typeof OBJECT_BROWSER_DOWNLOADED_FILE;
path: string;
}
export const REWIND_FILE_MODE_ENABLED = "BUCKET_BROWSER/FILE_MODE_ENABLED";
interface RewindSetEnabled {
type: typeof REWIND_SET_ENABLE;
@@ -81,74 +30,15 @@ interface RewindReset {
type: typeof REWIND_RESET_REWIND;
}
interface FileModeEnabled {
type: typeof REWIND_FILE_MODE_ENABLED;
status: boolean;
}
export type ObjectBrowserActionTypes =
| AddRouteAction
| ResetRoutesList
| RemoveRouteLevel
| SetAllRoutes
| CreateFolder
| SetLastAsFile
| SetFileDownload
| FileDownloaded
| RewindSetEnabled
| RewindReset;
export const addRoute = (route: string, label: string, routeType: string) => {
return {
type: OBJECT_BROWSER_ADD_ROUTE,
route,
label,
routeType,
};
};
export const resetRoutesList = (reset: boolean) => {
return {
type: OBJECT_BROWSER_RESET_ROUTES_LIST,
reset,
};
};
export const removeRouteLevel = (toRoute: string) => {
return {
type: OBJECT_BROWSER_REMOVE_ROUTE_LEVEL,
toRoute,
};
};
export const setAllRoutes = (currentRoute: string) => {
return {
type: OBJECT_BROWSER_SET_ALL_ROUTES,
currentRoute,
};
};
export const createFolder = (newRoute: string) => {
return {
type: OBJECT_BROWSER_CREATE_FOLDER,
newRoute,
};
};
export const setLastAsFile = () => {
return {
type: OBJECT_BROWSER_SET_LAST_AS_FILE,
};
};
export const fileIsBeingPrepared = (path: string) => {
return {
type: OBJECT_BROWSER_DOWNLOAD_FILE_LOADER,
path,
};
};
export const fileDownloadStarted = (path: string) => {
return {
type: OBJECT_BROWSER_DOWNLOADED_FILE,
path,
};
};
| RewindReset
| FileModeEnabled;
export const setRewindEnable = (
state: boolean,
@@ -168,3 +58,10 @@ export const resetRewind = () => {
type: REWIND_RESET_REWIND,
};
};
export const setFileModeEnabled = (status: boolean) => {
return {
type: REWIND_FILE_MODE_ENABLED,
status,
};
};

View File

@@ -13,19 +13,10 @@
//
// 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 history from "../../../history";
import {
OBJECT_BROWSER_ADD_ROUTE,
OBJECT_BROWSER_CREATE_FOLDER,
OBJECT_BROWSER_REMOVE_ROUTE_LEVEL,
OBJECT_BROWSER_RESET_ROUTES_LIST,
OBJECT_BROWSER_SET_ALL_ROUTES,
OBJECT_BROWSER_SET_LAST_AS_FILE,
OBJECT_BROWSER_DOWNLOAD_FILE_LOADER,
OBJECT_BROWSER_DOWNLOADED_FILE,
REWIND_SET_ENABLE,
REWIND_RESET_REWIND,
REWIND_FILE_MODE_ENABLED,
ObjectBrowserActionTypes,
} from "./actions";
@@ -42,8 +33,7 @@ export interface RewindItem {
}
export interface ObjectBrowserState {
routesList: Route[];
downloadingFiles: string[];
fileMode: boolean;
rewind: RewindItem;
}
@@ -51,10 +41,6 @@ export interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
const initialRoute = [
{ route: "/object-browser", label: "All Buckets", type: "path" },
];
const defaultRewind = {
rewindEnabled: false,
bucketToRewind: "",
@@ -62,8 +48,7 @@ const defaultRewind = {
};
const initialState: ObjectBrowserState = {
routesList: initialRoute,
downloadingFiles: [],
fileMode: false,
rewind: {
...defaultRewind,
},
@@ -74,107 +59,6 @@ export function objectBrowserReducer(
action: ObjectBrowserActionTypes
): ObjectBrowserState {
switch (action.type) {
case OBJECT_BROWSER_ADD_ROUTE:
const newRouteList = [
...state.routesList,
{ route: action.route, label: action.label, type: action.routeType },
];
history.push(action.route);
return { ...state, routesList: newRouteList };
case OBJECT_BROWSER_RESET_ROUTES_LIST:
return {
...state,
routesList: [...initialRoute],
};
case OBJECT_BROWSER_REMOVE_ROUTE_LEVEL:
const indexOfTopPath =
state.routesList.findIndex(
(element) => element.route === action.toRoute
) + 1;
const newRouteLevels = state.routesList.slice(0, indexOfTopPath);
return {
...state,
routesList: newRouteLevels,
};
case OBJECT_BROWSER_SET_ALL_ROUTES:
const splitRoutes = action.currentRoute.split("/");
const routesArray: Route[] = [];
let initRoute = initialRoute[0].route;
splitRoutes.forEach((route) => {
if (route !== "" && route !== "object-browser") {
initRoute = `${initRoute}/${route}`;
routesArray.push({
route: initRoute,
label: route,
type: "path",
});
}
});
const newSetOfRoutes = [...initialRoute, ...routesArray];
return {
...state,
routesList: newSetOfRoutes,
};
case OBJECT_BROWSER_CREATE_FOLDER:
const newFoldersRoutes = [...state.routesList];
let lastRoute = state.routesList[state.routesList.length - 1].route;
const splitElements = action.newRoute.split("/");
splitElements.forEach((element) => {
const folderTrim = element.trim();
if (folderTrim !== "") {
lastRoute = `${lastRoute}/${folderTrim}`;
const newItem = { route: lastRoute, label: folderTrim, type: "path" };
newFoldersRoutes.push(newItem);
}
});
history.push(lastRoute);
return {
...state,
routesList: newFoldersRoutes,
};
case OBJECT_BROWSER_SET_LAST_AS_FILE:
const currentList = state.routesList;
const lastItem = currentList.slice(-1)[0];
if (lastItem.type === "path") {
lastItem.type = "file";
}
const newList = [...currentList.slice(0, -1), lastItem];
return {
...state,
routesList: newList,
};
case OBJECT_BROWSER_DOWNLOAD_FILE_LOADER:
const actualFiles = [...state.downloadingFiles];
actualFiles.push(action.path);
return {
...state,
downloadingFiles: [...actualFiles],
};
case OBJECT_BROWSER_DOWNLOADED_FILE:
const downloadingFiles = state.downloadingFiles.filter(
(item) => item !== action.path
);
return {
...state,
downloadingFiles: [...downloadingFiles],
};
case REWIND_SET_ENABLE:
const rewindSetEnabled = {
...state.rewind,
@@ -190,6 +74,8 @@ export function objectBrowserReducer(
dateToRewind: null,
};
return { ...state, rewind: resetItem };
case REWIND_FILE_MODE_ENABLED:
return { ...state, fileMode: action.status };
default:
return state;
}