Compare commits

...

69 Commits

Author SHA1 Message Date
Minio Trusted
4a02c5848b update to v0.4.0 2020-10-05 12:47:31 -07:00
Lenin Alevski
e16a926ef8 Add support for loading multiple TLS certificates (#304)
- update operator version to latest version
- create tenant endpoint now supports multiple TLS certificates for
  MinIO TLS configuration
- update certificates endpoint now support multiple TLS certificates

Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-05 12:09:34 -07:00
Alex
78f4978a9a Changed navbar & header styles (#311)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-05 11:33:27 -07:00
Cesar N
42d617caf9 Add list objects UI and integrate with listing and delete api (#310) 2020-10-02 17:37:08 -07:00
Daniel Valdivia
28eb8784a9 Set Bucket Quota on Creation (#308)
Introces the capability to set bucket quota on bucket creation and adds the API to set the bucket on it's own
2020-10-01 18:59:20 -07:00
Cesar N
fcf5d5c9f7 Add delete objects api (#303)
Supports single and multiple objects which needs to be defined by recursive flag.
An object to be deleted needs to be defined by a query parameter, path, since it can be
an object or a folder.
2020-10-01 17:00:32 -07:00
Alex
a42f1ff4ee Added buckets-object browser view (#307)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-01 13:21:55 -07:00
Alex
98f897ed5b Added object browser main paths (#302)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-09-30 17:46:07 -07:00
Cesar N
7afd608faa Add list objects api (#291)
This includes the basic information of an object
2020-09-29 14:34:51 -07:00
Alex
8313a62f17 Add support for Remote Buckets and Replication (#287)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-28 10:46:08 -07:00
Alex
459e2bf61c Added file name visualization in file select (#289)
* Added missing validations in add tenant modal

* Added file name visualization in file selector

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-09-28 10:36:31 -07:00
Minio Trusted
858d363e97 update to version v0.3.26 2020-09-23 08:52:43 -07:00
Lenin Alevski
47704189d1 fix kes empty configuration (#286)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-22 20:49:25 -07:00
Daniel Valdivia
b72d424ec9 UI: Tweaks to multiple elements (#284) 2020-09-22 20:31:00 -07:00
Lenin Alevski
86426e95f7 Added Annotations, Labels and NodeSelector fields (#285)
For Console/Encryption objects in the  CreateTenant Api
2020-09-22 15:50:37 -07:00
Daniel Valdivia
e5f7870f5e Parity API (#280) 2020-09-22 11:15:21 -07:00
Lenin Alevski
c0ee739624 IV generation for ChaCha20 poly auth scheme (#283)
Generate 16 bytes IV instead of an IV of 32 bytes (and then use half of it) when using ChaCha20 to
encrypt tokens, this is to prevent tokens to become malleable.
2020-09-22 10:49:34 -07:00
Minio Trusted
1dc99498d9 update v0.3.25 2020-09-21 22:07:11 -07:00
Cesar N
319d96c725 Use operator port variables (#282) 2020-09-21 21:31:30 -07:00
Minio Trusted
6d58290a89 update to v0.3.24 2020-09-17 18:30:56 -07:00
Lenin Alevski
666904f902 fix regression when calculating token using chacha20 (#281) 2020-09-17 18:21:54 -07:00
Cesar N
064533d8aa Set annotations and labels at Tenant level (#279)
on Tenant Creation request api
2020-09-17 06:44:16 -07:00
Lenin Alevski
1768af9026 Fix tenant details screen (#277) 2020-09-16 23:01:28 -07:00
Alex
cb7513e9f0 Replaces create tenant functionality (#278)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-09-16 21:47:38 -07:00
Lenin Alevski
645b45cf35 fix tenant details screen (#276) 2020-09-15 14:00:28 -07:00
Cesar N
9f6d965ba2 Add missing validations on tenant info test (#273)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-10 15:43:43 -07:00
Cesar N
5348400665 Delete secrets created if it fails on tenant creation (#274)
Also a fix on a parity condition has been fixed.
2020-09-09 17:08:34 -07:00
Minio Trusted
812fd5f253 update to v0.3.23 2020-09-08 12:28:44 -07:00
Lenin Alevski
da9b393e1b fix regression on update update cert and encryption config endpoint (#272) 2020-09-08 12:20:38 -07:00
Minio Trusted
aeaa1a23ce update to v0.3.22 2020-09-07 17:14:21 -07:00
Lenin Alevski
a8d403a216 fix bug for tenant image pull credentials (#271)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-06 23:20:27 -07:00
Minio Trusted
7bd898b2c7 update to v0.3.21 2020-09-05 23:50:46 -07:00
Lenin Alevski
dad66db49a Support for adding prometheus annotations on update minio tenant (#269) 2020-09-05 23:48:51 -07:00
Daniel Valdivia
adf3f929a4 Add Tenant Deletion Date to tenant responses (#270) 2020-09-05 23:37:01 -07:00
Lenin Alevski
3b23e877b5 delete unnecessary logs (#268) 2020-09-05 17:39:21 -07:00
Minio Trusted
af4bebb6eb fix go mod tidy 2020-09-04 20:42:30 -07:00
Minio Trusted
8530eb5368 update to v0.3.20 2020-09-04 20:41:46 -07:00
Lenin Alevski
0ba1e76400 centralize errors on a single error function (#266)
prepareError receives an array of errors and return *model.Error object
with a message and error code, we can extend this function to add more
error types/code
2020-09-04 20:32:57 -07:00
Daniel Valdivia
94096ee657 Fix Bug Creating Tenant Pull Secret. (#267) 2020-09-04 17:09:17 -07:00
Minio Trusted
c59387c2b4 update v0.3.19 2020-09-04 11:45:13 -07:00
Lenin Alevski
c5a3eff745 Added endpoint for update tenant certificates (minio/console) (#258) 2020-09-03 10:20:58 -07:00
Cesar N
624891ae1f Replace resources api to return the max allocatable memory (#264) 2020-09-02 17:06:02 -07:00
Minio Trusted
83435e1ab9 update v0.3.18 2020-09-02 12:04:02 -07:00
Lenin Alevski
2b4606e773 fix tls certPool client regression (#263) 2020-08-31 21:40:33 -07:00
Cesar N
30f5943f8a Add api to get cluster nodes' resources (#260) 2020-08-28 21:06:45 -07:00
Cesar N
412ac0a603 Add Tenant Update Zones api (#257)
Since the Tenant's zones is an array, a PUT operation was done where
all zone elements on the Tenant are replaced by the defined ones on the request.
2020-08-26 17:12:59 -07:00
Minio Trusted
b2aa1349f8 update to v0.3.17 2020-08-24 15:27:12 -07:00
Lenin Alevski
8b62aec7fb Added support for prometheus addnotations #293 (#256) 2020-08-24 15:07:36 -07:00
Minio Trusted
83fe33b499 update to v0.3.16 2020-08-20 23:09:02 -07:00
Daniel Valdivia
54d0a1d342 Support for labels at pvc level (#254) 2020-08-20 22:46:07 -07:00
Minio Trusted
c59737a71d update v0.3.15 2020-08-20 21:02:34 -07:00
Lenin Alevski
7c2ba707eb add labels to tenant secrets for easy deletion (#252)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-08-20 19:09:13 -07:00
Cesar N
545a890c45 Delete secrets on tenant deletion (#253) 2020-08-20 18:57:34 -07:00
Minio Trusted
4b42308484 update console update to v0.3.14 2020-08-19 20:36:45 -07:00
Cesar N
5a95fed35b Add option to delete tenant's pvcs on tenant deletion (#251) 2020-08-19 20:34:43 -07:00
Lenin Alevski
f880e3976f encrypt token session using aes-gcm if cpu support it or ChaCha20 (#248)
Harsha's improvement to use binary encoding instead of json encoding
2020-08-18 12:42:13 -07:00
Daniel Valdivia
25fa2f3275 YARN upograde Dependencies (#247) 2020-08-15 21:52:36 -07:00
Minio Trusted
9f005b7537 update version v0.3.13 2020-08-11 22:30:18 -07:00
Daniel Valdivia
1ad6e977f2 Tolerate DL MinIO unreachable (#246) 2020-08-11 22:29:33 -07:00
Minio Trusted
e9a64c5479 update to v0.3.12 2020-08-11 21:15:37 -07:00
Daniel Valdivia
a2e7259ccb Allow to Specify the Tenant Console Image. Support Image Pull Secrets… (#245)
* Allow to Specify the Tenant Console Image. Support Image Pull Secrets by Name.

This PR adds support for `console_image` on create tenant and update tenant so the console image can be set by the caller. This is in case the image used is hosted in a private registry.

Also adds support to specify the Image Pull Secret, if it's not specified, the individual image registry credentials can still be specified.

* Add tests for new fields.
2020-08-11 18:20:43 -07:00
Lenin Alevski
d28e66a353 prepareSTSClientTransport tls function refactor (#244)
- Reading root ca certificates operation will run only once after Console
starts, reduce the chance of panics happening during runtime
- Fixed bug in which tls.config insecureSkipVerification configuration
  could get overrided after variable reasignation
2020-08-11 11:32:44 -07:00
Minio Trusted
e0ff6623bb update to version v0.3.11 2020-08-09 19:39:46 -07:00
Lenin Alevski
3d59e9ac30 fix npe for tls console/minio (#243) 2020-08-09 17:19:39 -07:00
Lenin Alevski
cff712f071 rename SSL to TLS in labels, env variables and normal variables/constants (#242) 2020-08-09 16:08:58 -07:00
Minio Trusted
b8bca9d2fe update version to v0.3.10 2020-08-09 14:48:42 -07:00
Lenin Alevski
a6ccae52d2 Enable user provided certificates for Console (#239)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-08-09 14:47:06 -07:00
Daniel Valdivia
bdfa6dc9bf Support Usage API talk to MinIO over TLS with Insecure (#241)
* Support Usage API talk to MinIO over TLS with Insecure

Right now if MinIO is running  with TLS, and the certificate is not trusted by console, we fail usage requests. We need to leverage the support for insecure connections so we can read Health Checks and Usage information.

* Remove unusd import
2020-08-09 14:36:55 -07:00
Lenin Alevski
6eb5731eb5 Upgrade Minio and MC versions (#240)
- Minio: RELEASE.2020-08-08T04-50-06Z
- Mc: RELEASE.2020-08-08T02-33-58Z
2020-08-08 16:32:30 -07:00
220 changed files with 26196 additions and 8420 deletions

View File

@@ -3,10 +3,10 @@ name: Go
on:
pull_request:
branches:
- master
- master
push:
branches:
- master
- master
jobs:
build:
@@ -14,7 +14,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.13.x, 1.14.x]
go-version: [1.14.x]
os: [ubuntu-latest]
steps:
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}

View File

@@ -76,21 +76,19 @@ func newApp(name string) *cli.App {
findClosestCommands := func(command string) []string {
var closestCommands []string
for _, value := range commandsTree.PrefixMatch(command) {
closestCommands = append(closestCommands, value.(string))
}
closestCommands = append(closestCommands, commandsTree.PrefixMatch(command)...)
sort.Strings(closestCommands)
// Suggest other close commands - allow missed, wrongly added and
// even transposed characters
for _, value := range commandsTree.Walk(commandsTree.Root()) {
if sort.SearchStrings(closestCommands, value.(string)) < len(closestCommands) {
if sort.SearchStrings(closestCommands, value) < len(closestCommands) {
continue
}
// 2 is arbitrary and represents the max
// allowed number of typed errors
if words.DamerauLevenshteinDistance(command, value.(string)) < 2 {
closestCommands = append(closestCommands, value.(string))
if words.DamerauLevenshteinDistance(command, value) < 2 {
closestCommands = append(closestCommands, value)
}
}

View File

@@ -47,12 +47,12 @@ var serverCmd = cli.Command{
},
cli.StringFlag{
Name: "tls-host",
Value: restapi.GetSSLHostname(),
Value: restapi.GetTLSHostname(),
Usage: "HTTPS server hostname",
},
cli.IntFlag{
Name: "tls-port",
Value: restapi.GetSSLPort(),
Value: restapi.GetTLSPort(),
Usage: "HTTPS server port",
},
cli.StringFlag{

15
go.mod
View File

@@ -16,16 +16,17 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/minio/cli v1.22.0
github.com/minio/kes v0.11.0
github.com/minio/mc v0.0.0-20200725183142-90d22b271f60
github.com/minio/minio v0.0.0-20200725154241-abbf6ce6ccf8
github.com/minio/minio-go/v7 v7.0.2
github.com/minio/operator v0.0.0-20200806194125-c2ff646f4af1
github.com/minio/mc v0.0.0-20201001165056-7f2df96e4821
github.com/minio/minio v0.0.0-20200927172404-27d9bd04e544
github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089
github.com/minio/operator v0.0.0-20200930213302-ab2bbdfae96c
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/secure-io/sio-go v0.3.1
github.com/stretchr/testify v1.6.1
github.com/unrolled/secure v1.0.7
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
gopkg.in/yaml.v2 v2.3.0
k8s.io/api v0.18.6
k8s.io/apimachinery v0.18.6

760
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@ rules:
- ""
resources:
- namespaces
- secrets
- pods
- services
- events
@@ -18,6 +17,18 @@ rules:
- create
- list
- patch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- create
- list
- patch
- deletecollection
- delete
- apiGroups:
- "storage.k8s.io"
resources:

View File

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

View File

@@ -8,4 +8,4 @@ resources:
- console-configmap.yaml
- console-service.yaml
- console-deployment.yaml
- minio-operator.yaml
- https://github.com/minio/operator/?ref=v3.0.10

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ rules:
- list
- patch
- update
- deletecollection
- apiGroups:
- ""
resources:
@@ -22,12 +23,21 @@ rules:
- services
- events
- resourcequotas
- nodes
verbs:
- get
- watch
- create
- list
- patch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
verbs:
- deletecollection
- list
- get
- apiGroups:
- "storage.k8s.io"
resources:

View File

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

View File

@@ -8,4 +8,4 @@ resources:
- console-configmap.yaml
- console-service.yaml
- console-deployment.yaml
- minio-operator.yaml
- https://github.com/minio/operator/?ref=v3.0.19

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// AddBucketReplication add bucket replication
//
// swagger:model addBucketReplication
type AddBucketReplication struct {
// arn
Arn string `json:"arn,omitempty"`
// destination bucket
DestinationBucket string `json:"destination_bucket,omitempty"`
}
// Validate validates this add bucket replication
func (m *AddBucketReplication) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *AddBucketReplication) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *AddBucketReplication) UnmarshalBinary(b []byte) error {
var res AddBucketReplication
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

69
models/bucket_object.go Normal file
View File

@@ -0,0 +1,69 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// BucketObject bucket object
//
// swagger:model bucketObject
type BucketObject struct {
// content type
ContentType string `json:"content_type,omitempty"`
// last modified
LastModified string `json:"last_modified,omitempty"`
// name
Name string `json:"name,omitempty"`
// size
Size int64 `json:"size,omitempty"`
}
// Validate validates this bucket object
func (m *BucketObject) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *BucketObject) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketObject) UnmarshalBinary(b []byte) error {
var res BucketObject
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,60 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// BucketReplicationDestination bucket replication destination
//
// swagger:model bucketReplicationDestination
type BucketReplicationDestination struct {
// bucket
Bucket string `json:"bucket,omitempty"`
}
// Validate validates this bucket replication destination
func (m *BucketReplicationDestination) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *BucketReplicationDestination) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketReplicationDestination) UnmarshalBinary(b []byte) error {
var res BucketReplicationDestination
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,97 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// BucketReplicationResponse bucket replication response
//
// swagger:model bucketReplicationResponse
type BucketReplicationResponse struct {
// rules
Rules []*BucketReplicationRule `json:"rules"`
}
// Validate validates this bucket replication response
func (m *BucketReplicationResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateRules(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *BucketReplicationResponse) validateRules(formats strfmt.Registry) error {
if swag.IsZero(m.Rules) { // not required
return nil
}
for i := 0; i < len(m.Rules); i++ {
if swag.IsZero(m.Rules[i]) { // not required
continue
}
if m.Rules[i] != nil {
if err := m.Rules[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("rules" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *BucketReplicationResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketReplicationResponse) UnmarshalBinary(b []byte) error {
var res BucketReplicationResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,173 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// BucketReplicationRule bucket replication rule
//
// swagger:model bucketReplicationRule
type BucketReplicationRule struct {
// delete marker replication
DeleteMarkerReplication *BucketReplicationRuleMarker `json:"delete_marker_replication,omitempty"`
// destination
Destination *BucketReplicationDestination `json:"destination,omitempty"`
// id
ID string `json:"id,omitempty"`
// priority
Priority int32 `json:"priority,omitempty"`
// status
// Enum: [Enabled Disabled]
Status string `json:"status,omitempty"`
}
// Validate validates this bucket replication rule
func (m *BucketReplicationRule) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateDeleteMarkerReplication(formats); err != nil {
res = append(res, err)
}
if err := m.validateDestination(formats); err != nil {
res = append(res, err)
}
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *BucketReplicationRule) validateDeleteMarkerReplication(formats strfmt.Registry) error {
if swag.IsZero(m.DeleteMarkerReplication) { // not required
return nil
}
if m.DeleteMarkerReplication != nil {
if err := m.DeleteMarkerReplication.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("delete_marker_replication")
}
return err
}
}
return nil
}
func (m *BucketReplicationRule) validateDestination(formats strfmt.Registry) error {
if swag.IsZero(m.Destination) { // not required
return nil
}
if m.Destination != nil {
if err := m.Destination.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("destination")
}
return err
}
}
return nil
}
var bucketReplicationRuleTypeStatusPropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["Enabled","Disabled"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
bucketReplicationRuleTypeStatusPropEnum = append(bucketReplicationRuleTypeStatusPropEnum, v)
}
}
const (
// BucketReplicationRuleStatusEnabled captures enum value "Enabled"
BucketReplicationRuleStatusEnabled string = "Enabled"
// BucketReplicationRuleStatusDisabled captures enum value "Disabled"
BucketReplicationRuleStatusDisabled string = "Disabled"
)
// prop value enum
func (m *BucketReplicationRule) validateStatusEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, bucketReplicationRuleTypeStatusPropEnum, true); err != nil {
return err
}
return nil
}
func (m *BucketReplicationRule) validateStatus(formats strfmt.Registry) error {
if swag.IsZero(m.Status) { // not required
return nil
}
// value enum
if err := m.validateStatusEnum("status", "body", m.Status); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *BucketReplicationRule) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketReplicationRule) UnmarshalBinary(b []byte) error {
var res BucketReplicationRule
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,117 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// BucketReplicationRuleMarker bucket replication rule marker
//
// swagger:model bucketReplicationRuleMarker
type BucketReplicationRuleMarker struct {
// status
// Enum: [Enabled Disabled]
Status string `json:"status,omitempty"`
}
// Validate validates this bucket replication rule marker
func (m *BucketReplicationRuleMarker) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
var bucketReplicationRuleMarkerTypeStatusPropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["Enabled","Disabled"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
bucketReplicationRuleMarkerTypeStatusPropEnum = append(bucketReplicationRuleMarkerTypeStatusPropEnum, v)
}
}
const (
// BucketReplicationRuleMarkerStatusEnabled captures enum value "Enabled"
BucketReplicationRuleMarkerStatusEnabled string = "Enabled"
// BucketReplicationRuleMarkerStatusDisabled captures enum value "Disabled"
BucketReplicationRuleMarkerStatusDisabled string = "Disabled"
)
// prop value enum
func (m *BucketReplicationRuleMarker) validateStatusEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, bucketReplicationRuleMarkerTypeStatusPropEnum, true); err != nil {
return err
}
return nil
}
func (m *BucketReplicationRuleMarker) validateStatus(formats strfmt.Registry) error {
if swag.IsZero(m.Status) { // not required
return nil
}
// value enum
if err := m.validateStatusEnum("status", "body", m.Status); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *BucketReplicationRuleMarker) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketReplicationRuleMarker) UnmarshalBinary(b []byte) error {
var res BucketReplicationRuleMarker
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,60 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// BucketVersioningResponse bucket versioning response
//
// swagger:model bucketVersioningResponse
type BucketVersioningResponse struct {
// is versioned
IsVersioned bool `json:"is_versioned,omitempty"`
}
// Validate validates this bucket versioning response
func (m *BucketVersioningResponse) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *BucketVersioningResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketVersioningResponse) UnmarshalBinary(b []byte) error {
var res BucketVersioningResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,117 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// ConsoleConfiguration console configuration
//
// swagger:model consoleConfiguration
type ConsoleConfiguration struct {
MetadataFields
// image
Image string `json:"image,omitempty"`
}
// UnmarshalJSON unmarshals this object from a JSON structure
func (m *ConsoleConfiguration) UnmarshalJSON(raw []byte) error {
// AO0
var aO0 MetadataFields
if err := swag.ReadJSON(raw, &aO0); err != nil {
return err
}
m.MetadataFields = aO0
// AO1
var dataAO1 struct {
Image string `json:"image,omitempty"`
}
if err := swag.ReadJSON(raw, &dataAO1); err != nil {
return err
}
m.Image = dataAO1.Image
return nil
}
// MarshalJSON marshals this object to a JSON structure
func (m ConsoleConfiguration) MarshalJSON() ([]byte, error) {
_parts := make([][]byte, 0, 2)
aO0, err := swag.WriteJSON(m.MetadataFields)
if err != nil {
return nil, err
}
_parts = append(_parts, aO0)
var dataAO1 struct {
Image string `json:"image,omitempty"`
}
dataAO1.Image = m.Image
jsonDataAO1, errAO1 := swag.WriteJSON(dataAO1)
if errAO1 != nil {
return nil, errAO1
}
_parts = append(_parts, jsonDataAO1)
return swag.ConcatJSON(_parts...), nil
}
// Validate validates this console configuration
func (m *ConsoleConfiguration) Validate(formats strfmt.Registry) error {
var res []error
// validation for a type composition with MetadataFields
if err := m.MetadataFields.Validate(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// MarshalBinary interface implementation
func (m *ConsoleConfiguration) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ConsoleConfiguration) UnmarshalBinary(b []byte) error {
var res ConsoleConfiguration
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,162 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// CreateRemoteBucket create remote bucket
//
// swagger:model createRemoteBucket
type CreateRemoteBucket struct {
// access key
// Required: true
// Min Length: 3
AccessKey *string `json:"accessKey"`
// region
Region string `json:"region,omitempty"`
// secret key
// Required: true
// Min Length: 8
SecretKey *string `json:"secretKey"`
// source bucket
// Required: true
SourceBucket *string `json:"sourceBucket"`
// target bucket
// Required: true
TargetBucket *string `json:"targetBucket"`
// target URL
// Required: true
TargetURL *string `json:"targetURL"`
}
// Validate validates this create remote bucket
func (m *CreateRemoteBucket) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateAccessKey(formats); err != nil {
res = append(res, err)
}
if err := m.validateSecretKey(formats); err != nil {
res = append(res, err)
}
if err := m.validateSourceBucket(formats); err != nil {
res = append(res, err)
}
if err := m.validateTargetBucket(formats); err != nil {
res = append(res, err)
}
if err := m.validateTargetURL(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *CreateRemoteBucket) validateAccessKey(formats strfmt.Registry) error {
if err := validate.Required("accessKey", "body", m.AccessKey); err != nil {
return err
}
if err := validate.MinLength("accessKey", "body", string(*m.AccessKey), 3); err != nil {
return err
}
return nil
}
func (m *CreateRemoteBucket) validateSecretKey(formats strfmt.Registry) error {
if err := validate.Required("secretKey", "body", m.SecretKey); err != nil {
return err
}
if err := validate.MinLength("secretKey", "body", string(*m.SecretKey), 8); err != nil {
return err
}
return nil
}
func (m *CreateRemoteBucket) validateSourceBucket(formats strfmt.Registry) error {
if err := validate.Required("sourceBucket", "body", m.SourceBucket); err != nil {
return err
}
return nil
}
func (m *CreateRemoteBucket) validateTargetBucket(formats strfmt.Registry) error {
if err := validate.Required("targetBucket", "body", m.TargetBucket); err != nil {
return err
}
return nil
}
func (m *CreateRemoteBucket) validateTargetURL(formats strfmt.Registry) error {
if err := validate.Required("targetURL", "body", m.TargetURL); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *CreateRemoteBucket) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *CreateRemoteBucket) UnmarshalBinary(b []byte) error {
var res CreateRemoteBucket
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -42,11 +42,20 @@ type CreateTenantRequest struct {
// annotations
Annotations map[string]string `json:"annotations,omitempty"`
// console
Console *ConsoleConfiguration `json:"console,omitempty"`
// console image
ConsoleImage string `json:"console_image,omitempty"`
// enable console
EnableConsole *bool `json:"enable_console,omitempty"`
// enable ssl
EnableSsl *bool `json:"enable_ssl,omitempty"`
// enable prometheus
EnablePrometheus *bool `json:"enable_prometheus,omitempty"`
// enable tls
EnableTLS *bool `json:"enable_tls,omitempty"`
// encryption
Encryption *EncryptionConfiguration `json:"encryption,omitempty"`
@@ -60,9 +69,15 @@ type CreateTenantRequest struct {
// image
Image string `json:"image,omitempty"`
// image pull secret
ImagePullSecret string `json:"image_pull_secret,omitempty"`
// image registry
ImageRegistry *ImageRegistry `json:"image_registry,omitempty"`
// labels
Labels map[string]string `json:"labels,omitempty"`
// mounth path
MounthPath string `json:"mounth_path,omitempty"`
@@ -93,6 +108,10 @@ type CreateTenantRequest struct {
func (m *CreateTenantRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateConsole(formats); err != nil {
res = append(res, err)
}
if err := m.validateEncryption(formats); err != nil {
res = append(res, err)
}
@@ -127,6 +146,24 @@ func (m *CreateTenantRequest) Validate(formats strfmt.Registry) error {
return nil
}
func (m *CreateTenantRequest) validateConsole(formats strfmt.Registry) error {
if swag.IsZero(m.Console) { // not required
return nil
}
if m.Console != nil {
if err := m.Console.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("console")
}
return err
}
}
return nil
}
func (m *CreateTenantRequest) validateEncryption(formats strfmt.Registry) error {
if swag.IsZero(m.Encryption) { // not required

View File

@@ -0,0 +1,60 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// DeleteTenantRequest delete tenant request
//
// swagger:model deleteTenantRequest
type DeleteTenantRequest struct {
// delete pvcs
DeletePvcs bool `json:"delete_pvcs,omitempty"`
}
// Validate validates this delete tenant request
func (m *DeleteTenantRequest) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *DeleteTenantRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *DeleteTenantRequest) UnmarshalBinary(b []byte) error {
var res DeleteTenantRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -26,19 +26,19 @@ import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// EncryptionConfiguration encryption configuration
//
// swagger:model encryptionConfiguration
type EncryptionConfiguration struct {
MetadataFields
// aws
Aws *AwsConfiguration `json:"aws,omitempty"`
// client
Client *EncryptionConfigurationClient `json:"client,omitempty"`
Client *KeyPairConfiguration `json:"client,omitempty"`
// gemalto
Gemalto *GemaltoConfiguration `json:"gemalto,omitempty"`
@@ -47,16 +47,106 @@ type EncryptionConfiguration struct {
Image string `json:"image,omitempty"`
// server
Server *EncryptionConfigurationServer `json:"server,omitempty"`
Server *KeyPairConfiguration `json:"server,omitempty"`
// vault
Vault *VaultConfiguration `json:"vault,omitempty"`
}
// UnmarshalJSON unmarshals this object from a JSON structure
func (m *EncryptionConfiguration) UnmarshalJSON(raw []byte) error {
// AO0
var aO0 MetadataFields
if err := swag.ReadJSON(raw, &aO0); err != nil {
return err
}
m.MetadataFields = aO0
// AO1
var dataAO1 struct {
Aws *AwsConfiguration `json:"aws,omitempty"`
Client *KeyPairConfiguration `json:"client,omitempty"`
Gemalto *GemaltoConfiguration `json:"gemalto,omitempty"`
Image string `json:"image,omitempty"`
Server *KeyPairConfiguration `json:"server,omitempty"`
Vault *VaultConfiguration `json:"vault,omitempty"`
}
if err := swag.ReadJSON(raw, &dataAO1); err != nil {
return err
}
m.Aws = dataAO1.Aws
m.Client = dataAO1.Client
m.Gemalto = dataAO1.Gemalto
m.Image = dataAO1.Image
m.Server = dataAO1.Server
m.Vault = dataAO1.Vault
return nil
}
// MarshalJSON marshals this object to a JSON structure
func (m EncryptionConfiguration) MarshalJSON() ([]byte, error) {
_parts := make([][]byte, 0, 2)
aO0, err := swag.WriteJSON(m.MetadataFields)
if err != nil {
return nil, err
}
_parts = append(_parts, aO0)
var dataAO1 struct {
Aws *AwsConfiguration `json:"aws,omitempty"`
Client *KeyPairConfiguration `json:"client,omitempty"`
Gemalto *GemaltoConfiguration `json:"gemalto,omitempty"`
Image string `json:"image,omitempty"`
Server *KeyPairConfiguration `json:"server,omitempty"`
Vault *VaultConfiguration `json:"vault,omitempty"`
}
dataAO1.Aws = m.Aws
dataAO1.Client = m.Client
dataAO1.Gemalto = m.Gemalto
dataAO1.Image = m.Image
dataAO1.Server = m.Server
dataAO1.Vault = m.Vault
jsonDataAO1, errAO1 := swag.WriteJSON(dataAO1)
if errAO1 != nil {
return nil, errAO1
}
_parts = append(_parts, jsonDataAO1)
return swag.ConcatJSON(_parts...), nil
}
// Validate validates this encryption configuration
func (m *EncryptionConfiguration) Validate(formats strfmt.Registry) error {
var res []error
// validation for a type composition with MetadataFields
if err := m.MetadataFields.Validate(formats); err != nil {
res = append(res, err)
}
if err := m.validateAws(formats); err != nil {
res = append(res, err)
}
@@ -190,139 +280,3 @@ func (m *EncryptionConfiguration) UnmarshalBinary(b []byte) error {
*m = res
return nil
}
// EncryptionConfigurationClient encryption configuration client
//
// swagger:model EncryptionConfigurationClient
type EncryptionConfigurationClient struct {
// crt
// Required: true
Crt *string `json:"crt"`
// key
// Required: true
Key *string `json:"key"`
}
// Validate validates this encryption configuration client
func (m *EncryptionConfigurationClient) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCrt(formats); err != nil {
res = append(res, err)
}
if err := m.validateKey(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *EncryptionConfigurationClient) validateCrt(formats strfmt.Registry) error {
if err := validate.Required("client"+"."+"crt", "body", m.Crt); err != nil {
return err
}
return nil
}
func (m *EncryptionConfigurationClient) validateKey(formats strfmt.Registry) error {
if err := validate.Required("client"+"."+"key", "body", m.Key); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *EncryptionConfigurationClient) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *EncryptionConfigurationClient) UnmarshalBinary(b []byte) error {
var res EncryptionConfigurationClient
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}
// EncryptionConfigurationServer encryption configuration server
//
// swagger:model EncryptionConfigurationServer
type EncryptionConfigurationServer struct {
// crt
// Required: true
Crt *string `json:"crt"`
// key
// Required: true
Key *string `json:"key"`
}
// Validate validates this encryption configuration server
func (m *EncryptionConfigurationServer) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCrt(formats); err != nil {
res = append(res, err)
}
if err := m.validateKey(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *EncryptionConfigurationServer) validateCrt(formats strfmt.Registry) error {
if err := validate.Required("server"+"."+"crt", "body", m.Crt); err != nil {
return err
}
return nil
}
func (m *EncryptionConfigurationServer) validateKey(formats strfmt.Registry) error {
if err := validate.Required("server"+"."+"key", "body", m.Key); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *EncryptionConfigurationServer) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *EncryptionConfigurationServer) UnmarshalBinary(b []byte) error {
var res EncryptionConfigurationServer
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -35,7 +35,7 @@ import (
type Error struct {
// code
Code int64 `json:"code,omitempty"`
Code int32 `json:"code,omitempty"`
// message
// Required: true

View File

@@ -130,8 +130,8 @@ type IdpConfigurationActiveDirectory struct {
// server insecure
ServerInsecure bool `json:"server_insecure,omitempty"`
// skip ssl verification
SkipSslVerification bool `json:"skip_ssl_verification,omitempty"`
// skip tls verification
SkipTLSVerification bool `json:"skip_tls_verification,omitempty"`
// url
// Required: true

View File

@@ -0,0 +1,98 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// KeyPairConfiguration key pair configuration
//
// swagger:model keyPairConfiguration
type KeyPairConfiguration struct {
// crt
// Required: true
Crt *string `json:"crt"`
// key
// Required: true
Key *string `json:"key"`
}
// Validate validates this key pair configuration
func (m *KeyPairConfiguration) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCrt(formats); err != nil {
res = append(res, err)
}
if err := m.validateKey(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *KeyPairConfiguration) validateCrt(formats strfmt.Registry) error {
if err := validate.Required("crt", "body", m.Crt); err != nil {
return err
}
return nil
}
func (m *KeyPairConfiguration) validateKey(formats strfmt.Registry) error {
if err := validate.Required("key", "body", m.Key); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *KeyPairConfiguration) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *KeyPairConfiguration) UnmarshalBinary(b []byte) error {
var res KeyPairConfiguration
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,100 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// ListObjectsResponse list objects response
//
// swagger:model listObjectsResponse
type ListObjectsResponse struct {
// list of resulting objects
Objects []*BucketObject `json:"objects"`
// number of objects
Total int64 `json:"total,omitempty"`
}
// Validate validates this list objects response
func (m *ListObjectsResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateObjects(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *ListObjectsResponse) validateObjects(formats strfmt.Registry) error {
if swag.IsZero(m.Objects) { // not required
return nil
}
for i := 0; i < len(m.Objects); i++ {
if swag.IsZero(m.Objects[i]) { // not required
continue
}
if m.Objects[i] != nil {
if err := m.Objects[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("objects" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *ListObjectsResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ListObjectsResponse) UnmarshalBinary(b []byte) error {
var res ListObjectsResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,100 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// ListRemoteBucketsResponse list remote buckets response
//
// swagger:model listRemoteBucketsResponse
type ListRemoteBucketsResponse struct {
// list of remote buckets
Buckets []*RemoteBucket `json:"buckets"`
// number of remote buckets accessible to user
Total int64 `json:"total,omitempty"`
}
// Validate validates this list remote buckets response
func (m *ListRemoteBucketsResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateBuckets(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *ListRemoteBucketsResponse) validateBuckets(formats strfmt.Registry) error {
if swag.IsZero(m.Buckets) { // not required
return nil
}
for i := 0; i < len(m.Buckets); i++ {
if swag.IsZero(m.Buckets[i]) { // not required
continue
}
if m.Buckets[i] != nil {
if err := m.Buckets[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("buckets" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *ListRemoteBucketsResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ListRemoteBucketsResponse) UnmarshalBinary(b []byte) error {
var res ListRemoteBucketsResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -37,6 +37,12 @@ type MakeBucketRequest struct {
// name
// Required: true
Name *string `json:"name"`
// quota
Quota *SetBucketQuota `json:"quota,omitempty"`
// versioning
Versioning bool `json:"versioning,omitempty"`
}
// Validate validates this make bucket request
@@ -47,6 +53,10 @@ func (m *MakeBucketRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
if err := m.validateQuota(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -62,6 +72,24 @@ func (m *MakeBucketRequest) validateName(formats strfmt.Registry) error {
return nil
}
func (m *MakeBucketRequest) validateQuota(formats strfmt.Registry) error {
if swag.IsZero(m.Quota) { // not required
return nil
}
if m.Quota != nil {
if err := m.Quota.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("quota")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *MakeBucketRequest) MarshalBinary() ([]byte, error) {
if m == nil {

View File

@@ -0,0 +1,60 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// MaxAllocatableMemResponse max allocatable mem response
//
// swagger:model maxAllocatableMemResponse
type MaxAllocatableMemResponse struct {
// max memory
MaxMemory int64 `json:"max_memory,omitempty"`
}
// Validate validates this max allocatable mem response
func (m *MaxAllocatableMemResponse) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *MaxAllocatableMemResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *MaxAllocatableMemResponse) UnmarshalBinary(b []byte) error {
var res MaxAllocatableMemResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

66
models/metadata_fields.go Normal file
View File

@@ -0,0 +1,66 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// MetadataFields metadata fields
//
// swagger:model metadataFields
type MetadataFields struct {
// annotations
Annotations map[string]string `json:"annotations,omitempty"`
// labels
Labels map[string]string `json:"labels,omitempty"`
// node selector
NodeSelector map[string]string `json:"node_selector,omitempty"`
}
// Validate validates this metadata fields
func (m *MetadataFields) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *MetadataFields) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *MetadataFields) UnmarshalBinary(b []byte) error {
var res MetadataFields
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

37
models/parity_response.go Normal file
View File

@@ -0,0 +1,37 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
)
// ParityResponse parity response
//
// swagger:model parityResponse
type ParityResponse []string
// Validate validates this parity response
func (m ParityResponse) Validate(formats strfmt.Registry) error {
return nil
}

200
models/remote_bucket.go Normal file
View File

@@ -0,0 +1,200 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// RemoteBucket remote bucket
//
// swagger:model remoteBucket
type RemoteBucket struct {
// access key
// Required: true
// Min Length: 3
AccessKey *string `json:"accessKey"`
// remote a r n
// Required: true
RemoteARN *string `json:"remoteARN"`
// secret key
// Min Length: 8
SecretKey string `json:"secretKey,omitempty"`
// service
// Enum: [replication]
Service string `json:"service,omitempty"`
// source bucket
// Required: true
SourceBucket *string `json:"sourceBucket"`
// status
Status string `json:"status,omitempty"`
// target bucket
TargetBucket string `json:"targetBucket,omitempty"`
// target URL
TargetURL string `json:"targetURL,omitempty"`
}
// Validate validates this remote bucket
func (m *RemoteBucket) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateAccessKey(formats); err != nil {
res = append(res, err)
}
if err := m.validateRemoteARN(formats); err != nil {
res = append(res, err)
}
if err := m.validateSecretKey(formats); err != nil {
res = append(res, err)
}
if err := m.validateService(formats); err != nil {
res = append(res, err)
}
if err := m.validateSourceBucket(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *RemoteBucket) validateAccessKey(formats strfmt.Registry) error {
if err := validate.Required("accessKey", "body", m.AccessKey); err != nil {
return err
}
if err := validate.MinLength("accessKey", "body", string(*m.AccessKey), 3); err != nil {
return err
}
return nil
}
func (m *RemoteBucket) validateRemoteARN(formats strfmt.Registry) error {
if err := validate.Required("remoteARN", "body", m.RemoteARN); err != nil {
return err
}
return nil
}
func (m *RemoteBucket) validateSecretKey(formats strfmt.Registry) error {
if swag.IsZero(m.SecretKey) { // not required
return nil
}
if err := validate.MinLength("secretKey", "body", string(m.SecretKey), 8); err != nil {
return err
}
return nil
}
var remoteBucketTypeServicePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["replication"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
remoteBucketTypeServicePropEnum = append(remoteBucketTypeServicePropEnum, v)
}
}
const (
// RemoteBucketServiceReplication captures enum value "replication"
RemoteBucketServiceReplication string = "replication"
)
// prop value enum
func (m *RemoteBucket) validateServiceEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, remoteBucketTypeServicePropEnum, true); err != nil {
return err
}
return nil
}
func (m *RemoteBucket) validateService(formats strfmt.Registry) error {
if swag.IsZero(m.Service) { // not required
return nil
}
// value enum
if err := m.validateServiceEnum("service", "body", m.Service); err != nil {
return err
}
return nil
}
func (m *RemoteBucket) validateSourceBucket(formats strfmt.Registry) error {
if err := validate.Required("sourceBucket", "body", m.SourceBucket); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *RemoteBucket) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *RemoteBucket) UnmarshalBinary(b []byte) error {
var res RemoteBucket
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

137
models/set_bucket_quota.go Normal file
View File

@@ -0,0 +1,137 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// SetBucketQuota set bucket quota
//
// swagger:model setBucketQuota
type SetBucketQuota struct {
// amount
Amount int64 `json:"amount,omitempty"`
// enabled
// Required: true
Enabled *bool `json:"enabled"`
// quota type
// Enum: [fifo hard]
QuotaType string `json:"quota_type,omitempty"`
}
// Validate validates this set bucket quota
func (m *SetBucketQuota) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateEnabled(formats); err != nil {
res = append(res, err)
}
if err := m.validateQuotaType(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *SetBucketQuota) validateEnabled(formats strfmt.Registry) error {
if err := validate.Required("enabled", "body", m.Enabled); err != nil {
return err
}
return nil
}
var setBucketQuotaTypeQuotaTypePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["fifo","hard"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
setBucketQuotaTypeQuotaTypePropEnum = append(setBucketQuotaTypeQuotaTypePropEnum, v)
}
}
const (
// SetBucketQuotaQuotaTypeFifo captures enum value "fifo"
SetBucketQuotaQuotaTypeFifo string = "fifo"
// SetBucketQuotaQuotaTypeHard captures enum value "hard"
SetBucketQuotaQuotaTypeHard string = "hard"
)
// prop value enum
func (m *SetBucketQuota) validateQuotaTypeEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, setBucketQuotaTypeQuotaTypePropEnum, true); err != nil {
return err
}
return nil
}
func (m *SetBucketQuota) validateQuotaType(formats strfmt.Registry) error {
if swag.IsZero(m.QuotaType) { // not required
return nil
}
// value enum
if err := m.validateQuotaTypeEnum("quota_type", "body", m.QuotaType); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *SetBucketQuota) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *SetBucketQuota) UnmarshalBinary(b []byte) error {
var res SetBucketQuota
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,60 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// SetBucketVersioning set bucket versioning
//
// swagger:model setBucketVersioning
type SetBucketVersioning struct {
// versioning
Versioning bool `json:"versioning,omitempty"`
}
// Validate validates this set bucket versioning
func (m *SetBucketVersioning) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *SetBucketVersioning) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *SetBucketVersioning) UnmarshalBinary(b []byte) error {
var res SetBucketVersioning
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -35,12 +35,21 @@ import (
// swagger:model tenant
type Tenant struct {
// console image
ConsoleImage string `json:"console_image,omitempty"`
// creation date
CreationDate string `json:"creation_date,omitempty"`
// current state
CurrentState string `json:"currentState,omitempty"`
// deletion date
DeletionDate string `json:"deletion_date,omitempty"`
// enable prometheus
EnablePrometheus bool `json:"enable_prometheus,omitempty"`
// image
Image string `json:"image,omitempty"`

View File

@@ -38,6 +38,9 @@ type TenantList struct {
// current state
CurrentState string `json:"currentState,omitempty"`
// deletion date
DeletionDate string `json:"deletion_date,omitempty"`
// instance count
InstanceCount int64 `json:"instance_count,omitempty"`

View File

@@ -23,10 +23,11 @@ package models
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// TLSConfiguration tls configuration
@@ -34,24 +35,22 @@ import (
// swagger:model tlsConfiguration
type TLSConfiguration struct {
// crt
// Required: true
Crt *string `json:"crt"`
// console
Console *KeyPairConfiguration `json:"console,omitempty"`
// key
// Required: true
Key *string `json:"key"`
// minio
Minio []*KeyPairConfiguration `json:"minio"`
}
// Validate validates this tls configuration
func (m *TLSConfiguration) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCrt(formats); err != nil {
if err := m.validateConsole(formats); err != nil {
res = append(res, err)
}
if err := m.validateKey(formats); err != nil {
if err := m.validateMinio(formats); err != nil {
res = append(res, err)
}
@@ -61,19 +60,44 @@ func (m *TLSConfiguration) Validate(formats strfmt.Registry) error {
return nil
}
func (m *TLSConfiguration) validateCrt(formats strfmt.Registry) error {
func (m *TLSConfiguration) validateConsole(formats strfmt.Registry) error {
if err := validate.Required("crt", "body", m.Crt); err != nil {
return err
if swag.IsZero(m.Console) { // not required
return nil
}
if m.Console != nil {
if err := m.Console.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("console")
}
return err
}
}
return nil
}
func (m *TLSConfiguration) validateKey(formats strfmt.Registry) error {
func (m *TLSConfiguration) validateMinio(formats strfmt.Registry) error {
if swag.IsZero(m.Minio) { // not required
return nil
}
for i := 0; i < len(m.Minio); i++ {
if swag.IsZero(m.Minio[i]) { // not required
continue
}
if m.Minio[i] != nil {
if err := m.Minio[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("minio" + "." + strconv.Itoa(i))
}
return err
}
}
if err := validate.Required("key", "body", m.Key); err != nil {
return err
}
return nil

View File

@@ -34,10 +34,20 @@ import (
// swagger:model updateTenantRequest
type UpdateTenantRequest struct {
// console image
// Pattern: ^((.*?)/(.*?):(.+))$
ConsoleImage string `json:"console_image,omitempty"`
// enable prometheus
EnablePrometheus bool `json:"enable_prometheus,omitempty"`
// image
// Pattern: ^((.*?)/(.*?):(.+))$
Image string `json:"image,omitempty"`
// image pull secret
ImagePullSecret string `json:"image_pull_secret,omitempty"`
// image registry
ImageRegistry *ImageRegistry `json:"image_registry,omitempty"`
}
@@ -46,6 +56,10 @@ type UpdateTenantRequest struct {
func (m *UpdateTenantRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateConsoleImage(formats); err != nil {
res = append(res, err)
}
if err := m.validateImage(formats); err != nil {
res = append(res, err)
}
@@ -60,6 +74,19 @@ func (m *UpdateTenantRequest) Validate(formats strfmt.Registry) error {
return nil
}
func (m *UpdateTenantRequest) validateConsoleImage(formats strfmt.Registry) error {
if swag.IsZero(m.ConsoleImage) { // not required
return nil
}
if err := validate.Pattern("console_image", "body", string(m.ConsoleImage), `^((.*?)/(.*?):(.+))$`); err != nil {
return err
}
return nil
}
func (m *UpdateTenantRequest) validateImage(formats strfmt.Registry) error {
if swag.IsZero(m.Image) { // not required

View File

@@ -207,6 +207,12 @@ func (m *Zone) UnmarshalBinary(b []byte) error {
// swagger:model ZoneVolumeConfiguration
type ZoneVolumeConfiguration struct {
// annotations
Annotations map[string]string `json:"annotations,omitempty"`
// labels
Labels map[string]string `json:"labels,omitempty"`
// size
// Required: true
Size *int64 `json:"size"`

View File

@@ -0,0 +1,99 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// ZoneUpdateRequest zone update request
//
// swagger:model zoneUpdateRequest
type ZoneUpdateRequest struct {
// zones
// Required: true
Zones []*Zone `json:"zones"`
}
// Validate validates this zone update request
func (m *ZoneUpdateRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateZones(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *ZoneUpdateRequest) validateZones(formats strfmt.Registry) error {
if err := validate.Required("zones", "body", m.Zones); err != nil {
return err
}
for i := 0; i < len(m.Zones); i++ {
if swag.IsZero(m.Zones[i]) { // not required
continue
}
if m.Zones[i] != nil {
if err := m.Zones[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("zones" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *ZoneUpdateRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ZoneUpdateRequest) UnmarshalBinary(b []byte) error {
var res ZoneUpdateRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

3
package-lock.json generated
View File

@@ -1,3 +0,0 @@
{
"lockfileVersion": 1
}

View File

@@ -22,22 +22,26 @@ import (
// endpoints definition
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/tenants/:tenantName"
heal = "/heal"
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
heal = "/heal"
remoteBuckets = "/remote-buckets"
replication = "/replication"
objectBrowser = "/object-browser/:bucket?"
mainObjectBrowser = "/object-browser"
)
type ConfigurationActionSet struct {
@@ -208,22 +212,50 @@ var healActionSet = ConfigurationActionSet{
),
}
var remoteBucketsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
var replicationActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
// objectBrowserActionSet no actions needed for this module to work
var objectBrowserActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -50,7 +50,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
args: args{
[]string{"admin:ServerInfo"},
},
want: 2,
want: 4,
},
{
name: "policies endpoint",
@@ -63,7 +63,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 2,
want: 4,
},
{
name: "all admin endpoints",
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 11,
want: 15,
},
{
name: "all s3 endpoints",
@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 4,
want: 6,
},
{
name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 14,
want: 18,
},
{
name: "no endpoints",

View File

@@ -17,14 +17,17 @@
package auth
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strings"
@@ -33,6 +36,9 @@ import (
"github.com/minio/console/models"
"github.com/minio/console/pkg/auth/token"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/secure-io/sio-go/sioutil"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/pbkdf2"
)
@@ -40,6 +46,7 @@ var (
errNoAuthToken = errors.New("session token missing")
errReadingToken = errors.New("session token internal data is malformed")
errClaimsFormat = errors.New("encrypted session token claims not in the right format")
errorGeneric = errors.New("an error has occurred")
)
// derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT
@@ -102,9 +109,10 @@ func NewEncryptedTokenForClient(credentials *credentials.Value, actions []string
// returns a base64 encoded ciphertext
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
ciphertext, err := encrypt(payload)
ciphertext, err := encrypt(payload, []byte{})
if err != nil {
return "", err
log.Println(err)
return "", errorGeneric
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
@@ -116,7 +124,7 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
log.Println(err)
return nil, errClaimsFormat
}
plaintext, err := decrypt(decoded)
plaintext, err := decrypt(decoded, []byte{})
if err != nil {
log.Println(err)
return nil, errClaimsFormat
@@ -136,37 +144,137 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
}, nil
}
// Encrypt a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
func encrypt(plaintext []byte) ([]byte, error) {
block, _ := aes.NewCipher(derivedKey)
gcm, err := cipher.NewGCM(block)
const (
aesGcm = 0x00
c20p1305 = 0x01
)
// Encrypt a blob of data using AEAD scheme, AES-GCM if the executing CPU
// provides AES hardware support, otherwise will use ChaCha20-Poly1305
// with a pbkdf2 derived key, this function should be used to encrypt a session
// or data key provided as plaintext.
//
// The returned ciphertext data consists of:
// AEAD ID | iv | nonce | encrypted data
// 1 16 12 ~ len(data)
func encrypt(plaintext, associatedData []byte) ([]byte, error) {
iv, err := sioutil.Random(16) // 16 bytes IV
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
var algorithm byte
if sioutil.NativeAES() {
algorithm = aesGcm
} else {
algorithm = c20p1305
}
var aead cipher.AEAD
switch algorithm {
case aesGcm:
mac := hmac.New(sha256.New, derivedKey)
mac.Write(iv)
sealingKey := mac.Sum(nil)
var block cipher.Block
block, err = aes.NewCipher(sealingKey)
if err != nil {
return nil, err
}
aead, err = cipher.NewGCM(block)
if err != nil {
return nil, err
}
case c20p1305:
var sealingKey []byte
sealingKey, err = chacha20.HChaCha20(derivedKey, iv) // HChaCha20 expects nonce of 16 bytes
if err != nil {
return nil, err
}
aead, err = chacha20poly1305.New(sealingKey)
if err != nil {
return nil, err
}
}
nonce, err := sioutil.Random(aead.NonceSize())
if err != nil {
return nil, err
}
cipherText := gcm.Seal(nonce, nonce, plaintext, nil)
return cipherText, nil
sealedBytes := aead.Seal(nil, nonce, plaintext, associatedData)
// ciphertext = AEAD ID | iv | nonce | sealed bytes
var buf bytes.Buffer
buf.WriteByte(algorithm)
buf.Write(iv)
buf.Write(nonce)
buf.Write(sealedBytes)
return buf.Bytes(), nil
}
// Decrypts a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
func decrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(derivedKey)
// Decrypts a blob of data using AEAD scheme AES-GCM if the executing CPU
// provides AES hardware support, otherwise will use ChaCha20-Poly1305with
// and a pbkdf2 derived key
func decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
var (
algorithm [1]byte
iv [16]byte
nonce [12]byte // This depends on the AEAD but both used ciphers have the same nonce length.
)
r := bytes.NewReader(ciphertext)
if _, err := io.ReadFull(r, algorithm[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, iv[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, nonce[:]); err != nil {
return nil, err
}
var aead cipher.AEAD
switch algorithm[0] {
case aesGcm:
mac := hmac.New(sha256.New, derivedKey)
mac.Write(iv[:])
sealingKey := mac.Sum(nil)
block, err := aes.NewCipher(sealingKey[:])
if err != nil {
return nil, err
}
aead, err = cipher.NewGCM(block)
if err != nil {
return nil, err
}
case c20p1305:
sealingKey, err := chacha20.HChaCha20(derivedKey, iv[:]) // HChaCha20 expects nonce of 16 bytes
if err != nil {
return nil, err
}
aead, err = chacha20poly1305.New(sealingKey)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("invalid algorithm: %v", algorithm)
}
if len(nonce) != aead.NonceSize() {
return nil, fmt.Errorf("invalid nonce size %d, expected %d", len(nonce), aead.NonceSize())
}
sealedBytes, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, cipherText := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
plaintext, err := aead.Open(nil, nonce[:], sealedBytes, associatedData)
if err != nil {
return nil, err
}
return plaintext, nil
}

View File

@@ -36,12 +36,12 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : NewEncryptedTokenForClient() is generated correctly without errors
function := "NewEncryptedTokenForClient()"
jwt, err := NewEncryptedTokenForClient(creds, []string{""})
if err != nil || jwt == "" {
token, err := NewEncryptedTokenForClient(creds, []string{""})
if err != nil || token == "" {
t.Errorf("Failed on %s:, error occurred: %s", function, err)
}
// saving jwt for future tests
goodToken = jwt
// saving token for future tests
goodToken = token
// Test-2 : NewEncryptedTokenForClient() throws error because of empty credentials
if _, err = NewEncryptedTokenForClient(nil, []string{""}); err != nil {
funcAssert.Equal("provided credentials are empty", err.Error())

219
pkg/utils/parity.go Normal file
View File

@@ -0,0 +1,219 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package utils
import (
"errors"
"fmt"
"sort"
"github.com/minio/minio/pkg/ellipses"
)
// This file implements and supports ellipses pattern for
// `minio server` command line arguments.
// Supported set sizes this is used to find the optimal
// single set size.
var setSizes = []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
// getDivisibleSize - returns a greatest common divisor of
// all the ellipses sizes.
func getDivisibleSize(totalSizes []uint64) (result uint64) {
gcd := func(x, y uint64) uint64 {
for y != 0 {
x, y = y, x%y
}
return x
}
result = totalSizes[0]
for i := 1; i < len(totalSizes); i++ {
result = gcd(result, totalSizes[i])
}
return result
}
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
var isValidSetSize = func(count uint64) bool {
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1])
}
// possibleSetCountsWithSymmetry returns symmetrical setCounts based on the
// input argument patterns, the symmetry calculation is to ensure that
// we also use uniform number of drives common across all ellipses patterns.
func possibleSetCountsWithSymmetry(setCounts []uint64, argPatterns []ellipses.ArgPattern) []uint64 {
var newSetCounts = make(map[uint64]struct{})
for _, ss := range setCounts {
var symmetry bool
for _, argPattern := range argPatterns {
for _, p := range argPattern {
if uint64(len(p.Seq)) > ss {
symmetry = uint64(len(p.Seq))%ss == 0
} else {
symmetry = ss%uint64(len(p.Seq)) == 0
}
}
}
// With no arg patterns, it is expected that user knows
// the right symmetry, so either ellipses patterns are
// provided (recommended) or no ellipses patterns.
if _, ok := newSetCounts[ss]; !ok && (symmetry || argPatterns == nil) {
newSetCounts[ss] = struct{}{}
}
}
setCounts = []uint64{}
for setCount := range newSetCounts {
setCounts = append(setCounts, setCount)
}
// Not necessarily needed but it ensures to the readers
// eyes that we prefer a sorted setCount slice for the
// subsequent function to figure out the right common
// divisor, it avoids loops.
sort.Slice(setCounts, func(i, j int) bool {
return setCounts[i] < setCounts[j]
})
return setCounts
}
func commonSetDriveCount(divisibleSize uint64, setCounts []uint64) (setSize uint64) {
// prefers setCounts to be sorted for optimal behavior.
if divisibleSize < setCounts[len(setCounts)-1] {
return divisibleSize
}
// Figure out largest value of total_drives_in_erasure_set which results
// in least number of total_drives/total_drives_erasure_set ratio.
prevD := divisibleSize / setCounts[0]
for _, cnt := range setCounts {
if divisibleSize%cnt == 0 {
d := divisibleSize / cnt
if d <= prevD {
prevD = d
setSize = cnt
}
}
}
return setSize
}
// getSetIndexes returns list of indexes which provides the set size
// on each index, this function also determines the final set size
// The final set size has the affinity towards choosing smaller
// indexes (total sets)
func getSetIndexes(args []string, totalSizes []uint64, argPatterns []ellipses.ArgPattern) (setIndexes [][]uint64, err error) {
if len(totalSizes) == 0 || len(args) == 0 {
return nil, errors.New("invalid argument")
}
setIndexes = make([][]uint64, len(totalSizes))
for _, totalSize := range totalSizes {
// Check if totalSize has minimum range upto setSize
if totalSize < setSizes[0] {
return nil, fmt.Errorf("incorrect number of endpoints provided %s", args)
}
}
commonSize := getDivisibleSize(totalSizes)
possibleSetCounts := func(setSize uint64) (ss []uint64) {
for _, s := range setSizes {
if setSize%s == 0 {
ss = append(ss, s)
}
}
return ss
}
setCounts := possibleSetCounts(commonSize)
if len(setCounts) == 0 {
err = fmt.Errorf("incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
return nil, err
}
// Returns possible set counts with symmetry.
setCounts = possibleSetCountsWithSymmetry(setCounts, argPatterns)
if len(setCounts) == 0 {
err = fmt.Errorf("no symmetric distribution detected with input endpoints provided %s, disks %d cannot be spread symmetrically by any supported erasure set sizes %d", args, commonSize, setSizes)
return nil, err
}
// Final set size with all the symmetry accounted for.
setSize := commonSetDriveCount(commonSize, setCounts)
// Check whether setSize is with the supported range.
if !isValidSetSize(setSize) {
err = fmt.Errorf("incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
return nil, err
}
for i := range totalSizes {
for j := uint64(0); j < totalSizes[i]/setSize; j++ {
setIndexes[i] = append(setIndexes[i], setSize)
}
}
return setIndexes, nil
}
// Return the total size for each argument patterns.
func getTotalSizes(argPatterns []ellipses.ArgPattern) []uint64 {
var totalSizes []uint64
for _, argPattern := range argPatterns {
var totalSize uint64 = 1
for _, p := range argPattern {
totalSize = totalSize * uint64(len(p.Seq))
}
totalSizes = append(totalSizes, totalSize)
}
return totalSizes
}
// PossibleParityValues returns possible parities for input args,
// parties are calculated in uniform manner for one zone or
// multiple zones, ensuring that parities returned are common
// and applicable across all zones.
func PossibleParityValues(args ...string) ([]string, error) {
setIndexes, err := parseEndpointSet(args...)
if err != nil {
return nil, err
}
maximumParity := setIndexes[0][0] / 2
var parities []string
for maximumParity >= 2 {
parities = append(parities, fmt.Sprintf("EC:%d", maximumParity))
maximumParity--
}
return parities, nil
}
// Parses all arguments and returns an endpointSet which is a collection
// of endpoints following the ellipses pattern, this is what is used
// by the object layer for initializing itself.
func parseEndpointSet(args ...string) (setIndexes [][]uint64, err error) {
var argPatterns = make([]ellipses.ArgPattern, len(args))
for i, arg := range args {
patterns, err := ellipses.FindEllipsesPatterns(arg)
if err != nil {
return nil, err
}
argPatterns[i] = patterns
}
return getSetIndexes(args, getTotalSizes(argPatterns), argPatterns)
}

281
pkg/utils/parity_test.go Normal file
View File

@@ -0,0 +1,281 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// All rights reserved
package utils
import (
"reflect"
"testing"
"github.com/minio/minio/pkg/ellipses"
)
func TestGetDivisibleSize(t *testing.T) {
testCases := []struct {
totalSizes []uint64
result uint64
}{{[]uint64{24, 32, 16}, 8},
{[]uint64{32, 8, 4}, 4},
{[]uint64{8, 8, 8}, 8},
{[]uint64{24}, 24},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
gotGCD := getDivisibleSize(testCase.totalSizes)
if testCase.result != gotGCD {
t.Errorf("Expected %v, got %v", testCase.result, gotGCD)
}
})
}
}
// Test tests calculating set indexes.
func TestGetSetIndexes(t *testing.T) {
testCases := []struct {
args []string
totalSizes []uint64
indexes [][]uint64
success bool
}{
// Invalid inputs.
{
[]string{"data{1...3}"},
[]uint64{3},
nil,
false,
},
{
[]string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"},
[]uint64{2, 4, 8},
nil,
false,
},
{
[]string{"data{1...17}/export{1...52}"},
[]uint64{14144},
nil,
false,
},
// Valid inputs.
{
[]string{"data{1...27}"},
[]uint64{27},
[][]uint64{{9, 9, 9}},
true,
},
{
[]string{"http://host{1...3}/data{1...180}"},
[]uint64{540},
[][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
true,
},
{
[]string{"http://host{1...2}.rack{1...4}/data{1...180}"},
[]uint64{1440},
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
true,
},
{
[]string{"http://host{1...2}/data{1...180}"},
[]uint64{360},
[][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
true,
},
{
[]string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"},
[]uint64{4, 8, 12},
[][]uint64{{4}, {4, 4}, {4, 4, 4}},
true,
},
{
[]string{"data{1...64}"},
[]uint64{64},
[][]uint64{{16, 16, 16, 16}},
true,
},
{
[]string{"data{1...24}"},
[]uint64{24},
[][]uint64{{12, 12}},
true,
},
{
[]string{"data/controller{1...11}/export{1...8}"},
[]uint64{88},
[][]uint64{{11, 11, 11, 11, 11, 11, 11, 11}},
true,
},
{
[]string{"data{1...4}"},
[]uint64{4},
[][]uint64{{4}},
true,
},
{
[]string{"data/controller1/export{1...10}, data/controller2/export{1...10}, data/controller3/export{1...10}"},
[]uint64{10, 10, 10},
[][]uint64{{10}, {10}, {10}},
true,
},
{
[]string{"data{1...16}/export{1...52}"},
[]uint64{832},
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
true,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
var argPatterns = make([]ellipses.ArgPattern, len(testCase.args))
for i, arg := range testCase.args {
patterns, err := ellipses.FindEllipsesPatterns(arg)
if err != nil {
t.Fatalf("Unexpected failure %s", err)
}
argPatterns[i] = patterns
}
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, argPatterns)
if err != nil && testCase.success {
t.Errorf("Expected success but failed instead %s", err)
}
if err == nil && !testCase.success {
t.Errorf("Expected failure but passed instead")
}
if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
}
})
}
}
// Test tests possible parities returned for any input args
func TestPossibleParities(t *testing.T) {
testCases := []struct {
arg string
parities []string
success bool
}{
// Tests invalid inputs.
{
"...",
nil,
false,
},
// No range specified.
{
"{...}",
nil,
false,
},
// Invalid range.
{
"http://minio{2...3}/export/set{1...0}",
nil,
false,
},
// Range cannot be smaller than 4 minimum.
{
"/export{1..2}",
nil,
false,
},
// Unsupported characters.
{
"/export/test{1...2O}",
nil,
false,
},
// Tests valid inputs.
{
"{1...27}",
[]string{"EC:4", "EC:3", "EC:2"},
true,
},
{
"/export/set{1...64}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// Valid input for distributed setup.
{
"http://minio{2...3}/export/set{1...64}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// Supporting some advanced cases.
{
"http://minio{1...64}.mydomain.net/data",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
{
"http://rack{1...4}.mydomain.minio{1...16}/data",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// Supporting kubernetes cases.
{
"http://minio{0...15}.mydomain.net/data{0...1}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// No host regex, just disks.
{
"http://server1/data{1...32}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// No host regex, just disks with two position numerics.
{
"http://server1/data{01...32}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// More than 2 ellipses are supported as well.
{
"http://minio{2...3}/export/set{1...64}/test{1...2}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// More than 1 ellipses per argument for standalone setup.
{
"/export{1...10}/disk{1...10}",
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// IPv6 ellipses with hexadecimal expansion
{
"http://[2001:3984:3989::{1...a}]/disk{1...10}",
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// IPv6 ellipses with hexadecimal expansion with 3 position numerics.
{
"http://[2001:3984:3989::{001...00a}]/disk{1...10}",
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
gotPs, err := PossibleParityValues(testCase.arg)
if err != nil && testCase.success {
t.Errorf("Expected success but failed instead %s", err)
}
if err == nil && !testCase.success {
t.Errorf("Expected failure but passed instead")
}
if !reflect.DeepEqual(testCase.parities, gotPs) {
t.Errorf("Expected %v, got %v", testCase.parities, gotPs)
}
})
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "consoleTestUserAddOnly",
"Statement": [
{
"Action": [
"admin:CreateUser"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
]
}
],
"version": "2012-10-17"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
const rewireReactHotLoader = require('react-app-rewire-hot-loader');
/* config-overrides.js */
module.exports = function override(config, env) {
if (env === 'development') {
config.resolve.alias['react-dom'] = '@hot-loader/react-dom';
}
config = rewireReactHotLoader(config, env);
return config;
};

View File

@@ -5,6 +5,7 @@
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.7.4",
"@babel/plugin-transform-react-jsx-development": "^7.9.0",
"@hot-loader/react-dom": "^16.9.0",
"@material-ui/core": "^4.9.12",
"@material-ui/icons": "^4.9.1",
"@types/history": "^4.7.3",
@@ -29,9 +30,13 @@
"moment": "^2.24.0",
"npm": "^6.14.4",
"react": "^16.13.1",
"react-app-rewire-hot-loader": "^2.0.1",
"react-app-rewired": "^2.1.6",
"react-async-hook": "^3.6.1",
"react-chartjs-2": "^2.9.0",
"react-codemirror2": "^7.1.0",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.13.0",
"react-moment": "^0.9.7",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
@@ -42,10 +47,11 @@
"superagent": "^5.1.0",
"typeface-roboto": "^0.0.75",
"typescript": "3.6.4",
"use-debounce": "^5.0.1",
"websocket": "^1.0.31"
},
"scripts": {
"start": "PORT=5000 react-scripts start",
"start": "PORT=5000 react-app-rewired start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"

View File

@@ -25,6 +25,7 @@ import { connect } from "react-redux";
import { AppState } from "./store";
import { userLoggedIn } from "./actions";
import LoginCallback from "./screens/LoginPage/LoginCallback";
import { hot } from "react-hot-loader/root";
const isLoggedIn = () => {
return (
@@ -75,4 +76,4 @@ class Routes extends React.Component<RoutesProps> {
}
}
export default connector(Routes);
export default hot(connector(Routes));

View File

@@ -0,0 +1,333 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright (c) 2020 MinIO, Inc. All rights reserved. */
export interface ITenantsObject {
tenants: ITenant[];
}
export interface ITenant {
creation_date: string;
deletion_date: string;
currentState: string;
image: string;
console_image: string;
instance_count: string;
name: string;
namespace?: string;
total_size: string;
used_size: string;
volume_count: string;
volume_size: string;
volumes_per_server?: string;
zone_count: string;
zones?: IZoneModel[];
used_capacity?: string;
endpoint?: string;
storage_class?: string;
enable_prometheus: boolean;
}
export interface IVolumeConfiguration {
size: string;
storage_class_name: string;
labels?: any;
}
export interface ITenantCreator {
name: string;
service_name: string;
enable_console: boolean;
enable_prometheus: boolean;
enable_tls: boolean;
access_key: string;
secret_key: string;
image: string;
console_image: string;
zones: IZoneModel[];
namespace: string;
erasureCodingParity: number;
tls?: ITLSTenantConfiguration;
encryption?: IEncryptionConfiguration;
idp?: IIDPConfiguration;
annotations?: Object;
}
export interface ITenantUpdateObject {
image: string;
image_registry?: IRegistryObject;
}
export interface IRegistryObject {
registry: string;
username: string;
password: string;
}
export interface ITenantUsage {
used: string;
disk_used: string;
}
export interface IAffinityModel {
podAntiAffinity: IPodAntiAffinityModel;
}
export interface IPodAntiAffinityModel {
requiredDuringSchedulingIgnoredDuringExecution: IPodAffinityTerm[];
}
export interface IPodAffinityTerm {
labelSelector: IPodAffinityTermLabelSelector;
topologyKey: string;
}
export interface IPodAffinityTermLabelSelector {
matchExpressions: IMatchExpressionItem[];
}
export interface IMatchExpressionItem {
key: string;
operator: string;
values: string[];
}
export interface ITolerationModel {
effect: string;
key: string;
operator: string;
value?: string;
tolerationSeconds?: ITolerationSeconds;
}
export interface ITolerationSeconds {
seconds: number;
}
export interface IResourceModel {
requests: IResourceRequests;
limits?: IResourceLimits;
}
export interface IResourceRequests {
memory: number;
}
export interface IResourceLimits {
memory: number;
}
export interface ITLSTenantConfiguration {
minio: ITLSConfiguration;
console: ITLSConfiguration;
}
export interface ITLSConfiguration {
crt: string;
key: string;
}
export interface IEncryptionConfiguration {
server: ITLSConfiguration;
client: ITLSConfiguration;
master_key?: string;
gemalto?: IGemaltoConfig;
aws?: IAWSConfig;
vault?: IVaultConfig;
}
export interface IVaultConfig {
endpoint: string;
engine?: string;
namespace?: string;
prefix?: string;
approle: IApproleConfig;
tls: IVaultTLSConfig;
status: IVaultStatusConfig;
}
export interface IGemaltoConfig {
keysecure: IKeysecureConfig;
}
export interface IAWSConfig {
secretsmanager: ISecretsManagerConfig;
}
export interface IApproleConfig {
engine: string;
id: string;
secret: string;
retry: number;
}
export interface IVaultTLSConfig {
key: string;
crt: string;
ca: string;
}
export interface IVaultStatusConfig {
ping: number;
}
export interface IKeysecureConfig {
endpoint: string;
credentials: IGemaltoCredentials;
tls: IGemaltoTLS;
}
export interface IGemaltoCredentials {
token: string;
domain: string;
retry?: number;
}
export interface IGemaltoTLS {
ca: string;
}
export interface ISecretsManagerConfig {
endpoint: string;
region: string;
kmskey?: string;
credentials: IAWSCredentials;
}
export interface IAWSCredentials {
accesskey: string;
secretkey: string;
token?: string;
}
export interface IIDPConfiguration {
oidc?: IOpenIDConfiguration;
active_directory: IActiveDirectoryConfiguration;
}
export interface IOpenIDConfiguration {
url: string;
client_id: string;
secret_id: string;
}
export interface IActiveDirectoryConfiguration {
url: string;
skip_tls_verification: boolean;
server_insecure: boolean;
user_search_filter: string;
group_Search_base_dn: string;
group_search_filter: string;
group_name_attribute: string;
}
export interface IStorageDistribution {
error: number;
nodes: number;
persistentVolumes: number;
disks: number;
pvSize: number;
}
export interface IErasureCodeCalc {
error: number;
maxEC: string;
erasureCodeSet: number;
rawCapacity: string;
storageFactors: IStorageFactors[];
defaultEC: string;
}
export interface IStorageFactors {
erasureCode: string;
storageFactor: number;
maxCapacity: string;
}
export interface ITenantHealthInList {
name: string;
namespace: string;
status?: string;
message?: string;
}
export interface ITenantsListHealthRequest {
tenants: ITenantHealthInList[];
}
export interface IMaxAllocatableMemoryRequest {
num_nodes: number;
}
export interface IMaxAllocatableMemoryResponse {
max_memory: number;
}
export interface IEncryptionUpdateRequest {
encryption: IEncryptionConfiguration;
}
export interface IArchivedTenantsList {
tenants: IArchivedTenant[];
}
export interface IArchivedTenant {
namespace: string;
tenant: string;
number_volumes: number;
capacity: number;
}
export interface IZoneModel {
name?: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
affinity?: IAffinityModel;
tolerations?: ITolerationModel[];
resources?: IResourceModel;
}
export interface IUpdateZone {
zones: IZoneModel[];
}
export interface INode {
name: string;
freeSpace: string;
totalSpace: string;
disks: IDisk[];
}
export interface IStorageType {
freeSpace: string;
totalSpace: string;
storageClasses: string[];
nodes: INode[];
schedulableNodes: INode[];
}
export interface IDisk {
name: string;
freeSpace: string;
totalSpace: string;
}
export interface ICapacity {
value: string;
unit: string;
}

View File

@@ -15,6 +15,10 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import storage from "local-storage-fallback";
import { ICapacity, IStorageType, IZoneModel } from "./types";
const minStReq = 1073741824; // Minimal Space required for MinIO
const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes
export const units = [
"B",
@@ -28,6 +32,8 @@ export const units = [
"YiB",
];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const k8sCalcUnits = ["B", ...k8sUnits];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -90,12 +96,19 @@ export const k8sfactorForDropdown = () => {
};
//getBytes, converts from a value and a unit from units array to bytes
export const getBytes = (value: string, unit: string) => {
export const getBytes = (
value: string,
unit: string,
fork8s: boolean = false
) => {
const vl: number = parseFloat(value);
const powFactor = units.findIndex((element) => element === unit);
if (powFactor == -1) {
return 0;
const unitsTake = fork8s ? k8sCalcUnits : units;
const powFactor = unitsTake.findIndex((element) => element === unit);
if (powFactor === -1) {
return "0";
}
const factor = Math.pow(1024, powFactor);
const total = vl * factor;
@@ -105,6 +118,220 @@ export const getBytes = (value: string, unit: string) => {
//getTotalSize gets the total size of a value & unit
export const getTotalSize = (value: string, unit: string) => {
const bytes = getBytes(value, unit).toString(10);
const bytes = getBytes(value, unit, true).toString();
return niceBytes(bytes);
};
export const setMemoryResource = (
memorySize: number,
capacitySize: string,
maxMemorySize: number
) => {
// value always comes as Gi
const requestedSizeBytes = getBytes(memorySize.toString(10), "Gi", true);
const memReqSize = parseInt(requestedSizeBytes, 10);
if (maxMemorySize === 0) {
return {
error: "There is no memory available for the selected number of nodes",
request: 0,
limit: 0,
};
}
if (maxMemorySize < minMemReq) {
return {
error: "There are not enough memory resources available",
request: 0,
limit: 0,
};
}
if (memReqSize < minMemReq) {
return {
error: "The requested memory size must be greater than 2Gi",
request: 0,
limit: 0,
};
}
if (memReqSize > maxMemorySize) {
return {
error:
"The requested memory is greater than the max available memory for the selected number of nodes",
request: 0,
limit: 0,
};
}
const capSize = parseInt(capacitySize, 10);
let memLimitSize = memReqSize;
// set memory limit based on the capacitySize
// if capacity size is lower than 1TiB we use the limit equal to request
if (capSize >= parseInt(getBytes("1", "Pi", true), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("64", "Gi", true), 10)
);
} else if (capSize >= parseInt(getBytes("100", "Ti"), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("32", "Gi", true), 10)
);
} else if (capSize >= parseInt(getBytes("10", "Ti"), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("16", "Gi", true), 10)
);
} else if (capSize >= parseInt(getBytes("1", "Ti"), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("8", "Gi", true), 10)
);
}
return {
error: "",
request: memReqSize,
limit: memLimitSize,
};
};
export const calculateDistribution = (
capacityToUse: ICapacity,
forcedNodes: number = 0,
limitSize: number = 0
) => {
let numberOfNodes = {};
const requestedSizeBytes = getBytes(
capacityToUse.value,
capacityToUse.unit,
true
);
if (parseInt(requestedSizeBytes, 10) < minStReq) {
return {
error: "The zone size must be greater than 1Gi",
nodes: 0,
persistentVolumes: 0,
disks: 0,
pvSize: 0,
};
}
if (forcedNodes < 4) {
return {
error: "Number of nodes cannot be less than 4",
nodes: 0,
persistentVolumes: 0,
disks: 0,
pvSize: 0,
};
}
numberOfNodes = calculateStorage(requestedSizeBytes, forcedNodes, limitSize);
return numberOfNodes;
};
const calculateStorage = (
requestedBytes: string,
forcedNodes: number,
limitSize: number
) => {
// Size validation
const intReqBytes = parseInt(requestedBytes, 10);
const maxDiskSize = minStReq * 256; // 256 GiB
// We get the distribution
return structureCalc(forcedNodes, intReqBytes, maxDiskSize, limitSize);
};
const structureCalc = (
nodes: number,
desiredCapacity: number,
maxDiskSize: number,
maxClusterSize: number,
disksPerNode: number = 0
) => {
if (
isNaN(nodes) ||
isNaN(desiredCapacity) ||
isNaN(maxDiskSize) ||
isNaN(maxClusterSize)
) {
return {
error: "Some provided data is invalid, please try again.",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
}; // Invalid Data
}
let persistentVolumeSize = 0;
let numberPersistentVolumes = 0;
let volumesPerServer = 0;
if (disksPerNode === 0) {
persistentVolumeSize = Math.floor(
Math.min(desiredCapacity / Math.max(4, nodes), maxDiskSize)
); // pVS = min((desiredCapacity / max(4 | nodes)) | maxDiskSize)
numberPersistentVolumes = desiredCapacity / persistentVolumeSize; // nPV = dC / pVS
volumesPerServer = numberPersistentVolumes / nodes; // vPS = nPV / n
}
if (disksPerNode) {
volumesPerServer = disksPerNode;
numberPersistentVolumes = volumesPerServer * nodes;
persistentVolumeSize = Math.floor(
desiredCapacity / numberPersistentVolumes
);
}
// Volumes are not exact, we force the volumes number & minimize the volume size
if (volumesPerServer % 1 > 0) {
volumesPerServer = Math.ceil(volumesPerServer); // Increment of volumes per server
numberPersistentVolumes = volumesPerServer * nodes; // nPV = vPS * n
persistentVolumeSize = Math.floor(
desiredCapacity / numberPersistentVolumes
); // pVS = dC / nPV
const limitSize = persistentVolumeSize * volumesPerServer * nodes; // lS = pVS * vPS * n
if (limitSize > maxClusterSize) {
return {
error: "We were not able to allocate this server.",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
}; // Cannot allocate this server
}
}
if (persistentVolumeSize < minStReq) {
return {
error:
"Disk Size with this combination would be less than 1Gi, please try another combination",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
}; // Cannot allocate this volume size
}
return {
error: "",
nodes,
persistentVolumes: numberPersistentVolumes,
disks: volumesPerServer,
pvSize: persistentVolumeSize,
};
};
// Zone Name Generator
export const generateZoneName = (zones: IZoneModel[]) => {
const zoneCounter = zones.length;
return `zone-${zoneCounter}`;
};

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.879 23.119"><defs><style>.cls-1{fill:#1b1556;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M10.086,13.4,8.969,14.573a4.2,4.2,0,0,0-3.01-1.279,3.886,3.886,0,0,0-3.884,4.093,3.888,3.888,0,0,0,3.884,4.1A4.4,4.4,0,0,0,9,20.2l1.082,1.186A5.344,5.344,0,0,1,6,23.119a5.512,5.512,0,0,1-5.7-5.732A5.509,5.509,0,0,1,6,11.667,5.328,5.328,0,0,1,10.086,13.4Z"/><path class="cls-1" d="M23.608,17.387a5.637,5.637,0,0,1-5.8,5.732,5.628,5.628,0,0,1-5.791-5.732,5.626,5.626,0,0,1,5.791-5.72A5.635,5.635,0,0,1,23.608,17.387Zm-9.825,0a4.024,4.024,0,1,0,8.046,0,4.024,4.024,0,1,0-8.046,0Z"/><path class="cls-1" d="M36.862,16.073V22.9H35.129V16.643c0-2.093-1.163-3.325-3.174-3.325a3.24,3.24,0,0,0-3.371,3.372V22.9H26.839V11.888H28.56v1.569a4.354,4.354,0,0,1,3.709-1.79A4.261,4.261,0,0,1,36.862,16.073Z"/><path class="cls-1" d="M48.2,14.225a6.872,6.872,0,0,0-3.605-1.035c-1.569,0-2.6.663-2.6,1.732,0,.919.8,1.372,2.244,1.547l1.3.163c2.338.3,3.7,1.267,3.7,3.069,0,2.093-1.884,3.407-4.849,3.407A7.725,7.725,0,0,1,39.791,21.7l.8-1.3a5.8,5.8,0,0,0,3.815,1.2c1.86,0,3.034-.616,3.034-1.778,0-.884-.744-1.419-2.3-1.605l-1.314-.151c-2.477-.3-3.639-1.408-3.639-3.046,0-2.082,1.755-3.338,4.4-3.338a8.067,8.067,0,0,1,4.372,1.2Z"/><path class="cls-1" d="M63.033,17.387a5.8,5.8,0,0,1-11.593,0,5.8,5.8,0,0,1,11.593,0Zm-9.825,0a4.023,4.023,0,1,0,8.045,0,4.023,4.023,0,1,0-8.045,0Z"/><path class="cls-1" d="M68.008,22.9H66.264V6.155h1.744Z"/><path class="cls-1" d="M81.879,17.353a5.606,5.606,0,0,1-.035.65H73.019a3.743,3.743,0,0,0,3.9,3.593A5.1,5.1,0,0,0,80.4,20.213l.931,1.186a6.179,6.179,0,0,1-4.524,1.72A5.394,5.394,0,0,1,71.24,17.4a5.406,5.406,0,0,1,5.465-5.732C79.693,11.667,81.856,14,81.879,17.353ZM73.043,16.6h7.069a3.446,3.446,0,0,0-3.442-3.384A3.59,3.59,0,0,0,73.043,16.6Z"/><rect class="cls-1" x="13.484" y="0.12" width="2.328" height="6.875"/><path class="cls-1" d="M10.662.215,5.936,3.1a.21.21,0,0,1-.219,0L.992.215A.651.651,0,0,0,.654.12H.648A.648.648,0,0,0,0,.768v6.22H2.327V4.028a.233.233,0,0,1,.354-.2l2.648,1.62a.829.829,0,0,0,.853.008l2.8-1.639a.232.232,0,0,1,.35.2V6.988h2.327V.768A.648.648,0,0,0,11.006.12H11A.651.651,0,0,0,10.662.215Z"/><path class="cls-1" d="M27.422.12H25.061V3.25a.233.233,0,0,1-.342.205L18.6.2A.662.662,0,0,0,18.3.12h0a.648.648,0,0,0-.648.648v6.22h2.342V3.863a.233.233,0,0,1,.342-.206L26.47,6.915a.646.646,0,0,0,.3.076h0a.648.648,0,0,0,.648-.648Z"/><path class="cls-1" d="M29.252,7V.12h1.072V7Z"/><path class="cls-1" d="M36.629,7.119c-2.882,0-4.927-1.368-4.927-3.56S33.759,0,36.629,0s4.938,1.367,4.938,3.559S39.547,7.119,36.629,7.119Zm0-6.208c-2.143,0-3.794.936-3.794,2.648s1.651,2.648,3.794,2.648,3.805-.923,3.805-2.648S38.772.911,36.629.911Z"/></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="121.755" height="29.822" viewBox="0 0 121.755 29.822"><defs><style>.a{fill:#fff;}</style></defs><g transform="translate(3016.56 -1037.757)"><path class="a" d="M-3000.913,1053.692l-1.772,1.194a6.088,6.088,0,0,0-5.135-2.652,6.348,6.348,0,0,0-6.522,6.654,6.348,6.348,0,0,0,6.522,6.654,6.031,6.031,0,0,0,5.124-2.64l1.735,1.266a8.126,8.126,0,0,1-6.859,3.411,8.422,8.422,0,0,1-8.74-8.691,8.422,8.422,0,0,1,8.74-8.691A7.963,7.963,0,0,1-3000.913,1053.692Z"/><path class="a" d="M-2980.919,1058.888a8.422,8.422,0,0,1-8.74,8.691,8.422,8.422,0,0,1-8.739-8.691,8.421,8.421,0,0,1,8.739-8.691A8.422,8.422,0,0,1-2980.919,1058.888Zm-15.261,0a6.347,6.347,0,0,0,6.521,6.654,6.347,6.347,0,0,0,6.521-6.654,6.347,6.347,0,0,0-6.521-6.654A6.347,6.347,0,0,0-2996.18,1058.888Z"/><path class="a" d="M-2962.831,1067.338h-1.917l-10.2-13.26-.012,13.248h-2.122v-16.888h1.917l10.21,13.26V1050.45h2.122Z"/><path class="a" d="M-2947.009,1053.777a8.835,8.835,0,0,0-5-1.555c-2.471,0-4.231,1.109-4.231,2.929,0,1.531,1.29,2.315,3.821,2.628l1.484.181c2.856.35,5.3,1.507,5.3,4.484,0,3.364-3.05,5.123-6.7,5.123a10.935,10.935,0,0,1-6.654-2.194l1.157-1.687a9.018,9.018,0,0,0,5.5,1.868c2.519,0,4.5-1.025,4.5-2.929,0-1.567-1.41-2.314-4.038-2.64l-1.567-.193c-2.784-.337-5-1.627-5-4.508,0-3.255,2.893-5.075,6.449-5.075a10.336,10.336,0,0,1,6.076,1.844Z"/><path class="a" d="M-2925.292,1058.888a8.422,8.422,0,0,1-8.74,8.691,8.422,8.422,0,0,1-8.739-8.691,8.421,8.421,0,0,1,8.739-8.691A8.422,8.422,0,0,1-2925.292,1058.888Zm-15.261,0a6.348,6.348,0,0,0,6.521,6.654,6.347,6.347,0,0,0,6.521-6.654,6.347,6.347,0,0,0-6.521-6.654A6.348,6.348,0,0,0-2940.553,1058.888Z"/><path class="a" d="M-2909.663,1067.326h-11.79V1050.45h2.122v14.863h9.668Z"/><path class="a" d="M-2894.8,1067.326h-11.982V1050.45h11.862v1.988h-9.74v5.389h9.427v2h-9.427v5.509h9.86Z"/><rect class="a" width="2.576" height="7.547" transform="translate(-3001.66 1037.924)"/><path class="a" d="M-3004.759,1037.995l-5.23,3.194a.229.229,0,0,1-.242,0l-5.23-3.194a.726.726,0,0,0-.374-.1h-.006a.717.717,0,0,0-.717.717v6.864h2.574v-3.257a.258.258,0,0,1,.392-.22l2.931,1.793a.919.919,0,0,0,.944.009l3.092-1.814a.258.258,0,0,1,.388.222v3.267h2.575v-6.864a.717.717,0,0,0-.717-.717h-.006A.723.723,0,0,0-3004.759,1037.995Z"/><path class="a" d="M-2986.212,1037.922h-2.613v3.463a.258.258,0,0,1-.379.228l-6.771-3.607a.723.723,0,0,0-.337-.084h0a.717.717,0,0,0-.717.717v6.832h2.592v-3.408a.258.258,0,0,1,.379-.227l6.8,3.606a.714.714,0,0,0,.336.083h0a.716.716,0,0,0,.717-.717v-6.886Z"/><path class="a" d="M-2984.121,1045.469v-7.547h1.2v7.547Z"/><path class="a" d="M-2976.024,1045.635c-3.189,0-5.451-1.513-5.451-3.939s2.276-3.939,5.451-3.939,5.466,1.513,5.466,3.939S-2972.794,1045.635-2976.024,1045.635Zm0-6.87c-2.371,0-4.2,1.036-4.2,2.931s1.826,2.93,4.2,2.93,4.212-1.022,4.212-2.93S-2973.652,1038.765-2976.024,1038.765Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,4 +1,4 @@
// This file is part of MinIO Buckets Server
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
@@ -19,7 +19,7 @@ import {
createStyles,
StyledProps,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import history from "../../../history";
@@ -28,7 +28,7 @@ import {
RouteComponentProps,
Router,
Switch,
withRouter
withRouter,
} from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../../store";
@@ -41,62 +41,62 @@ import ViewBucket from "./ViewBucket/ViewBucket";
const styles = (theme: Theme) =>
createStyles({
root: {
display: "flex"
display: "flex",
},
toolbar: {
background: theme.palette.background.default,
color: "black",
paddingRight: 24 // keep right padding when drawer closed
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...theme.mixins.toolbar
...theme.mixins.toolbar,
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
duration: theme.transitions.duration.leavingScreen,
}),
},
menuButton: {
marginRight: 36
marginRight: 36,
},
menuButtonHidden: {
display: "none"
display: "none",
},
title: {
flexGrow: 1
flexGrow: 1,
},
appBarSpacer: {
height: "5px"
height: "5px",
},
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
overflow: "auto",
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4)
paddingBottom: theme.spacing(4),
},
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
fixedHeight: {
minHeight: 240
}
minHeight: 240,
},
});
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen
open: state.system.sidebarOpen,
});
const connector = connect(mapState, { setMenuOpen });

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 from "react";
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
@@ -23,6 +23,21 @@ import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { factorForDropdown, getBytes } from "../../../../common/utils";
import { AppState } from "../../../../store";
import { connect } from "react-redux";
import {
addBucketName,
addBucketQuota,
addBucketQuotaSize,
addBucketQuotaType,
addBucketQuotaUnit,
addBucketVersioned,
} from "../actions";
import { useDebounce } from "use-debounce";
import { MakeBucketRequest } from "../types";
const styles = (theme: Theme) =>
createStyles({
@@ -32,6 +47,15 @@ const styles = (theme: Theme) =>
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
alignSelf: "flex-start" as const,
},
...modalBasic,
});
@@ -39,119 +63,238 @@ interface IAddBucketProps {
classes: any;
open: boolean;
closeModalAndRefresh: () => void;
}
interface IAddBucketState {
addLoading: boolean;
addError: string;
addBucketName: typeof addBucketName;
addBucketVersioned: typeof addBucketVersioned;
addBucketQuota: typeof addBucketQuota;
addBucketQuotaType: typeof addBucketQuotaType;
addBucketQuotaSize: typeof addBucketQuotaSize;
addBucketQuotaUnit: typeof addBucketQuotaUnit;
bucketName: string;
versioned: boolean;
enableQuota: boolean;
quotaType: string;
quotaSize: string;
quotaUnit: string;
}
class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
state: IAddBucketState = {
addLoading: false,
addError: "",
bucketName: "",
};
const AddBucket = ({
classes,
open,
closeModalAndRefresh,
addBucketName,
addBucketVersioned,
addBucketQuota,
addBucketQuotaType,
addBucketQuotaSize,
addBucketQuotaUnit,
bucketName,
versioned,
enableQuota,
quotaType,
quotaSize,
quotaUnit,
}: IAddBucketProps) => {
const [bName, setBName] = useState<string>(bucketName);
const [addLoading, setAddLoading] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
addRecord(event: React.FormEvent) {
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
const { bucketName, addLoading } = this.state;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
api
.invoke("POST", "/api/v1/buckets", {
name: bucketName,
})
.then((res) => {
this.setState(
{
addLoading: false,
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch((err) => {
this.setState({
addLoading: false,
addError: err,
});
});
});
}
setAddLoading(true);
render() {
const { classes, open } = this.props;
const { addLoading, addError, bucketName } = this.state;
return (
<ModalWrapper
title="Create Bucket"
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
this.props.closeModalAndRefresh();
});
let request: MakeBucketRequest = {
name: bucketName,
versioning: versioned,
};
if (enableQuota) {
const amount = getBytes(quotaSize, quotaUnit, false);
request.quota = {
enabled: true,
quota_type: quotaType,
amount: parseInt(amount),
};
}
api
.invoke("POST", "/api/v1/buckets", request)
.then((res) => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setAddError(err);
});
};
const [value] = useDebounce(bName, 1000);
useEffect(() => {
console.log("called");
addBucketName(value);
}, [value]);
return (
<ModalWrapper
title="Create Bucket"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh();
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addRecord(e);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ bucketName: e.target.value });
}}
label="Bucket Name"
value={bucketName}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setBName(event.target.value);
}}
label="Bucket Name"
value={bName}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="versioned"
id="versioned"
name="versioned"
checked={versioned}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
addBucketVersioned(event.target.checked);
}}
label={"Turn On Versioning"}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="bucket_quota"
id="bucket_quota"
name="bucket_quota"
checked={enableQuota}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
addBucketQuota(event.target.checked);
}}
label={"Enable Bucket Quota"}
/>
</Grid>
{enableQuota && (
<React.Fragment>
<Grid item xs={12}>
<SelectWrapper
value={quotaType}
label="Quota Type"
id="quota_type"
name="quota_type"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
addBucketQuotaType(e.target.value as string);
}}
options={[
{ value: "hard", label: "Hard" },
{ value: "fifo", label: "FIFO" },
]}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
type="number"
id="quota_size"
name="quota_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
addBucketQuotaSize(e.target.value);
}}
label="Size"
value={quotaSize}
required
min="1"
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
id="quota_unit"
name="quota_unit"
value={quotaUnit}
onChange={(
e: React.ChangeEvent<{ value: unknown }>
) => {
addBucketQuotaUnit(e.target.value as string);
}}
options={factorForDropdown()}
/>
</div>
</div>
</Grid>
</React.Fragment>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddBucket);
const mapState = (state: AppState) => ({
addBucketModalOpen: state.buckets.open,
bucketName: state.buckets.addBucketName,
versioned: state.buckets.addBucketVersioning,
enableQuota: state.buckets.addBucketQuotaEnabled,
quotaType: state.buckets.addBucketQuotaType,
quotaSize: state.buckets.addBucketQuotaSize,
quotaUnit: state.buckets.addBucketQuotaUnit,
});
const connector = connect(mapState, {
addBucketName: addBucketName,
addBucketVersioned: addBucketVersioned,
addBucketQuota: addBucketQuota,
addBucketQuotaType: addBucketQuotaType,
addBucketQuotaSize: addBucketQuotaSize,
addBucketQuotaUnit: addBucketQuotaUnit,
});
export default connector(withStyles(styles)(AddBucket));

View File

@@ -23,7 +23,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
LinearProgress,
} from "@material-ui/core";
import api from "../../../../common/api";
import { BucketList } from "../types";
@@ -32,8 +32,8 @@ import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IDeleteBucketProps {
@@ -54,7 +54,7 @@ class DeleteBucket extends React.Component<
> {
state: IDeleteBucketState = {
deleteLoading: false,
deleteError: ""
deleteError: "",
};
removeRecord() {
@@ -66,23 +66,23 @@ class DeleteBucket extends React.Component<
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/buckets/${selectedBucket}`, {
name: selectedBucket
name: selectedBucket,
})
.then((res: BucketList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
deleteError: "",
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
deleteLoading: false,
deleteError: err
deleteError: err,
});
});
});

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 from "react";
import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
@@ -31,6 +31,12 @@ import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
import { AppState } from "../../../../store";
import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "../../Logs/actions";
import { addBucketOpen, addBucketReset } from "../actions";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
@@ -74,176 +80,153 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
interface IListBucketsProps {
classes: any;
addBucketOpen: typeof addBucketOpen;
addBucketModalOpen: boolean;
addBucketReset: typeof addBucketReset;
}
interface IListBucketsState {
records: Bucket[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedBucket: string;
filterBuckets: string;
}
const ListBuckets = ({
classes,
addBucketOpen,
addBucketModalOpen,
addBucketReset,
}: IListBucketsProps) => {
const [records, setRecords] = useState<Bucket[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [deleteError, setDeleteError] = useState<string>("");
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedBucket, setSelectedBucket] = useState<string>("");
const [filterBuckets, setFilterBuckets] = useState<string>("");
class ListBuckets extends React.Component<
IListBucketsProps,
IListBucketsState
> {
state: IListBucketsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: "",
filterBuckets: "",
useEffect(() => {
if (loading) {
const fetchRecords = () => {
setLoading(true);
const offset = page * rowsPerPage;
api
.invoke(
"GET",
`/api/v1/buckets?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: BucketList) => {
setLoading(false);
setRecords(res.buckets || []);
setTotalRecords(!res.buckets ? 0 : res.total);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
setPage(newPage);
setLoading(true);
}
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
};
fetchRecords();
}
}, [loading, page, rowsPerPage]);
const closeAddModalAndRefresh = () => {
addBucketOpen(false);
addBucketReset();
setLoading(true);
};
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/buckets?offset=${offset}&limit=${rowsPerPage}`)
.then((res: BucketList) => {
this.setState({
loading: false,
records: res.buckets || [],
totalRecords: !res.buckets ? 0 : res.total,
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
}
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
}
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
setLoading(true);
}
};
closeAddModalAndRefresh() {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
}
useEffect(() => {
setLoading(true);
}, []);
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
useEffect(() => {
setLoading(true);
}, [page, rowsPerPage]);
componentDidMount(): void {
this.fetchRecords();
}
const confirmDeleteBucket = (bucket: string) => {
setDeleteOpen(true);
setSelectedBucket(bucket);
};
bucketFilter(): void {}
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedBucket,
filterBuckets,
} = this.state;
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
];
const offset = page * rowsPerPage;
const offset = page * rowsPerPage;
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const displayParsedDate = (date: string) => {
return <Moment>{date}</Moment>;
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP });
};
const confirmDeleteBucket = (bucket: string) => {
this.setState({ deleteOpen: true, selectedBucket: bucket });
};
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
];
const displayParsedDate = (date: string) => {
return <Moment>{date}</Moment>;
};
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: Bucket) => {
if (filterBuckets === "") {
const filteredRecords = records
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
return false;
}
});
}
})
.slice(offset, offset + rowsPerPage);
return (
<React.Fragment>
{addScreenOpen && (
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
)}
{deleteOpen && (
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Buckets</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
return (
<React.Fragment>
{addBucketModalOpen && (
<AddBucket
open={addBucketModalOpen}
closeModalAndRefresh={() => {
closeAddModalAndRefresh();
}}
/>
)}
{deleteOpen && (
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<PageHeader label={"Buckets"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
@@ -251,9 +234,7 @@ class ListBuckets extends React.Component<
id="search-resource"
label=""
onChange={(val) => {
this.setState({
filterBuckets: val.target.value,
});
setFilterBuckets(val.target.value);
}}
InputProps={{
disableUnderline: true,
@@ -269,9 +250,7 @@ class ListBuckets extends React.Component<
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true,
});
addBucketOpen(true);
}}
>
Create Bucket
@@ -317,9 +296,18 @@ class ListBuckets extends React.Component<
/>
</Grid>
</Grid>
</React.Fragment>
);
}
}
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ListBuckets);
const mapState = (state: AppState) => ({
addBucketModalOpen: state.buckets.open,
});
const connector = connect(mapState, {
addBucketOpen: addBucketOpen,
addBucketReset: addBucketReset,
});
export default connector(withStyles(styles)(ListBuckets));

View File

@@ -0,0 +1,158 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../../../../common/api";
import { BucketObjectsList } from "../ListObjects/types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
});
interface IDeleteObjectProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedObject: string;
selectedBucket: string;
}
interface IDeleteObjectState {
deleteLoading: boolean;
deleteError: string;
}
class DeleteObject extends React.Component<
IDeleteObjectProps,
IDeleteObjectState
> {
state: IDeleteObjectState = {
deleteLoading: false,
deleteError: "",
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedObject, selectedBucket } = this.props;
if (deleteLoading) {
return;
}
var recursive = false;
if (selectedObject.endsWith("/")) {
recursive = true;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&recursive=${recursive}`
)
.then((res: BucketObjectsList) => {
this.setState(
{
deleteLoading: false,
deleteError: "",
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch((err) => {
this.setState({
deleteLoading: false,
deleteError: err,
});
});
});
}
render() {
const { classes, deleteOpen, selectedObject } = this.props;
const { deleteLoading, deleteError } = this.state;
return (
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete: <b>{selectedObject}</b>?{" "}
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
this.removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
}
}
export default withStyles(styles)(DeleteObject);

View File

@@ -0,0 +1,253 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { BucketObject, BucketObjectsList } from "./types";
import api from "../../../../../../common/api";
import React, { useEffect, useState } from "react";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../.././../../../icons";
import { niceBytes } from "../../../../../../common/utils";
import Moment from "react-moment";
import DeleteObject from "./DeleteObject";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
errorBlock: {
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
});
interface IListObjectsProps {
classes: any;
match: any;
}
interface IListObjectsState {
records: BucketObject[];
totalRecords: number;
loading: boolean;
error: string;
deleteOpen: boolean;
deleteError: string;
selectedObject: string;
selectedBucket: string;
filterObjects: string;
}
class ListObjects extends React.Component<
IListObjectsProps,
IListObjectsState
> {
state: IListObjectsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteOpen: false,
deleteError: "",
selectedObject: "",
selectedBucket: "",
filterObjects: "",
};
fetchRecords = () => {
this.setState({ loading: true }, () => {
const { match } = this.props;
const bucketName = match.params["bucket"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects`)
.then((res: BucketObjectsList) => {
this.setState({
loading: false,
selectedBucket: bucketName,
records: res.objects || [],
totalRecords: !res.objects ? 0 : res.total,
error: "",
});
// TODO:
// if we get 0 results, and page > 0 , go down 1 page
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
};
componentDidMount(): void {
this.fetchRecords();
}
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
bucketFilter(): void {}
render() {
const { classes } = this.props;
const {
records,
loading,
selectedObject,
selectedBucket,
deleteOpen,
filterObjects,
} = this.state;
const displayParsedDate = (date: string) => {
return <Moment>{date}</Moment>;
};
const confirmDeleteObject = (object: string) => {
this.setState({ deleteOpen: true, selectedObject: object });
};
const tableActions = [
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
const filteredRecords = records.filter((b: BucketObject) => {
if (filterObjects === "") {
return true;
} else {
if (b.name.indexOf(filterObjects) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
selectedObject={selectedObject}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Objects</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
this.setState({
filterObjects: val.target.value,
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
</Grid>
</Grid>
</React.Fragment>
);
}
}
export default withStyles(styles)(ListObjects);

View File

@@ -0,0 +1,27 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface BucketObject {
name: string;
size: number;
last_modified: Date;
content_type: string;
}
export interface BucketObjectsList {
objects: BucketObject[];
total: number;
}

View File

@@ -0,0 +1,212 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import get from "lodash/get";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
import {
IRemoteBucket,
IRemoteBucketsResponse,
} from "../../RemoteBuckets/types";
import RemoteBucketsList from "../../RemoteBuckets/RemoteBuckets";
interface IReplicationModal {
open: boolean;
closeModalAndRefresh: () => any;
classes: any;
bucketName: string;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
const AddReplicationModal = ({
open,
closeModalAndRefresh,
classes,
bucketName,
}: IReplicationModal) => {
const [addError, setAddError] = useState("");
const [loadingForm, setLoadingForm] = useState(true);
const [addLoading, setAddLoading] = useState(false);
const [remoteURL, setRemoteURL] = useState("");
const [source, setSource] = useState("");
const [target, setTarget] = useState("");
const [ARN, setARN] = useState("");
const [arnValues, setARNValues] = useState([]);
useEffect(() => {
if (addLoading) {
addRecord();
}
}, [addLoading]);
useEffect(() => {
if (loadingForm) {
getARNValues();
}
});
const addRecord = () => {
const replicationInfo = {
destination_bucket: target,
arn: ARN,
};
api
.invoke(
"POST",
`/api/v1/buckets/${bucketName}/replication`,
replicationInfo
)
.then((res) => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setAddError(err);
});
};
const getARNValues = () => {
api
.invoke("GET", "/api/v1/remote-buckets")
.then((res: any) => {
const remoteBuckets = get(res, "buckets", []);
const remoteARNS = remoteBuckets.map((itemRemote: IRemoteBucket) => {
return { label: itemRemote.remoteARN, value: itemRemote.remoteARN };
});
setLoadingForm(false);
setARNValues(remoteARNS);
})
.catch((err) => {
setLoadingForm(false);
});
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh();
}}
title="Set Bucket Replication"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddLoading(true);
}}
>
{loadingForm && (
<Grid container>
<Grid item xs={12}>
<LinearProgress />
</Grid>
</Grid>
)}
{!loadingForm && (
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTarget(e.target.value);
}}
label="Destination Bucket"
value={target}
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setARN(e.target.value as string);
}}
id="arn"
name="arn"
label={"ARN"}
value={ARN}
options={arnValues}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
)}
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddReplicationModal);

View File

@@ -23,8 +23,18 @@ import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import CircularProgress from "@material-ui/core/CircularProgress";
import api from "../../../../common/api";
import { BucketEvent, BucketEventList, BucketInfo, BucketList } from "../types";
import { Button } from "@material-ui/core";
import {
BucketEvent,
BucketEventList,
BucketInfo,
BucketList,
BucketReplication,
BucketReplicationDestination,
BucketReplicationRule,
BucketReplicationRuleDeleteMarker,
BucketVersioning,
} from "../types";
import { Box, Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import SetAccessPolicy from "./SetAccessPolicy";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
@@ -33,6 +43,9 @@ import AddEvent from "./AddEvent";
import DeleteEvent from "./DeleteEvent";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../common/utils";
import AddReplicationModal from "./AddReplicationModal";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
@@ -117,6 +130,14 @@ const styles = (theme: Theme) =>
capitalizeFirst: {
textTransform: "capitalize",
},
doubleElement: {
display: "flex",
justifyContent: "space-between",
},
tabPan: {
marginTop: "5px",
},
...containerForHeader(theme.spacing(4)),
});
interface IViewBucketProps {
@@ -124,9 +145,40 @@ interface IViewBucketProps {
match: any;
}
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
style={{ marginTop: "5px" }}
{...other}
>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
function a11yProps(index: any) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
interface IViewBucketState {
info: BucketInfo | null;
records: BucketEvent[];
replicationRules: BucketReplicationRule[];
totalRecords: number;
loadingBucket: boolean;
loadingEvents: boolean;
@@ -137,18 +189,23 @@ interface IViewBucketState {
setAccessPolicyScreenOpen: boolean;
page: number;
rowsPerPage: number;
curTab: number;
addScreenOpen: boolean;
deleteOpen: boolean;
selectedBucket: string;
selectedEvent: BucketEvent | null;
bucketSize: string;
errorSize: string;
replicationSet: boolean;
openSetReplication: boolean;
isVersioned: boolean;
}
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
state: IViewBucketState = {
info: null,
records: [],
replicationRules: [],
totalRecords: 0,
loadingBucket: true,
loadingEvents: true,
@@ -158,6 +215,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
errBucket: "",
setAccessPolicyScreenOpen: false,
page: 0,
curTab: 0,
rowsPerPage: 10,
addScreenOpen: false,
deleteOpen: false,
@@ -165,6 +223,9 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
selectedEvent: null,
bucketSize: "0",
errorSize: "",
replicationSet: false,
openSetReplication: false,
isVersioned: false,
};
fetchEvents() {
@@ -195,6 +256,29 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
.catch((err: any) => {
this.setState({ loadingEvents: false, error: err });
});
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
this.setState({
isVersioned: res.is_versioned,
});
})
.catch((err: any) => {
this.setState({ error: err });
});
api
.invoke("GET", `/api/v1/buckets/${bucketName}/replication`)
.then((res: BucketReplication) => {
const r = res.rules ? res.rules : [];
this.setState({
replicationRules: r,
});
})
.catch((err: any) => {
this.setState({ error: err });
});
});
}
@@ -279,6 +363,11 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
selectedEvent,
bucketSize,
loadingSize,
replicationSet,
openSetReplication,
isVersioned,
replicationRules,
curTab,
} = this.state;
const offset = page * rowsPerPage;
@@ -301,6 +390,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
};
let accessPolicy = "n/a";
if (info !== null) {
accessPolicy = info.access;
}
@@ -309,9 +399,26 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
return <React.Fragment>{events.join(", ")}</React.Fragment>;
};
const ruleDestDisplay = (events: BucketReplicationDestination) => {
return (
<React.Fragment>
{events.bucket.replace("arn:aws:s3:::", "")}
</React.Fragment>
);
};
const ruleDelDisplay = (events: BucketReplicationRuleDeleteMarker) => {
return <React.Fragment>{events.status}</React.Fragment>;
};
const setOpenReplicationOpen = (open = false) => {
this.setState({ openSetReplication: open });
};
const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }];
const filteredRecords = records.slice(offset, offset + rowsPerPage);
const filteredRules = replicationRules.slice(offset, offset + rowsPerPage);
return (
<React.Fragment>
@@ -335,130 +442,201 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
}}
/>
)}
{openSetReplication && (
<AddReplicationModal
closeModalAndRefresh={() => {
setOpenReplicationOpen(false);
this.fetchEvents();
}}
open={openSetReplication}
bucketName={bucketName}
/>
)}
<PageHeader label={`Bucket > ${match.params["bucketName"]}`} />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Bucket > {match.params["bucketName"]}
</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<div className={classes.headerContainer}>
<div>
<Paper className={classes.paperContainer}>
<div className={classes.gridContainer}>
<div>Access Policy:</div>
<div className={classes.capitalizeFirst}>
{loadingBucket ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</div>
<div>Reported Usage:</div>
<div>
{loadingSize ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
niceBytes(bucketSize)
)}
</div>
</div>
</Paper>
</div>
<div className={classes.masterActions}>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12}>
<div className={classes.headerContainer}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
size="medium"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
<Paper className={classes.paperContainer}>
<div className={classes.gridContainer}>
<div>Access Policy:</div>
<div className={classes.capitalizeFirst}>
{loadingBucket ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</div>
<div>Reported Usage:</div>
<div>
{loadingSize ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
niceBytes(bucketSize)
)}
</div>
<div>Replication:</div>
<div className={classes.doubleElement}>
<span>{replicationRules.length ? "Yes" : "No"}</span>
</div>
<div>Versioning:</div>
<div>{isVersioned ? "Yes" : "No"}&nbsp;</div>
</div>
</Paper>
</div>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
size="medium"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
</div>
</div>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Tabs
value={0}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
<Tab label="Events" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
});
}}
>
Subcribe 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={filteredRecords}
entityName="Events"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
this.setState({ curTab: newValue });
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
<Tab label="Events" {...a11yProps(0)} />
<Tab label="Replication" {...a11yProps(1)} />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{curTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
});
}}
>
Subscribe to Event
</Button>
)}
{curTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
openSetReplication: true,
});
}}
>
Add Replication Rule
</Button>
)}
</Grid>
<Grid item xs={12}>
<TabPanel index={0} value={curTab}>
<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={filteredRecords}
entityName="Events"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</TabPanel>
<TabPanel index={1} value={curTab}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "ID", elementKey: "id" },
{
label: "Priority",
elementKey: "priority",
},
{
label: "Destination",
elementKey: "destination",
renderFunction: ruleDestDisplay,
},
{
label: "Delete Replication",
elementKey: "delete_marker_replication",
renderFunction: ruleDelDisplay,
},
{ label: "Status", elementKey: "status" },
]}
isLoading={loadingEvents}
records={filteredRules}
entityName="Replication Rules"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</TabPanel>
</Grid>
</Grid>
</Grid>

View File

@@ -0,0 +1,126 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const ADD_BUCKET_OPEN = "ADD_BUCKET_OPEN";
export const ADD_BUCKET_NAME = "ADD_BUCKET_NAME";
export const ADD_BUCKET_VERSIONED = "ADD_BUCKET_VERSIONED";
export const ADD_BUCKET_QUOTA = "ADD_BUCKET_QUOTA";
export const ADD_BUCKET_QUOTA_TYPE = "ADD_BUCKET_QUOTA_TYPE";
export const ADD_BUCKET_QUOTA_SIZE = "ADD_BUCKET_QUOTA_SIZE";
export const ADD_BUCKET_QUOTA_UNIT = "ADD_BUCKET_QUOTA_UNIT";
export const ADD_BUCKET_RESET = "ADD_BUCKET_RESET";
interface AddBucketOpenAction {
type: typeof ADD_BUCKET_OPEN;
open: boolean;
}
interface AddBucketNameAction {
type: typeof ADD_BUCKET_NAME;
name: string;
}
interface AddBucketVersionedAction {
type: typeof ADD_BUCKET_VERSIONED;
versioned: boolean;
}
interface AddBucketQuotaAction {
type: typeof ADD_BUCKET_QUOTA;
quota: boolean;
}
interface AddBucketQuotaTypeAction {
type: typeof ADD_BUCKET_QUOTA_TYPE;
quotaType: string;
}
interface AddBucketQuotaSizeAction {
type: typeof ADD_BUCKET_QUOTA_SIZE;
quotaSize: string;
}
interface AddBucketQuotaUnitAction {
type: typeof ADD_BUCKET_QUOTA_UNIT;
quotaUnit: string;
}
interface AddBucketResetAction {
type: typeof ADD_BUCKET_RESET;
}
export type BucketActionTypes =
| AddBucketOpenAction
| AddBucketNameAction
| AddBucketVersionedAction
| AddBucketQuotaAction
| AddBucketQuotaTypeAction
| AddBucketQuotaSizeAction
| AddBucketQuotaUnitAction
| AddBucketResetAction;
export function addBucketOpen(open: boolean) {
return {
type: ADD_BUCKET_OPEN,
open: open,
};
}
export function addBucketName(name: string) {
return {
type: ADD_BUCKET_NAME,
name: name,
};
}
export function addBucketVersioned(versioned: boolean) {
return {
type: ADD_BUCKET_VERSIONED,
versioned: versioned,
};
}
export function addBucketQuota(quota: boolean) {
return {
type: ADD_BUCKET_QUOTA,
quota: quota,
};
}
export function addBucketQuotaType(quotaType: string) {
return {
type: ADD_BUCKET_QUOTA_TYPE,
quotaType: quotaType,
};
}
export function addBucketQuotaSize(quotaSize: string) {
return {
type: ADD_BUCKET_QUOTA_SIZE,
quotaSize: quotaSize,
};
}
export function addBucketQuotaUnit(quotaUnit: string) {
return {
type: ADD_BUCKET_QUOTA_UNIT,
quotaUnit: quotaUnit,
};
}
export function addBucketReset() {
return {
type: ADD_BUCKET_RESET,
};
}

View File

@@ -0,0 +1,102 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
ADD_BUCKET_NAME,
ADD_BUCKET_OPEN,
ADD_BUCKET_QUOTA,
ADD_BUCKET_QUOTA_SIZE,
ADD_BUCKET_QUOTA_TYPE,
ADD_BUCKET_QUOTA_UNIT,
ADD_BUCKET_RESET,
ADD_BUCKET_VERSIONED,
BucketActionTypes,
} from "./actions";
export interface BucketsState {
open: boolean;
addBucketName: string;
addBucketVersioning: boolean;
addBucketQuotaEnabled: boolean;
addBucketQuotaType: string;
addBucketQuotaSize: string;
addBucketQuotaUnit: string;
}
const initialState: BucketsState = {
open: false,
addBucketName: "",
addBucketVersioning: false,
addBucketQuotaEnabled: false,
addBucketQuotaType: "hard",
addBucketQuotaSize: "1",
addBucketQuotaUnit: "TiB",
};
export function bucketsReducer(
state = initialState,
action: BucketActionTypes
): BucketsState {
switch (action.type) {
case ADD_BUCKET_OPEN:
return {
...state,
open: action.open,
};
case ADD_BUCKET_NAME:
return {
...state,
addBucketName: action.name,
};
case ADD_BUCKET_VERSIONED:
return {
...state,
addBucketVersioning: action.versioned,
};
case ADD_BUCKET_QUOTA:
return {
...state,
addBucketQuotaEnabled: action.quota,
};
case ADD_BUCKET_QUOTA_TYPE:
return {
...state,
addBucketQuotaType: action.quotaType,
};
case ADD_BUCKET_QUOTA_SIZE:
return {
...state,
addBucketQuotaSize: action.quotaSize,
};
case ADD_BUCKET_QUOTA_UNIT:
return {
...state,
addBucketQuotaUnit: action.quotaUnit,
};
case ADD_BUCKET_RESET:
return {
...state,
addBucketName: "",
addBucketVersioning: false,
addBucketQuotaEnabled: false,
addBucketQuotaType: "hard",
addBucketQuotaSize: "1",
addBucketQuotaUnit: "TiB",
};
default:
return state;
}
}

View File

@@ -45,3 +45,37 @@ export interface BucketEventList {
export interface ArnList {
arns: string[];
}
export interface BucketVersioning {
is_versioned: boolean;
}
export interface BucketReplicationRuleDeleteMarker {
status: string;
}
export interface BucketReplicationDestination {
bucket: string;
}
export interface BucketReplicationRule {
id: string;
status: string;
priority: number;
delete_marker_replication: BucketReplicationRuleDeleteMarker;
Destination: BucketReplicationDestination;
}
export interface BucketReplication {
rules: BucketReplicationRule[];
}
export interface QuotaRequest {
enabled: boolean;
quota_type: string;
amount: number;
}
export interface MakeBucketRequest {
name: string;
versioning: boolean;
quota?: QuotaRequest;
}

View File

@@ -15,6 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { NewServiceAccount } from "./types";
import { Button } from "@material-ui/core";
@@ -67,6 +68,8 @@ const CredentialsPrompt = ({
return null;
}
const consoleCreds = get(newServiceAccount, "console", null);
return (
<ModalWrapper
modalOpen={open}
@@ -87,6 +90,21 @@ const CredentialsPrompt = ({
<b>Secret Key:</b> {newServiceAccount.secretKey}
</li>
</ul>
{consoleCreds && (
<React.Fragment>
<Grid item xs={12}>
<strong>Console Credentials</strong>
<ul>
<li>
<b>Access Key:</b> {consoleCreds.accessKey}
</li>
<li>
<b>Secret Key:</b> {consoleCreds.secretKey}
</li>
</ul>
</Grid>
</React.Fragment>
)}
<Typography
component="p"
variant="body1"
@@ -99,11 +117,23 @@ const CredentialsPrompt = ({
<Grid item xs={12} className={classes.buttonContainer}>
<Button
onClick={() => {
let consoleExtras = {};
if (consoleCreds) {
consoleExtras = {
console: {
access_key: consoleCreds.accessKey,
secret_key: consoleCreds.secretKey,
},
};
}
download(
"credentials.json",
JSON.stringify({
access_key: newServiceAccount.accessKey,
secret_key: newServiceAccount.secretKey,
...consoleExtras,
})
);
}}

View File

@@ -17,4 +17,10 @@
export interface NewServiceAccount {
accessKey: string;
secretKey: string;
console?: ConsoleSA;
}
export interface ConsoleSA {
accessKey: string;
secretKey: string;
}

View File

@@ -0,0 +1,186 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from "react";
import get from "lodash/get";
import { Grid, InputLabel, Tooltip } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import CancelIcon from "@material-ui/icons/Cancel";
import HelpIcon from "@material-ui/icons/Help";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import { fileProcess } from "./utils";
interface InputBoxProps {
label: string;
classes: any;
onChange: (e: string, i: string) => void;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
required?: boolean;
error?: string;
accept?: string;
value?: string;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
textBoxContainer: {
flexGrow: 1,
position: "relative",
flexDirection: "column",
},
errorState: {
color: "#b53b4b",
fontSize: 14,
position: "absolute",
top: 7,
right: 7,
},
errorText: {
margin: "0",
fontSize: "0.75rem",
marginTop: 3,
textAlign: "left",
fontFamily: "Lato,sans-serif",
fontWeight: 400,
lineHeight: "1.66",
color: "#dc1f2e",
},
valueString: {
maxWidth: 350,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginTop: 2,
},
fileReselect: {
display: "flex",
alignItems: "center",
},
});
const FileSelector = ({
label,
classes,
onChange,
id,
name,
disabled = false,
tooltip = "",
required,
error = "",
accept = "",
value = "",
}: InputBoxProps) => {
const [showFileSelector, setShowSelector] = useState(false);
return (
<React.Fragment>
<Grid
item
xs={12}
className={`${classes.fieldContainer} ${
error !== "" ? classes.errorInField : ""
}`}
>
{label !== "" && (
<InputLabel
htmlFor={id}
className={`${error !== "" ? classes.fieldLabelError : ""} ${
classes.inputLabel
}`}
>
<span>
{label}
{required ? "*" : ""}
</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
{showFileSelector || value === "" ? (
<div className={classes.textBoxContainer}>
<input
type="file"
name={name}
onChange={(e) => {
const fileName = get(e, "target.files[0].name", "");
fileProcess(e, (data: any) => {
onChange(data, fileName);
});
}}
accept={accept}
required={required}
disabled={disabled}
/>
{value !== "" && (
<IconButton
color="primary"
aria-label="upload picture"
component="span"
onClick={() => {
setShowSelector(false);
}}
disableRipple={false}
disableFocusRipple={false}
>
<CancelIcon />
</IconButton>
)}
{error !== "" && (
<React.Fragment>
<br />
<span className={classes.errorText}>{error}</span>
</React.Fragment>
)}
</div>
) : (
<div className={classes.fileReselect}>
<div className={classes.valueString}>{value}</div>
<IconButton
color="primary"
aria-label="upload picture"
component="span"
onClick={() => {
setShowSelector(true);
}}
disableRipple={false}
disableFocusRipple={false}
>
<AttachFileIcon />
</IconButton>
</div>
)}
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(FileSelector);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const fileProcess = (evt: any, callback: any) => {
const file = evt.target.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
// reader.readAsDataURL(file) output will be something like: data:application/x-x509-ca-cert;base64,LS0tLS1CRUdJTiBDRVJUSU
// we care only about the actual base64 part (everything after "data:application/x-x509-ca-cert;base64,")
const fileBase64 = reader.result;
if (fileBase64) {
const fileArray = fileBase64.toString().split("base64,");
if (fileArray.length === 2) {
callback(fileArray[1]);
}
}
};
};

View File

@@ -42,6 +42,7 @@ interface SelectProps {
onChange: (
e: React.ChangeEvent<{ name?: string | undefined; value: unknown }>
) => void;
disabled?: boolean;
classes: any;
}
@@ -51,7 +52,7 @@ const styles = (theme: Theme) =>
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
width: 215,
},
});
@@ -88,6 +89,7 @@ const SelectWrapper = ({
label,
tooltip = "",
value,
disabled = false,
}: SelectProps) => {
return (
<React.Fragment>
@@ -111,6 +113,7 @@ const SelectWrapper = ({
value={value}
onChange={onChange}
input={<SelectStyled />}
disabled={disabled}
>
{options.map((option) => (
<MenuItem

View File

@@ -79,3 +79,19 @@ export const checkboxIcons = {
backgroundColor: "#201763",
},
};
export const containerForHeader = (bottomSpacing: any) => ({
container: {
padding: "110px 33px 30px",
paddingBottom: bottomSpacing,
"& h6": {
color: "#777777",
fontSize: 14,
},
"& p": {
"& span": {
fontSize: 16,
},
},
},
});

View File

@@ -0,0 +1,48 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
interface IPageHeader {
classes: any;
label: any;
}
const styles = (theme: Theme) =>
createStyles({
headerContainer: {
position: "absolute",
width: "100%",
height: 77,
display: "flex",
backgroundColor: "#fff",
borderBottom: "#E3E3E3 1px solid",
left: 0,
},
label: {
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
},
labelStyle: {
color: "#000",
fontSize: 18,
fontWeight: 700,
marginLeft: 55,
marginTop: 8,
},
});
const PageHeader = ({ classes, label }: IPageHeader) => {
return (
<Grid container className={classes.headerContainer}>
<Grid item xs={12} className={classes.label}>
<Typography variant="h4" className={classes.labelStyle}>
{label}
</Typography>
</Grid>
</Grid>
);
};
export default withStyles(styles)(PageHeader);

View File

@@ -49,6 +49,7 @@ interface IColumns {
elementKey: string;
sortable?: boolean;
renderFunction?: (input: any) => any;
globalClass?: any;
}
interface IPaginatorConfig {
@@ -161,7 +162,10 @@ const styles = (theme: Theme) =>
const titleColumnsMap = (columns: IColumns[]) => {
return columns.map((column: IColumns, index: number) => {
return (
<TableCell key={`tbCT-${column.elementKey}-${index}`}>
<TableCell
key={`tbCT-${column.elementKey}-${index}`}
className={column.globalClass}
>
{column.label}
</TableCell>
);
@@ -265,7 +269,7 @@ const TableWrapper = ({
</Grid>
</Grid>
)}
{records && records.length > 0 ? (
{records && !isLoading && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
<TableRow>
@@ -279,7 +283,10 @@ const TableWrapper = ({
</TableCell>
)}
{titleColumnsMap(columns)}
{itemActions && itemActions.length > 0 && (
{((itemActions && itemActions.length > 1) ||
(itemActions &&
itemActions.length === 1 &&
itemActions[0].type !== "view")) && (
<TableCell
align="center"
className={classes.actionsContainer}
@@ -329,7 +336,10 @@ const TableWrapper = ({
</TableCell>
)}
{rowColumnsMap(columns, record, classes, isSelected)}
{itemActions && itemActions.length > 0 && (
{((itemActions && itemActions.length > 1) ||
(itemActions &&
itemActions.length === 1 &&
itemActions[0].type !== "view")) && (
<TableCell
align="center"
className={classes.actionsContainer}

View File

@@ -27,6 +27,8 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { configurationElements } from "../utils";
import { IConfigurationElement } from "../types";
import EditConfiguration from "../CustomForms/EditConfiguration";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
interface IListConfiguration {
classes: any;
@@ -58,6 +60,7 @@ const styles = (theme: Theme) =>
iconText: {
lineHeight: "24px",
},
...containerForHeader(theme.spacing(4)),
});
const ConfigurationsList = ({ classes }: IListConfiguration) => {
@@ -103,47 +106,44 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
selectedConfiguration={selectedConfiguration}
/>
)}
<PageHeader label="Configurations List" />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Configurations List</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<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>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" },
]}
isLoading={false}
records={filteredRecords}
entityName="Configurations"
idField="configuration_id"
/>
<Grid item xs={12} className={classes.container}>
{error !== "" && <Grid container>{error}</Grid>}
<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>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" },
]}
isLoading={false}
records={filteredRecords}
entityName="Configurations"
idField="configuration_id"
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -51,8 +51,6 @@ import Permissions from "./Permissions/Permissions";
import Dashboard from "./Dashboard/Dashboard";
import Menu from "./Menu/Menu";
import api from "../../common/api";
import storage from "local-storage-fallback";
import NotFoundPage from "../NotFoundPage";
import ServiceAccounts from "./ServiceAccounts/ServiceAccounts";
import Users from "./Users/Users";
import Groups from "./Groups/Groups";
@@ -69,6 +67,9 @@ import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
import { clearSession } from "../../common/utils";
import RemoteBuckets from "./RemoteBuckets/RemoteBuckets";
import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
import ListObjects from "./Buckets/ListBuckets/Objects/ListObjects/ListObjects";
function Copyright() {
return (
@@ -83,7 +84,7 @@ function Copyright() {
);
}
const drawerWidth = 254;
const drawerWidth = 245;
const styles = (theme: Theme) =>
createStyles({
@@ -135,6 +136,9 @@ const styles = (theme: Theme) =>
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: "hidden",
background:
"transparent linear-gradient(90deg, #073052 0%, #081C42 100%) 0% 0% no-repeat padding-box",
boxShadow: "0px 3px 7px #00000014",
},
drawerPaperClose: {
overflowX: "hidden",
@@ -147,17 +151,15 @@ const styles = (theme: Theme) =>
width: theme.spacing(9),
},
},
appBarSpacer: {
height: "5px",
},
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto",
position: "relative",
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
margin: 0,
},
paper: {
padding: theme.spacing(2),
@@ -253,6 +255,14 @@ const Console = ({
component: Buckets,
path: "/buckets/:bucketName",
},
{
component: ObjectBrowser,
path: "/object-browser",
},
{
component: ListObjects,
path: "/object-browser/:bucket?",
},
{
component: Watch,
path: "/watch",
@@ -269,6 +279,10 @@ const Console = ({
component: Policies,
path: "/policies",
},
{
component: RemoteBuckets,
path: "/remote-buckets",
},
{
component: Trace,
path: "/trace",
@@ -311,7 +325,7 @@ const Console = ({
},
{
component: TenantDetails,
path: "/tenants/:tenantName",
path: "/namespaces/:tenantNamespace/tenants/:tenantName",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
@@ -359,8 +373,7 @@ const Console = ({
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Container className={classes.container}>
<Router history={history}>
<Switch>
{allowedRoutes.map((route: any) => (

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -27,6 +27,8 @@ import { Usage } from "./types";
import api from "../../../common/api";
import { niceBytes } from "../../../common/utils";
import { LinearProgress } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
@@ -77,26 +79,20 @@ const styles = (theme: Theme) =>
height: "100vh",
overflow: "auto",
},
container: {
paddingBottom: theme.spacing(4),
"& h6": {
color: "#777777",
fontSize: 14,
},
"& p": {
"& span": {
fontSize: 16,
},
},
},
...containerForHeader(theme.spacing(4)),
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
border: "#eaedee 1px solid",
borderRadius: 5,
boxShadow: "none",
},
fixedHeight: {
minHeight: 240,
minHeight: 165,
minWidth: 247,
marginRight: 20,
},
consumptionValue: {
color: "#000000",
@@ -107,6 +103,9 @@ const styles = (theme: Theme) =>
marginRight: 10,
color: "#777777",
},
notationContainer: {
display: "flex",
},
});
interface IDashboardProps {
@@ -155,11 +154,9 @@ const Dashboard = ({ classes }: IDashboardProps) => {
return (
<React.Fragment>
<PageHeader label="Dashboard" />
<Grid container>
<Grid container spacing={3} className={classes.container}>
<Grid container>
<Typography variant="h2">MinIO Console</Typography>
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
{loading ? (
<Grid item xs={12} md={12} lg={12}>
@@ -167,22 +164,33 @@ const Dashboard = ({ classes }: IDashboardProps) => {
</Grid>
) : (
<React.Fragment>
<Grid item xs={12} md={4} lg={4}>
<Grid item className={classes.notationContainer}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
</Grid>
<Grid item>
<Typography variant="h6">Total Buckets</Typography>
<Typography variant="h6">All Buckets</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
@@ -197,21 +205,6 @@ const Dashboard = ({ classes }: IDashboardProps) => {
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
</Grid>
</React.Fragment>
)}
</Grid>

View File

@@ -31,6 +31,8 @@ import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
interface IGroupsProps {
classes: any;
@@ -84,6 +86,7 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
const Groups = ({ classes }: IGroupsProps) => {
@@ -215,71 +218,68 @@ const Groups = ({ classes }: IGroupsProps) => {
}}
/>
)}
<PageHeader label={"Groups"} />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Groups</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" ? <Grid container>{error}</Grid> : <React.Fragment />}
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setSelectedGroup(null);
setGroupOpen(true);
}}
>
Create Group
</Button>
</Grid>
<Grid item xs={12} className={classes.container}>
{error !== "" ? <Grid container>{error}</Grid> : <React.Fragment />}
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setSelectedGroup(null);
setGroupOpen(true);
}}
>
Create Group
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Name", elementKey: "" }]}
isLoading={loading}
records={filteredRecords}
entityName="Groups"
idField=""
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Name", elementKey: "" }]}
isLoading={loading}
records={filteredRecords}
entityName="Groups"
idField=""
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -15,6 +15,8 @@ import { FormControl, MenuItem, Select } from "@material-ui/core";
import { BucketList, Bucket } from "../Watch/types";
import { HealStatus, colorH } from "./types";
import { niceBytes } from "../../../common/utils";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
@@ -58,6 +60,7 @@ const styles = (theme: Theme) =>
lastElementWPadding: {
paddingRight: "78",
},
...containerForHeader(theme.spacing(4)),
});
interface IHeal {
@@ -195,130 +198,127 @@ const Heal = ({ classes }: IHeal) => {
}));
return (
<React.Fragment>
<PageHeader label="Heal" />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Heal</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<FormControl variant="outlined">
<Select
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => {
setBucketName(e.target.value as string);
}}
className={classes.fieldContainer}
disabled={false}
>
<MenuItem value="" key={`select-bucket-name-default`}>
Select Bucket
</MenuItem>
{bucketNames.map((option) => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
>
{option.label}
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<FormControl variant="outlined">
<Select
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => {
setBucketName(e.target.value as string);
}}
className={classes.fieldContainer}
disabled={false}
>
<MenuItem value="" key={`select-bucket-name-default`}>
Select Bucket
</MenuItem>
))}
</Select>
</FormControl>
<TextField
placeholder="Prefix"
className={classes.inputField}
id="prefix-resource"
label=""
disabled={false}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => {
setPrefix(e.target.value);
{bucketNames.map((option) => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
placeholder="Prefix"
className={classes.inputField}
id="prefix-resource"
label=""
disabled={false}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => {
setPrefix(e.target.value);
}}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={start}
onClick={() => setStart(true)}
>
Start
</Button>
<Grid item xs={12}>
<span>{"Recursive"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={recursive}
onChange={(e) => {
setRecursive(e.target.checked);
}}
disabled={false}
/>
<span>{"Force Start"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={forceStart}
onChange={(e) => {
setForceStart(e.target.checked);
}}
disabled={false}
/>
<span>{"Force Stop"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={forceStop}
onChange={(e) => {
setForceStop(e.target.checked);
}}
disabled={false}
/>
<span className={classes.lastElementWPadding}>{""}</span>
</Grid>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<HorizontalBar
data={data}
width={100}
height={30}
options={{
title: {
display: true,
text: "Item's Health Status [%]",
fontSize: 20,
},
legend: {
display: true,
position: "right",
},
}}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={start}
onClick={() => setStart(true)}
>
Start
</Button>
<Grid item xs={12}>
<span>{"Recursive"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={recursive}
onChange={(e) => {
setRecursive(e.target.checked);
}}
disabled={false}
/>
<span>{"Force Start"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={forceStart}
onChange={(e) => {
setForceStart(e.target.checked);
}}
disabled={false}
/>
<span>{"Force Stop"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={forceStop}
onChange={(e) => {
setForceStop(e.target.checked);
}}
disabled={false}
/>
<span className={classes.lastElementWPadding}>{""}</span>
<br />
Size scanned: {hStatus.sizeScanned}
<br />
Objects healed: {hStatus.objectsHealed} / {hStatus.objectsScanned}
<br />
Healing time: {hStatus.healDuration}s
</Grid>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<HorizontalBar
data={data}
width={100}
height={30}
options={{
title: {
display: true,
text: "Item's Health Status [%]",
fontSize: 20,
},
legend: {
display: true,
position: "right",
},
}}
/>
<Grid item xs={12}>
<br />
Size scanned: {hStatus.sizeScanned}
<br />
Objects healed: {hStatus.objectsHealed} / {hStatus.objectsScanned}
<br />
Healing time: {hStatus.healDuration}s
</Grid>
</Grid>
</React.Fragment>
);

View File

@@ -23,6 +23,9 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { timeFromDate } from "../../../common/utils";
import { isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import { Grid } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
@@ -54,6 +57,7 @@ const styles = (theme: Theme) =>
ansidefault: {
color: "black",
},
...containerForHeader(theme.spacing(4)),
});
interface ILogs {
@@ -243,16 +247,20 @@ const Logs = ({
};
return (
<div>
<h1>Logs</h1>
<div className={classes.logList}>
<ul>
{messages.map((m) => {
return renderLog(m);
})}
</ul>
</div>
</div>
<React.Fragment>
<PageHeader label="Logs" />
<Grid container>
<Grid className={classes.container} item xs={12}>
<div className={classes.logList}>
<ul>
{messages.map((m) => {
return renderLog(m);
})}
</ul>
</div>
</Grid>
</Grid>
</React.Fragment>
);
};

View File

@@ -21,6 +21,9 @@ import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import HealingIcon from "@material-ui/icons/Healing";
import CloudUploadIcon from "@material-ui/icons/CloudUpload";
import DescriptionIcon from "@material-ui/icons/Description";
import FileCopyIcon from "@material-ui/icons/FileCopy";
import Collapse from "@material-ui/core/Collapse";
import ListItemText from "@material-ui/core/ListItemText";
import List from "@material-ui/core/List";
@@ -55,44 +58,53 @@ import { clearSession } from "../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
logo: {
paddingTop: "42px",
marginBottom: "20px",
textAlign: "center",
paddingTop: 25,
marginBottom: 30,
paddingLeft: 45,
"& img": {
width: "120px",
width: 120,
},
},
menuList: {
"& .active": {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
borderTopLeftRadius: 2,
borderBottomLeftRadius: 2,
color: "#fff",
background:
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box;",
boxShadow: "4px 4px 4px #A5A5A512",
fontWeight: 700,
backgroundColor: "rgba(255, 255, 255, .18)",
"& .MuiSvgIcon-root": {
color: "white",
},
"& .MuiTypography-root": {
color: "#fff",
fontWeight: 700,
},
},
paddingLeft: "30px",
"& .MuiSvgIcon-root": {
fontSize: 16,
color: "#362585",
color: "rgba(255, 255, 255, 0.8)",
maxWidth: 14,
},
"& .MuiListItemIcon-root": {
minWidth: "25px",
minWidth: 25,
},
"& .MuiTypography-root": {
fontSize: "12px",
color: "#2e2e2e",
fontSize: 12,
color: "rgba(255, 255, 255, 0.8)",
},
"& .MuiListItem-gutters": {
paddingRight: 0,
fontWeight: 300,
},
"& .MuiListItem-root": {
padding: "2px 0 2px 16px",
marginBottom: 8,
marginLeft: 30,
width: "calc(100% - 30px)",
},
"& .MuiCollapse-container .MuiCollapse-wrapper .MuiCollapse-wrapperInner .MuiDivider-root": {
backgroundColor: "rgba(112,112,112,0.5)",
marginBottom: 12,
height: 1,
},
},
extraMargin: {
@@ -101,31 +113,34 @@ const styles = (theme: Theme) =>
},
},
groupTitle: {
color: "#220c7c",
color: "#fff",
fontSize: 10,
textTransform: "uppercase",
fontWeight: 700,
marginBottom: 3,
cursor: "pointer",
userSelect: "none",
display: "flex",
justifyContent: "space-between",
},
subTitleMenu: {
fontWeight: 700,
marginLeft: 10,
"&.MuiTypography-root": {
fontSize: 13,
color: "#220c7c",
color: "#fff",
},
},
selectorArrow: {
marginLeft: 3,
marginRight: 20,
marginTop: 1,
display: "inline-block",
width: 0,
height: 0,
borderStyle: "solid",
borderWidth: "3px 2.5px 0 2.5px",
borderColor: "#220C7C transparent transparent transparent",
borderWidth: "4px 4px 0 4px",
borderColor:
"rgba(255, 255, 255, .29) transparent transparent transparent",
transform: "rotateZ(0deg)",
transitionDuration: "0.2s",
},
@@ -185,17 +200,17 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
group: "User",
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
to: "/service-accounts",
name: "Service Accounts",
icon: <ServiceAccountsIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <ServiceAccountsIcon />,
to: "/object-browser",
name: "Object Browser",
icon: <DescriptionIcon />,
},
{
group: "Admin",
@@ -213,6 +228,14 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "Groups",
icon: <GroupsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
group: "Admin",
type: "item",
@@ -221,6 +244,14 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "IAM Policies",
icon: <IAMPoliciesIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/remote-buckets",
name: "Remote Buckets",
icon: <CloudUploadIcon />,
},
{
group: "Tools",
type: "item",
@@ -253,12 +284,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "Heal",
icon: <HealingIcon />,
},
{
group: "Admin",
type: "title",
name: "Configurations",
component: Typography,
},
{
group: "Admin",
type: "item",
@@ -266,7 +291,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
to: "/notification-endpoints",
name: "Lambda Notifications",
icon: <LambdaNotificationsIcon />,
extraMargin: true,
},
{
group: "Admin",
@@ -275,7 +299,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
to: "/configurations-list",
name: "Configurations List",
icon: <ConfigurationsListIcon />,
extraMargin: true,
},
{
group: "Operator",
@@ -351,7 +374,7 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
}
}}
>
{groupMember.label}
<span>{groupMember.label}</span>
{groupMember.collapsible && (
<span
className={`${classes.selectorArrow} ${

View File

@@ -27,7 +27,7 @@ import { MinTablePaginationActions } from "../../../common/MinTablePaginationAct
import {
NotificationEndpointItem,
NotificationEndpointsList,
TransformedEndpointItem
TransformedEndpointItem,
} from "./types";
import { notificationTransform } from "./utils";
import { CreateIcon } from "../../../icons";
@@ -35,6 +35,8 @@ import api from "../../../common/api";
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddNotificationEndpoint from "./AddNotificationEndpoint";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
interface IListNotificationEndpoints {
classes: any;
@@ -43,29 +45,30 @@ interface IListNotificationEndpoints {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
...containerForHeader(theme.spacing(4)),
});
const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
@@ -96,7 +99,7 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
setError("");
setIsLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
setIsLoading(false);
});
@@ -115,8 +118,8 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
type: "delete",
onClick: (row: any) => {
//confirmDeleteBucket(row.name);
}
}
},
},
];
const filteredRecords = records.filter((b: TransformedEndpointItem) => {
@@ -136,7 +139,7 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
<div
style={{
display: "flex",
alignItems: "center"
alignItems: "center",
}}
>
<FiberManualRecordIcon
@@ -158,83 +161,80 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
}}
/>
)}
<PageHeader label="Lambda Notification Targets" />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Lambda Notification Targets</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<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 />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Notification Target
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Service", elementKey: "service_name" },
{
label: "Status",
elementKey: "status",
renderFunction: statusDisplay
}
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Notification Endpoints"
idField="service_name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
},
onChangePage: (event: unknown, newPage: number) => {
setPage(newPage);
},
onChangeRowsPerPage: (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setRowsPerPage(rPP);
},
ActionsComponent: MinTablePaginationActions
}}
/>
<Grid item xs={12} className={classes.container}>
{error !== "" && <Grid container>{error}</Grid>}
<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 />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Notification Target
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Service", elementKey: "service_name" },
{
label: "Status",
elementKey: "status",
renderFunction: statusDisplay,
},
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Notification Endpoints"
idField="service_name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: (event: unknown, newPage: number) => {
setPage(newPage);
},
onChangeRowsPerPage: (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setRowsPerPage(rPP);
},
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -0,0 +1,240 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import AddBucket from "../Buckets/ListBuckets/AddBucket";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { Button } from "@material-ui/core";
import { CreateIcon } from "../../../icons";
import { niceBytes } from "../../../common/utils";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { Bucket, BucketList } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import history from "../../../history";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
errorBlock: {
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
usedSpaceCol: {
width: 150,
},
subTitleLabel: {
alignItems: "center",
display: "flex",
},
});
interface IBrowseBucketsProps {
classes: any;
}
const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
const [loading, setLoading] = useState<boolean>(true);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [error, setError] = useState<string>("");
const [records, setRecords] = useState<Bucket[]>([]);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [filterBuckets, setFilterBuckets] = useState<string>("");
const offset = page * rowsPerPage;
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/buckets?offset=${offset}&limit=${rowsPerPage}`)
.then((res: BucketList) => {
const buckets = get(res, "buckets", []);
setLoading(false);
setRecords(buckets);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
setPage(newPage);
setLoading(true);
}
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
}
}, [loading]);
const closeAddModalAndRefresh = () => {
setAddScreenOpen(false);
setLoading(false);
};
const filteredRecords = records
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
}
return b.name.indexOf(filterBuckets) >= 0;
})
.slice(offset, offset + rowsPerPage);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
return (
<React.Fragment>
{addScreenOpen && (
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={6} className={classes.subTitleLabel}>
<Typography variant="h6">Buckets</Typography>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterBuckets(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Bucket
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
{error !== "" && <span className={classes.errorBlock}>{error}</span>}
<TableWrapper
itemActions={[
{ type: "view", to: `/object-browser`, sendOnlyId: true },
]}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Used Space",
elementKey: "size",
renderFunction: niceBytes,
globalClass: classes.usedSpaceCol,
},
]}
isLoading={loading}
records={filteredRecords}
entityName="Buckets"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(BrowseBuckets);

View File

@@ -0,0 +1,90 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, Typography } from "@material-ui/core";
import BrowseBuckets from "./BrowseBuckets";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
interface IObjectBrowserProps {
match: any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
watchList: {
background: "white",
maxHeight: "400",
overflow: "auto",
"& ul": {
margin: "4",
padding: "0",
},
"& ul li": {
listStyle: "none",
margin: "0",
padding: "0",
borderBottom: "1px solid #dedede",
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012",
},
fieldContainer: {
background: "#FFFFFF",
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206",
boxShadow: "0px 3px 6px #00000012",
},
lastElementWPadding: {
paddingRight: "78",
},
...containerForHeader(theme.spacing(4)),
});
const ObjectBrowser = ({ match, classes }: IObjectBrowserProps) => {
const pathIn = get(match, "path", "");
return (
<React.Fragment>
<PageHeader label={"Object Browser"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
{pathIn === "/object-browser" && <BrowseBuckets />}
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ObjectBrowser);

View File

@@ -30,6 +30,8 @@ import AddPolicy from "./AddPolicy";
import DeletePolicy from "./DeletePolicy";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
@@ -73,6 +75,7 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
interface IPoliciesProps {
@@ -222,70 +225,67 @@ const Policies = ({ classes }: IPoliciesProps) => {
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader label="IAM Policies" />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">IAM Policies</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Policies"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setPage(0);
setFilterPolicies(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setPolicyEdit(null);
}}
>
Create Policy
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loading}
records={paginatedRecords}
entityName="Policies"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Policies"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setPage(0);
setFilterPolicies(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setPolicyEdit(null);
}}
>
Create Policy
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loading}
records={paginatedRecords}
entityName="Policies"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -0,0 +1,209 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IAddBucketProps {
classes: any;
open: boolean;
closeModalAndRefresh: () => void;
}
const AddRemoteBucket = ({
classes,
open,
closeModalAndRefresh,
}: IAddBucketProps) => {
const [addLoading, setAddLoading] = useState(false);
const [addError, setAddError] = useState("");
const [accessKey, setAccessKey] = useState("");
const [secretKey, setSecretKey] = useState("");
const [sourceBucket, setSourceBucket] = useState("");
const [targetURL, setTargetURL] = useState("");
const [targetBucket, setTargetBucket] = useState("");
const [region, setRegion] = useState("");
useEffect(() => {
if (addLoading) {
addRecord();
}
}, [addLoading]);
const addRecord = () => {
const remoteBucketInfo = {
accessKey: accessKey,
secretKey: secretKey,
sourceBucket: sourceBucket,
targetURL: targetURL,
targetBucket: targetBucket,
region: region,
};
api
.invoke("POST", "/api/v1/remote-buckets", remoteBucketInfo)
.then((res) => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setAddError(err);
});
};
return (
<ModalWrapper
title="Create Remote Bucket"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh();
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
addRecord();
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="sourceBucket"
name="sourceBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSourceBucket(e.target.value);
}}
label="Source Bucket"
value={sourceBucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="targetURL"
name="targetURL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="https://play.min.io:9000"
label="Target URL"
value={targetURL}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="targetBucket"
name="targetBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetBucket(e.target.value);
}}
label="Target Bucket"
value={targetBucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="region"
name="region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddRemoteBucket);

View File

@@ -0,0 +1,137 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import get from "lodash/get";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
});
interface IDeleteEventProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
bucketName: any;
sourceBucket: string;
}
interface IDeleteEventState {
deleteLoading: boolean;
deleteError: string;
}
const DeleteRemoteBucket = ({
deleteOpen,
closeDeleteModalAndRefresh,
classes,
bucketName,
sourceBucket,
}: IDeleteEventProps) => {
const [deleteError, setDeleteError] = useState("");
const [deleteLoading, setDeleteLoading] = useState(false);
useEffect(() => {
if (deleteLoading) {
removeRecord();
}
}, [deleteLoading]);
const removeRecord = () => {
api
.invoke("DELETE", `/api/v1/remote-buckets/${sourceBucket}/${bucketName}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Remote Bucket</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete <strong>'{bucketName}'</strong> Remote
Bucket?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
setDeleteLoading(true);
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteRemoteBucket);

View File

@@ -0,0 +1,279 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import Moment from "react-moment";
import api from "../../../common/api";
import { Bucket } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddRemoteBucket from "./AddRemoteBucket";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../icons";
import { IRemoteBucket, IRemoteBucketsResponse } from "./types";
import DeleteRemoteBucket from "./DeleteRemoteBucket";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
errorBlock: {
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
interface IRemoteListBucketsProps {
classes: any;
}
const RemoteBucketsList = ({ classes }: IRemoteListBucketsProps) => {
const [records, setRecords] = useState<IRemoteBucket[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [deleteScreenOpen, setDeleteOpen] = useState<boolean>(false);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [selectedBucket, setSelectedBucket] = useState<IRemoteBucket>({
remoteARN: "",
accessKey: "",
name: "",
secretKey: "",
service: "",
sourceBucket: "",
status: "",
targetBucket: "",
targetURL: "",
});
const [filterBuckets, setFilterBuckets] = useState<string>("");
useEffect(() => {
if (loading) {
fetchRecords();
}
}, [loading]);
const closeAddModalAndRefresh = () => {
setAddScreenOpen(false);
setLoading(true);
};
const closeDeleteModalAndRefresh = (reload: boolean) => {
setDeleteOpen(false);
if (reload) {
setLoading(true);
}
};
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/remote-buckets`)
.then((res: IRemoteBucketsResponse) => {
setLoading(false);
setRecords(res.buckets || []);
setTotalRecords(!res.buckets ? 0 : res.total);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
setPage(newPage);
setLoading(true);
}
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
};
const offset = page * rowsPerPage;
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const confirmDeleteRemoteBucket = (arnRemoteBucket: IRemoteBucket) => {
setSelectedBucket(arnRemoteBucket);
setDeleteOpen(true);
};
const tableActions = [{ type: "delete", onClick: confirmDeleteRemoteBucket }];
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: IRemoteBucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
{addScreenOpen && (
<AddRemoteBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
closeAddModalAndRefresh();
}}
/>
)}
{deleteScreenOpen && (
<DeleteRemoteBucket
bucketName={selectedBucket.remoteARN}
sourceBucket={selectedBucket.sourceBucket}
closeDeleteModalAndRefresh={(reload) => {
closeDeleteModalAndRefresh(reload);
}}
deleteOpen={deleteScreenOpen}
/>
)}
<PageHeader label="Remote Buckets" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Remote Buckets"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterBuckets(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Remote Bucket
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Remote ARN", elementKey: "remoteARN" },
{ label: "Source Bucket", elementKey: "sourceBucket" },
{ label: "Target Bucket", elementKey: "targetBucket" },
{ label: "Status", elementKey: "status" },
]}
isLoading={loading}
records={filteredRecords}
entityName="Remote Buckets"
idField="remoteARN"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(RemoteBucketsList);

View File

@@ -0,0 +1,32 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface IRemoteBucketsResponse {
buckets: IRemoteBucket[];
total: number;
}
export interface IRemoteBucket {
name: string;
accessKey: string;
secretKey: string;
sourceBucket: string;
targetURL: string;
targetBucket: string;
remoteARN: string;
status: string;
service: string;
}

View File

@@ -31,6 +31,8 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { stringSort } from "../../../utils/sortFunctions";
import PageHeader from "../Common/PageHeader/PageHeader";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
@@ -85,6 +87,7 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
interface IServiceAccountsProps {
@@ -225,81 +228,78 @@ const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
entity="Service Account"
/>
)}
<PageHeader label="Service Accounts" />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
setPage(0);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
Create service account
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
setPage(0);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
{error}
</Typography>
Create service account
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={paginatedRecords}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 4,
count: records.length,
rowsPerPage: rowsPerPage,
page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
)}
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={paginatedRecords}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 4,
count: records.length,
rowsPerPage: rowsPerPage,
page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,12 @@ import {
import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import { ITenant } from "./types";
interface IDeleteTenant {
classes: any;
deleteOpen: boolean;
selectedTenant: string;
selectedTenant: ITenant;
closeDeleteModalAndRefresh: (refreshList: boolean) => any;
}
@@ -54,7 +55,10 @@ const DeleteTenant = ({
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/tenants/${selectedTenant}`)
.invoke(
"DELETE",
`/api/v1/namespaces/${selectedTenant.namespace}/tenants/${selectedTenant.name}`
)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
@@ -85,7 +89,7 @@ const DeleteTenant = ({
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete tenant <b>{selectedTenant}</b>?
Are you sure you want to delete tenant <b>{selectedTenant.name}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />

View File

@@ -20,7 +20,7 @@ import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { Button } from "@material-ui/core";
import { Button, IconButton } from "@material-ui/core";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
@@ -32,6 +32,8 @@ import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history";
import RefreshIcon from "@material-ui/icons/Refresh";
interface ITenantsList {
classes: any;
@@ -122,11 +124,16 @@ const ListTenants = ({ classes }: ITenantsList) => {
}
};
const confirmDeleteTenant = (tenant: string) => {
const confirmDeleteTenant = (tenant: ITenant) => {
setSelectedTenant(tenant);
setDeleteOpen(true);
};
const redirectToTenantDetails = (tenant: ITenant) => {
history.push(`/namespaces/${tenant.namespace}/tenants/${tenant.name}`);
return;
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setCreatedAccount(null);
@@ -149,8 +156,8 @@ const ListTenants = ({ classes }: ITenantsList) => {
};
const tableActions = [
{ type: "view", to: `/tenants`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteTenant, sendOnlyId: true },
{ type: "view", onClick: redirectToTenantDetails },
{ type: "delete", onClick: confirmDeleteTenant },
];
const filteredRecords = records
@@ -187,9 +194,7 @@ const ListTenants = ({ classes }: ITenantsList) => {
}
for (let i = 0; i < resTenants.length; i++) {
const total =
resTenants[i].volume_count * resTenants[i].volume_size;
resTenants[i].capacity = niceBytes(total + "");
resTenants[i].capacity = niceBytes(resTenants[i].total_size + "");
}
setRecords(resTenants);
@@ -248,6 +253,17 @@ const ListTenants = ({ classes }: ITenantsList) => {
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<TextField
placeholder="Search Tenants"
className={classes.searchField}

View File

@@ -86,7 +86,14 @@ const ZonesMultiSelector = ({
onChange,
classes,
}: IZonesMultiSelector) => {
const defaultZone: IZone = { name: "", servers: 0, capacity: "", volumes: 0 };
const defaultZone: IZone = {
name: "",
servers: 0,
capacity: "",
volumes: 0,
volumes_per_server: 0,
volume_configuration: { size: 0, storage_class: "", labels: null },
};
const [currentElements, setCurrentElements] = useState<IZone[]>([]);
const [internalCounter, setInternalCounter] = useState<number>(1);

View File

@@ -17,18 +17,32 @@
export interface IZone {
name: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
// computed
capacity: string;
volumes: number;
}
export interface IAddZoneRequest {
name: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
}
export interface IVolumeConfiguration {
size: number;
storage_class: string;
labels: { [key: string]: any } | null;
}
export interface ITenant {
total_size: number;
name: string;
namespace: string;
image: string;
console_image: string;
zone_count: number;
currentState: string;
instance_count: 4;

View File

@@ -11,13 +11,14 @@ import {
niceBytes,
} from "../../../../common/utils";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
import { IAddZoneRequest, ITenant } from "../ListTenants/types";
interface IAddZoneProps {
tenant: ITenant;
classes: any;
open: boolean;
onCloseZoneAndReload: (shouldReload: boolean) => void;
volumesPerInstance: number;
volumeSize: number;
}
const styles = (theme: Theme) =>
@@ -63,18 +64,18 @@ const styles = (theme: Theme) =>
});
const AddZoneModal = ({
tenant,
classes,
open,
onCloseZoneAndReload,
volumesPerInstance,
volumeSize,
}: IAddZoneProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [zoneName, setZoneName] = useState<string>("");
const [numberOfInstances, setNumberOfInstances] = useState<number>(0);
const [numberOfNodes, setNumberOfNodes] = useState<number>(0);
const [volumeSize, setVolumeSize] = useState<number>(0);
const [volumesPerServer, setVolumesPerSever] = useState<number>(0);
const instanceCapacity: number = volumeSize * volumesPerInstance;
const totalCapacity: number = instanceCapacity * numberOfInstances;
const instanceCapacity: number = volumeSize * 1073741824 * volumesPerServer;
const totalCapacity: number = instanceCapacity * numberOfNodes;
return (
<ModalWrapper
@@ -88,30 +89,66 @@ const AddZoneModal = ({
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
const data: IAddZoneRequest = {
name: "",
servers: numberOfNodes,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: volumeSize * 1073741824,
storage_class: "",
labels: null,
},
};
api
.invoke(
"POST",
`/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/zones`,
data
)
.then(() => {
setAddSending(false);
onCloseZoneAndReload(true);
})
.catch((err) => {
setAddSending(false);
// setDeleteError(err);
});
}}
>
<Grid item xs={12}>
<InputBoxWrapper
id="zone_name"
name="zone_name"
type="string"
id="number_of_nodes"
name="number_of_nodes"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setZoneName(e.target.value);
setNumberOfNodes(parseInt(e.target.value));
}}
label="Name"
value={zoneName}
label="Number o Nodes"
value={numberOfNodes.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="number_instances"
name="number_instances"
id="zone_size"
name="zone_size"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfInstances(parseInt(e.target.value));
setVolumeSize(parseInt(e.target.value));
}}
label="Drives per Server"
value={numberOfInstances.toString(10)}
label="Volume Size (Gi)"
value={volumeSize.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_sever"
name="volumes_per_sever"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerSever(parseInt(e.target.value));
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
/>
</Grid>
<Grid item xs={12}>

Some files were not shown because too many files have changed in this diff Show More