Added URl routing for tabs in buckets (#769)

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

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2021-05-25 18:16:15 -05:00
committed by GitHub
parent 213d7589c6
commit 622f9ed3cd
25 changed files with 1468 additions and 990 deletions

View File

@@ -22,34 +22,41 @@ import (
// endpoints definition
var (
configuration = "/settings"
users = "/users"
usersDetail = "/users/:userName"
groups = "/groups"
iamPolicies = "/policies"
policiesDetail = "/policies/:policyName"
dashboard = "/dashboard"
profiling = "/profiling"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/account"
changePassword = "/account/change-password"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
storage = "/storage"
storageVolumes = "/storage/volumes"
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"
trace = "/trace"
logs = "/logs"
healthInfo = "/health-info"
configuration = "/settings"
users = "/users"
usersDetail = "/users/:userName"
groups = "/groups"
iamPolicies = "/policies"
policiesDetail = "/policies/:policyName"
dashboard = "/dashboard"
profiling = "/profiling"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
bucketsDetailSummary = "/buckets/:bucketName/summary"
bucketsDetailEvents = "/buckets/:bucketName/events"
bucketsDetailReplication = "/buckets/:bucketName/replication"
bucketsDetailLifecycle = "/buckets/:bucketName/lifecycle"
bucketsDetailAccess = "/buckets/:bucketName/access"
bucketsDetailAccessPolicies = "/buckets/:bucketName/access/policies"
bucketsDetailAccessUsers = "/buckets/:bucketName/access/users"
serviceAccounts = "/account"
changePassword = "/account/change-password"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
storage = "/storage"
storageVolumes = "/storage/volumes"
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"
trace = "/trace"
logs = "/logs"
healthInfo = "/health-info"
)
type ConfigurationActionSet struct {
@@ -276,29 +283,36 @@ 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,
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,
profiling: profilingActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
bucketsDetailSummary: bucketsActionSet,
bucketsDetailEvents: bucketsActionSet,
bucketsDetailReplication: bucketsActionSet,
bucketsDetailLifecycle: bucketsActionSet,
bucketsDetailAccess: bucketsActionSet,
bucketsDetailAccessPolicies: bucketsActionSet,
bucketsDetailAccessUsers: 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,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 7,
want: 14,
},
{
name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 23,
want: 30,
},
{
name: "Console User - default endpoints",

View File

@@ -1,25 +1,25 @@
{
"files": {
"main.css": "/static/css/main.a19f3d53.chunk.css",
"main.js": "/static/js/main.0047f81c.chunk.js",
"main.js.map": "/static/js/main.0047f81c.chunk.js.map",
"main.js": "/static/js/main.ad0a1441.chunk.js",
"main.js.map": "/static/js/main.ad0a1441.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.f48e99e5.js",
"runtime-main.js.map": "/static/js/runtime-main.f48e99e5.js.map",
"static/css/2.13d7fda8.chunk.css": "/static/css/2.13d7fda8.chunk.css",
"static/js/2.791bc763.chunk.js": "/static/js/2.791bc763.chunk.js",
"static/js/2.791bc763.chunk.js.map": "/static/js/2.791bc763.chunk.js.map",
"static/css/2.76b14b73.chunk.css": "/static/css/2.76b14b73.chunk.css",
"static/js/2.1bd26e6c.chunk.js": "/static/js/2.1bd26e6c.chunk.js",
"static/js/2.1bd26e6c.chunk.js.map": "/static/js/2.1bd26e6c.chunk.js.map",
"index.html": "/index.html",
"static/css/2.13d7fda8.chunk.css.map": "/static/css/2.13d7fda8.chunk.css.map",
"static/css/2.76b14b73.chunk.css.map": "/static/css/2.76b14b73.chunk.css.map",
"static/css/main.a19f3d53.chunk.css.map": "/static/css/main.a19f3d53.chunk.css.map",
"static/js/2.791bc763.chunk.js.LICENSE.txt": "/static/js/2.791bc763.chunk.js.LICENSE.txt",
"static/js/2.1bd26e6c.chunk.js.LICENSE.txt": "/static/js/2.1bd26e6c.chunk.js.LICENSE.txt",
"static/media/minio_console_logo.0837460e.svg": "/static/media/minio_console_logo.0837460e.svg",
"static/media/minio_operator_logo.1312b7c9.svg": "/static/media/minio_operator_logo.1312b7c9.svg"
},
"entrypoints": [
"static/js/runtime-main.f48e99e5.js",
"static/css/2.13d7fda8.chunk.css",
"static/js/2.791bc763.chunk.js",
"static/css/2.76b14b73.chunk.css",
"static/js/2.1bd26e6c.chunk.js",
"static/css/main.a19f3d53.chunk.css",
"static/js/main.0047f81c.chunk.js"
"static/js/main.ad0a1441.chunk.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.13d7fda8.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.791bc763.chunk.js"></script><script src="/static/js/main.0047f81c.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.76b14b73.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.1bd26e6c.chunk.js"></script><script src="/static/js/main.ad0a1441.chunk.js"></script></body></html>

View File

@@ -1,2 +1,2 @@
.ReactVirtualized__Table__headerRow{font-weight:700;text-transform:uppercase}.ReactVirtualized__Table__headerRow,.ReactVirtualized__Table__row{display:flex;flex-direction:row;align-items:center}.ReactVirtualized__Table__headerTruncatedText{display:inline-block;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.ReactVirtualized__Table__headerColumn,.ReactVirtualized__Table__rowColumn{margin-right:10px;min-width:0}.ReactVirtualized__Table__rowColumn{text-overflow:ellipsis;white-space:nowrap}.ReactVirtualized__Table__headerColumn:first-of-type,.ReactVirtualized__Table__rowColumn:first-of-type{margin-left:10px}.ReactVirtualized__Table__sortableHeaderColumn{cursor:pointer}.ReactVirtualized__Table__sortableHeaderIconContainer{display:flex;align-items:center}.ReactVirtualized__Table__sortableHeaderIcon{flex:0 0 24px;height:1em;width:1em;fill:currentColor}.react-grid-layout{position:relative;transition:height .2s ease}.react-grid-item{transition:all .2s ease;transition-property:left,top}.react-grid-item img{pointer-events:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.react-grid-item.cssTransforms{transition-property:transform}.react-grid-item.resizing{z-index:1;will-change:width,height}.react-grid-item.react-draggable-dragging{transition:none;z-index:3;will-change:transform}.react-grid-item.dropping{visibility:hidden}.react-grid-item.react-grid-placeholder{background:red;opacity:.2;transition-duration:.1s;z-index:2;-webkit-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.react-grid-item>.react-resizable-handle{position:absolute;width:20px;height:20px}.react-grid-item>.react-resizable-handle:after{content:"";position:absolute;right:3px;bottom:3px;width:5px;height:5px;border-right:2px solid rgba(0,0,0,.4);border-bottom:2px solid rgba(0,0,0,.4)}.react-resizable-hide>.react-resizable-handle{display:none}.react-grid-item>.react-resizable-handle.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;transform:rotate(90deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;transform:rotate(180deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;transform:rotate(270deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e,.react-grid-item>.react-resizable-handle.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-n,.react-grid-item>.react-resizable-handle.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}.react-resizable{position:relative}.react-resizable-handle{position:absolute;width:20px;height:20px;background-repeat:no-repeat;background-origin:content-box;box-sizing:border-box;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgd2lkdGg9IjYiIGhlaWdodD0iNiI+PHBhdGggZD0iTTYgNkgwVjQuMmg0LjJWMEg2djZ6IiBvcGFjaXR5PSIuMzAyIi8+PC9zdmc+");background-position:100% 100%;padding:0 3px 3px 0}.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;transform:rotate(90deg)}.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;transform:rotate(180deg)}.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;transform:rotate(270deg)}.react-resizable-handle-e,.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-resizable-handle-n,.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}
/*# sourceMappingURL=2.13d7fda8.chunk.css.map */
/*# sourceMappingURL=2.76b14b73.chunk.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -78,7 +78,7 @@
},
"proxy": "http://localhost:9090/",
"devDependencies": {
"prettier": "2.2.1",
"prettier": "2.3.0",
"typescript": "^4.1.3"
}
}

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, { useEffect, useState } from "react";
import React, { useEffect, useState, Fragment } from "react";
import { connect } from "react-redux";
import { AppState } from "../../../../store";
import { setErrorSnackMessage } from "../../../../actions";
@@ -46,15 +46,15 @@ function a11yProps(index: any) {
}
interface IAccessDetailsProps {
bucketName: string;
session: ISessionResponse;
setErrorSnackMessage: typeof setErrorSnackMessage;
classes: any;
match: any;
}
const AccessDetails = ({
bucketName,
classes,
match,
setErrorSnackMessage,
session,
}: IAccessDetailsProps) => {
@@ -64,6 +64,8 @@ const AccessDetails = ({
const [loadingUsers, setLoadingUsers] = useState<boolean>(true);
const [bucketUsers, setBucketUsers] = useState<User[]>([]);
const bucketName = match.params["bucketName"];
const usersEnabled = session.pages?.indexOf("/users") > -1;
const PolicyActions = [
@@ -115,46 +117,49 @@ const AccessDetails = ({
}, [loadingPolicies, setErrorSnackMessage, bucketName]);
return (
<Paper>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurTab(newValue);
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
variant="scrollable"
scrollButtons="auto"
>
<Tab label="Policies" {...a11yProps(0)} />
{usersEnabled && <Tab label="Users" {...a11yProps(1)} />}
</Tabs>
<TabPanel index={0} value={curTab}>
<TableWrapper
noBackground={true}
itemActions={PolicyActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingPolicies}
records={bucketPolicy}
entityName="Policies"
idField="name"
/>
</TabPanel>
{usersEnabled && (
<TabPanel index={1} value={curTab}>
<Fragment>
<br />
<Paper>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurTab(newValue);
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
variant="scrollable"
scrollButtons="auto"
>
<Tab label="Policies" {...a11yProps(0)} />
{usersEnabled && <Tab label="Users" {...a11yProps(1)} />}
</Tabs>
<TabPanel index={0} value={curTab}>
<TableWrapper
noBackground={true}
itemActions={userTableActions}
columns={[{ label: "User", elementKey: "accessKey" }]}
isLoading={loadingUsers}
records={bucketUsers}
entityName="Users"
idField="accessKey"
itemActions={PolicyActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingPolicies}
records={bucketPolicy}
entityName="Policies"
idField="name"
/>
</TabPanel>
)}
</Paper>
{usersEnabled && (
<TabPanel index={1} value={curTab}>
<TableWrapper
noBackground={true}
itemActions={userTableActions}
columns={[{ label: "User", elementKey: "accessKey" }]}
isLoading={loadingUsers}
records={bucketUsers}
entityName="Users"
idField="accessKey"
/>
</TabPanel>
)}
</Paper>
</Fragment>
);
};

View File

@@ -0,0 +1,191 @@
// 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 { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, TextField } from "@material-ui/core";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { CreateIcon } from "../../../../icons";
import { BucketEvent, BucketEventList } from "../types";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import {
actionsTray,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import api from "../../../../common/api";
import DeleteEvent from "./DeleteEvent";
import AddEvent from "./AddEvent";
const styles = (theme: Theme) =>
createStyles({
...searchField,
...actionsTray,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});
interface IBucketEventsProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const BucketEventsPanel = ({
classes,
match,
setErrorSnackMessage,
}: IBucketEventsProps) => {
const [addEventScreenOpen, setAddEventScreenOpen] = useState<boolean>(false);
const [loadingEvents, setLoadingEvents] = useState<boolean>(true);
const [records, setRecords] = useState<BucketEvent[]>([]);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedEvent, setSelectedEvent] = useState<BucketEvent | null>(null);
const [filter, setFilter] = useState<string>("");
const bucketName = match.params["bucketName"];
useEffect(() => {
if (loadingEvents) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/events`)
.then((res: BucketEventList) => {
const events = get(res, "events", []);
setLoadingEvents(false);
setRecords(events || []);
})
.catch((err: any) => {
setLoadingEvents(false);
setErrorSnackMessage(err);
});
}
}, [loadingEvents, setErrorSnackMessage, bucketName]);
const eventsDisplay = (events: string[]) => {
return <Fragment>{events.join(", ")}</Fragment>;
};
const confirmDeleteEvent = (evnt: BucketEvent) => {
setDeleteOpen(true);
setSelectedEvent(evnt);
};
const closeAddEventAndRefresh = () => {
setAddEventScreenOpen(false);
setLoadingEvents(true);
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
setLoadingEvents(true);
}
};
const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }];
return (
<Fragment>
{deleteOpen && (
<DeleteEvent
deleteOpen={deleteOpen}
selectedBucket={bucketName}
bucketEvent={selectedEvent}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{addEventScreenOpen && (
<AddEvent
open={addEventScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={closeAddEventAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setAddEventScreenOpen(true);
}}
>
Subscribe to Event
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "SQS", elementKey: "arn" },
{
label: "Events",
elementKey: "events",
renderFunction: eventsDisplay,
},
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loadingEvents}
records={records}
entityName="Events"
idField="id"
/>
</Grid>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
session: state.console.session,
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(BucketEventsPanel));

View File

@@ -0,0 +1,233 @@
// 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, useState, useEffect } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, TextField } from "@material-ui/core";
import get from "lodash/get";
import * as reactMoment from "react-moment";
import Grid from "@material-ui/core/Grid";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { LifeCycleItem } from "../types";
import { CreateIcon } from "../../../../icons";
import {
actionsTray,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import api from "../../../../common/api";
import EditLifecycleConfiguration from "./EditLifecycleConfiguration";
import AddLifecycleModal from "./AddLifecycleModal";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { AppState } from "../../../../store";
const styles = (theme: Theme) =>
createStyles({
...searchField,
...actionsTray,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});
interface IBucketLifecyclePanelProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const BucketLifecyclePanel = ({
classes,
match,
setErrorSnackMessage,
}: IBucketLifecyclePanelProps) => {
const [loadingLifecycle, setLoadingLifecycle] = useState<boolean>(true);
const [lifecycleRecords, setLifecycleRecords] = useState<LifeCycleItem[]>([]);
const [addLifecycleOpen, setAddLifecycleOpen] = useState<boolean>(false);
const [editLifecycleOpen, setEditLifecycleOpen] = useState<boolean>(false);
const bucketName = match.params["bucketName"];
useEffect(() => {
if (loadingLifecycle) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/lifecycle`)
.then((res: any) => {
const records = get(res, "lifecycle", []);
setLifecycleRecords(records || []);
setLoadingLifecycle(false);
})
.catch((err) => {
console.error(err);
setLoadingLifecycle(false);
});
}
}, [loadingLifecycle, setLoadingLifecycle, bucketName]);
const closeEditLCAndRefresh = (refresh: boolean) => {
setEditLifecycleOpen(false);
if (refresh) {
setLoadingLifecycle(true);
}
};
const closeAddLCAndRefresh = (refresh: boolean) => {
setAddLifecycleOpen(false);
if (refresh) {
setLoadingLifecycle(true);
}
};
const expirationRender = (expiration: any) => {
if (expiration.days) {
return `${expiration.days} day${expiration.days > 1 ? "s" : ""}`;
}
if (expiration.date === "0001-01-01T00:00:00Z") {
return "";
}
return <reactMoment.default>{expiration.date}</reactMoment.default>;
};
const transitionRender = (transition: any) => {
if (transition.days) {
return `${transition.days} day${transition.days > 1 ? "s" : ""}`;
}
if (transition.date === "0001-01-01T00:00:00Z") {
return "";
}
return <reactMoment.default>{transition.date}</reactMoment.default>;
};
const renderStorageClass = (objectST: any) => {
const stClass = get(objectST, "transition.storage_class", "");
return stClass;
};
const lifecycleColumns = [
{ label: "ID", elementKey: "id" },
{
label: "Prefix",
elementKey: "prefix",
},
{
label: "Status",
elementKey: "status",
},
{
label: "Expiration",
elementKey: "expiration",
renderFunction: expirationRender,
},
{
label: "Transition",
elementKey: "transition",
renderFunction: transitionRender,
},
{
label: "Storage Class",
elementKey: "storage_class",
renderFunction: renderStorageClass,
renderFullObject: true,
},
];
return (
<Fragment>
{editLifecycleOpen && (
<EditLifecycleConfiguration
open={editLifecycleOpen}
closeModalAndRefresh={closeEditLCAndRefresh}
selectedBucket={bucketName}
lifecycle={{
id: "",
}}
/>
)}
{addLifecycleOpen && (
<AddLifecycleModal
open={addLifecycleOpen}
bucketName={bucketName}
closeModalAndRefresh={closeAddLCAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={(event) => {
// setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setAddLifecycleOpen(true);
}}
>
Add Lifecycle Rule
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={[]}
columns={lifecycleColumns}
isLoading={loadingLifecycle}
records={lifecycleRecords}
entityName="Lifecycle"
customEmptyMessage="There are no Lifecycle rules yet"
idField="id"
/>
</Grid>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
session: state.console.session,
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(BucketLifecyclePanel));

View File

@@ -0,0 +1,280 @@
// 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, useState, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, TextField } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { CreateIcon } from "../../../../icons";
import { setErrorSnackMessage } from "../../../../actions";
import {
actionsTray,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import {
BucketReplication,
BucketReplicationDestination,
BucketReplicationRule,
BucketReplicationRuleDeleteMarker,
HasPermissionResponse,
} from "../types";
import api from "../../../../common/api";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import AddReplicationModal from "./AddReplicationModal";
import DeleteReplicationRule from "./DeleteReplicationRule";
import { AppState } from "../../../../store";
interface IBucketReplicationProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
...searchField,
...actionsTray,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});
const BucketReplicationPanel = ({
classes,
match,
setErrorSnackMessage,
}: IBucketReplicationProps) => {
const [canPutReplication, setCanPutReplication] = useState<boolean>(false);
const [loadingReplication, setLoadingReplication] = useState<boolean>(true);
const [replicationRules, setReplicationRules] = useState<any[]>([]);
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canGetReplication, setCanGetReplication] = useState<boolean>(false);
const [deleteReplicationModal, setDeleteReplicationModal] =
useState<boolean>(false);
const [openSetReplication, setOpenSetReplication] = useState<boolean>(false);
const [selectedRRule, setSelectedRRule] = useState<string>("");
const bucketName = match.params["bucketName"];
useEffect(() => {
if (loadingPerms) {
api
.invoke("POST", `/api/v1/has-permission`, {
actions: [
{
id: "PutReplicationConfiguration",
action: "s3:PutReplicationConfiguration",
bucket_name: bucketName,
},
{
id: "GetReplicationConfiguration",
action: "s3:GetReplicationConfiguration",
bucket_name: bucketName,
},
],
})
.then((res: HasPermissionResponse) => {
setLoadingPerms(false);
if (!res.permissions) {
return;
}
const actions = res.permissions ? res.permissions : [];
let userCanPutReplication = actions.find(
(s) => s.id === "PutReplicationConfiguration"
);
if (userCanPutReplication && userCanPutReplication.can) {
setCanPutReplication(true);
} else {
setCanPutReplication(false);
}
let canGetReplication = actions.find(
(s) => s.id === "GetReplicationConfiguration"
);
if (canGetReplication && canGetReplication.can) {
setCanGetReplication(true);
} else {
setCanGetReplication(false);
}
setLoadingPerms(false);
})
.catch((err: any) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [bucketName, loadingPerms, setErrorSnackMessage]);
useEffect(() => {
if (loadingReplication) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/replication`)
.then((res: BucketReplication) => {
const r = res.rules ? res.rules : [];
setReplicationRules(r);
setLoadingReplication(false);
})
.catch((err: any) => {
setErrorSnackMessage(err);
setLoadingReplication(false);
});
}
}, [loadingReplication, setErrorSnackMessage, bucketName]);
if (!canGetReplication) {
return null;
}
const closeAddReplication = () => {
setOpenReplicationOpen(false);
//loadAllBucketData();
};
const setOpenReplicationOpen = (open = false) => {
setOpenSetReplication(open);
};
const closeReplicationModalDelete = (refresh: boolean) => {
setDeleteReplicationModal(false);
if (refresh) {
setLoadingReplication(true);
}
};
const confirmDeleteReplication = (replication: BucketReplicationRule) => {
setSelectedRRule(replication.id);
setDeleteReplicationModal(true);
};
const ruleDestDisplay = (events: BucketReplicationDestination) => {
return <Fragment>{events.bucket.replace("arn:aws:s3:::", "")}</Fragment>;
};
const ruleDelDisplay = (events: BucketReplicationRuleDeleteMarker) => {
return null;
};
const replicationTableActions: any = [
{
type: "delete",
onClick: confirmDeleteReplication,
disableButtonFunction: () => replicationRules.length <= 1,
},
];
return (
<Fragment>
{openSetReplication && (
<AddReplicationModal
closeModalAndRefresh={closeAddReplication}
open={openSetReplication}
bucketName={bucketName}
/>
)}
{deleteReplicationModal && (
<DeleteReplicationRule
deleteOpen={deleteReplicationModal}
selectedBucket={bucketName}
closeDeleteModalAndRefresh={closeReplicationModalDelete}
ruleToDelete={selectedRRule}
/>
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={(event) => {
// setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
{canPutReplication && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={replicationTableActions}
columns={[
{ label: "ID", elementKey: "id" },
{
label: "Priority",
elementKey: "priority",
},
{
label: "Destination",
elementKey: "destination",
renderFunction: ruleDestDisplay,
},
{
label: "Delete Marker Replication",
elementKey: "delete_marker_replication",
renderFunction: ruleDelDisplay,
},
{ label: "Status", elementKey: "status" },
]}
isLoading={loadingReplication}
records={replicationRules}
entityName="Replication Rules"
idField="id"
/>
</Grid>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
session: state.console.session,
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(BucketReplicationPanel));

View File

@@ -0,0 +1,464 @@
// 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, useState, useEffect } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, CircularProgress } from "@material-ui/core";
import get from "lodash/get";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { AppState } from "../../../../store";
import { setErrorSnackMessage } from "../../../../actions";
import {
BucketEncryptionInfo,
BucketInfo,
BucketObjectLocking,
BucketReplication,
BucketVersioning,
} from "../types";
import { niceBytes } from "../../../../common/utils";
import { BucketList } from "../../Watch/types";
import { hrClass } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import SetAccessPolicy from "./SetAccessPolicy";
import SetRetentionConfig from "./SetRetentionConfig";
import EnableBucketEncryption from "./EnableBucketEncryption";
import EnableVersioningModal from "./EnableVersioningModal";
import UsageIcon from "../../../../icons/UsageIcon";
interface IBucketSummaryProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
paperContainer: {
padding: 15,
paddingLeft: 50,
display: "flex",
},
elementTitle: {
fontWeight: 500,
color: "#777777",
fontSize: 14,
marginTop: -9,
},
consumptionValue: {
color: "#000000",
fontSize: "48px",
fontWeight: "bold",
},
reportedUsage: {
padding: "15px",
},
...hrClass,
});
const BucketSummary = ({
classes,
match,
setErrorSnackMessage,
}: IBucketSummaryProps) => {
const [info, setInfo] = useState<BucketInfo | null>(null);
const [encryptionCfg, setEncryptionCfg] =
useState<BucketEncryptionInfo | null>(null);
const [bucketSize, setBucketSize] = useState<string>("0");
const [hasObjectLocking, setHasObjectLocking] = useState<boolean>(false);
const [accessPolicyScreenOpen, setAccessPolicyScreenOpen] =
useState<boolean>(false);
const [replicationRules, setReplicationRules] = useState<boolean>(false);
const [loadingObjectLocking, setLoadingLocking] = useState<boolean>(true);
const [loadingSize, setLoadingSize] = useState<boolean>(true);
const [loadingBucket, setLoadingBucket] = useState<boolean>(true);
const [loadingEncryption, setLoadingEncryption] = useState<boolean>(true);
const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
const [loadingReplication, setLoadingReplication] = useState<boolean>(true);
const [isVersioned, setIsVersioned] = useState<boolean>(false);
const [encryptionEnabled, setEncryptionEnabled] = useState<boolean>(false);
const [retentionConfigOpen, setRetentionConfigOpen] =
useState<boolean>(false);
const [enableEncryptionScreenOpen, setEnableEncryptionScreenOpen] =
useState<boolean>(false);
const [enableVersioningOpen, setEnableVersioningOpen] =
useState<boolean>(false);
const bucketName = match.params["bucketName"];
let accessPolicy = "n/a";
if (info !== null) {
accessPolicy = info.access;
}
// Effects
useEffect(() => {
if (loadingBucket) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
setLoadingBucket(false);
setInfo(res);
})
.catch((err) => {
setLoadingBucket(false);
setErrorSnackMessage(err);
});
}
}, [loadingBucket, setErrorSnackMessage, bucketName]);
useEffect(() => {
if (loadingEncryption) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/encryption/info`)
.then((res: BucketEncryptionInfo) => {
if (res.algorithm) {
setEncryptionEnabled(true);
setEncryptionCfg(res);
}
setLoadingEncryption(false);
})
.catch((err) => {
if (
err === "The server side encryption configuration was not found"
) {
setEncryptionEnabled(false);
setEncryptionCfg(null);
}
setLoadingEncryption(false);
});
}
}, [loadingEncryption, bucketName]);
useEffect(() => {
if (loadingVersioning) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
setIsVersioned(res.is_versioned);
setLoadingVersioning(false);
})
.catch((err: any) => {
setErrorSnackMessage(err);
setLoadingVersioning(false);
});
}
}, [loadingVersioning, setErrorSnackMessage, bucketName]);
useEffect(() => {
if (loadingVersioning) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
.then((res: BucketObjectLocking) => {
setHasObjectLocking(res.object_locking_enabled);
setLoadingLocking(false);
})
.catch((err: any) => {
setErrorSnackMessage(err);
setLoadingLocking(false);
});
}
}, [
loadingObjectLocking,
setErrorSnackMessage,
bucketName,
loadingVersioning,
]);
useEffect(() => {
if (loadingSize) {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
const resBuckets = get(res, "buckets", []);
const bucketInfo = resBuckets.find(
(bucket) => bucket.name === bucketName
);
const size = get(bucketInfo, "size", "0");
setLoadingSize(false);
setBucketSize(size);
})
.catch((err: any) => {
setLoadingSize(false);
setErrorSnackMessage(err);
});
}
}, [loadingSize, setErrorSnackMessage, bucketName]);
useEffect(() => {
if (loadingReplication) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/replication`)
.then((res: BucketReplication) => {
const r = res.rules ? res.rules : [];
setReplicationRules(r.length > 0);
setLoadingReplication(false);
})
.catch((err: any) => {
setErrorSnackMessage(err);
setLoadingReplication(false);
});
}
}, [loadingReplication, setErrorSnackMessage, bucketName]);
const loadAllBucketData = () => {
setLoadingBucket(true);
setLoadingSize(true);
setLoadingVersioning(true);
setLoadingEncryption(true);
};
const setBucketVersioning = () => {
setEnableVersioningOpen(true);
};
const closeEnableBucketEncryption = () => {
setEnableEncryptionScreenOpen(false);
loadAllBucketData();
};
const closeSetAccessPolicy = () => {
setAccessPolicyScreenOpen(false);
loadAllBucketData();
};
const closeRetentionConfig = () => {
setRetentionConfigOpen(false);
loadAllBucketData();
};
const closeEnableVersioning = (refresh: boolean) => {
setEnableVersioningOpen(false);
if (refresh) {
loadAllBucketData();
}
};
return (
<Fragment>
{enableEncryptionScreenOpen && (
<EnableBucketEncryption
open={enableEncryptionScreenOpen}
selectedBucket={bucketName}
encryptionEnabled={encryptionEnabled}
encryptionCfg={encryptionCfg}
closeModalAndRefresh={closeEnableBucketEncryption}
/>
)}
{accessPolicyScreenOpen && (
<SetAccessPolicy
bucketName={bucketName}
open={accessPolicyScreenOpen}
actualPolicy={accessPolicy}
closeModalAndRefresh={closeSetAccessPolicy}
/>
)}
{retentionConfigOpen && (
<SetRetentionConfig
bucketName={bucketName}
open={retentionConfigOpen}
closeModalAndRefresh={closeRetentionConfig}
/>
)}
{enableVersioningOpen && (
<EnableVersioningModal
closeVersioningModalAndRefresh={closeEnableVersioning}
modalOpen={enableVersioningOpen}
selectedBucket={bucketName}
versioningCurrentState={isVersioned}
/>
)}
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={9}>
<h2>Details</h2>
<hr className={classes.hrClass} />
<table width={"100%"}>
<tbody>
<tr>
<td className={classes.titleCol}>Access Policy:</td>
<td className={classes.capitalizeFirst}>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setAccessPolicyScreenOpen(true);
}}
>
{loadingBucket ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</Button>
</td>
<td className={classes.titleCol}>Encryption:</td>
<td>
{loadingEncryption ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setEnableEncryptionScreenOpen(true);
}}
>
{encryptionEnabled ? "Enabled" : "Disabled"}
</Button>
)}
</td>
</tr>
<tr>
<td className={classes.titleCol}>Replication:</td>
<td className={classes.doubleElement}>
<span>{replicationRules ? "Enabled" : "Disabled"}</span>
</td>
{!hasObjectLocking ? (
<React.Fragment>
<td className={classes.titleCol}>Object Locking:</td>
<td>Disabled</td>
</React.Fragment>
) : (
<React.Fragment>
<td colSpan={2}></td>
</React.Fragment>
)}
</tr>
</tbody>
</table>
</Grid>
<Grid item xs={3} className={classes.reportedUsage}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon} xs={2}>
<UsageIcon />
</Grid>
<Grid item xs={10}>
<Typography className={classes.elementTitle}>
Reported Usage
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{niceBytes(bucketSize)}
</Typography>
</Grid>
</Grid>
</Paper>
<br />
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<h2>Versioning</h2>
<hr className={classes.hrClass} />
<table>
<tbody>
<tr>
<td className={classes.titleCol}>Versioning:</td>
<td>
{loadingVersioning ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Fragment>
<Button
color="primary"
className={classes.anchorButton}
onClick={setBucketVersioning}
>
{isVersioned ? "Enabled" : "Disabled"}
</Button>
</Fragment>
)}
</td>
</tr>
</tbody>
</table>
</Grid>
</Grid>
</Paper>
<br />
<br />
{hasObjectLocking && (
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<h2>Object Locking</h2>
<hr className={classes.hrClass} />
<table>
<tbody>
<tr className={classes.gridContainer}>
<td className={classes.titleCol}>Retention:</td>
<td>
{loadingVersioning ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Fragment>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setRetentionConfigOpen(true);
}}
>
Configure
</Button>
</Fragment>
)}
</td>
</tr>
</tbody>
</table>
</Grid>
</Grid>
</Paper>
)}
</Fragment>
);
};
const mapState = (state: AppState) => ({
session: state.console.session,
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(BucketSummary));

View File

@@ -34,6 +34,7 @@ const Buckets = () => {
return (
<Router history={history}>
<Switch>
<Route path="/buckets/:bucketName/*" component={BucketDetails} />
<Route path="/buckets/:bucketName" component={BucketDetails} />
<Route path="/" component={ListBuckets} />
<Route component={NotFoundPage} />

View File

@@ -27,6 +27,7 @@ export const ADD_BUCKET_RETENTION = "ADD_BUCKET_RETENTION";
export const ADD_BUCKET_RETENTION_MODE = "ADD_BUCKET_RETENTION_MODE";
export const ADD_BUCKET_RETENTION_UNIT = "ADD_BUCKET_RETENTION_UNIT";
export const ADD_BUCKET_RETENTION_VALIDITY = "ADD_BUCKET_RETENTION_VALIDITY";
export const BUCKET_DETAILS_SET_TAB = "BUCKET_DETAILS/SET_TAB";
interface AddBucketOpenAction {
type: typeof ADD_BUCKET_OPEN;
@@ -90,6 +91,11 @@ interface AddBucketRetentionValidityAction {
retentionValidity: number;
}
interface SetBucketDetailsTab {
type: typeof BUCKET_DETAILS_SET_TAB;
tab: string;
}
export type BucketActionTypes =
| AddBucketOpenAction
| AddBucketNameAction
@@ -103,7 +109,8 @@ export type BucketActionTypes =
| AddBucketRetentionAction
| AddBucketRetentionModeAction
| AddBucketRetentionUnitAction
| AddBucketRetentionValidityAction;
| AddBucketRetentionValidityAction
| SetBucketDetailsTab;
export function addBucketOpen(open: boolean) {
return {
@@ -193,3 +200,10 @@ export function addBucketRetentionValidity(validity: number) {
retentionValidity: validity,
};
}
export function setBucketDetailsTab(tab: string) {
return {
type: BUCKET_DETAILS_SET_TAB,
tab,
};
}

View File

@@ -29,6 +29,7 @@ import {
ADD_BUCKET_RETENTION_VALIDITY,
BucketActionTypes,
ADD_BUCKET_LOCKING,
BUCKET_DETAILS_SET_TAB,
} from "./actions";
export interface BucketsState {
@@ -44,6 +45,11 @@ export interface BucketsState {
addBucketRetentionMode: string;
addBucketRetentionUnit: string;
addBucketRetentionValidity: number;
bucketDetails: BucketDetailsState;
}
export interface BucketDetailsState {
selectedTab: string;
}
const initialState: BucketsState = {
@@ -59,6 +65,9 @@ const initialState: BucketsState = {
addBucketRetentionMode: "compliance",
addBucketRetentionUnit: "days",
addBucketRetentionValidity: 1,
bucketDetails: {
selectedTab: "summary",
},
};
export function bucketsReducer(
@@ -126,7 +135,14 @@ export function bucketsReducer(
...state,
addBucketRetentionValidity: action.retentionValidity,
};
case BUCKET_DETAILS_SET_TAB:
return {
...state,
bucketDetails: {
...state.bucketDetails,
selectedTab: action.tab,
},
};
case ADD_BUCKET_RESET:
return {
...state,

View File

@@ -230,6 +230,38 @@ const Console = ({
component: Buckets,
path: "/buckets/:bucketName",
},
{
component: Buckets,
path: "/buckets/:bucketName/summary",
},
{
component: Buckets,
path: "/buckets/:bucketName/events",
},
{
component: Buckets,
path: "/buckets/:bucketName/replication",
},
{
component: Buckets,
path: "/buckets/:bucketName/lifecycle",
},
{
component: Buckets,
path: "/buckets/:bucketName/access",
},
{
component: Buckets,
path: "/buckets/:bucketName/access",
},
{
component: Buckets,
path: "/buckets/:bucketName/access/policies",
},
{
component: Buckets,
path: "/buckets/:bucketName/access/users",
},
{
component: ObjectBrowser,
path: "/object-browser",

View File

@@ -9268,10 +9268,10 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
prettier@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
pretty-bytes@^5.3.0:
version "5.5.0"