Added navigation to object browser (#358)

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
This commit is contained in:
Alex
2020-10-31 01:22:46 -06:00
committed by GitHub
parent afbb83e081
commit 547eb41e96
25 changed files with 1094 additions and 451 deletions

View File

@@ -22,27 +22,28 @@ import (
// endpoints definition
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
heal = "/heal"
remoteBuckets = "/remote-buckets"
replication = "/replication"
objectBrowser = "/object-browser/:bucket?"
mainObjectBrowser = "/object-browser"
license = "/license"
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
heal = "/heal"
remoteBuckets = "/remote-buckets"
replication = "/replication"
objectBrowser = "/object-browser/:bucket/*"
objectBrowserBucket = "/object-browser/:bucket"
mainObjectBrowser = "/object-browser"
license = "/license"
)
type ConfigurationActionSet struct {
@@ -245,25 +246,26 @@ var licenseActionSet = ConfigurationActionSet{
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
license: licenseActionSet,
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
objectBrowserBucket: objectBrowserActionSet,
license: licenseActionSet,
}
// 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: 5,
want: 6,
},
{
name: "policies endpoint",
@@ -63,7 +63,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 5,
want: 6,
},
{
name: "all admin endpoints",
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 16,
want: 17,
},
{
name: "all s3 endpoints",
@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 7,
want: 8,
},
{
name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 19,
want: 20,
},
{
name: "no endpoints",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.174" height="11" viewBox="0 0 11.174 11">
<defs>
<style>.a{fill:none;stroke:#081c42;stroke-linecap:round;}</style>
</defs>
<path class="a" d="M8.392,10H1.608L0,0H10Z" transform="translate(0.587 0.5)"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.174" height="11" viewBox="0 0 11.174 11">
<defs>
<style>.a{fill:#081c42;stroke:#081c42;stroke-linecap:round;}</style>
</defs>
<path class="a" d="M8.392,10H1.608L0,0H10Z" transform="translate(0.587 0.5)"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.442" height="15.302" viewBox="0 0 11.442 15.302">
<defs>
<style>.a,.b{fill:none;stroke:#081c42;}.b{stroke-linejoin:round;}</style>
</defs>
<g transform="translate(0.5 0.5)">
<path class="a" d="M-12060-11667.842v14.261h10.442v-10.591l-3.671-3.67Z"
transform="translate(12059.999 11667.883)"/>
<path class="b" d="M-12051.353-11664.255v-3.639l3.528,3.639Z" transform="translate(12058.188 11667.894)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.442" height="15.302" viewBox="0 0 11.442 15.302">
<defs>
<style>.a,.b{fill:#081c42;stroke:#081c42;}.b{stroke-linejoin:round;fill:#fff}</style>
</defs>
<g transform="translate(0.5 0.5)">
<path class="a" d="M-12060-11667.842v14.261h10.442v-10.591l-3.671-3.67Z"
transform="translate(12059.999 11667.883)"/>
<path class="b" d="M-12051.353-11664.255v-3.639l3.528,3.639Z" transform="translate(12058.188 11667.894)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.999" height="13.999" viewBox="0 0 15.999 13.999">
<defs>
<style>.a{fill:none;stroke-linecap:square;}.b,.c{stroke:none;}.c{fill:#081c42;}</style>
</defs>
<g class="a" transform="translate(-0.001 0.001)">
<path class="b" d="M0,14V0H8.572V2.411H16V14Z"/>
<path class="c"
d="M 15.00020027160645 12.99860000610352 L 15.00020027160645 3.411099910736084 L 8.571599960327148 3.411099910736084 L 7.571600437164307 3.411099910736084 L 7.571600437164307 2.411099910736084 L 7.571600437164307 0.9990998506546021 L 1.000900268554688 0.9990998506546021 L 1.000900268554688 2.411099910736084 L 1.000900268554688 12.99860000610352 L 15.00020027160645 12.99860000610352 M 16.00020027160645 13.99860000610352 L 0.0009002700680866838 13.99860000610352 L 0.0009002700680866838 2.411099910736084 L 0.0009002700680866838 -0.0009001312428154051 L 8.571599960327148 -0.0009001312428154051 L 8.571599960327148 2.411099910736084 L 16.00020027160645 2.411099910736084 L 16.00020027160645 13.99860000610352 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.999" height="13.999" viewBox="0 0 15.999 13.999">
<defs>
<style>.a{fill:none;stroke-linecap:square;}.b,.c{stroke:none;fill:#081c42}.c{fill:#081c42;}</style>
</defs>
<g class="a" transform="translate(-0.001 0.001)">
<path class="b" d="M0,14V0H8.572V2.411H16V14Z"/>
<path class="c"
d="M 15.00020027160645 12.99860000610352 L 15.00020027160645 3.411099910736084 L 8.571599960327148 3.411099910736084 L 7.571600437164307 3.411099910736084 L 7.571600437164307 2.411099910736084 L 7.571600437164307 0.9990998506546021 L 1.000900268554688 0.9990998506546021 L 1.000900268554688 2.411099910736084 L 1.000900268554688 12.99860000610352 L 15.00020027160645 12.99860000610352 M 16.00020027160645 13.99860000610352 L 0.0009002700680866838 13.99860000610352 L 0.0009002700680866838 2.411099910736084 L 0.0009002700680866838 -0.0009001312428154051 L 8.571599960327148 -0.0009001312428154051 L 8.571599960327148 2.411099910736084 L 16.00020027160645 2.411099910736084 L 16.00020027160645 13.99860000610352 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -14,6 +14,7 @@
// 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 } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
@@ -21,7 +22,6 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { BucketObject, BucketObjectsList } from "./types";
import api from "../../../../../../common/api";
import React from "react";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../../../common/utils";
import DeleteObject from "./DeleteObject";
@@ -29,6 +29,7 @@ import DeleteObject from "./DeleteObject";
import {
actionsTray,
containerForHeader,
objectBrowserCommon,
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
@@ -38,6 +39,20 @@ import { Button, Input } from "@material-ui/core";
import * as reactMoment from "react-moment";
import { CreateIcon } from "../../../../../../icons";
import Snackbar from "@material-ui/core/Snackbar";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import get from "lodash/get";
import { withRouter } from "react-router-dom";
import { addRoute, setAllRoutes } from "../../../../ObjectBrowser/actions";
import { connect } from "react-redux";
import { ObjectBrowserState, Route } from "../../../../ObjectBrowser/reducers";
const commonIcon = {
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
width: 16,
height: 40,
marginRight: 10,
};
const styles = (theme: Theme) =>
createStyles({
@@ -69,93 +84,115 @@ const styles = (theme: Theme) =>
},
},
},
fileName: {
display: "flex",
alignItems: "center",
},
iconFolder: {
backgroundImage: "url(/images/ob_folder_clear.svg)",
...commonIcon,
},
iconFile: {
backgroundImage: "url(/images/ob_file_clear.svg)",
...commonIcon,
},
"@global": {
".rowElementRaw:hover .iconFileElm": {
backgroundImage: "url(/images/ob_file_filled.svg)",
},
".rowElementRaw:hover .iconFolderElm": {
backgroundImage: "url(/images/ob_folder_filled.svg)",
},
},
...actionsTray,
...searchField,
...objectBrowserCommon,
...containerForHeader(theme.spacing(4)),
});
interface IListObjectsProps {
classes: any;
match: any;
addRoute: (param1: string, param2: string) => any;
setAllRoutes: (path: string) => any;
routesList: Route[];
}
interface IListObjectsState {
records: BucketObject[];
totalRecords: number;
loading: boolean;
error: string;
deleteOpen: boolean;
deleteError: string;
selectedObject: string;
selectedBucket: string;
filterObjects: string;
openSnackbar: boolean;
snackBarMessage: string;
interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
class ListObjects extends React.Component<
IListObjectsProps,
IListObjectsState
> {
state: IListObjectsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteOpen: false,
deleteError: "",
selectedObject: "",
selectedBucket: "",
filterObjects: "",
openSnackbar: false,
snackBarMessage: "",
const ListObjects = ({
classes,
match,
addRoute,
setAllRoutes,
routesList,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [deleteError, setDeleteError] = useState<string>("");
const [selectedObject, setSelectedObject] = useState<string>("");
const [selectedBucket, setSelectedBucket] = useState<string>("");
const [filterObjects, setFilterObjects] = useState<string>("");
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const [snackBarMessage, setSnackbarMessage] = useState<string>("");
useEffect(() => {
const bucketName = match.params["bucket"];
const internalPaths = match.params[0];
let extraPath = "";
if (internalPaths) {
extraPath = `?prefix=${internalPaths}/`;
}
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`)
.then((res: BucketObjectsList) => {
setLoading(false);
setSelectedBucket(bucketName);
setRecords(res.objects || []);
setTotalRecords(!res.objects ? 0 : res.total);
setError("");
// TODO:
// if we get 0 results, and page > 0 , go down 1 page
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
}, [loading, match]);
useEffect(() => {
const url = get(match, "url", "/object-browser");
if (url !== routesList[routesList.length - 1].route) {
setAllRoutes(url);
}
}, [match, routesList, setAllRoutes]);
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
setLoading(true);
}
};
fetchRecords = () => {
this.setState({ loading: true }, () => {
const { match } = this.props;
const bucketName = match.params["bucket"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects`)
.then((res: BucketObjectsList) => {
this.setState({
loading: false,
selectedBucket: bucketName,
records: res.objects || [],
totalRecords: !res.objects ? 0 : res.total,
error: "",
});
// TODO:
// if we get 0 results, and page > 0 , go down 1 page
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
const showSnackBarMessage = (text: string) => {
setSnackbarMessage(text);
setOpenSnackbar(true);
};
componentDidMount(): void {
this.fetchRecords();
}
const closeSnackBar = () => {
setSnackbarMessage("");
setOpenSnackbar(false);
};
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
showSnackBarMessage(text: string) {
this.setState({ openSnackbar: true, snackBarMessage: text });
}
closeSnackBar() {
this.setState({ openSnackbar: false, snackBarMessage: `` });
}
upload(e: any, bucketName: string, path: string) {
let listObjects = this;
const upload = (e: any, bucketName: string, path: string) => {
if (isNullOrUndefined(e) || isNullOrUndefined(e.target)) {
return;
}
@@ -175,24 +212,20 @@ class ListObjects extends React.Component<
xhr.onload = function (event) {
// TODO: handle status
if (xhr.status === 401 || xhr.status === 403) {
listObjects.showSnackBarMessage(
"An error occurred while uploading the file."
);
showSnackBarMessage("An error occurred while uploading the file.");
}
if (xhr.status === 500) {
listObjects.showSnackBarMessage(
"An error occurred while uploading the file."
);
showSnackBarMessage("An error occurred while uploading the file.");
}
if (xhr.status === 200) {
listObjects.showSnackBarMessage("Object uploaded successfully.");
listObjects.fetchRecords();
showSnackBarMessage("Object uploaded successfully.");
setLoading(true);
}
};
xhr.upload.addEventListener("error", (event) => {
// TODO: handle error
this.showSnackBarMessage("An error occurred while uploading the file.");
showSnackBarMessage("An error occurred while uploading the file.");
});
xhr.upload.addEventListener("progress", (event) => {
@@ -200,24 +233,22 @@ class ListObjects extends React.Component<
});
xhr.onerror = () => {
listObjects.showSnackBarMessage(
"An error occurred while uploading the file."
);
showSnackBarMessage("An error occurred while uploading the file.");
};
var formData = new FormData();
var blobFile = new Blob([file]);
const formData = new FormData();
const blobFile = new Blob([file]);
formData.append("upfile", blobFile);
xhr.send(formData);
e.target.value = null;
}
};
download(bucketName: string, objectName: string) {
var anchor = document.createElement("a");
const download = (bucketName: string, objectName: string) => {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
const token: string = storage.getItem("token")!;
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open(
"GET",
@@ -229,10 +260,10 @@ class ListObjects extends React.Component<
xhr.onload = function (e) {
if (this.status === 200) {
var blob = new Blob([this.response], {
const blob = new Blob([this.response], {
type: "octet/stream",
});
var blobUrl = window.URL.createObjectURL(blob);
const blobUrl = window.URL.createObjectURL(blob);
anchor.href = blobUrl;
anchor.download = objectName;
@@ -243,158 +274,199 @@ class ListObjects extends React.Component<
}
};
xhr.send();
}
};
bucketFilter(): void {}
const displayParsedDate = (date: string) => {
return <reactMoment.default>{date}</reactMoment.default>;
};
render() {
const { classes } = this.props;
const {
records,
loading,
selectedObject,
selectedBucket,
deleteOpen,
filterObjects,
snackBarMessage,
openSnackbar,
} = this.state;
const displayParsedDate = (date: string) => {
return <reactMoment.default>{date}</reactMoment.default>;
};
const confirmDeleteObject = (object: string) => {
setDeleteOpen(true);
setSelectedObject(object);
};
const confirmDeleteObject = (object: string) => {
this.setState({ deleteOpen: true, selectedObject: object });
};
const downloadObject = (object: string) => {
download(selectedBucket, object);
};
const downloadObject = (object: string) => {
this.download(selectedBucket, object);
};
const openPath = (idElement: string) => {
const currentPath = get(match, "url", "/object-browser");
const uploadObject = (e: any): void => {
// TODO: handle deeper paths/folders
let file = e.target.files[0];
this.showSnackBarMessage(`Uploading: ${file.name}`);
this.upload(e, selectedBucket, "");
};
// Element is a folder, we redirect to it
if (idElement.endsWith("/")) {
const idElementClean = idElement
.substr(0, idElement.length - 1)
.split("/");
const lastIndex = idElementClean.length - 1;
const newPath = `${currentPath}/${idElementClean[lastIndex]}`;
const snackBarAction = (
<Button
color="secondary"
size="small"
onClick={() => {
this.closeSnackBar();
}}
>
Dismiss
</Button>
);
addRoute(newPath, idElementClean[lastIndex]);
return;
}
const tableActions = [
{ type: "download", onClick: downloadObject, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
// Element is a file. we open details here
// TODO: Add details open function here.
//console.log("object", idElementClean);
};
const filteredRecords = records.filter((b: BucketObject) => {
if (filterObjects === "") {
return true;
} else {
if (b.name.indexOf(filterObjects) >= 0) {
return true;
} else {
return false;
}
}
});
const uploadObject = (e: any): void => {
// TODO: handle deeper paths/folders
let file = e.target.files[0];
showSnackBarMessage(`Uploading: ${file.name}`);
upload(e, selectedBucket, "");
};
const snackBarAction = (
<Button
color="secondary"
size="small"
onClick={() => {
closeSnackBar();
}}
>
Dismiss
</Button>
);
const tableActions = [
{ type: "view", onClick: openPath, sendOnlyId: true },
{ type: "download", onClick: downloadObject, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
const displayName = (element: string) => {
let elementString = element;
let icon = `${classes.iconFile} iconFileElm`;
// Element is a folder
if (element.endsWith("/")) {
icon = `${classes.iconFolder} iconFolderElm`;
elementString = element.substr(0, element.length - 1);
}
const splitItem = elementString.split("/");
return (
<React.Fragment>
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
selectedObject={selectedObject}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<Snackbar
open={openSnackbar}
message={snackBarMessage}
action={snackBarAction}
/>
<PageHeader label="Objects" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
this.setState({
filterObjects: val.target.value,
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<div className={classes.fileName}>
<div className={icon} />
<span>{splitItem[splitItem.length - 1]}</span>
</div>
);
};
<>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
component="label"
>
Upload Object
<Input
type="file"
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
/>
</Button>
</>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
</Grid>
const filteredRecords = records.filter((b: BucketObject) => {
if (filterObjects === "") {
return true;
} else {
if (b.name.indexOf(filterObjects) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
selectedObject={selectedObject}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Snackbar
open={openSnackbar}
message={snackBarMessage}
action={snackBarAction}
/>
<PageHeader label="Object Browser" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.obTitleSection}>
<div>
<BrowserBreadcrumbs />
</div>
<div>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
component="label"
>
Upload Object
<Input
type="file"
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
/>
</Button>
</div>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterObjects(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={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
},
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
</Grid>
</Grid>
</React.Fragment>
);
}
}
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ListObjects);
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
routesList: get(objectBrowser, "routesList", []),
});
const mapDispatchToProps = {
addRoute,
setAllRoutes,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(ListObjects)));

View File

@@ -14,19 +14,12 @@
// 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, { ChangeEvent } from "react";
import React from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox";
import Table from "@material-ui/core/Table";
import { ArnList } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";

View File

@@ -14,7 +14,7 @@
// 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, { ChangeEvent } from "react";
import React from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
@@ -47,7 +47,6 @@ import AddReplicationModal from "./AddReplicationModal";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
import Checkbox from "@material-ui/core/Checkbox";
import TableCell from "@material-ui/core/TableCell";
import EnableBucketEncryption from "./EnableBucketEncryption";
const styles = (theme: Theme) =>

View File

@@ -14,21 +14,8 @@
// 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 {
Checkbox,
Grid,
InputLabel,
TextField,
TextFieldProps,
Tooltip,
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
import { Checkbox, Grid, InputLabel, Tooltip } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
checkboxIcons,
fieldBasic,

View File

@@ -179,3 +179,31 @@ export const predefinedList = {
minHeight: 41,
},
};
export const objectBrowserCommon = {
obTitleSection: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 20,
},
sectionTitle: {
fontSize: 22,
color: "#000",
fontWeight: 600,
height: 40,
lineHeight: "40px",
},
breadcrumbs: {
fontSize: 10,
color: "#000",
marginTop: 2,
"& a": {
textDecoration: "none",
color: "#000",
"&:hover": {
textDecoration: "underline",
},
},
},
};

View File

@@ -7,13 +7,17 @@ const DeleteIcon = ({ active = false }: IIcon) => {
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
viewBox="0 0 10.402 13"
>
<g transform="translate(-1225 -657)">
<g transform="translate(0.004 -28.959)">
<path
fill={active ? selected : unSelected}
d="M0,8H0a8,8,0,0,0,8,8H8a8,8,0,0,0,8-8h0A8,8,0,0,0,8,0H8A8,8,0,0,0,0,8Zm10.007,3.489L8,9.482,5.993,11.489,4.511,10.007,6.518,8,4.511,5.993,5.993,4.511,8,6.518l2.007-2.007,1.482,1.482L9.482,8l2.007,2.007Z"
transform="translate(1225 657)"
d="M6.757,29.959v-1H3.636v1H0v1H10.4v-1Z"
/>
<path
fill={active ? selected : unSelected}
d="M0,31.957l1.672,10H8.724l1.673-10ZM3.412,40.2,2.86,33.722h.653l.553,6.472Zm3.569,0H6.328l.551-6.472h.654Z"
transform="translate(0 0)"
/>
</g>
</svg>

View File

@@ -54,6 +54,7 @@ interface IColumns {
renderFunction?: (input: any) => any;
renderFullObject?: boolean;
globalClass?: any;
rowClass?: any;
}
interface IPaginatorConfig {
@@ -89,12 +90,18 @@ interface TableWrapperProps {
paginatorConfig?: IPaginatorConfig;
}
const borderColor = "#eaeaea";
const borderColor = "#9c9c9c80";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor,
borderWidth: "0.5px",
height: 40,
transitionDuration: "0.3s",
padding: "initial",
paddingRight: 6,
paddingLeft: 6,
};
const styles = (theme: Theme) =>
@@ -112,19 +119,29 @@ const styles = (theme: Theme) =>
border: "#EAEDEE 1px solid",
borderRadius: 3,
},
allTableSettings: {
"& .MuiTableCell-sizeSmall:last-child": {
paddingRight: "initial",
},
"& .MuiTableCell-body.MuiTableCell-sizeSmall:last-child": {
paddingRight: 6,
},
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor,
borderColor: "#39393980",
borderWidth: "0.5px",
padding: "6px 0 10px",
},
},
},
rowUnselected: {
...rowText,
color: "#393939",
},
rowSelected: {
...rowText,
@@ -137,8 +154,12 @@ const styles = (theme: Theme) =>
padding: "5px 38px",
},
checkBoxHeader: {
width: 50,
textAlign: "left",
paddingRight: 10,
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9,
paddingBottom: 4,
paddingLeft: 0,
},
},
actionsContainer: {
@@ -150,6 +171,7 @@ const styles = (theme: Theme) =>
},
checkBoxRow: {
borderColor: borderColor,
padding: "0 10px 0 0",
},
loadingBox: {
paddingTop: "100px",
@@ -160,6 +182,10 @@ const styles = (theme: Theme) =>
"&:hover": {
backgroundColor: "#ececec",
"& td": {
fontWeight: 600,
},
},
},
rowClickable: {
@@ -202,7 +228,9 @@ const rowColumnsMap = (
return (
<TableCell
key={`tbRE-${column.elementKey}-${index}`}
className={isSelected ? classes.rowSelected : classes.rowUnselected}
className={`${column.rowClass} ${
isSelected ? classes.rowSelected : classes.rowUnselected
}`}
>
{renderElement}
</TableCell>
@@ -285,15 +313,15 @@ const TableWrapper = ({
</Grid>
)}
{records && !isLoading && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<Table
size="small"
stickyHeader={stickyHeader}
className={classes.allTableSettings}
>
<TableHead className={classes.minTableHeader}>
<TableRow>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxHeader}
>
<TableCell align="center" className={classes.checkBoxHeader}>
Select
</TableCell>
)}
@@ -324,17 +352,13 @@ const TableWrapper = ({
key={`tb-${entityName}-${index.toString()}`}
className={`${findView ? classes.rowClickable : ""} ${
classes.rowElement
}`}
} rowElementRaw`}
onClick={() => {
clickAction(record);
}}
>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxRow}
>
<TableCell align="center" className={classes.checkBoxRow}>
<Checkbox
value={isString(record) ? record : record[idField]}
color="primary"

View File

@@ -232,7 +232,11 @@ const Console = ({
},
{
component: ListObjects,
path: "/object-browser/:bucket?",
path: "/object-browser/:bucket",
},
{
component: ListObjects,
path: "/object-browser/:bucket/*",
},
{
component: Watch,

View File

@@ -1,12 +1,6 @@
import { HorizontalBar } from "react-chartjs-2";
import React, { useEffect, useState } from "react";
import {
Button,
Grid,
TextField,
Checkbox,
InputBase,
} from "@material-ui/core";
import { Button, Grid, TextField, InputBase } from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { wsProtocol } from "../../../utils/wsUtils";

View File

@@ -15,11 +15,11 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } 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 AddBucket from "../Buckets/ListBuckets/AddBucket";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
@@ -28,12 +28,16 @@ import { CreateIcon } from "../../../icons";
import { niceBytes } from "../../../common/utils";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { Bucket, BucketList } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import {
actionsTray,
objectBrowserCommon,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { addRoute, resetRoutesList } from "./actions";
import BrowserBreadcrumbs from "./BrowserBreadcrumbs";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddBucket from "../Buckets/ListBuckets/AddBucket";
import api from "../../../common/api";
const styles = (theme: Theme) =>
createStyles({
@@ -67,20 +71,47 @@ const styles = (theme: Theme) =>
},
usedSpaceCol: {
width: 150,
textAlign: "right",
},
subTitleLabel: {
alignItems: "center",
display: "flex",
},
bucketName: {
display: "flex",
alignItems: "center",
},
iconBucket: {
backgroundImage: "url(/images/ob_bucket_clear.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
width: 16,
height: 40,
marginRight: 10,
},
"@global": {
".rowElementRaw:hover .iconBucketElm": {
backgroundImage: "url(/images/ob_bucket_filled.svg)",
},
},
...actionsTray,
...searchField,
...objectBrowserCommon,
});
interface IBrowseBucketsProps {
classes: any;
addRoute: (path: string, label: string) => any;
resetRoutesList: (doVar: boolean) => any;
match: any;
}
const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
const BrowseBuckets = ({
classes,
match,
addRoute,
resetRoutesList,
}: IBrowseBucketsProps) => {
const [loading, setLoading] = useState<boolean>(true);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
@@ -91,6 +122,10 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
const offset = page * rowsPerPage;
useEffect(() => {
resetRoutesList(true);
}, [match]);
useEffect(() => {
if (loading) {
api
@@ -146,6 +181,22 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
setRowsPerPage(rPP);
};
const handleViewChange = (idElement: string) => {
const currentPath = get(match, "url", "/object-browser");
const newPath = `${currentPath}/${idElement}`;
addRoute(newPath, idElement);
};
const renderBucket = (bucketName: string) => {
return (
<div className={classes.bucketName}>
<div className={`${classes.iconBucket} iconBucketElm`} />
<span>{bucketName}</span>
</div>
);
};
return (
<React.Fragment>
{addScreenOpen && (
@@ -155,10 +206,24 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
/>
)}
<Grid container>
<Grid item xs={2} className={classes.subTitleLabel}>
<Typography variant="h6">Buckets</Typography>
<Grid item xs={12} className={classes.obTitleSection}>
<div>
<BrowserBreadcrumbs />
</div>
<div>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Bucket
</Button>
</div>
</Grid>
<Grid item xs={10} className={classes.actionsTray}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
className={classes.searchField}
@@ -176,16 +241,6 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Bucket
</Button>
</Grid>
<Grid item xs={12}>
<br />
@@ -194,15 +249,24 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
{error !== "" && <span className={classes.errorBlock}>{error}</span>}
<TableWrapper
itemActions={[
{ type: "view", to: `/object-browser`, sendOnlyId: true },
{
type: "view",
sendOnlyId: true,
onClick: handleViewChange,
},
]}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Name",
elementKey: "name",
renderFunction: renderBucket,
},
{
label: "Used Space",
elementKey: "size",
renderFunction: niceBytes,
globalClass: classes.usedSpaceCol,
rowClass: classes.usedSpaceCol,
},
]}
isLoading={loading}
@@ -230,4 +294,11 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
);
};
export default withStyles(styles)(BrowseBuckets);
const mapDispatchToProps = {
addRoute,
resetRoutesList,
};
const connector = connect(null, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(BrowseBuckets)));

View File

@@ -0,0 +1,90 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
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 { objectBrowserCommon } from "../Common/FormComponents/common/styleLibrary";
import { Link } from "react-router-dom";
interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
interface IObjectBrowser {
classes: any;
objectsList: Route[];
removeRouteLevel: (path: string) => any;
}
const styles = (theme: Theme) =>
createStyles({
...objectBrowserCommon,
});
const BrowserBreadcrumbs = ({
classes,
objectsList,
removeRouteLevel,
}: 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>
);
});
return (
<React.Fragment>
<Grid item xs={12}>
<div className={classes.sectionTitle}>
{objectsList && objectsList.length > 0
? objectsList.slice(-1)[0].label
: ""}
</div>
</Grid>
<Grid item xs={12} className={classes.breadcrumbs}>
{listBreadcrumbs}
</Grid>
</React.Fragment>
);
};
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
objectsList: get(objectBrowser, "routesList", []),
});
const mapDispatchToProps = {
removeRouteLevel,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(withStyles(styles)(BrowserBreadcrumbs));

View File

@@ -73,7 +73,7 @@ const styles = (theme: Theme) =>
});
const ObjectBrowser = ({ match, classes }: IObjectBrowserProps) => {
const pathIn = get(match, "path", "");
const pathIn = get(match, "url", "");
return (
<React.Fragment>

View File

@@ -0,0 +1,79 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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";
interface AddRouteAction {
type: typeof OBJECT_BROWSER_ADD_ROUTE;
route: string;
label: 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;
}
export type ObjectBrowserActionTypes =
| AddRouteAction
| ResetRoutesList
| RemoveRouteLevel
| SetAllRoutes;
export const addRoute = (route: string, label: string) => {
return {
type: OBJECT_BROWSER_ADD_ROUTE,
route,
label,
};
};
export const resetRoutesList = (reset: boolean) => {
console.log("RESET");
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,
};
};

View File

@@ -0,0 +1,91 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import history from "../../../history";
import {
OBJECT_BROWSER_ADD_ROUTE,
OBJECT_BROWSER_REMOVE_ROUTE_LEVEL,
OBJECT_BROWSER_RESET_ROUTES_LIST,
OBJECT_BROWSER_SET_ALL_ROUTES,
ObjectBrowserActionTypes,
} from "./actions";
export interface Route {
route: string;
label: string;
}
export interface ObjectBrowserState {
routesList: Route[];
}
const initialRoute = [{ route: "/object-browser", label: "All Buckets" }];
const initialState: ObjectBrowserState = {
routesList: initialRoute,
};
export function objectBrowserReducer(
state = initialState,
action: ObjectBrowserActionTypes
): ObjectBrowserState {
switch (action.type) {
case OBJECT_BROWSER_ADD_ROUTE:
const newRouteList = [
...state.routesList,
{ route: action.route, label: action.label },
];
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 });
}
});
const newSetOfRoutes = [...initialRoute, ...routesArray];
return {
...state,
routesList: newSetOfRoutes,
};
default:
return state;
}
}

View File

@@ -45,6 +45,12 @@ const styles = (theme: Theme) =>
borderBottom: "1px solid #dedede",
},
},
sizeItem: {
width: 150,
},
timeItem: {
width: 100,
},
...containerForHeader(theme.spacing(4)),
});
@@ -112,6 +118,7 @@ const Trace = ({
const timeParse = new Date(time);
return timeFromDate(timeParse);
},
globalClass: classes.timeItem,
},
{ label: "Name", elementKey: "api" },
{
@@ -128,16 +135,22 @@ const Trace = ({
`${fullElement.host} ${fullElement.client}`,
renderFullObject: true,
},
{ label: "Load Time", elementKey: "callStats.duration" },
{
label: "Load Time",
elementKey: "callStats.duration",
globalClass: classes.timeItem,
},
{
label: "Upload",
elementKey: "callStats.rx",
renderFunction: niceBytes,
globalClass: classes.sizeItem,
},
{
label: "Download",
elementKey: "callStats.tx",
renderFunction: niceBytes,
globalClass: classes.sizeItem,
},
]}
isLoading={false}

View File

@@ -22,6 +22,7 @@ import { logReducer } from "./screens/Console/Logs/reducers";
import { watchReducer } from "./screens/Console/Watch/reducers";
import { consoleReducer } from "./screens/Console/reducer";
import { bucketsReducer } from "./screens/Console/Buckets/reducers";
import { objectBrowserReducer } from "./screens/Console/ObjectBrowser/reducers";
const globalReducer = combineReducers({
system: systemReducer,
@@ -30,6 +31,7 @@ const globalReducer = combineReducers({
watch: watchReducer,
console: consoleReducer,
buckets: bucketsReducer,
objectBrowser: objectBrowserReducer,
});
declare global {