Compare commits

...

5 Commits

Author SHA1 Message Date
Daniel Valdivia
7e4d34958e Release v0.8.2 (#917)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-08-02 11:40:02 -07:00
adfost
9d6ee7f9b0 Changing error for too few nodes (#899)
* changing error

* change variable name

Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
Co-authored-by: Adam Stafford <adamstafford@Adams-MacBook-Pro.local>
Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-08-02 11:33:15 -07:00
Daniel Valdivia
aa16e75b39 Update Tenant Details and Fix Warnings (#915)
* Update Tenant Details and Fix Warnings

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* Storage

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-08-02 10:50:41 -07:00
Alex
283a00bde2 Added custom metadata to object details (#914)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2021-08-02 10:20:29 -07:00
Daniel Valdivia
0c78359832 Tabs to Lists for Configurations, Policy (#913)
* Tabs to Lists for Configurations, Policy

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* Fix Tests

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* Logs

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-07-30 17:01:55 -07:00
54 changed files with 999 additions and 666 deletions

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.8.1
image: minio/console:v0.8.2
imagePullPolicy: "IfNotPresent"
args:
- server

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.8.1
image: minio/console:v0.8.2
imagePullPolicy: "IfNotPresent"
env:
- name: CONSOLE_OPERATOR_MODE

View File

@@ -55,6 +55,9 @@ type BucketObject struct {
// legal hold status
LegalHoldStatus string `json:"legal_hold_status,omitempty"`
// metadata
Metadata map[string]string `json:"metadata,omitempty"`
// name
Name string `json:"name,omitempty"`
@@ -70,6 +73,9 @@ type BucketObject struct {
// tags
Tags map[string]string `json:"tags,omitempty"`
// user metadata
UserMetadata map[string]string `json:"user_metadata,omitempty"`
// user tags
UserTags map[string]string `json:"user_tags,omitempty"`

View File

@@ -39,7 +39,7 @@ type ListBucketsResponse struct {
// list of resulting buckets
Buckets []*Bucket `json:"buckets"`
// number of buckets accessible to tenant user
// number of buckets accessible to the user
Total int64 `json:"total,omitempty"`
}

View File

@@ -59,7 +59,7 @@ const (
// Image versions
const (
KESImageVersion = "minio/kes:v0.13.4"
ConsoleImageDefaultVersion = "minio/console:v0.8.1"
ConsoleImageDefaultVersion = "minio/console:v0.8.2"
)
// K8s

View File

@@ -38,6 +38,9 @@ var (
errLicenseNotFound = errors.New("license not found")
errAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself")
errAccessDenied = errors.New("access denied")
errTooFewNodes = errors.New("at least 4 nodes are required in cluster")
errTooFewSchedulableNodes = errors.New("at least 4 schedulable nodes are required in cluster")
errFewerThanFourNodes = errors.New("at least 4 nodes are required in request")
)
// prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message
@@ -156,6 +159,18 @@ func prepareError(err ...error) *models.Error {
if errors.Is(err[0], errRemoteTierExists) {
errorMessage = err[0].Error()
}
if errors.Is(err[0], errTooFewNodes) {
errorCode = 507
errorMessage = errTooFewNodes.Error()
}
if errors.Is(err[0], errTooFewSchedulableNodes) {
errorCode = 507
errorMessage = errTooFewSchedulableNodes.Error()
}
if errors.Is(err[0], errFewerThanFourNodes) {
errorCode = 507
errorMessage = errFewerThanFourNodes.Error()
}
}
return &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err[0].Error())}
}

View File

@@ -56,8 +56,8 @@ func registerNodesHandlers(api *operations.OperatorAPI) {
// getMaxAllocatableMemory get max allocatable memory given a desired number of nodes
func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface, numNodes int32) (*models.MaxAllocatableMemResponse, error) {
if numNodes == 0 {
return nil, errors.New("error NumNodes must be greated than 0")
if numNodes < 4 {
return nil, errors.New("error NumNodes must be at least 4")
}
// get all nodes from cluster
@@ -65,6 +65,18 @@ func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface,
if err != nil {
return nil, err
}
if len(nodes.Items) < int(numNodes) {
return nil, errTooFewNodes
}
activeNodes := 0
for i := 0; i < len(nodes.Items); i++ {
if !nodes.Items[i].Spec.Unschedulable {
activeNodes++
}
}
if activeNodes < int(numNodes) {
return nil, errTooFewSchedulableNodes
}
availableMemSizes := []int64{}
OUTER:

View File

@@ -1036,7 +1036,7 @@ func Test_UpdateTenantAction(t *testing.T) {
},
params: operator_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
ConsoleImage: "minio/console:v0.8.1",
ConsoleImage: "minio/console:v0.8.2",
},
},
},

View File

@@ -22,50 +22,43 @@ import (
// endpoints definition
var (
configuration = "/settings"
users = "/users"
usersDetail = "/users/:userName+"
groups = "/groups"
iamPolicies = "/policies"
policiesDetail = "/policies/:policyName"
dashboard = "/dashboard"
metrics = "/metrics"
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"
tenantHop = "/namespaces/:tenantNamespace/tenants/:tenantName/hop"
podsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName"
tenantsDetailSummary = "/namespaces/:tenantNamespace/tenants/:tenantName/summary"
tenantsDetailMetrics = "/namespaces/:tenantNamespace/tenants/:tenantName/metrics"
tenantsDetailPods = "/namespaces/:tenantNamespace/tenants/:tenantName/pods"
tenantsDetailPools = "/namespaces/:tenantNamespace/tenants/:tenantName/pools"
tenantsDetailLicense = "/namespaces/:tenantNamespace/tenants/:tenantName/license"
tenantsDetailSecurity = "/namespaces/:tenantNamespace/tenants/:tenantName/security"
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"
metrics = "/metrics"
profiling = "/profiling"
buckets = "/buckets"
bucketsDetail = "/buckets/*"
serviceAccounts = "/account"
changePassword = "/account/change-password"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
tenantHop = "/namespaces/:tenantNamespace/tenants/:tenantName/hop"
podsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName"
tenantsDetailSummary = "/namespaces/:tenantNamespace/tenants/:tenantName/summary"
tenantsDetailMetrics = "/namespaces/:tenantNamespace/tenants/:tenantName/metrics"
tenantsDetailPods = "/namespaces/:tenantNamespace/tenants/:tenantName/pods"
tenantsDetailPools = "/namespaces/:tenantNamespace/tenants/:tenantName/pools"
tenantsDetailLicense = "/namespaces/:tenantNamespace/tenants/:tenantName/license"
tenantsDetailSecurity = "/namespaces/:tenantNamespace/tenants/:tenantName/security"
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 {
@@ -288,37 +281,30 @@ var displayRules = map[string]func() bool{
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
usersDetail: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
policiesDetail: iamPoliciesActionSet,
dashboard: dashboardActionSet,
metrics: dashboardActionSet,
profiling: profilingActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
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,
configuration: configurationActionSet,
users: usersActionSet,
usersDetail: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
policiesDetail: iamPoliciesActionSet,
dashboard: dashboardActionSet,
metrics: dashboardActionSet,
profiling: profilingActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
changePassword: changePasswordActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
objectBrowserBucket: objectBrowserActionSet,
license: licenseActionSet,
watch: watchActionSet,
heal: healActionSet,
trace: traceActionSet,
logs: logsActionSet,
healthInfo: healthInfoActionSet,
}
// 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: 15,
want: 8,
},
{
name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 31,
want: 24,
},
{
name: "Console User - default endpoints",

View File

@@ -1,25 +1,25 @@
{
"files": {
"main.css": "/static/css/main.8cfac526.chunk.css",
"main.js": "/static/js/main.2d317177.chunk.js",
"main.js.map": "/static/js/main.2d317177.chunk.js.map",
"main.js": "/static/js/main.68952acf.chunk.js",
"main.js.map": "/static/js/main.68952acf.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.43a31377.js",
"runtime-main.js.map": "/static/js/runtime-main.43a31377.js.map",
"static/css/2.60e04a19.chunk.css": "/static/css/2.60e04a19.chunk.css",
"static/js/2.e135a1be.chunk.js": "/static/js/2.e135a1be.chunk.js",
"static/js/2.e135a1be.chunk.js.map": "/static/js/2.e135a1be.chunk.js.map",
"static/js/2.d3eeac57.chunk.js": "/static/js/2.d3eeac57.chunk.js",
"static/js/2.d3eeac57.chunk.js.map": "/static/js/2.d3eeac57.chunk.js.map",
"index.html": "/index.html",
"static/css/2.60e04a19.chunk.css.map": "/static/css/2.60e04a19.chunk.css.map",
"static/css/main.8cfac526.chunk.css.map": "/static/css/main.8cfac526.chunk.css.map",
"static/js/2.e135a1be.chunk.js.LICENSE.txt": "/static/js/2.e135a1be.chunk.js.LICENSE.txt",
"static/js/2.d3eeac57.chunk.js.LICENSE.txt": "/static/js/2.d3eeac57.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.43a31377.js",
"static/css/2.60e04a19.chunk.css",
"static/js/2.e135a1be.chunk.js",
"static/js/2.d3eeac57.chunk.js",
"static/css/main.8cfac526.chunk.css",
"static/js/main.2d317177.chunk.js"
"static/js/main.68952acf.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 href="/styles/root-styles.css" 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.60e04a19.chunk.css" rel="stylesheet"><link href="/static/css/main.8cfac526.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="loader-block"><svg class="loader-svg-container" viewBox="22 22 44 44"><circle class="loader-style MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate" cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6"></circle></svg></div></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.e135a1be.chunk.js"></script><script src="/static/js/main.2d317177.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 href="/styles/root-styles.css" 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.60e04a19.chunk.css" rel="stylesheet"><link href="/static/css/main.8cfac526.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="loader-block"><svg class="loader-svg-container" viewBox="22 22 44 44"><circle class="loader-style MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate" cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6"></circle></svg></div></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.d3eeac57.chunk.js"></script><script src="/static/js/main.68952acf.chunk.js"></script></body></html>

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

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

@@ -119,6 +119,7 @@ const AccessDetails = ({
return (
<Fragment>
<h1 className={classes.sectionTitle}>Access Audit</h1>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {

View File

@@ -282,17 +282,16 @@ const BucketDetails = ({
>
<ListItemText primary="Events" />
</ListItem>
{canGetReplication && (
<ListItem
button
selected={selectedTab === "replication"}
onClick={() => {
changeRoute("replication");
}}
>
<ListItemText primary="Replication" />
</ListItem>
)}
<ListItem
button
disabled={!canGetReplication}
selected={selectedTab === "replication"}
onClick={() => {
changeRoute("replication");
}}
>
<ListItemText primary="Replication" />
</ListItem>
<ListItem
button
@@ -310,7 +309,7 @@ const BucketDetails = ({
changeRoute("access");
}}
>
<ListItemText primary="Audit Access" />
<ListItemText primary="Access Audit" />
</ListItem>
</List>
</Grid>

View File

@@ -121,7 +121,7 @@ const BucketEventsPanel = ({
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>Events</h1>
<h1 className={classes.sectionTitle}>Events</h1>
<Button
variant="contained"
color="primary"

View File

@@ -169,7 +169,7 @@ const BucketLifecyclePanel = ({
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>Lifecycle Rules</h1>
<h1 className={classes.sectionTitle}>Lifecycle Rules</h1>
<Button
variant="contained"
color="primary"

View File

@@ -61,7 +61,6 @@ const BucketReplicationPanel = ({
BucketReplicationRule[]
>([]);
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canGetReplication, setCanGetReplication] = useState<boolean>(false);
const [deleteReplicationModal, setDeleteReplicationModal] =
useState<boolean>(false);
const [openSetReplication, setOpenSetReplication] = useState<boolean>(false);
@@ -102,15 +101,6 @@ const BucketReplicationPanel = ({
} else {
setCanPutReplication(false);
}
let canGetReplication = actions.find(
(s) => s.id === "GetReplicationConfiguration"
);
if (canGetReplication && canGetReplication.can) {
setCanGetReplication(true);
} else {
setCanGetReplication(false);
}
setLoadingPerms(false);
})
@@ -137,10 +127,6 @@ const BucketReplicationPanel = ({
}
}, [loadingReplication, setErrorSnackMessage, bucketName]);
if (!canGetReplication) {
return null;
}
const closeAddReplication = () => {
setOpenReplicationOpen(false);
setLoadingReplication(true);
@@ -199,21 +185,19 @@ const BucketReplicationPanel = ({
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>Replication</h1>
{canPutReplication && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
)}
<h1 className={classes.sectionTitle}>Replication</h1>
<Button
variant="contained"
color="primary"
disabled={!canPutReplication}
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
</Grid>
<Grid item xs={12}>
<br />

View File

@@ -393,7 +393,7 @@ const BucketSummary = ({
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>Summary</h1>
<h1 className={classes.sectionTitle}>Summary</h1>
</Grid>
<Grid item xs={12}>
<br />

View File

@@ -14,13 +14,24 @@
// 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 get from "lodash/get";
import * as reactMoment from "react-moment";
import clsx from "clsx";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { CircularProgress } from "@material-ui/core";
import {
CircularProgress,
Paper,
Table,
TableBody,
TableCell,
TableRow,
Tooltip,
} from "@material-ui/core";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import List from "@material-ui/core/List";
import Grid from "@material-ui/core/Grid";
import Chip from "@material-ui/core/Chip";
import TextField from "@material-ui/core/TextField";
@@ -32,10 +43,12 @@ import CloseIcon from "@material-ui/icons/Close";
import ShareFile from "./ShareFile";
import {
actionsTray,
buttonsStyles,
containerForHeader,
hrClass,
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "./types";
import { FileInfoResponse, IFileInfo } from "./types";
import {
fileDownloadStarted,
fileIsBeingPrepared,
@@ -43,6 +56,7 @@ import {
} from "../../../../ObjectBrowser/actions";
import { Route } from "../../../../ObjectBrowser/reducers";
import { download } from "../utils";
import { TabPanel } from "../../../../../shared/tabs";
import history from "../../../../../../history";
import api from "../../../../../../common/api";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
@@ -140,6 +154,33 @@ const styles = (theme: Theme) =>
marginRight: 0,
},
},
paperContainer: {
padding: 15,
paddingLeft: 50,
display: "flex",
},
elementTitle: {
fontWeight: 500,
color: "#777777",
fontSize: 14,
marginTop: -9,
},
dualCardLeft: {
paddingRight: "5px",
},
dualCardRight: {
paddingLeft: "5px",
},
capitalizeFirst: {
textTransform: "capitalize",
},
titleCol: {
width: "25%",
},
titleItem: {
width: "35%",
},
"@global": {
".progressDetails": {
paddingTop: 3,
@@ -154,6 +195,8 @@ const styles = (theme: Theme) =>
top: 3,
},
},
...hrClass,
...buttonsStyles,
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
@@ -211,6 +254,9 @@ const ObjectDetails = ({
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [filterVersion, setFilterVersion] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [metadataLoad, setMetadataLoad] = useState<boolean>(false);
const [metadata, setMetadata] = useState<any>({});
const [selectedTab, setSelectedTab] = useState<number>(0);
const currentItem = routesList[routesList.length - 1];
const allPathData = currentItem.route.split("/");
@@ -241,6 +287,7 @@ const ObjectDetails = ({
}
setLoadObjectData(false);
setMetadataLoad(true);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
@@ -255,6 +302,28 @@ const ObjectDetails = ({
distributedSetup,
]);
useEffect(() => {
if (metadataLoad) {
const encodedPath = encodeURIComponent(pathInBucket);
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${encodedPath}&with_metadata=true`
)
.then((res: FileInfoResponse) => {
const fileData = res.objects[0];
let metadata = get(fileData, "user_metadata", {});
setMetadata(metadata);
console.log("metadata:", res);
setMetadataLoad(false);
})
.catch((error: ErrorResponseHandler) => {
setMetadataLoad(false);
});
}
}, [bucketName, metadataLoad, pathInBucket]);
let tagKeys: string[] = [];
if (actualInfo.tags) {
@@ -381,7 +450,61 @@ const ObjectDetails = ({
return (
<React.Fragment>
<PageHeader label={"Object Browser"} />
<PageHeader
label={"Object Browser > Details"}
actions={
<Fragment>
<Tooltip title="Share">
<IconButton
color="primary"
aria-label="share"
onClick={() => {
shareObject();
}}
disabled={actualInfo.is_delete_marker}
>
<ShareIcon />
</IconButton>
</Tooltip>
{downloadingFiles.includes(`${bucketName}/${actualInfo.name}`) ? (
<div className="progressDetails">
<CircularProgress
color="primary"
size={17}
variant="indeterminate"
/>
</div>
) : (
<Tooltip title="Download">
<IconButton
color="primary"
aria-label="download"
onClick={() => {
downloadObject(actualInfo);
}}
disabled={actualInfo.is_delete_marker}
>
<DownloadIcon />
</IconButton>
</Tooltip>
)}
<Tooltip title="Delete Object">
<IconButton
color="primary"
aria-label="delete"
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Fragment>
}
/>
{shareFileModalOpen && (
<ShareFile
open={shareFileModalOpen}
@@ -437,222 +560,243 @@ const ObjectDetails = ({
actualInfo={actualInfo}
/>
)}
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.obTitleSection}>
<div>
<BrowserBreadcrumbs />
</div>
</Grid>
<br />
<Grid item xs={12} className={classes.propertiesContainer}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<React.Fragment>
<div className={classes.propertiesItem}>
<div>
<span className={classes.propertiesItemBold}>
Legal Hold:
</span>
<span className={classes.propertiesValue}>
{actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"}
</span>
</div>
<div>
<IconButton
color="primary"
aria-label="legal-hold"
size="small"
className={classes.propertiesIcon}
onClick={() => {
setLegalholdOpen(true);
}}
>
<PencilIcon active={true} />
</IconButton>
</div>
</div>
<div className={classes.propertiesItem}>
<div>
<span className={classes.propertiesItemBold}>
Retention:
</span>
<span className={classes.propertiesValue}>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "Undefined"}
</span>
</div>
<div>
<IconButton
color="primary"
aria-label="retention"
size="small"
className={classes.propertiesIcon}
onClick={() => {
openRetentionModal();
}}
>
<PencilIcon active={true} />
</IconButton>
</div>
</div>
</React.Fragment>
)}
<div className={classes.propertiesItem}>
<div className={classes.propertiesItemBold}>File Actions:</div>
<div className={classes.actionsIconContainer}>
<IconButton
color="primary"
aria-label="share"
size="small"
className={classes.actionsIcon}
onClick={() => {
shareObject();
}}
disabled={actualInfo.is_delete_marker}
>
<ShareIcon />
</IconButton>
</div>
<div className={classes.actionsIconContainer}>
{downloadingFiles.includes(
`${bucketName}/${actualInfo.name}`
) ? (
<div className="progressDetails">
<CircularProgress
color="primary"
size={17}
variant="indeterminate"
/>
</div>
) : (
<IconButton
color="primary"
aria-label="download"
size="small"
className={classes.actionsIcon}
onClick={() => {
downloadObject(actualInfo);
}}
disabled={actualInfo.is_delete_marker}
>
<DownloadIcon />
</IconButton>
)}
</div>
<div className={classes.actionsIconContainer}>
<IconButton
color="primary"
aria-label="delete"
size="small"
className={classes.actionsIcon}
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
>
<DeleteIcon />
</IconButton>
</div>
</div>
</Grid>
<Grid item xs={12} className={classes.tagsContainer}>
<div className={classes.tagText}>Tags:</div>
{tagKeys &&
tagKeys.map((tagKey, index) => {
const tag = get(actualInfo, `tags.${tagKey}`, "");
if (tag !== "") {
return (
<Chip
key={`chip-${index}`}
className={classes.tag}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
deleteTag(tagKey, tag);
}}
/>
);
}
return null;
})}
<Chip
className={classes.tag}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
<Grid container className={classes.container}>
<Grid item xs={12} className={classes.obTitleSection}>
<div>
<BrowserBreadcrumbs />
</div>
<hr style={{ border: 0, borderTop: "1px solid #EAEAEA" }} />
</Grid>
<Grid item xs={2}>
<List component="nav" dense={true}>
<ListItem
button
selected={selectedTab === 0}
onClick={() => {
setTagModalOpen(true);
setSelectedTab(0);
}}
/>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<TextField
placeholder={`Search ${objectName}`}
className={clsx(classes.search, classes.searchField)}
id="search-resource"
label=""
onChange={(val) => {
setFilterVersion(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
)}
</Grid>
>
<ListItemText primary="Details" />
</ListItem>
<ListItem
button
selected={selectedTab === 1}
onClick={() => {
setSelectedTab(1);
}}
disabled={
!(actualInfo.version_id && actualInfo.version_id !== "null")
}
>
<ListItemText primary="Versions" />
</ListItem>
</List>
</Grid>
<Grid item xs={10}>
<Grid item xs={12}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "",
width: 20,
renderFullObject: true,
renderFunction: (r) => {
const versOrd = versions.length - versions.indexOf(r);
return `v${versOrd}`;
},
},
{ label: "Version ID", elementKey: "version_id" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Deleted",
width: 60,
contentTextAlign: "center",
renderFullObject: true,
renderFunction: (r) => {
const versOrd = r.is_delete_marker ? "Yes" : "No";
return `${versOrd}`;
},
},
]}
isLoading={false}
entityName="Versions"
idField="version_id"
records={filteredRecords}
/>
)}
<TabPanel index={0} value={selectedTab}>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Details</h1>
</div>
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={10}>
<table width={"100%"}>
<tbody>
<tr>
<td className={classes.titleCol}>Legal Hold:</td>
<td className={classes.capitalizeFirst}>
{actualInfo.version_id &&
actualInfo.version_id !== "null" ? (
<Fragment>
{actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"}
<IconButton
color="primary"
aria-label="legal-hold"
size="small"
className={classes.propertiesIcon}
onClick={() => {
setLegalholdOpen(true);
}}
>
<PencilIcon active={true} />
</IconButton>
</Fragment>
) : (
"Disabled"
)}
</td>
</tr>
<tr>
<td className={classes.titleCol}>Retention:</td>
<td className={classes.capitalizeFirst}>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "Undefined"}
<IconButton
color="primary"
aria-label="retention"
size="small"
className={classes.propertiesIcon}
onClick={() => {
openRetentionModal();
}}
>
<PencilIcon active={true} />
</IconButton>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Tags:</td>
<td>
{tagKeys &&
tagKeys.map((tagKey, index) => {
const tag = get(
actualInfo,
`tags.${tagKey}`,
""
);
if (tag !== "") {
return (
<Chip
key={`chip-${index}`}
className={classes.tag}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
deleteTag(tagKey, tag);
}}
/>
);
}
return null;
})}
<Chip
className={classes.tag}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
onClick={() => {
setTagModalOpen(true);
}}
/>
</td>
</tr>
</tbody>
</table>
</Grid>
</Grid>
</Paper>
<br />
<br />
<Paper className={classes.paperContainer}>
<Grid item xs={12}>
<Grid item xs={12}>
<h2>Object Metadata</h2>
<hr className={classes.hr}></hr>
</Grid>
<Grid item xs={12}>
<Table className={classes.table} aria-label="simple table">
<TableBody>
{Object.keys(metadata).map((element) => {
return (
<TableRow>
<TableCell
component="th"
scope="row"
className={classes.titleItem}
>
{element}
</TableCell>
<TableCell align="right">
{metadata[element]}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Grid>
</Grid>
</Paper>
</TabPanel>
<TabPanel index={1} value={selectedTab}>
<Fragment>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Versions</h1>
</div>
<br />
<Grid item xs={12} className={classes.actionsTray}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<TextField
placeholder={`Search ${objectName}`}
className={clsx(classes.search, classes.searchField)}
id="search-resource"
label=""
onChange={(val) => {
setFilterVersion(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
)}
</Grid>
<Grid item xs={12}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "",
width: 20,
renderFullObject: true,
renderFunction: (r) => {
const versOrd =
versions.length - versions.indexOf(r);
return `v${versOrd}`;
},
},
{ label: "Version ID", elementKey: "version_id" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Deleted",
width: 60,
contentTextAlign: "center",
renderFullObject: true,
renderFunction: (r) => {
const versOrd = r.is_delete_marker ? "Yes" : "No";
return `${versOrd}`;
},
},
]}
isLoading={false}
entityName="Versions"
idField="version_id"
records={filteredRecords}
/>
)}
</Grid>
</Fragment>
</TabPanel>
</Grid>
</Grid>
</Grid>

View File

@@ -25,4 +25,10 @@ export interface IFileInfo {
tags?: object;
version_id: string | null;
is_delete_marker?: boolean;
user_metadata?: object;
}
export interface FileInfoResponse {
objects: IFileInfo[];
total: number;
}

View File

@@ -138,6 +138,10 @@ export const containerForHeader = (bottomSpacing: any) => ({
},
},
},
sectionTitle: {
padding: "0px",
margin: "0px",
},
topSpacer: {
height: "8px",
},

View File

@@ -19,14 +19,15 @@ import PageHeader from "../Common/PageHeader/PageHeader";
import { Grid } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import ConfigurationsList from "./ConfigurationPanels/ConfigurationsList";
import ListNotificationEndpoints from "./NotificationEndpoints/ListNotificationEndpoints";
import ListTiersConfiguration from "./TiersConfiguration/ListTiersConfiguration";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { ISessionResponse } from "../types";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
interface IConfigurationMain {
classes: any;
@@ -55,40 +56,57 @@ const ConfigurationMain = ({
return (
<Fragment>
<PageHeader label="Settings" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="tenant-tabs"
variant="scrollable"
scrollButtons="auto"
>
<Tab label="Configurations" />
<Tab label="Lambda Notifications" />
<Tab label="Tiers" disabled={!distributedSetup} />
</Tabs>
<Grid item xs={12}>
{selectedTab === 0 && (
<Grid item xs={12}>
<ConfigurationsList />
</Grid>
)}
{selectedTab === 1 && (
<Grid item xs={12}>
<ListNotificationEndpoints />
</Grid>
)}
{selectedTab === 2 && distributedSetup && (
<Grid item xs={12}>
<ListTiersConfiguration />
</Grid>
)}
</Grid>
<Grid container className={classes.container}>
<Grid item xs={2}>
<List component="nav" dense={true}>
<ListItem
button
selected={selectedTab === 0}
onClick={() => {
setSelectedTab(0);
}}
>
<ListItemText primary="Configurations" />
</ListItem>
<ListItem
button
selected={selectedTab === 1}
onClick={() => {
setSelectedTab(1);
}}
>
<ListItemText primary="Lambda Notifications" />
</ListItem>
<ListItem
button
selected={selectedTab === 2}
onClick={() => {
setSelectedTab(2);
}}
>
<ListItemText primary="Tiers" />
</ListItem>
</List>
</Grid>
<Grid item xs={10}>
{selectedTab === 0 && (
<Grid item xs={12}>
<h1 className={classes.sectionTitle}>Configurations</h1>
<ConfigurationsList />
</Grid>
)}
{selectedTab === 1 && (
<Grid item xs={12}>
<h1 className={classes.sectionTitle}>Lambda Notifications</h1>
<ListNotificationEndpoints />
</Grid>
)}
{selectedTab === 2 && distributedSetup && (
<Grid item xs={12}>
<h1 className={classes.sectionTitle}>Tiers</h1>
<ListTiersConfiguration />
</Grid>
)}
</Grid>
</Grid>
</Fragment>

View File

@@ -237,39 +237,7 @@ 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",
path: "/buckets/*",
},
{
component: ObjectBrowser,

View File

@@ -249,7 +249,7 @@ const DirectCSIMain = ({
}}
/>
)}
<h1 className={classes.sectionTitle}>Drives</h1>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Drives"

View File

@@ -17,10 +17,8 @@
import React, { Fragment, useState } from "react";
import { connect } from "react-redux";
import PageHeader from "../Common/PageHeader/PageHeader";
import { Grid } from "@material-ui/core";
import { Grid, List, ListItem, ListItemText } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import ErrorLogs from "./ErrorLogs/ErrorLogs";
import LogsSearchMain from "./LogSearch/LogsSearchMain";
@@ -50,39 +48,43 @@ const LogsMain = ({ classes, features }: ILogsMainProps) => {
return (
<Fragment>
<PageHeader label="Logs" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Fragment>
<Grid item xs={12} className={classes.headerLabel}>
All Logs
</Grid>
<Tabs
value={currentTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurrentTab(newValue);
<Grid container className={classes.container}>
<Grid item xs={2}>
<List component="nav" dense={true}>
<ListItem
button
selected={currentTab === 0}
onClick={() => {
setCurrentTab(0);
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
variant="scrollable"
scrollButtons="auto"
>
<Tab label="Error Logs" />
{logSearchEnabled && <Tab label="Logs Search" />}
</Tabs>
<Grid item xs={12}>
{currentTab === 0 && (
<Grid item xs={12}>
<ErrorLogs />
</Grid>
)}
{currentTab === 1 && logSearchEnabled && (
<Grid item xs={12}>
<LogsSearchMain />
</Grid>
)}
</Grid>
</Fragment>
<ListItemText primary="Error Logs" />
</ListItem>
<ListItem
button
selected={currentTab === 1}
disabled={!logSearchEnabled}
onClick={() => {
setCurrentTab(1);
}}
>
<ListItemText primary="Audit Logs" />
</ListItem>
</List>
</Grid>
<Grid item xs={10}>
{currentTab === 0 && (
<Fragment>
<h1 className={classes.sectionTitle}>Error Logs</h1>
<ErrorLogs />
</Fragment>
)}
{currentTab === 1 && logSearchEnabled && (
<Fragment>
<h1 className={classes.sectionTitle}>Audit Logs</h1>
<LogsSearchMain />
</Fragment>
)}
</Grid>
</Grid>
</Fragment>

View File

@@ -18,14 +18,14 @@ import { Policy } from "./types";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
actionsTray,
containerForHeader,
modalBasic,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import { Button, LinearProgress } from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import PageHeader from "../Common/PageHeader/PageHeader";
@@ -36,11 +36,10 @@ import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMi
import history from "../../../history";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import {
actionsTray,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import TextField from "@material-ui/core/TextField";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import List from "@material-ui/core/List";
interface IPolicyDetailsProps {
classes: any;
@@ -300,141 +299,167 @@ const PolicyDetails = ({
</Fragment>
}
/>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="policy-tabs"
>
<Tab label="Details" />
<Tab label="Users" />
<Tab label="Groups" />
</Tabs>
</Grid>
{selectedTab === 0 && (
<Paper className={classes.paperContainer}>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
<Grid container className={classes.container}>
<Grid item xs={2}>
<List component="nav" dense={true}>
<ListItem
button
selected={selectedTab === 0}
onClick={() => {
setSelectedTab(0);
}}
>
<ListItemText primary="Details" />
</ListItem>
<ListItem
button
selected={selectedTab === 1}
onClick={() => {
setSelectedTab(1);
}}
>
<ListItemText primary="Users" />
</ListItem>
<ListItem
button
selected={selectedTab === 2}
onClick={() => {
setSelectedTab(2);
}}
>
<ListItemText primary="Groups" />
</ListItem>
</List>
</Grid>
<Grid item xs={10}>
{selectedTab === 0 && (
<Fragment>
<h1 className={classes.sectionTitle}>Edit Policy</h1>
<Paper className={classes.paperContainer}>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
<CodeMirrorWrapper
value={policyDefinition}
onBeforeChange={(editor, data, value) => {
setPolicyDefinition(value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
{!policy && (
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={() => {
resetForm();
}}
>
Clear
</button>
)}
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading || !validSave}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</Paper>
</Fragment>
)}
{selectedTab === 1 && (
<Fragment>
<h1 className={classes.sectionTitle}>Users</h1>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
<CodeMirrorWrapper
label={`${policy ? "Edit" : "Write"} Policy`}
value={policyDefinition}
onBeforeChange={(editor, data, value) => {
setPolicyDefinition(value);
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Users"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterUsers(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
{!policy && (
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={() => {
resetForm();
}}
>
Clear
</button>
)}
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading || !validSave}
>
Save
</Button>
<Grid item xs={12} className={classes.actionsTray}>
<br />
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<TableWrapper
itemActions={userTableActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingUsers}
records={filteredUsers}
entityName="Users"
idField="name"
/>
</Grid>
</form>
</Paper>
)}
{selectedTab === 1 && (
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Users"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterUsers(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<br />
</Grid>
<TableWrapper
itemActions={userTableActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingUsers}
records={filteredUsers}
entityName="Users"
idField="name"
/>
</Grid>
)}
{selectedTab === 2 && (
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterGroups(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<br />
</Grid>
<TableWrapper
itemActions={[]}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingGroups}
records={filteredGroups}
entityName="Groups"
idField="name"
/>
</Grid>
)}
</Fragment>
)}
{selectedTab === 2 && (
<Fragment>
<h1 className={classes.sectionTitle}>Groups</h1>
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterGroups(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<br />
</Grid>
<TableWrapper
itemActions={[]}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingGroups}
records={filteredGroups}
entityName="Groups"
idField="name"
/>
</Grid>
</Fragment>
)}
</Grid>
</Grid>
</React.Fragment>
);

View File

@@ -16,7 +16,7 @@
import React, { Fragment, useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, Tab, Tabs } from "@material-ui/core";
import { Grid, ListItem, ListItemText, Tab, Tabs } from "@material-ui/core";
import { Route, Router, Switch, Redirect } from "react-router-dom";
import {
actionsTray,
@@ -27,6 +27,7 @@ import history from "../../../history";
import PageHeader from "../Common/PageHeader/PageHeader";
import StoragePVCs from "./StoragePVCs";
import DirectCSIDrives from "../DirectCSI/DirectCSIDrives";
import List from "@material-ui/core/List";
interface IStorageProps {
classes: any;
@@ -41,9 +42,6 @@ const styles = (theme: Theme) =>
color: "#000",
marginTop: 4,
},
tabsContainer: {
marginBottom: 15,
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
@@ -59,27 +57,37 @@ const Storage = ({ classes, match }: IStorageProps) => {
setSelectedTab(index);
}, [match]);
const routeChange = (e: React.ChangeEvent<{}>, newValue: number) => {
const routeChange = (newValue: number) => {
history.push(routes[newValue]);
};
return (
<Fragment>
<PageHeader label={"Storage"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.tabsContainer}>
<Tabs
value={selectedTab}
onChange={routeChange}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
<Grid container className={classes.container}>
<Grid item xs={2}>
<List component="nav" dense={true}>
<ListItem
button
selected={selectedTab === 0}
onClick={() => {
routeChange(0);
}}
>
<Tab label="Volumes" />
<Tab label="Drives" />
</Tabs>
</Grid>
<ListItemText primary="Volumes" />
</ListItem>
<ListItem
button
selected={selectedTab === 1}
onClick={() => {
routeChange(1);
}}
>
<ListItemText primary="Drives" />
</ListItem>
</List>
</Grid>
<Grid item xs={10}>
<Router history={history}>
<Switch>
<Route path={routes[0]} component={StoragePVCs} />

View File

@@ -98,9 +98,10 @@ const StorageVolumes = ({
return (
<Fragment>
<h1 className={classes.sectionTitle}>Volumes</h1>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search PVC"
placeholder="Search Volumes (PVCs)"
className={classes.searchField}
id="search-resource"
label=""

View File

@@ -332,7 +332,7 @@ const Configure = ({
label="Console's Image"
value={consoleImage}
error={validationErrors["consoleImage"] || ""}
placeholder="E.g. minio/console:v0.8.1"
placeholder="E.g. minio/console:v0.8.2"
/>
</Grid>
<Grid item xs={12}>

View File

@@ -100,6 +100,8 @@ const TenantSize = ({
selectedStorageClass,
}: ITenantSizeProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const [errorFlag, setErrorFlag] = useState<boolean>(false);
const [nodeError, setNodeError] = useState<string>("");
const usableInformation = ecParityCalc.storageFactors.find(
(element) => element.erasureCode === ecParity
);
@@ -131,7 +133,6 @@ const TenantSize = ({
clusterSizeFactor
);
const memoSize = setMemoryResource(memSize, clusterSizeBytes, maxMemSize);
updateField("memorySize", memoSize);
}, [maxAllocableMemo, memoryNode, sizeFactor, updateField, volumeSize]);
@@ -146,8 +147,9 @@ const TenantSize = ({
const maxMemory = res.max_memory ? res.max_memory : 0;
updateField("maxAllocableMemo", maxMemory);
})
.catch((err: ErrorResponseHandler) => {
updateField("maxAllocableMemo", 0);
.catch((err: any) => {
setErrorFlag(true);
setNodeError(err.errorMessage);
console.error(err);
});
}
@@ -227,7 +229,6 @@ const TenantSize = ({
useEffect(() => {
const parsedSize = getBytes(volumeSize, sizeFactor, true);
const commonValidation = commonFormValidation([
{
fieldKey: "nodes",
@@ -236,6 +237,13 @@ const TenantSize = ({
customValidation: parseInt(nodes) < 4,
customValidationMessage: "Number of nodes cannot be less than 4",
},
{
fieldKey: "nodes",
required: true,
value: nodes,
customValidation: errorFlag,
customValidationMessage: nodeError,
},
{
fieldKey: "volume_size",
required: true,
@@ -288,6 +296,8 @@ const TenantSize = ({
limitSize,
selectedStorageClass,
isPageValid,
errorFlag,
nodeError
]);
/* End Validation of pages */

View File

@@ -124,6 +124,7 @@ const PodsSummary = ({
/>
)}
<div className={classes.topSpacer} />
<h1 className={classes.sectionTitle}>Pods</h1>
<TableWrapper
columns={[
{ label: "Name", elementKey: "name" },

View File

@@ -104,6 +104,7 @@ const PoolsSummary = ({
/>
)}
<div className={classes.topSpacer} />
<h1 className={classes.sectionTitle}>Pools</h1>
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField

View File

@@ -18,12 +18,11 @@ import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Link, Redirect, Route, Router, Switch } from "react-router-dom";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { IconButton, Menu, MenuItem } from "@material-ui/core";
import { IconButton, Tooltip } from "@material-ui/core";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import RefreshIcon from "@material-ui/icons/Refresh";
import { setErrorSnackMessage } from "../../../../actions";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import {
setTenantDetailsLoad,
setTenantInfo,
@@ -49,6 +48,9 @@ import TenantSecurity from "./TenantSecurity";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { DeleteIcon } from "../../../../icons";
import DeleteTenant from "../ListTenants/DeleteTenant";
import PencilIcon from "../../Common/TableWrapper/TableActionIcons/PencilIcon";
interface ITenantDetailsProps {
classes: any;
@@ -57,8 +59,10 @@ interface ITenantDetailsProps {
loadingTenant: boolean;
currentTab: string;
selectedTenant: string;
tenantInfo: ITenant | null;
selectedNamespace: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
setTenantName: typeof setTenantName;
setTenantInfo: typeof setTenantInfo;
@@ -90,8 +94,10 @@ const TenantDetails = ({
loadingTenant,
currentTab,
selectedTenant,
tenantInfo,
selectedNamespace,
setErrorSnackMessage,
setSnackBarMessage,
setTenantDetailsLoad,
setTenantName,
setTenantInfo,
@@ -102,6 +108,7 @@ const TenantDetails = ({
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
const [anchorEl, setAnchorEl] = React.useState(null);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
useEffect(() => {
if (!loadingTenant) {
@@ -188,6 +195,19 @@ const TenantDetails = ({
);
};
const confirmDeleteTenant = () => {
setDeleteOpen(true);
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
if (reloadData) {
setSnackBarMessage("Tenant Deleted");
history.push(`/tenants`);
}
};
return (
<Fragment>
{yamlScreenOpen && (
@@ -198,6 +218,13 @@ const TenantDetails = ({
namespace={tenantNamespace}
/>
)}
{deleteOpen && tenantInfo !== null && (
<DeleteTenant
deleteOpen={deleteOpen}
selectedTenant={tenantInfo}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader
label={
<Fragment>
@@ -205,38 +232,47 @@ const TenantDetails = ({
Tenants
</Link>
{` > ${match.params["tenantName"]}`}
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={handleTenantMenu}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={editYaml}
>
<MenuItem key="yaml" onClick={editYaml}>
Edit YAML
</MenuItem>
</Menu>
</Fragment>
}
actions={
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
setTenantDetailsLoad(true);
}}
>
<RefreshIcon />
</IconButton>
<Fragment>
<Tooltip title={"Delete"}>
<IconButton
color="primary"
aria-label="Delete"
component="span"
onClick={() => {
confirmDeleteTenant();
}}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Edit YAML"}>
<IconButton
color="primary"
aria-label="Edit YAML"
component="span"
onClick={() => {
editYaml();
}}
>
<PencilIcon active={true} />
</IconButton>
</Tooltip>
<Tooltip title={"Refresh"}>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
setTenantDetailsLoad(true);
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</Fragment>
}
/>
<Grid item xs={12} className={classes.container} />
@@ -347,10 +383,12 @@ const mapState = (state: AppState) => ({
currentTab: state.tenants.tenantDetails.currentTab,
selectedTenant: state.tenants.tenantDetails.currentTenant,
selectedNamespace: state.tenants.tenantDetails.currentNamespace,
tenantInfo: state.tenants.tenantDetails.tenantInfo,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setSnackBarMessage,
setTenantDetailsLoad,
setTenantName,
setTenantInfo,

View File

@@ -98,6 +98,7 @@ const TenantLicense = ({
return (
<Fragment>
<div className={classes.topSpacer} />
<h1 className={classes.sectionTitle}>License</h1>
{loadingTenant ? (
<div className={classes.loaderAlign}>
<CircularProgress />

View File

@@ -59,6 +59,7 @@ const TenantMetrics = ({ classes, match }: ITenantMetrics) => {
return (
<React.Fragment>
<h1 className={classes.sectionTitle}>Metrics</h1>
{loading && (
<div style={{ marginTop: "80px" }}>
<LinearProgress />

View File

@@ -427,6 +427,7 @@ const TenantSecurity = ({
</Paper>
) : (
<Fragment>
<h1 className={classes.sectionTitle}>Security</h1>
<Paper className={classes.paperContainer}>
<Grid item xs={12} className={classes.title}>
<FormSwitchWrapper

View File

@@ -176,6 +176,7 @@ const TenantSummary = ({
/>
)}
<div className={classes.topSpacer} />
<h1 className={classes.sectionTitle}>Summary</h1>
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={8}>

View File

@@ -308,7 +308,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
<List component="nav" dense={true}>
<ListItem
button
selected={curTab == 0}
selected={curTab === 0}
onClick={() => {
setCurTab(0);
}}
@@ -317,7 +317,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
</ListItem>
<ListItem
button
selected={curTab == 1}
selected={curTab === 1}
onClick={() => {
setCurTab(1);
}}
@@ -326,7 +326,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
</ListItem>
<ListItem
button
selected={curTab == 2}
selected={curTab === 2}
onClick={() => {
setCurTab(2);
}}
@@ -339,7 +339,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
<Grid item xs={12}>
<TabPanel index={0} value={curTab}>
<div className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>Groups</h1>
<h1 className={classes.sectionTitle}>Groups</h1>
<Button
variant="contained"
color="primary"
@@ -364,9 +364,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
</TabPanel>
<TabPanel index={1} value={curTab}>
<div className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>
Service Accounts
</h1>
<h1 className={classes.sectionTitle}>Service Accounts</h1>
<Button
variant="contained"
color="primary"
@@ -384,7 +382,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
</TabPanel>
<TabPanel index={2} value={curTab}>
<div className={classes.actionsTray}>
<h1 style={{ padding: "0px", margin: "0px" }}>Policies</h1>
<h1 className={classes.sectionTitle}>Policies</h1>
<Button
variant="contained"
color="primary"

View File

@@ -263,7 +263,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
})
.catch((err) => {
setLoginSending(false);
setError(err.message);
setError({ detailedError: "", errorMessage: err.message });
});
};

View File

@@ -1014,6 +1014,11 @@ func init() {
"type": "boolean",
"name": "with_versions",
"in": "query"
},
{
"type": "boolean",
"name": "with_metadata",
"in": "query"
}
],
"responses": {
@@ -3480,6 +3485,12 @@ func init() {
"legal_hold_status": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"name": {
"type": "string"
},
@@ -3499,6 +3510,12 @@ func init() {
"type": "string"
}
},
"user_metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"user_tags": {
"type": "object",
"additionalProperties": {
@@ -6402,6 +6419,11 @@ func init() {
"type": "boolean",
"name": "with_versions",
"in": "query"
},
{
"type": "boolean",
"name": "with_metadata",
"in": "query"
}
],
"responses": {
@@ -8990,6 +9012,12 @@ func init() {
"legal_hold_status": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"name": {
"type": "string"
},
@@ -9009,6 +9037,12 @@ func init() {
"type": "string"
}
},
"user_metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"user_tags": {
"type": "object",
"additionalProperties": {

View File

@@ -65,6 +65,10 @@ type ListObjectsParams struct {
/*
In: query
*/
WithMetadata *bool
/*
In: query
*/
WithVersions *bool
}
@@ -94,6 +98,11 @@ func (o *ListObjectsParams) BindRequest(r *http.Request, route *middleware.Match
res = append(res, err)
}
qWithMetadata, qhkWithMetadata, _ := qs.GetOK("with_metadata")
if err := o.bindWithMetadata(qWithMetadata, qhkWithMetadata, route.Formats); err != nil {
res = append(res, err)
}
qWithVersions, qhkWithVersions, _ := qs.GetOK("with_versions")
if err := o.bindWithVersions(qWithVersions, qhkWithVersions, route.Formats); err != nil {
res = append(res, err)
@@ -159,6 +168,29 @@ func (o *ListObjectsParams) bindRecursive(rawData []string, hasKey bool, formats
return nil
}
// bindWithMetadata binds and validates parameter WithMetadata from query.
func (o *ListObjectsParams) bindWithMetadata(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("with_metadata", "query", "bool", raw)
}
o.WithMetadata = &value
return nil
}
// bindWithVersions binds and validates parameter WithVersions from query.
func (o *ListObjectsParams) bindWithVersions(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string

View File

@@ -37,6 +37,7 @@ type ListObjectsURL struct {
Prefix *string
Recursive *bool
WithMetadata *bool
WithVersions *bool
_basePath string
@@ -96,6 +97,14 @@ func (o *ListObjectsURL) Build() (*url.URL, error) {
qs.Set("recursive", recursiveQ)
}
var withMetadataQ string
if o.WithMetadata != nil {
withMetadataQ = swag.FormatBool(*o.WithMetadata)
}
if withMetadataQ != "" {
qs.Set("with_metadata", withMetadataQ)
}
var withVersionsQ string
if o.WithVersions != nil {
withVersionsQ = swag.FormatBool(*o.WithVersions)

View File

@@ -145,6 +145,8 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
var prefix string
var recursive bool
var withVersions bool
var withMetadata bool
if params.Prefix != nil {
prefix = *params.Prefix
}
@@ -154,6 +156,9 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
if isErasureBackend() && params.WithVersions != nil {
withVersions = *params.WithVersions
}
if params.WithMetadata != nil {
withMetadata = *params.WithMetadata
}
// bucket request needed to proceed
if params.BucketName == "" {
return nil, prepareError(errBucketNameNotInRequest)
@@ -166,7 +171,7 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
// defining the client to be used
minioClient := minioClient{client: mClient}
objs, err := listBucketObjects(params.HTTPRequest.Context(), minioClient, params.BucketName, prefix, recursive, withVersions)
objs, err := listBucketObjects(params.HTTPRequest.Context(), minioClient, params.BucketName, prefix, recursive, withVersions, withMetadata)
if err != nil {
return nil, prepareError(err)
}
@@ -179,12 +184,13 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
}
// listBucketObjects gets an array of objects in a bucket
func listBucketObjects(ctx context.Context, client MinioClient, bucketName string, prefix string, recursive, withVersions bool) ([]*models.BucketObject, error) {
func listBucketObjects(ctx context.Context, client MinioClient, bucketName string, prefix string, recursive, withVersions bool, withMetadata bool) ([]*models.BucketObject, error) {
var objects []*models.BucketObject
for lsObj := range client.listObjects(ctx, bucketName, minio.ListObjectsOptions{Prefix: prefix, Recursive: recursive, WithVersions: withVersions}) {
for lsObj := range client.listObjects(ctx, bucketName, minio.ListObjectsOptions{Prefix: prefix, Recursive: recursive, WithVersions: withVersions, WithMetadata: withMetadata}) {
if lsObj.Err != nil {
return nil, lsObj.Err
}
obj := &models.BucketObject{
Name: lsObj.Key,
Size: lsObj.Size,
@@ -194,6 +200,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
IsLatest: lsObj.IsLatest,
IsDeleteMarker: lsObj.IsDeleteMarker,
UserTags: lsObj.UserTags,
UserMetadata: lsObj.UserMetadata,
}
// only if single object with or without versions; get legalhold, retention and tags
if !lsObj.IsDeleteMarker && prefix != "" && !strings.HasSuffix(prefix, "/") {

View File

@@ -107,6 +107,7 @@ func Test_listObjects(t *testing.T) {
prefix string
recursive bool
withVersions bool
withMetadata bool
listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
objectLegalHoldFunc func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
objectRetentionFunc func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
@@ -125,6 +126,7 @@ func Test_listObjects(t *testing.T) {
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -201,6 +203,7 @@ func Test_listObjects(t *testing.T) {
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
defer close(objectStatCh)
@@ -235,6 +238,7 @@ func Test_listObjects(t *testing.T) {
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -286,6 +290,7 @@ func Test_listObjects(t *testing.T) {
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -361,6 +366,7 @@ func Test_listObjects(t *testing.T) {
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -408,6 +414,7 @@ func Test_listObjects(t *testing.T) {
prefix: "prefix/folder/",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -475,6 +482,7 @@ func Test_listObjects(t *testing.T) {
prefix: "",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -540,7 +548,7 @@ func Test_listObjects(t *testing.T) {
minioGetObjectLegalHoldMock = tt.args.objectLegalHoldFunc
minioGetObjectRetentionMock = tt.args.objectRetentionFunc
minioGetObjectTaggingMock = tt.args.objectGetTaggingFunc
resp, err := listBucketObjects(ctx, minClient, tt.args.bucketName, tt.args.prefix, tt.args.recursive, tt.args.withVersions)
resp, err := listBucketObjects(ctx, minClient, tt.args.bucketName, tt.args.prefix, tt.args.recursive, tt.args.withVersions, tt.args.withMetadata)
if !reflect.DeepEqual(err, tt.wantError) {
t.Errorf("listBucketObjects() error: %v, wantErr: %v", err, tt.wantError)
return

View File

@@ -329,6 +329,10 @@ paths:
in: query
required: false
type: boolean
- name: with_metadata
in: query
required: false
type: boolean
responses:
200:
description: A successful response.
@@ -2273,6 +2277,14 @@ definitions:
type: object
additionalProperties:
type: string
metadata:
type: object
additionalProperties:
type: string
user_metadata:
type: object
additionalProperties:
type: string
makeBucketRequest:
type: object