Compare commits

...

32 Commits

Author SHA1 Message Date
César Nieto
1aec2d879e Remove unused swagger autogenerated files (#180) 2020-06-22 20:56:52 -07:00
Alex
f77770bb6e Changed create tenant form to be a wizard (#179) 2020-06-22 12:21:19 -05:00
Alex
34ff3d7157 Implemented validation in create tenant form (#177)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-06-19 11:16:16 -07:00
Alex
4b6700d4ac Fixed usability issues on IAM Policies module (#175)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-06-15 21:38:42 -07:00
Alex
f2c8f15fbf Added delete option to multi-zone selector (#167)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-06-15 16:29:43 -07:00
Anton Huck
991204cd46 Remove trailing comma in policy (#160)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
Co-authored-by: César Nieto <ces.nietor@gmail.com>
Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
Co-authored-by: Lenin Alevski <alevsk.8772@gmail.com>
2020-06-08 19:24:51 -07:00
Justin Hutchings
4bac7040a1 Add CodeQL security scanning (#157)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-06-08 17:44:22 -07:00
dependabot[bot]
a247bf6a0c Bump websocket-extensions from 0.1.3 to 0.1.4 in /portal-ui (#165)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-08 14:04:18 -07:00
Daniel Valdivia
f4d08a7502 Tenant Details (#162) 2020-06-08 13:37:14 -07:00
Daniel Valdivia
fb59e8c353 Remove Policy OPA (#164) 2020-06-04 18:37:59 -07:00
Daniel Valdivia
6e6ed300b7 Listen to all ips by default (#161) 2020-06-04 15:57:13 -07:00
Harshavardhana
48e6b1bb7c stick to go1.13 for now, update credits (#163)
fix release tags for mcs
2020-06-04 13:15:56 -07:00
César Nieto
8949fbe245 Integrate mkube storageclass api with UI (#156) 2020-06-04 11:22:33 -07:00
Daniel Valdivia
d8e6bd7f4a Fix Add Tenant Image and Delete Tenant URL (#155) 2020-06-04 11:00:28 -07:00
Alex
4edfeb22c6 Removed horizontal scrollbar in menu (#159) 2020-06-04 10:05:04 -07:00
Alex
2d5d0d16ca Changed menu design for mcs (#158)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-06-03 18:56:48 -07:00
César Nieto
16f8ee485a add logs to mkube api handler (#154) 2020-06-02 20:07:58 -07:00
Daniel Valdivia
2d28f8bf35 Pass Storage Class when adding a tenant (#153) 2020-06-02 13:24:07 -05:00
Daniel Valdivia
8af3665ae2 Connect List,Add Tenants (#148) 2020-06-02 11:52:37 -05:00
Daniel Valdivia
0fa1d4bf7c Update Menu with Tools section (#147)
Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
2020-05-28 15:03:29 -07:00
Daniel Valdivia
8139416323 Proxy API For Mkube (#145) 2020-05-27 15:46:18 -07:00
Alex
be5cd7f148 Added flag for operator only features (#144)
Added flag to only enable operator endpoints / links in mcs
2020-05-26 19:35:44 -07:00
César Nieto
fa068b6d4a Add admin heal api and ui (#142) 2020-05-26 17:28:14 -07:00
Alex
a805a49662 Added loaders to bucket information block (#141)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-22 22:46:42 -07:00
Daniel Valdivia
296e4ff5ce Set Policy For Groups (#140) 2020-05-22 16:09:24 -07:00
Alex
20749d2eae Implemented calculation for zone size in zone modal (#137) 2020-05-22 14:49:42 -05:00
Alex
ff4e959d11 Fixed styles in users policy modal (#139)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-22 12:36:41 -07:00
Daniel Valdivia
37195fefa8 Set Policy UI (#138) 2020-05-22 08:48:55 -07:00
Alex
13ef83cee4 Added Clusters mockups (#133) 2020-05-21 20:03:36 -05:00
Alex
b89b2d0c6a Changed bucket detail styles & minor fixes (#136)
Changed bucket detail styles & fixed minor issues for edit access policy & usage report not shown in page

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-21 17:19:50 -07:00
Daniel Valdivia
edf687fd8a Loading text on TableWrapper (#135) 2020-05-21 16:49:31 -07:00
Daniel Valdivia
cb60eba373 Landing images (#134) 2020-05-21 12:55:30 -07:00
101 changed files with 10255 additions and 4254 deletions

View File

@@ -3,3 +3,4 @@ dist/
target/
mcs
!mcs/
portal-ui/node_modules/

52
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 19 * * 0'
jobs:
CodeQL-Build:
# CodeQL runs on ubuntu-latest and windows-latest
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

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

View File

@@ -1,48 +0,0 @@
name: goreleaser
on:
pull_request:
push:
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Unshallow
run: git fetch --prune --unshallow
-
name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@53acad1befee355d46f71cccf6ab4d885eb4f77f
with:
version: latest
args: release --skip-publish --rm-dist --snapshot
-
name: Upload Win64 Binaries
uses: actions/upload-artifact@v1
if: success()
with:
name: MCS-Snapshot-Build-Win64
path: dist/mcs_windows_amd64
-
name: Upload Linux Binaries
uses: actions/upload-artifact@v1
if: success()
with:
name: MCS-Snapshot-Build-Linux-amd64
path: dist/mcs_linux_amd64
-
name: Upload MacOS Binaries
uses: actions/upload-artifact@v1
if: success()
with:
name: MCS-Snapshot-Build-MacOSX-amd64
path: dist/mcs_darwin_amd64

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ public.crt
# Ignore VsCode files
.vscode/
*.code-workspace
*~

View File

@@ -23,7 +23,7 @@ builds:
- -trimpath
- --tags=kqueue
ldflags:
- -s -w -X github.com/minio/mcs/pkg.ReleaseTag={{.Tag}} -X github.com/minio/warp/pkg.CommitID={{.FullCommit}} -X github.com/minio/warp/pkg.Version={{.Version}} -X github.com/minio/warp/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/warp/pkg.ReleaseTime={{.Date}}
- -s -w -X github.com/minio/mcs/pkg.ReleaseTag={{.Tag}} -X github.com/minio/mcs/pkg.CommitID={{.FullCommit}} -X github.com/minio/mcs/pkg.Version={{.Version}} -X github.com/minio/mcs/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/mcs/pkg.ReleaseTime={{.Date}}
archives:
-
replacements:

84
CREDITS
View File

@@ -20967,6 +20967,31 @@ THE SOFTWARE.
================================================================
go.uber.org/tools
https://go.uber.org/tools
----------------------------------------------------------------
Copyright (c) 2017 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================
go.uber.org/zap
https://go.uber.org/zap
----------------------------------------------------------------
@@ -21025,6 +21050,39 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/lint
https://golang.org/x/lint
----------------------------------------------------------------
Copyright (c) 2013 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/mod
https://golang.org/x/mod
----------------------------------------------------------------
@@ -23786,3 +23844,29 @@ https://gopkg.in/yaml.v2
================================================================
honnef.co/go/tools
https://honnef.co/go/tools
----------------------------------------------------------------
Copyright (c) 2016 Dominik Honnef
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================

View File

@@ -1,8 +1,9 @@
FROM golang:1.14.1
FROM golang:1.13
ADD go.mod /go/src/github.com/minio/mcs/go.mod
ADD go.sum /go/src/github.com/minio/mcs/go.sum
WORKDIR /go/src/github.com/minio/mcs/
# Get dependencies - will also be cached if we won't change mod/sum
RUN go mod download

View File

@@ -31,6 +31,8 @@ install: mcs
swagger-gen:
@echo "Generating swagger server code from yaml"
@rm -rf models
@rm -rf restapi/operations
@swagger generate server -A mcs --main-package=mcs --exclude-main -P models.Principal -f ./swagger.yml -r NOTICE
assets:

View File

@@ -2,6 +2,11 @@
A graphical user interface for [MinIO](https://github.com/minio/minio)
| Dashboard | Adding A User |
| ------------- | ------------- |
| ![Dashboard](images/pic1.png) | ![Dashboard](images/pic2.png) |
## Setup
All `mcs` needs is a MinIO user with admin privileges and URL pointing to your MinIO deployment.
@@ -57,7 +62,7 @@ Additionally, you can create policies to limit the privileges for `mcs` users, f
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:ServerInfo",
"admin:ServerInfo"
],
"Effect": "Allow",
"Sid": ""

View File

@@ -21,6 +21,7 @@ import (
"os"
"path/filepath"
"sort"
"time"
"github.com/minio/mcs/pkg"
@@ -103,10 +104,12 @@ func newApp(name string) *cli.App {
app := cli.NewApp()
app.Name = name
app.Version = pkg.Version
app.Version = pkg.Version + " - " + pkg.ShortCommitID
app.Author = "MinIO, Inc."
app.Usage = "mcs"
app.Usage = "MinIO Console Server"
app.Description = `MinIO Console Server`
app.Copyright = "(c) 2020 MinIO, Inc."
app.Compiled, _ = time.Parse(time.RFC3339, pkg.ReleaseTime)
app.Commands = commands
app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`.
app.CustomAppHelpTemplate = mcsHelpTemplate

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/minio/mcs
go 1.14
go 1.13
require (
github.com/coreos/go-oidc v2.2.1+incompatible

BIN
images/pic1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

BIN
images/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

View File

@@ -1,147 +0,0 @@
// 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"
)
// AddNotificationEndpoint add notification endpoint
//
// swagger:model addNotificationEndpoint
type AddNotificationEndpoint struct {
// account
Account string `json:"account,omitempty"`
// properties
Properties map[string]string `json:"properties,omitempty"`
// service
// Enum: [webhook amqp kafka mqtt nats nsq mysql postgres elasticsearch redis]
Service string `json:"service,omitempty"`
}
// Validate validates this add notification endpoint
func (m *AddNotificationEndpoint) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateService(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
var addNotificationEndpointTypeServicePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["webhook","amqp","kafka","mqtt","nats","nsq","mysql","postgres","elasticsearch","redis"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
addNotificationEndpointTypeServicePropEnum = append(addNotificationEndpointTypeServicePropEnum, v)
}
}
const (
// AddNotificationEndpointServiceWebhook captures enum value "webhook"
AddNotificationEndpointServiceWebhook string = "webhook"
// AddNotificationEndpointServiceAmqp captures enum value "amqp"
AddNotificationEndpointServiceAmqp string = "amqp"
// AddNotificationEndpointServiceKafka captures enum value "kafka"
AddNotificationEndpointServiceKafka string = "kafka"
// AddNotificationEndpointServiceMqtt captures enum value "mqtt"
AddNotificationEndpointServiceMqtt string = "mqtt"
// AddNotificationEndpointServiceNats captures enum value "nats"
AddNotificationEndpointServiceNats string = "nats"
// AddNotificationEndpointServiceNsq captures enum value "nsq"
AddNotificationEndpointServiceNsq string = "nsq"
// AddNotificationEndpointServiceMysql captures enum value "mysql"
AddNotificationEndpointServiceMysql string = "mysql"
// AddNotificationEndpointServicePostgres captures enum value "postgres"
AddNotificationEndpointServicePostgres string = "postgres"
// AddNotificationEndpointServiceElasticsearch captures enum value "elasticsearch"
AddNotificationEndpointServiceElasticsearch string = "elasticsearch"
// AddNotificationEndpointServiceRedis captures enum value "redis"
AddNotificationEndpointServiceRedis string = "redis"
)
// prop value enum
func (m *AddNotificationEndpoint) validateServiceEnum(path, location string, value string) error {
if err := validate.Enum(path, location, value, addNotificationEndpointTypeServicePropEnum); err != nil {
return err
}
return nil
}
func (m *AddNotificationEndpoint) 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
}
// MarshalBinary interface implementation
func (m *AddNotificationEndpoint) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *AddNotificationEndpoint) UnmarshalBinary(b []byte) error {
var res AddNotificationEndpoint
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -1,72 +0,0 @@
// 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"
)
// PolicyStatement policy statement
//
// swagger:model policyStatement
type PolicyStatement struct {
// action
Action []string `json:"Action"`
// condition
Condition string `json:"Condition,omitempty"`
// effect
Effect string `json:"Effect,omitempty"`
// resource
Resource string `json:"Resource,omitempty"`
// sid
Sid string `json:"Sid,omitempty"`
}
// Validate validates this policy statement
func (m *PolicyStatement) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PolicyStatement) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PolicyStatement) UnmarshalBinary(b []byte) error {
var res PolicyStatement
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -1,62 +0,0 @@
// 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"
)
// PolicyStatements policy statements
//
// swagger:model policyStatements
type PolicyStatements []*PolicyStatement
// Validate validates this policy statements
func (m PolicyStatements) Validate(formats strfmt.Registry) error {
var res []error
for i := 0; i < len(m); i++ {
if swag.IsZero(m[i]) { // not required
continue
}
if m[i] != nil {
if err := m[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName(strconv.Itoa(i))
}
return err
}
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

29
pkg/acl/config.go Normal file
View File

@@ -0,0 +1,29 @@
// 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 acl
import (
"strings"
"github.com/minio/minio/pkg/env"
)
// GetOperatorOnly gets mcs operator mode status set on env variable
//or default one
func GetOperatorOnly() string {
return strings.ToLower(env.Get(McsOperatorOnly, "off"))
}

21
pkg/acl/const.go Normal file
View File

@@ -0,0 +1,21 @@
// 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 acl
const (
McsOperatorOnly = "MCS_OPERATOR_ONLY"
)

View File

@@ -16,7 +16,9 @@
package acl
import iampolicy "github.com/minio/minio/pkg/iam/policy"
import (
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
// endpoints definition
var (
@@ -31,8 +33,11 @@ var (
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketDetails = "/buckets/:bucketName"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/tenants/:tenantName"
heal = "/heal"
)
type ConfigurationActionSet struct {
@@ -181,12 +186,28 @@ var bucketsActionSet = ConfigurationActionSet{
),
}
// serviceAccountsActionSet contains the list of admin actions required for this endpoint to work
// serviceAccountsActionSet no actions needed for this module to work
var serviceAccountsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// tenantsActionSet temporally no actions needed for tenants sections to work
var tenantsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// healActionSet contains the list of admin actions required for this endpoint to work
var healActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealAdminAction,
),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
@@ -200,10 +221,20 @@ var endpointRules = map[string]ConfigurationActionSet{
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketDetails: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode
var operatorRules = map[string]ConfigurationActionSet{
tenants: tenantsActionSet,
tenantsDetail: tenantsActionSet,
}
// operatorOnly ENV variable
var operatorOnly = GetOperatorOnly()
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format
//
// ie:
@@ -253,13 +284,19 @@ func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
// GetAuthorizedEndpoints return a list of allowed endpoint based on a provided *iampolicy.Policy
// ie: pages the user should have access based on his current privileges
func GetAuthorizedEndpoints(actions []string) []string {
rangeTake := endpointRules
if operatorOnly == "on" {
rangeTake = operatorRules
}
if len(actions) == 0 {
return []string{}
}
// Prepare new ActionSet structure that will hold all the user actions
userAllowedAction := actionsStringToActionSet(actions)
allowedEndpoints := []string{}
for endpoint, rules := range endpointRules {
for endpoint, rules := range rangeTake {
// check if user policy matches s3:* or admin:* typesIntersection
endpointActionTypes := rules.actionTypes
typesIntersection := endpointActionTypes.Intersection(userAllowedAction)

View File

@@ -23,15 +23,28 @@ import (
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
func TestGetAuthorizedEndpoints(t *testing.T) {
type args struct {
actions []string
type args struct {
actions []string
}
type endpoint struct {
name string
args args
want int
}
func validateEndpoints(t *testing.T, configs []endpoint) {
for _, tt := range configs {
t.Run(tt.name, func(t *testing.T) {
if got := GetAuthorizedEndpoints(tt.args.actions); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetAuthorizedEndpoints() = %v, want %v", len(got), tt.want)
}
})
}
tests := []struct {
name string
args args
want int
}{
}
func TestGetAuthorizedEndpoints(t *testing.T) {
tests := []endpoint{
{
name: "dashboard endpoint",
args: args{
@@ -59,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 10,
want: 11,
},
{
name: "all s3 endpoints",
@@ -78,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 13,
want: 14,
},
{
name: "no endpoints",
@@ -88,13 +101,52 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetAuthorizedEndpoints(tt.args.actions); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetAuthorizedEndpoints() = %v, want %v", len(got), tt.want)
}
})
validateEndpoints(t, tests)
}
func TestOperatorOnlyEndpoints(t *testing.T) {
operatorOnly = "on"
tests := []endpoint{
{
name: "Operator Only - all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 2,
},
{
name: "Operator Only - all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 2,
},
{
name: "Operator Only - all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 2,
},
{
name: "Operator Only - no endpoints",
args: args{
[]string{},
},
want: 0,
},
}
validateEndpoints(t, tests)
}
func TestGetActionsStringFromPolicy(t *testing.T) {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"@types/webpack-env": "^1.14.1",
"@types/websocket": "^1.0.0",
"ansi-to-react": "^6.0.5",
"chart.js": "^2.9.3",
"codemirror": "^5.52.2",
"history": "^4.10.1",
"local-storage-fallback": "^4.1.1",
@@ -28,6 +29,7 @@
"moment": "^2.24.0",
"npm": "^6.14.4",
"react": "^16.13.1",
"react-chartjs-2": "^2.9.0",
"react-codemirror2": "^7.1.0",
"react-dom": "^16.12.0",
"react-moment": "^0.9.7",

View File

@@ -25,6 +25,7 @@ export const units = [
"ZiB",
"YiB",
];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -57,3 +58,37 @@ export const timeFromDate = (d: Date) => {
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// units to be used in a dropdown
export const factorForDropdown = () => {
return units.map((unit) => {
return { label: unit, value: unit };
});
};
// units to be used in a dropdown
export const k8sfactorForDropdown = () => {
return k8sUnits.map((unit) => {
return { label: unit, value: unit };
});
};
//getBytes, converts from a value and a unit from units array to bytes
export const getBytes = (value: string, unit: string) => {
const vl: number = parseFloat(value);
const powFactor = units.findIndex((element) => element === unit);
if (powFactor == -1) {
return 0;
}
const factor = Math.pow(1024, powFactor);
const total = vl * factor;
return total.toString(10);
};
//getTotalSize gets the total size of a value & unit
export const getTotalSize = (value: string, unit: string) => {
const bytes = getBytes(value, unit).toString(10);
return niceBytes(bytes);
};

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
@@ -14,22 +14,18 @@
// 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 {SvgIcon} from "@material-ui/core";
import { SvgIcon } from "@material-ui/core";
class BucketsIcon extends React.Component {
render() {
return (<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<title>ic_h_buckets</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<polygon className="cls-1" points="13.428 16 2.572 16 0 0 16 0 13.428 16"/>
</g>
</g>
</svg>
</SvgIcon>)
}
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path d="M8.392,10H1.608L0,0H10Z" />
</svg>
</SvgIcon>
);
}
}
export default BucketsIcon;

View File

@@ -0,0 +1,123 @@
// 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 { SvgIcon } from "@material-ui/core";
class ClustersIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 9">
<g transform="translate(79 438.479)">
<g>
<g>
<rect x="-77.9" y="-434.5" width="7.8" height="1" />
</g>
</g>
<g>
<g>
<rect
x="-77.9"
y="-434.5"
transform="matrix(0.4999 -0.8661 0.8661 0.4999 338.8698 -281.1237)"
width="7.8"
height="1"
/>
</g>
</g>
<g>
<g>
<rect
x="-74.5"
y="-437.9"
transform="matrix(0.866 -0.5001 0.5001 0.866 207.1129 -95.1668)"
width="1"
height="7.8"
/>
</g>
</g>
<g>
<g>
<path
d="M-71.8-430.1h-4.5l-2.2-3.9l2.2-3.9h4.5l2.2,3.9L-71.8-430.1z M-75.7-431.1h3.3l1.7-2.9l-1.7-2.9h-3.3
l-1.7,2.9L-75.7-431.1z"
/>
</g>
</g>
<g>
<g>
<path
d="M-72.3-434c0,0.9-0.7,1.7-1.7,1.7c-0.9,0-1.7-0.7-1.7-1.7c0-0.9,0.7-1.7,1.7-1.7
C-73.1-435.7-72.3-434.9-72.3-434z"
/>
</g>
</g>
<g>
<g>
<path
d="M-76.8-434c0,0.6-0.5,1.1-1.1,1.1c0,0,0,0,0,0c-0.6,0-1.1-0.5-1.1-1.1c0,0,0,0,0,0c0-0.6,0.5-1.1,1.1-1.1
c0,0,0,0,0,0C-77.3-435.1-76.8-434.6-76.8-434C-76.8-434-76.8-434-76.8-434z"
/>
</g>
</g>
<g>
<g>
<path
d="M-69-434c0,0.6-0.5,1.1-1.1,1.1c0,0,0,0,0,0c-0.6,0-1.1-0.5-1.1-1.1c0,0,0,0,0,0c0-0.6,0.5-1.1,1.1-1.1
c0,0,0,0,0,0C-69.5-435.1-69-434.6-69-434C-69-434-69-434-69-434z"
/>
</g>
</g>
<g>
<g>
<path
d="M-75.4-431.6c0.5,0.3,0.7,1,0.4,1.5c-0.3,0.5-1,0.7-1.5,0.4c0,0,0,0,0,0c-0.5-0.3-0.7-1-0.4-1.5
C-76.6-431.7-75.9-431.9-75.4-431.6C-75.4-431.6-75.4-431.6-75.4-431.6z"
/>
</g>
</g>
<g>
<g>
<path
d="M-71.5-438.3c0.5,0.3,0.7,1,0.4,1.5c-0.3,0.5-1,0.7-1.5,0.4c0,0,0,0,0,0c-0.5-0.3-0.7-1-0.4-1.5
C-72.7-438.5-72-438.6-71.5-438.3C-71.5-438.3-71.5-438.3-71.5-438.3z"
/>
</g>
</g>
<g>
<g>
<path
d="M-72.6-431.6c0.5-0.3,1.2-0.1,1.5,0.4c0,0,0,0,0,0c0.3,0.5,0.1,1.2-0.4,1.5c-0.5,0.3-1.2,0.1-1.5-0.4
c0,0,0,0,0,0C-73.3-430.6-73.1-431.3-72.6-431.6z"
/>
</g>
</g>
<g>
<g>
<path
d="M-76.5-438.3c0.5-0.3,1.2-0.1,1.5,0.4c0,0,0,0,0,0c0.3,0.5,0.1,1.2-0.4,1.5c-0.5,0.3-1.2,0.1-1.5-0.4
c0,0,0,0,0,0C-77.2-437.3-77-438-76.5-438.3z"
/>
</g>
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default ClustersIcon;

View File

@@ -0,0 +1,42 @@
// 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 { SvgIcon } from "@material-ui/core";
class ConfigurationsListIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<rect width="1.433" height="1" />
<rect width="7.828" height="1" transform="translate(2.172)" />
<rect width="1.433" height="1" transform="translate(0 6)" />
<rect width="1.433" height="1" transform="translate(0 3)" />
<rect width="1.433" height="1" transform="translate(0 9)" />
<rect width="1.368" height="0.569" transform="translate(6.316 9)" />
<path d="M5.566,9.569v-.31l-.238-.138-.269.155-.65.375L4.034,9V9H2.172v1H5.566Z" />
<path d="M9.966,9l-.375.65-.65-.375-.269-.155-.238.138V10H10V9H9.967Z" />
<path d="M3.625,6.793l.269-.155V6.362l-.269-.155L3.266,6H2.172V7H3.266Z" />
<path d="M8.434,3.431v.31l.238.138.269-.155.649-.375L9.966,4V4H10V3H8.434Z" />
<path d="M4.034,4l.375-.65.65.375.269.155.238-.138V3H2.172V4H4.033Z" />
<path d="M9.356,5.929,10,5.558,9.316,4.373l-.644.372-.988-.571V3.431H6.316v.743l-.988.571-.644-.372L4,5.558l.644.371V7.071L4,7.442l.684,1.185.644-.372.988.571v.743H7.684V8.826l.988-.571.644.372L10,7.442l-.644-.371ZM7,7.278A.778.778,0,1,1,7.778,6.5.779.779,0,0,1,7,7.278Z" />
</svg>
</SvgIcon>
);
}
}
export default ConfigurationsListIcon;

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
@@ -20,15 +20,24 @@ class DashboardIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<title>ic_h_dashboard</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect className="cls-1" x="9" width="7" height="7" />
<rect className="cls-1" width="7" height="7" />
<rect className="cls-1" x="9" y="9" width="7" height="7" />
<rect className="cls-1" y="9" width="7" height="7" />
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(249 720)">
<rect
width="6"
height="5"
transform="translate(-244 -720) rotate(90)"
/>
<rect width="4" height="4" transform="translate(-243 -720)" />
<rect
width="5"
height="4"
transform="translate(-239 -715) rotate(90)"
/>
<rect
width="5"
height="3"
transform="translate(-244 -710) rotate(180)"
/>
</g>
</svg>
</SvgIcon>

View File

@@ -0,0 +1,41 @@
// 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 { SvgIcon } from "@material-ui/core";
class GroupsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 9.787">
<g transform="translate(177 719.787)">
<g transform="translate(-105 -720)">
<path d="M-65,5a3,3,0,0,0-1.131.224A3.981,3.981,0,0,1-65,8v2h3V8A3,3,0,0,0-65,5Z" />
<path d="M-72,10h6V8a3,3,0,0,0-3-3,3,3,0,0,0-3,3Z" />
<path
className="a"
d="M-65,.213a1.993,1.993,0,0,0-1.384.561A2.967,2.967,0,0,1-66,2.213a2.964,2.964,0,0,1-.384,1.439A1.989,1.989,0,0,0-65,4.213a2,2,0,0,0,2-2A2,2,0,0,0-65,.213Z"
/>
<circle cx="2" cy="2" r="2" transform="translate(-71 0.213)" />
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default GroupsIcon;

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/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
class BucketsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8.75 10">
<path
d="M-44.625,10l-4.353-2.419L-53.375,10V0h8.75Z"
transform="translate(53.375)"
/>
</svg>
</SvgIcon>
);
}
}
export default BucketsIcon;

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/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
class LambdaNotificationsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path
d="M0,0v10l2.8-2.2H10V0H0z M6.6,6L5.6,6.4l-0.8-2l-1.5,2L2.5,5.9l1.9-2.6L4.1,2.4H3.2v-1h1.5l1.4,3.7l0.9-0.4
l0.4,0.9L6.6,6z"
/>
</svg>
</SvgIcon>
);
}
}
export default LambdaNotificationsIcon;

View File

@@ -0,0 +1,35 @@
// 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 { SvgIcon } from "@material-ui/core";
class MirroringIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(61 439)">
<rect width="1.5" height="10" transform="translate(-56.75 -439)" />
<path d="M6.5,10V0h.572L10,10Z" transform="translate(-61 -439)" />
<path d="M3.5,10V0H2.928L0,10Z" transform="translate(-61 -439)" />
</g>
</svg>
</SvgIcon>
);
}
}
export default MirroringIcon;

View File

@@ -0,0 +1,41 @@
// 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 { SvgIcon } from "@material-ui/core";
class ServiceAccountsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 9.5">
<g transform="translate(231 719.516)">
<path
d="M-125.5,7.984a4.5,4.5,0,0,1,4.5-4.5,4.5,4.5,0,0,1,4.5,4.5Z"
transform="translate(-105 -720)"
/>
<rect width="10" height="1" transform="translate(-231 -711.016)" />
<path
d="M-119.5.484h-3v1h1v1h1v-1h1Z"
transform="translate(-105 -720)"
/>
</g>
</svg>
</SvgIcon>
);
}
}
export default ServiceAccountsIcon;

View File

@@ -0,0 +1,62 @@
// 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 { SvgIcon } from "@material-ui/core";
class TraceIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9.998 10">
<g transform="translate(140.999 720)">
<g transform="translate(-105 -720)">
<rect
width="1.114"
height="1.667"
transform="translate(-27.116 8.333)"
/>
<path d="M-28.184,10H-29.3V8.154l2.182-3.037V3.147H-26V5.476l-2.182,3.037Z" />
<rect
width="1.114"
height="2.963"
transform="translate(-31.531)"
/>
<rect
width="1.114"
height="2.132"
transform="translate(-27.115 0)"
/>
<rect
width="1.114"
height="5.389"
transform="translate(-29.298)"
/>
<path d="M-30.417,10h-1.114V5.722l-2.233-3V0h1.114V2.353l2.233,3Z" />
<path d="M-32.65,10h-1.114V6.185l-2.234-3V0h1.114V2.815l2.234,3Z" />
<rect
width="1.114"
height="4.463"
transform="translate(-35.999 5.537)"
/>
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default TraceIcon;

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
@@ -14,27 +14,32 @@
// 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 {SvgIcon} from "@material-ui/core";
import { SvgIcon } from "@material-ui/core";
class UsersIcon extends React.Component {
render() {
return (<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14.874">
<title>ic_users</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path className="cls-1"
d="M3.5,6.875h0a3.5,3.5,0,0,1,3.5,3.5v4.5a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0v-4.5A3.5,3.5,0,0,1,3.5,6.875Z"/>
<path className="cls-1"
d="M12.5,6.875h0a3.5,3.5,0,0,1,3.5,3.5v4.5a0,0,0,0,1,0,0H9a0,0,0,0,1,0,0v-4.5A3.5,3.5,0,0,1,12.5,6.875Z"/>
<circle className="cls-1" cx="3.498" cy="2.859" r="2.859"/>
<circle className="cls-1" cx="12.502" cy="2.859" r="2.859"/>
</g>
</g>
</svg>
</SvgIcon>)
}
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6.131 10">
<g transform="translate(193 719.787)">
<g transform="translate(-193 -719.787)">
<path
d="M3,0h.131a3,3,0,0,1,3,3V5a0,0,0,0,1,0,0H0A0,0,0,0,1,0,5V3A3,3,0,0,1,3,0Z"
transform="translate(0 5)"
/>
<ellipse
cx="2.065"
cy="2"
rx="2.065"
ry="2"
transform="translate(1 0)"
/>
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default UsersIcon;

View File

@@ -0,0 +1,39 @@
// 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 { SvgIcon } from "@material-ui/core";
class WarpIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(43 439)">
<path d="M27.5,10" transform="translate(-61 -439)" />
<rect width="1.5" height="2" transform="translate(-43 -431)" />
<rect width="1.5" height="6" transform="translate(-38.75 -435)" />
<rect width="1.5" height="8" transform="translate(-36.625 -437)" />
<rect width="1.5" height="4" transform="translate(-40.875 -433)" />
<rect width="1.5" height="10" transform="translate(-34.5 -439)" />
<path d="M18.5,10" transform="translate(-61 -439)" />
</g>
</svg>
</SvgIcon>
);
}
}
export default WarpIcon;

View File

@@ -0,0 +1,59 @@
// 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 { SvgIcon } from "@material-ui/core";
class WatchIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(213 720)">
<g transform="translate(-105 -720)">
<rect width="1.5" height="4" transform="translate(-108)" />
<rect width="1.5" height="4" transform="translate(-108 6)" />
<rect width="1.5" height="4" transform="translate(-99.5 6)" />
<rect width="1.5" height="4" transform="translate(-99.5)" />
<rect
width="1.5"
height="4"
transform="translate(-98) rotate(90)"
/>
<rect
width="1.5"
height="4"
transform="translate(-104) rotate(90)"
/>
<rect
width="1.5"
height="4"
transform="translate(-104 8.5) rotate(90)"
/>
<rect
width="1.5"
height="4"
transform="translate(-98 8.5) rotate(90)"
/>
<circle cx="2" cy="2" r="2" transform="translate(-105 3)" />
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default WatchIcon;

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
@@ -14,10 +14,20 @@
// 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 { default as PermissionIcon } from './PermissionIcon';
export { default as CreateIcon } from './CreateIcon';
export { default as DeleteIcon } from './DeleteIcon';
export { default as ServiceAccountIcon } from './ServiceAccountIcon';
export { default as DashboardIcon } from './DashboardIcon';
export { default as BucketsIcon } from './BucketsIcon';
export { default as UsersIcon } from './UsersIcon';
export { default as PermissionIcon } from "./PermissionIcon";
export { default as CreateIcon } from "./CreateIcon";
export { default as DeleteIcon } from "./DeleteIcon";
export { default as ServiceAccountIcon } from "./ServiceAccountIcon";
export { default as DashboardIcon } from "./DashboardIcon";
export { default as BucketsIcon } from "./BucketsIcon";
export { default as UsersIcon } from "./UsersIcon";
export { default as ServiceAccountsIcon } from "./ServiceAccountsIcon";
export { default as GroupsIcon } from "./GroupsIcon";
export { default as IAMPoliciesIcon } from "./IAMPoliciesIcon";
export { default as TraceIcon } from "./TraceIcon";
export { default as LambdaNotificationsIcon } from "./LambdaNotificationsIcon";
export { default as ConfigurationsListIcon } from "./ConfigurationsListIcon";
export { default as ClustersIcon } from "./ClustersIcon";
export { default as MirroringIcon } from "./MirroringIcon";
export { default as WarpIcon } from "./WarpIcon";
export { default as WatchIcon } from "./WatchIcon";

View File

@@ -35,6 +35,7 @@ interface ISetAccessPolicyProps {
classes: any;
open: boolean;
bucketName: string;
actualPolicy: string;
closeModalAndRefresh: () => void;
}
@@ -86,8 +87,14 @@ class SetAccessPolicy extends React.Component<
});
}
componentDidMount() {
const { actualPolicy } = this.props;
this.setState({ accessPolicy: actualPolicy });
}
render() {
const { classes, open } = this.props;
const { classes, open, actualPolicy } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
return (
<ModalWrapper

View File

@@ -17,9 +17,13 @@
import React from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
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 } from "../types";
import { BucketEvent, BucketEventList, BucketInfo, BucketList } from "../types";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import SetAccessPolicy from "./SetAccessPolicy";
@@ -28,6 +32,7 @@ import { CreateIcon } from "../../../../icons";
import AddEvent from "./AddEvent";
import DeleteEvent from "./DeleteEvent";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
@@ -76,6 +81,42 @@ const styles = (theme: Theme) =>
textAlign: "center",
padding: "20px",
},
gridContainer: {
display: "grid",
gridTemplateColumns: "auto auto",
gridGap: 8,
justifyContent: "flex-start",
alignItems: "center",
"& div:not(.MuiCircularProgress-root)": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
minWidth: 150,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
paperContainer: {
padding: 15,
paddingLeft: 23,
},
headerContainer: {
display: "flex",
justifyContent: "space-between",
},
capitalizeFirst: {
textTransform: "capitalize",
},
});
interface IViewBucketProps {
@@ -87,9 +128,12 @@ interface IViewBucketState {
info: BucketInfo | null;
records: BucketEvent[];
totalRecords: number;
loading: boolean;
loadingBucket: boolean;
loadingEvents: boolean;
loadingSize: boolean;
error: string;
deleteError: string;
errBucket: string;
setAccessPolicyScreenOpen: boolean;
page: number;
rowsPerPage: number;
@@ -97,6 +141,8 @@ interface IViewBucketState {
deleteOpen: boolean;
selectedBucket: string;
selectedEvent: BucketEvent | null;
bucketSize: string;
errorSize: string;
}
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
@@ -104,9 +150,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
info: null,
records: [],
totalRecords: 0,
loading: false,
loadingBucket: true,
loadingEvents: true,
loadingSize: true,
error: "",
deleteError: "",
errBucket: "",
setAccessPolicyScreenOpen: false,
page: 0,
rowsPerPage: 10,
@@ -114,10 +163,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
deleteOpen: false,
selectedBucket: "",
selectedEvent: null,
bucketSize: "0",
errorSize: "",
};
fetchEvents() {
this.setState({ loading: true }, () => {
this.setState({ loadingBucket: true }, () => {
const { page } = this.state;
const { match } = this.props;
const bucketName = match.params["bucketName"];
@@ -128,7 +179,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
const total = get(res, "total", 0);
this.setState({
loading: false,
loadingEvents: false,
records: events || [],
totalRecords: total,
error: "",
@@ -142,7 +193,50 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
}
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
this.setState({ loadingEvents: false, error: err });
});
});
}
fetchBucketsSize() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
this.setState({ loadingSize: true }, () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
const resBuckets = get(res, "buckets", []);
const bucketInfo = resBuckets.find(
(bucket) => bucket.name === bucketName
);
const size = get(bucketInfo, "size", "0");
this.setState({
loadingSize: false,
errorSize: "",
bucketSize: size,
});
})
.catch((err: any) => {
this.setState({ loadingSize: false, errorSize: err });
});
});
}
loadInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
this.setState({ loadingBucket: true }, () => {
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
this.setState({ loadingBucket: false, info: res });
})
.catch((err) => {
this.setState({ loadingBucket: false, errBucket: err });
});
});
}
@@ -161,20 +255,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
});
}
loadInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
this.setState({ info: res });
})
.catch((err) => {});
}
componentDidMount(): void {
this.loadInfo();
this.fetchEvents();
this.fetchBucketsSize();
}
bucketFilter(): void {}
@@ -186,12 +270,15 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
records,
totalRecords,
setAccessPolicyScreenOpen,
loading,
loadingEvents,
loadingBucket,
page,
rowsPerPage,
deleteOpen,
addScreenOpen,
selectedEvent,
bucketSize,
loadingSize,
} = this.state;
const offset = page * rowsPerPage;
@@ -242,6 +329,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
actualPolicy={accessPolicy}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
@@ -258,36 +346,75 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<br />
</Grid>
<Grid item xs={12}>
Access Policy: {accessPolicy}
{" "}
<Button
variant="contained"
size="small"
color="primary"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
<br />
Reported Usage: 0 bytes
<br />
<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}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
size="medium"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
</div>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Typography variant="h6">Events</Typography>
<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"
size="small"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
@@ -313,7 +440,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loading}
isLoading={loadingEvents}
records={filteredRecords}
entityName="Events"
idField="id"

View File

@@ -0,0 +1,107 @@
// 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 {
Checkbox,
Grid,
InputLabel,
TextField,
TextFieldProps,
Tooltip,
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
import {
checkboxIcons,
fieldBasic,
tooltipHelper,
} from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface CheckBoxProps {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
index?: number;
checked: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
...checkboxIcons,
labelContainer: {
flexGrow: 1,
},
});
const CheckboxWrapper = ({
label,
onChange,
value,
id,
name,
checked = false,
disabled = false,
tooltip = "",
classes,
}: CheckBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.labelContainer}>
<Checkbox
name={name}
id={id}
value={value}
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={checked}
onChange={onChange}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
disabled={disabled}
/>
</div>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(CheckboxWrapper);

View File

@@ -44,6 +44,9 @@ interface InputBoxProps {
tooltip?: string;
autoComplete?: string;
index?: number;
error?: string;
required?: boolean;
placeholder?: string;
}
const styles = (theme: Theme) =>
@@ -52,6 +55,14 @@ const styles = (theme: Theme) =>
...tooltipHelper,
textBoxContainer: {
flexGrow: 1,
position: "relative",
},
errorState: {
color: "#b53b4b",
fontSize: 14,
position: "absolute",
top: 7,
right: 7,
},
});
@@ -66,6 +77,10 @@ const inputStyles = makeStyles((theme: Theme) =>
color: "#393939",
fontSize: 14,
},
error: {
color: "#b53b4b",
boxShadow: "inset 0px 0px 1px 1px #b53b4b",
},
})
);
@@ -92,14 +107,31 @@ const InputBoxWrapper = ({
multiline = false,
tooltip = "",
index = 0,
error = "",
required = false,
placeholder = "",
classes,
}: InputBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<Grid
item
xs={12}
className={`${classes.fieldContainer} ${
error !== "" ? classes.errorInField : ""
}`}
>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
<InputLabel
htmlFor={id}
className={`${error !== "" ? classes.fieldLabelError : ""} ${
classes.inputLabel
}`}
>
<span>
{label}
{required ? "*" : ""}
</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
@@ -112,7 +144,6 @@ const InputBoxWrapper = ({
<div className={classes.textBoxContainer}>
<InputField
className={classes.boxDesign}
id={id}
name={name}
variant="outlined"
@@ -124,6 +155,9 @@ const InputBoxWrapper = ({
multiline={multiline}
autoComplete={autoComplete}
inputProps={{ "data-index": index }}
error={error !== ""}
helperText={error}
placeholder={placeholder}
/>
</div>
</Grid>

View File

@@ -92,16 +92,18 @@ const SelectWrapper = ({
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<FormControl variant="outlined" fullWidth>
<Select
id={id}

View File

@@ -33,6 +33,9 @@ export const fieldBasic = {
alignItems: "center",
},
},
fieldLabelError: {
paddingBottom: 22,
},
fieldContainer: {
display: "flex",
alignItems: "center",
@@ -61,3 +64,18 @@ export const tooltipHelper = {
fontSize: 18,
},
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
};
export const checkboxIcons = {
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
},
};

View File

@@ -0,0 +1,128 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { IWizardMain } from "./types";
import WizardPage from "./WizardPage";
const styles = (theme: Theme) =>
createStyles({
wizardMain: {
display: "flex",
width: "100%",
flexGrow: 1,
},
wizardSteps: {
marginRight: 10,
"& ul": {
padding: 15,
"& li": {
listStyle: "lower-roman",
marginBottom: 12,
},
},
},
buttonList: {
backgroundColor: "transparent",
border: "none",
cursor: "pointer",
"&:not(:disabled):hover": {
textDecoration: "underline",
},
"&:selected, &:active, &:focus, &:focus:active": {
border: "none",
outline: 0,
boxShadow: "none",
},
},
wizardContainer: {
flexGrow: 1,
},
});
const GenericWizard = ({ classes, wizardSteps }: IWizardMain) => {
const [currentStep, setCurrentStep] = useState<number>(0);
const pageChange = (toElement: string | number) => {
const lastPage = wizardSteps.length - 1;
if (toElement === "++") {
let nextPage = currentStep + 1;
if (nextPage > lastPage) {
nextPage = lastPage;
}
setCurrentStep(nextPage);
}
if (toElement === "--") {
let prevPage = currentStep - 1;
if (prevPage < 0) {
prevPage = 0;
}
setCurrentStep(prevPage);
}
if (typeof toElement === "number") {
let pg = toElement;
if (toElement < 0) {
pg = 0;
}
if (toElement > lastPage) {
pg = lastPage;
}
setCurrentStep(pg);
}
};
if (wizardSteps.length === 0) {
return null;
}
return (
<div className={classes.wizardMain}>
<div className={classes.wizardSteps}>
<ul>
{wizardSteps.map((step, index) => {
return (
<li key={`wizard-${index.toString()}`}>
<button
onClick={() => pageChange(index)}
disabled={index > currentStep}
className={classes.buttonList}
>
{step.label}
</button>
</li>
);
})}
</ul>
</div>
<div className={classes.wizardContainer}>
<WizardPage page={wizardSteps[currentStep]} pageChange={pageChange} />
</div>
</div>
);
};
export default withStyles(styles)(GenericWizard);

View File

@@ -0,0 +1,89 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { IWizardButton, IWizardPage } from "./types";
import { Button } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
wizardStepContainer: {
display: "flex",
flexDirection: "column",
},
wizardComponent: {
height: 375,
overflowY: "auto",
marginBottom: 10,
},
buttonsContainer: {
display: "flex",
flexDirection: "row",
justifyContent: "flex-end" as const,
"& button": {
marginLeft: 10,
},
},
});
const WizardPage = ({ classes, page, pageChange }: IWizardPage) => {
const buttonAction = (btn: IWizardButton) => {
switch (btn.type) {
case "next":
pageChange("++");
break;
case "back":
pageChange("--");
break;
case "to":
pageChange(btn.toPage || 0);
default:
}
if (btn.action) {
btn.action();
}
};
console.log("buttons", page);
return (
<div className={classes.wizardStepContainer}>
<div className={classes.wizardComponent}>{page.componentRender}</div>
<div className={classes.buttonsContainer}>
{page.buttons.map((btn) => {
return (
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {
buttonAction(btn);
}}
disabled={!btn.enabled}
key={`button-${page.label}-${btn.label}`}
>
{btn.label}
</Button>
);
})}
</div>
</div>
);
};
export default withStyles(styles)(WizardPage);

View File

@@ -0,0 +1,40 @@
// 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 IWizardButton {
label: string;
type: string;
action?: () => void;
enabled?: boolean;
toPage?: number;
}
export interface IWizardElement {
label: string;
componentRender: any;
buttons: IWizardButton[];
}
export interface IWizardMain {
classes: any;
wizardSteps: IWizardElement[];
}
export interface IWizardPage {
classes: any;
page: IWizardElement;
pageChange: (to: string | number) => void;
}

View File

@@ -19,6 +19,7 @@ import { IconButton } from "@material-ui/core";
import ViewIcon from "./TableActionIcons/ViewIcon";
import PencilIcon from "./TableActionIcons/PencilIcon";
import DeleteIcon from "./TableActionIcons/DeleteIcon";
import DescriptionIcon from "./TableActionIcons/DescriptionIcon";
import { Link } from "react-router-dom";
interface IActionButton {
@@ -39,6 +40,8 @@ const defineIcon = (type: string, selected: boolean) => {
return <PencilIcon active={selected} />;
case "delete":
return <DeleteIcon active={selected} />;
case "description":
return <DescriptionIcon active={selected} />;
}
return null;
@@ -51,13 +54,14 @@ const TableActionButton = ({
idField,
selected,
to,
sendOnlyId = false
sendOnlyId = false,
}: IActionButton) => {
const valueClick = sendOnlyId ? valueToSend[idField] : valueToSend;
const buttonElement = (
<IconButton
aria-label={type}
size={"small"}
onClick={
onClick
? () => {

View File

@@ -0,0 +1,20 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const PencilIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill={active ? selected : unSelected}
d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"
></path>
</svg>
);
};
export default PencilIcon;

View File

@@ -27,10 +27,12 @@ import {
Paper,
Grid,
Checkbox,
Typography,
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
import { checkboxIcons } from "../FormComponents/common/styleLibrary";
//Interfaces for table Items
@@ -87,12 +89,6 @@ const rowText = {
borderColor: borderColor,
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
@@ -103,6 +99,7 @@ const styles = (theme: Theme) =>
overflow: "auto",
flexDirection: "column",
padding: "19px 38px",
minHeight: "200px",
},
minTableHeader: {
color: "#393939",
@@ -133,21 +130,20 @@ const styles = (theme: Theme) =>
},
},
actionsContainer: {
width: 120,
width: 150,
borderColor: borderColor,
},
paginatorComponent: {
borderBottom: 0,
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
},
checkBoxRow: {
borderColor: borderColor,
},
loadingBox: {
paddingTop: "100px",
paddingBottom: "100px",
},
...checkboxIcons,
});
// Function that renders Title Columns
@@ -226,7 +222,16 @@ const TableWrapper = ({
return (
<Grid item xs={12}>
<Paper className={classes.paper}>
{isLoading && <LinearProgress />}
{isLoading && (
<Grid container className={classes.loadingBox}>
<Grid item xs={12} style={{ textAlign: "center" }}>
<Typography component="h3">Loading...</Typography>
</Grid>
<Grid item xs={12}>
<LinearProgress />
</Grid>
</Grid>
)}
{records && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
@@ -298,7 +303,9 @@ const TableWrapper = ({
</TableBody>
</Table>
) : (
<div>{`There are no ${entityName} yet.`}</div>
<React.Fragment>
{!isLoading && <div>{`There are no ${entityName} yet.`}</div>}
</React.Fragment>
)}
</Paper>
{paginatorConfig && (

View File

@@ -42,10 +42,6 @@ export const configurationElements: IConfigurationElement[] = [
configuration_id: "identity_ldap",
configuration_label: "Identity LDAP Configuration",
},
{
configuration_id: "policy_opa",
configuration_label: "Policy OPA Configuration",
},
{
configuration_id: "kms_vault",
configuration_label: "KMS Vault Configuration",
@@ -312,26 +308,6 @@ export const fieldsConfigurations: any = {
multiline: true,
},
],
policy_opa: [
{
name: "url",
required: true,
label: "OPA URL",
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string",
},
{
name: "policy_opa",
required: true,
label: "Policy OPA",
type: "string",
},
],
kms_vault: [],
kms_kes: [],
logger_webhook: [

View File

@@ -49,7 +49,7 @@ import Buckets from "./Buckets/Buckets";
import Policies from "./Policies/Policies";
import Permissions from "./Permissions/Permissions";
import Dashboard from "./Dashboard/Dashboard";
import Menu from "./Menu";
import Menu from "./Menu/Menu";
import api from "../../common/api";
import storage from "local-storage-fallback";
import NotFoundPage from "../NotFoundPage";
@@ -62,9 +62,12 @@ import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import ListTenants from "./Tenants/ListTenants/ListTenants";
import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
function Copyright() {
return (
@@ -130,6 +133,7 @@ const styles = (theme: Theme) =>
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: "hidden",
},
drawerPaperClose: {
overflowX: "hidden",
@@ -269,6 +273,10 @@ const Console = ({
component: Logs,
path: "/logs",
},
{
component: Heal,
path: "/heal",
},
{
component: ListNotificationEndpoints,
path: "/notification-endpoints",
@@ -293,6 +301,14 @@ const Console = ({
component: WebhookPanel,
path: "/webhook/audit",
},
{
component: ListTenants,
path: "/tenants",
},
{
component: TenantDetails,
path: "/tenants/:tenantName",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);

View File

@@ -30,6 +30,7 @@ import { stringSort } from "../../../utils/sortFunctions";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
interface IGroupsProps {
classes: any;
@@ -96,6 +97,7 @@ const Groups = ({ classes }: IGroupsProps) => {
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
@@ -176,8 +178,14 @@ const Groups = ({ classes }: IGroupsProps) => {
setSelectedGroup(group);
};
const setPolicyAction = (selectionElement: any): void => {
setPolicyOpen(true);
setSelectedGroup(selectionElement);
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
@@ -197,6 +205,16 @@ const Groups = ({ classes }: IGroupsProps) => {
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{setPolicyOpen && (
<SetPolicy
open={policyOpen}
selectedGroup={selectedGroup}
selectedUser={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Groups</Typography>

View File

@@ -0,0 +1,327 @@
import { HorizontalBar } from "react-chartjs-2";
import React, { useEffect, useState } from "react";
import {
Button,
Grid,
Typography,
TextField,
Checkbox,
} from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api";
import { FormControl, MenuItem, Select } from "@material-ui/core";
import { BucketList, Bucket } from "../Watch/types";
import { HealStatus, colorH } from "./types";
import { niceBytes } from "../../../common/utils";
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",
},
});
interface IHeal {
classes: any;
}
const Heal = ({ classes }: IHeal) => {
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
const [bucketList, setBucketList] = useState<Bucket[]>([]);
const [prefix, setPrefix] = useState("");
const [recursive, setRecursive] = useState(false);
const [forceStart, setForceStart] = useState(false);
const [forceStop, setForceStop] = useState(false);
// healStatus states
const [hStatus, setHStatus] = useState({
beforeHeal: [0, 0, 0, 0],
afterHeal: [0, 0, 0, 0],
objectsHealed: 0,
objectsScanned: 0,
healDuration: 0,
sizeScanned: "",
});
const fetchBucketList = () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
let buckets: Bucket[] = [];
if (res.buckets !== null) {
buckets = res.buckets;
}
setBucketList(buckets);
})
.catch((err: any) => {
console.log(err);
});
};
useEffect(() => {
fetchBucketList();
}, []);
// forceStart and forceStop need to be mutually exclusive
useEffect(() => {
if (forceStart === true) {
setForceStop(false);
}
}, [forceStart]);
useEffect(() => {
if (forceStop === true) {
setForceStart(false);
}
}, [forceStop]);
const colorHealthArr = (color: colorH) => {
return [color.Green, color.Yellow, color.Red, color.Grey];
};
useEffect(() => {
// begin watch if bucketName in bucketList and start pressed
if (start) {
// values stored here to update chart
const cB: colorH = { Green: 0, Yellow: 0, Red: 0, Grey: 0 };
const cA: colorH = { Green: 0, Yellow: 0, Red: 0, Grey: 0 };
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/heal/${bucketName}?prefix=${prefix}&recursive=${recursive}&force-start=${forceStart}&force-stop=${forceStop}`
);
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
};
c.onmessage = (message: IMessageEvent) => {
let m: HealStatus = JSON.parse(message.data.toString());
// Store percentage per health color
for (const [key, value] of Object.entries(m.healthAfterCols)) {
cA[key] = (value * 100) / m.itemsScanned;
}
for (const [key, value] of Object.entries(m.healthBeforeCols)) {
cB[key] = (value * 100) / m.itemsScanned;
}
setHStatus({
beforeHeal: colorHealthArr(cB),
afterHeal: colorHealthArr(cA),
objectsHealed: m.objectsHealed,
objectsScanned: m.objectsScanned,
healDuration: m.healDuration,
sizeScanned: niceBytes(m.bytesScanned.toString()),
});
};
c.onclose = () => {
setStart(false);
console.log("connection closed by server");
};
return () => {
// close websocket on useEffect cleanup
c.close(1000);
console.log("closing websockets");
};
}
}
}, [start]);
let data = {
labels: ["Green", "Yellow", "Red", "Grey"],
datasets: [
{
label: "After Healing",
data: hStatus.afterHeal,
backgroundColor: "rgba(0, 0, 255, 0.2)",
borderColor: "rgba(54, 162, 235, 1)",
borderWidth: 1,
},
{
label: "Before Healing",
data: hStatus.beforeHeal,
backgroundColor: "rgba(153, 102, 255, 0.2)",
borderColor: "rgba(153, 102, 255, 1)",
borderWidth: 1,
},
],
};
const bucketNames = bucketList.map((bucketName) => ({
label: bucketName.name,
value: bucketName.name,
}));
return (
<React.Fragment>
<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}
</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",
},
}}
/>
<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>
);
};
export default withStyles(styles)(Heal);

View File

@@ -0,0 +1,64 @@
// 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 HealDriveInfo {
uuid: string;
endpoint: string;
state: string;
}
export interface MomentHealth {
color: string;
offline: number;
online: number;
missing: number;
corrupted: number;
drives: HealDriveInfo[];
}
export interface HealItemStatus {
status: string;
error: string;
type: string;
name: string;
before: MomentHealth;
after: MomentHealth;
size: number;
}
export interface HealStatus {
healDuration: number;
bytesScanned: number;
objectsScanned: number;
itemsScanned: number;
// Counters for healed objects and all kinds of healed items
objectsHealed: number;
itemsHealed: number;
itemsHealthStatus: HealItemStatus[];
// Map of health color code to number of objects with that
// health color code.
healthBeforeCols: Map<string, number>;
healthAfterCols: Map<string, number>;
}
// colorH used to save health's percentage per color
export interface colorH {
[Green: string]: number;
Yellow: number;
Red: number;
Grey: number;
}

View File

@@ -1,291 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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 ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import RoomServiceIcon from "@material-ui/icons/RoomService";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from "@material-ui/icons/CenterFocusWeak";
import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core";
import { ExitToApp } from "@material-ui/icons";
import { AppState } from "../../store";
import { connect } from "react-redux";
import { userLoggedIn } from "../../actions";
import List from "@material-ui/core/List";
import storage from "local-storage-fallback";
import history from "../../history";
import logo from "../../icons/minio_console_logo.svg";
import {
BucketsIcon,
DashboardIcon,
PermissionIcon,
UsersIcon,
} from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person";
import api from "../../common/api";
import NotificationsIcon from "@material-ui/icons/Notifications";
import ListAltIcon from "@material-ui/icons/ListAlt";
import LoopIcon from "@material-ui/icons/Loop";
const styles = (theme: Theme) =>
createStyles({
logo: {
paddingTop: "42px",
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "120px",
},
},
menuList: {
"& .active": {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
color: "white",
background:
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box",
"& .MuiSvgIcon-root": {
color: "white",
},
},
"& .MuiListItem-root": {
marginTop: "16px",
},
paddingLeft: "30px",
"& .MuiSvgIcon-root": {
fontSize: "18px",
color: "#393939",
},
"& .MuiListItemIcon-root": {
minWidth: "40px",
},
"& .MuiTypography-root": {
fontSize: "14px",
},
"& .MuiListItem-gutters": {
paddingRight: "0px",
},
},
});
const mapState = (state: AppState) => ({
open: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
interface MenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
pages: string[];
}
class Menu extends React.Component<MenuProps> {
logout() {
const deleteSession = () => {
storage.removeItem("token");
this.props.userLoggedIn(false);
history.push("/");
};
api
.invoke("POST", `/api/v1/logout`)
.then(() => {
deleteSession();
})
.catch((err: any) => {
console.log(err);
deleteSession();
});
}
render() {
const { classes, pages } = this.props;
const allowedPages = pages.reduce((result: any, item: any, index: any) => {
result[item] = true;
return result;
}, {});
const menu = [
{
type: "item",
component: NavLink,
to: "/dashboard",
name: "Dashboard",
icon: <DashboardIcon />,
},
{
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <RoomServiceIcon />,
forceDisplay: true,
},
{
type: "item",
component: NavLink,
to: "/watch",
name: "Watch",
icon: <CenterFocusWeakIcon />,
},
{
type: "divider",
key: "divider-1",
},
{
type: "title",
name: "Admin",
component: Typography,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/users",
name: "Users",
icon: <PersonIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/groups",
name: "Groups",
icon: <UsersIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/policies",
name: "IAM Policies",
icon: <PermissionIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/trace",
name: "Trace",
icon: <LoopIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/logs",
name: "Console Logs",
icon: <WebAssetIcon />,
},
{
type: "title",
name: "Configuration",
component: Typography,
},
{
group: "Configuration",
type: "item",
component: NavLink,
to: "/notification-endpoints",
name: "Lambda Notifications",
icon: <NotificationsIcon />,
},
{
group: "Configuration",
type: "item",
component: NavLink,
to: "/configurations-list",
name: "Configurations List",
icon: <ListAltIcon />,
},
{
type: "divider",
key: "divider-2",
},
];
const allowedItems = menu.filter(
(item: any) =>
allowedPages[item.to] || item.forceDisplay || item.type !== "item"
);
return (
<React.Fragment>
<div className={classes.logo}>
<img src={logo} alt="logo" />
</div>
<List className={classes.menuList}>
{allowedItems.map((page: any) => {
switch (page.type) {
case "divider": {
return <Divider key={page.key} />;
}
case "item": {
return (
<ListItem
key={page.to}
button
component={page.component}
to={page.to}
>
{page.icon && <ListItemIcon>{page.icon}</ListItemIcon>}
{page.name && <ListItemText primary={page.name} />}
</ListItem>
);
}
case "title": {
return (
(allowedItems || []).filter(
(item: any) => item.group === page.name
).length > 0 && (
<ListItem key={page.name} component={page.component}>
{page.name}
</ListItem>
)
);
}
default:
}
})}
<ListItem
button
onClick={() => {
this.logout();
}}
>
<ListItemIcon>
<ExitToApp />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</React.Fragment>
);
}
}
export default connector(withStyles(styles)(Menu));

View File

@@ -0,0 +1,424 @@
// 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 { connect } from "react-redux";
import { NavLink } from "react-router-dom";
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 Collapse from "@material-ui/core/Collapse";
import ListItemText from "@material-ui/core/ListItemText";
import List from "@material-ui/core/List";
import { Divider, Typography, withStyles } from "@material-ui/core";
import { ExitToApp } from "@material-ui/icons";
import storage from "local-storage-fallback";
import { createStyles, Theme } from "@material-ui/core/styles";
import history from "../../../history";
import logo from "../../../icons/minio_console_logo.svg";
import { AppState } from "../../../store";
import { userLoggedIn } from "../../../actions";
import api from "../../../common/api";
import WatchIcon from "../../../icons/WatchIcon";
import { menuGroups } from "./utils";
import { IMenuProps } from "./types";
import {
BucketsIcon,
ClustersIcon,
ConfigurationsListIcon,
DashboardIcon,
GroupsIcon,
IAMPoliciesIcon,
LambdaNotificationsIcon,
MirroringIcon,
ServiceAccountsIcon,
TraceIcon,
UsersIcon,
WarpIcon,
} from "../../../icons";
const styles = (theme: Theme) =>
createStyles({
logo: {
paddingTop: "42px",
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "120px",
},
},
menuList: {
"& .active": {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
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,
"& .MuiSvgIcon-root": {
color: "white",
},
"& .MuiTypography-root": {
color: "#fff",
},
},
paddingLeft: "30px",
"& .MuiSvgIcon-root": {
fontSize: 16,
color: "#362585",
maxWidth: 14,
},
"& .MuiListItemIcon-root": {
minWidth: "25px",
},
"& .MuiTypography-root": {
fontSize: "12px",
color: "#2e2e2e",
},
"& .MuiListItem-gutters": {
paddingRight: 0,
},
},
extraMargin: {
"&.MuiListItem-gutters": {
marginLeft: 5,
},
},
groupTitle: {
color: "#220c7c",
fontSize: 10,
textTransform: "uppercase",
fontWeight: 700,
marginBottom: 3,
cursor: "pointer",
userSelect: "none",
},
subTitleMenu: {
fontWeight: 700,
marginLeft: 10,
"&.MuiTypography-root": {
fontSize: 13,
color: "#220c7c",
},
},
selectorArrow: {
marginLeft: 3,
marginTop: 1,
display: "inline-block",
width: 0,
height: 0,
borderStyle: "solid",
borderWidth: "3px 2.5px 0 2.5px",
borderColor: "#220C7C transparent transparent transparent",
transform: "rotateZ(0deg)",
transitionDuration: "0.2s",
},
selectorArrowOpen: {
transform: "rotateZ(180deg)",
},
});
const mapState = (state: AppState) => ({
open: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
// Menu State builder for groups
const menuStateBuilder = () => {
let elements: any = [];
menuGroups.forEach((menuItem) => {
if (menuItem.collapsible) {
elements[menuItem.group] = true;
}
});
return elements;
};
const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
const [menuOpen, setMenuOpen] = useState<any>(menuStateBuilder());
const logout = () => {
const deleteSession = () => {
storage.removeItem("token");
userLoggedIn(false);
history.push("/");
};
api
.invoke("POST", `/api/v1/logout`)
.then(() => {
deleteSession();
})
.catch((err: any) => {
console.log(err);
deleteSession();
});
};
const menuItems = [
{
group: "common",
type: "item",
component: NavLink,
to: "/dashboard",
name: "Dashboard",
icon: <DashboardIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <ServiceAccountsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/users",
name: "Users",
icon: <UsersIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/groups",
name: "Groups",
icon: <GroupsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/policies",
name: "IAM Policies",
icon: <IAMPoliciesIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/logs",
name: "Console Logs",
icon: <WebAssetIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/watch",
name: "Watch",
icon: <WatchIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/trace",
name: "Trace",
icon: <TraceIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/heal",
name: "Heal",
icon: <HealingIcon />,
},
{
group: "Admin",
type: "title",
name: "Configurations",
component: Typography,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/notification-endpoints",
name: "Lambda Notifications",
icon: <LambdaNotificationsIcon />,
extraMargin: true,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/configurations-list",
name: "Configurations List",
icon: <ConfigurationsListIcon />,
extraMargin: true,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/tenants",
name: "Tenants",
icon: <ClustersIcon />,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/mirroring",
name: "Mirroring",
icon: <MirroringIcon />,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/warp",
name: "Warp",
icon: <WarpIcon />,
},
];
const allowedPages = pages.reduce((result: any, item: any, index: any) => {
result[item] = true;
return result;
}, {});
const allowedItems = menuItems.filter(
(item: any) =>
allowedPages[item.to] || item.forceDisplay || item.type !== "item"
);
const setMenuCollapse = (menuClicked: string) => {
let newMenu: any = { ...menuOpen };
newMenu[menuClicked] = !newMenu[menuClicked];
setMenuOpen(newMenu);
};
return (
<React.Fragment>
<div className={classes.logo}>
<img src={logo} alt="logo" />
</div>
<List className={classes.menuList}>
{menuGroups.map((groupMember, index) => {
const filterByGroup = (allowedItems || []).filter(
(item: any) => item.group === groupMember.group
);
const countableElements = filterByGroup.filter(
(menuItem: any) => menuItem.type !== "title"
);
if (countableElements.length == 0) {
return null;
}
return (
<React.Fragment key={`menuElem-${index.toString()}`}>
{groupMember.label !== "" && (
<ListItem
className={classes.groupTitle}
onClick={() => {
if (groupMember.collapsible) {
setMenuCollapse(groupMember.group);
}
}}
>
{groupMember.label}
{groupMember.collapsible && (
<span
className={`${classes.selectorArrow} ${
menuOpen[groupMember.group]
? classes.selectorArrowOpen
: ""
}`}
/>
)}
</ListItem>
)}
<Collapse
in={
groupMember.collapsible ? menuOpen[groupMember.group] : true
}
timeout="auto"
unmountOnExit
key={`menuGroup-${groupMember.group}`}
>
{filterByGroup.map((page: any) => {
switch (page.type) {
case "item": {
return (
<ListItem
key={page.to}
button
component={page.component}
to={page.to}
className={
page.extraMargin ? classes.extraMargin : null
}
>
{page.icon && (
<ListItemIcon>{page.icon}</ListItemIcon>
)}
{page.name && <ListItemText primary={page.name} />}
</ListItem>
);
}
case "title": {
return (
<ListItem
key={page.name}
component={page.component}
className={classes.subTitleMenu}
>
{page.name}
</ListItem>
);
}
default:
}
})}
<Divider />
</Collapse>
</React.Fragment>
);
})}
<ListItem button onClick={logout}>
<ListItemIcon>
<ExitToApp />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</React.Fragment>
);
};
export default connector(withStyles(styles)(Menu));

View File

@@ -0,0 +1,23 @@
// 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 { userLoggedIn } from "../../../actions";
export interface IMenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
pages: string[];
}

View File

@@ -0,0 +1,23 @@
// 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 menuGroups = [
{ label: "", group: "common", collapsible: false },
{ label: "User", group: "User", collapsible: true },
{ label: "Admin", group: "Admin", collapsible: true },
{ label: "Tools", group: "Tools", collapsible: true },
{ label: "Operator", group: "Operator", collapsible: true },
];

View File

@@ -51,7 +51,7 @@ const styles = (theme: Theme) =>
interface IAddPolicyProps {
classes: any;
open: boolean;
closeModalAndRefresh: () => void;
closeModalAndRefresh: (refresh: boolean) => void;
policyEdit: Policy;
}
@@ -89,7 +89,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
addError: "",
},
() => {
this.props.closeModalAndRefresh();
this.props.closeModalAndRefresh(true);
}
);
})
@@ -120,7 +120,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
this.props.closeModalAndRefresh();
this.props.closeModalAndRefresh(false);
});
}}
title={`${policyEdit ? "Info" : "Create"} Policy`}

View File

@@ -14,114 +14,114 @@
// 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, { useState, useEffect } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
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 { Policy, PolicyList } from "./types";
import AddPolicy from "./AddPolicy";
import DeletePolicy from "./DeletePolicy";
import api from "../../../common/api";
import { CreateIcon } from "../../../icons";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import AddPolicy from "./AddPolicy";
import DeletePolicy from "./DeletePolicy";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
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",
},
});
interface IPoliciesProps {
classes: any;
}
interface IPoliciesState {
records: Policy[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedPolicy: string;
filterPolicies: string;
policyEdit: any;
}
const Policies = ({ classes }: IPoliciesProps) => {
const [records, setRecords] = useState<Policy[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedPolicy, setSelectedPolicy] = useState<string>("");
const [filterPolicies, setFilterPolicies] = useState<string>("");
const [policyEdit, setPolicyEdit] = useState<any>(null);
class Policies extends React.Component<IPoliciesProps, IPoliciesState> {
state: IPoliciesState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedPolicy: "",
filterPolicies: "",
policyEdit: null
};
useEffect(() => {
fetchRecords();
}, []);
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
useEffect(() => {
if (loading) {
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/policies?offset=${offset}&limit=${rowsPerPage}`)
.then((res: PolicyList) => {
this.setState({
loading: false,
records: res.policies,
totalRecords: res.total,
error: ""
const policies = get(res, "policies", []);
const total = get(res, "total", 0);
policies.sort((pa, pb) => {
if (pa.name > pb.name) {
return 1;
}
if (pa.name < pb.name) {
return -1;
}
return 0;
});
setLoading(false);
setRecords(policies);
setTotalRecords(total);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.policies === undefined ||
@@ -130,187 +130,166 @@ class Policies extends React.Component<IPoliciesProps, IPoliciesState> {
page > 0
) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
setPage(newPage);
fetchRecords();
}
})
.catch(err => {
this.setState({ loading: false, error: err });
.catch((err) => {
setLoading(false);
setError(err);
});
});
}
}
}, [
loading,
setLoading,
setRecords,
setTotalRecords,
setError,
setPage,
setError,
]);
closeAddModalAndRefresh() {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
}
const fetchRecords = () => {
setLoading(true);
};
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
const closeAddModalAndRefresh = (refresh: boolean) => {
setAddScreenOpen(false);
policyFilter(): void {}
if (refresh) {
fetchRecords();
}
};
componentDidMount(): void {
this.fetchRecords();
}
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedPolicy,
filterPolicies,
policyEdit
} = this.state;
if (refresh) {
fetchRecords();
}
};
const offset = page * rowsPerPage;
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP });
};
const confirmDeletePolicy = (policy: string) => {
setDeleteOpen(true);
setSelectedPolicy(policy);
};
const confirmDeletePolicy = (policy: string) => {
this.setState({ deleteOpen: true, selectedPolicy: policy });
};
const viewAction = (row: any) => {
setAddScreenOpen(true);
setPolicyEdit(row);
};
const viewAction = (row: any) => {
this.setState({
addScreenOpen: true,
policyEdit: row
});
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: confirmDeletePolicy, sendOnlyId: true },
];
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: confirmDeletePolicy, sendOnlyId: true }
];
const filteredRecords = records.filter((elementItem) =>
elementItem.name.includes(filterPolicies)
);
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: Policy) => {
if (filterPolicies === "") {
return true;
} else {
if (b.name.indexOf(filterPolicies) >= 0) {
return true;
} else {
return false;
}
}
});
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
return (
<React.Fragment>
{addScreenOpen && (
<AddPolicy
open={addScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
policyEdit={policyEdit}
/>
)}
{deleteOpen && (
<DeletePolicy
deleteOpen={deleteOpen}
selectedPolicy={selectedPolicy}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<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 => {
this.setState({
filterPolicies: val.target.value
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true,
policyEdit: 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={filteredRecords}
entityName="Policies"
idField="name"
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>
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
return (
<React.Fragment>
{addScreenOpen && (
<AddPolicy
open={addScreenOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
policyEdit={policyEdit}
/>
)}
{deleteOpen && (
<DeletePolicy
deleteOpen={deleteOpen}
selectedPolicy={selectedPolicy}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">IAM Policies</Typography>
</Grid>
</React.Fragment>
);
}
}
<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>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(Policies);

View File

@@ -0,0 +1,190 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
Button,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { User } from "../Users/types";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import { Policy, PolicyList } from "./types";
import api from "../../../common/api";
import { policySort } from "../../../utils/sortFunctions";
import { Group } from "../Groups/types";
interface ISetPolicyProps {
classes: any;
closeModalAndRefresh: () => void;
selectedUser: User | null;
selectedGroup: string | null;
open: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
buttonContainer: {
textAlign: "right",
},
});
const SetPolicy = ({
classes,
closeModalAndRefresh,
selectedUser,
selectedGroup,
open,
}: ISetPolicyProps) => {
//Local States
const [records, setRecords] = useState<Policy[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const fetchRecords = () => {
setLoading(true);
api
.invoke("GET", `/api/v1/policies?limit=1000`)
.then((res: PolicyList) => {
const policies = res.policies === null ? [] : res.policies;
setLoading(false);
setRecords(policies.sort(policySort));
setError("");
})
.catch((err) => {
setLoading(false);
setError(err);
});
};
const setPolicyAction = (policyName: string) => {
let entity = "user";
let value = null;
if (selectedGroup !== null) {
entity = "group";
value = selectedGroup;
} else {
if (selectedUser !== null) {
value = selectedUser.accessKey;
}
}
setLoading(true);
api
.invoke("PUT", `/api/v1/set-policy/${policyName}`, {
entityName: value,
entityType: entity,
})
.then((res: any) => {
setLoading(false);
setError("");
closeModalAndRefresh();
})
.catch((err) => {
setLoading(false);
setError(err);
});
};
useEffect(() => {
if (open) {
fetchRecords();
}
}, [open]);
return (
<ModalWrapper
onClose={() => {
closeModalAndRefresh();
}}
modalOpen={open}
title={
selectedUser !== null ? "Set Policy to User" : "Set Policy to Group"
}
>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<TableContainer component={Paper}>
<Table
className={classes.table}
size="small"
aria-label="a dense table"
>
<TableHead>
<TableRow>
<TableCell>Policy</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map((row) => (
<TableRow key={row.name}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">
<Button
variant="contained"
color="primary"
size={"small"}
onClick={() => {
setPolicyAction(row.name);
}}
>
Set
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
onClick={() => {
closeModalAndRefresh();
}}
>
Cancel
</Button>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</ModalWrapper>
);
};
export default withStyles(styles)(SetPolicy);

View File

@@ -0,0 +1,707 @@
// 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, { useEffect, useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { LinearProgress } from "@material-ui/core";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import api from "../../../../common/api";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import { IVolumeConfiguration, IZone } from "./types";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { k8sfactorForDropdown } from "../../../../common/utils";
import ZonesMultiSelector from "./ZonesMultiSelector";
import {
commonFormValidation,
IValidation,
} from "../../../../utils/validationFunctions";
import GenericWizard from "../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../Common/GenericWizard/types";
interface IAddTenantProps {
open: boolean;
closeModalAndRefresh: (reloadData: boolean) => any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
alignSelf: "flex-start" as const,
},
headerElement: {
position: "sticky",
top: 0,
paddingTop: 5,
marginBottom: 10,
backgroundColor: "#fff",
},
tableTitle: {
fontWeight: 700,
width: "30%",
},
...modalBasic,
});
interface Opts {
label: string;
value: string;
}
const AddTenant = ({
open,
closeModalAndRefresh,
classes,
}: IAddTenantProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
const [tenantName, setTenantName] = useState<string>("");
const [imageName, setImageName] = useState<string>("");
const [serviceName, setServiceName] = useState<string>("");
const [zones, setZones] = useState<IZone[]>([]);
const [volumesPerServer, setVolumesPerServer] = useState<number>(0);
const [volumeConfiguration, setVolumeConfiguration] = useState<
IVolumeConfiguration
>({ size: "", storage_class: "" });
const [mountPath, setMountPath] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [enableMCS, setEnableMCS] = useState<boolean>(true);
const [enableSSL, setEnableSSL] = useState<boolean>(false);
const [sizeFactor, setSizeFactor] = useState<string>("Gi");
const [storageClasses, setStorageClassesList] = useState<Opts[]>([]);
const [validationErrors, setValidationErrors] = useState<any>({});
const [namespace, setNamespace] = useState<string>("");
const [nameTenantValid, setNameTenantValid] = useState<boolean>(false);
const [configValid, setConfigValid] = useState<boolean>(false);
useEffect(() => {
fetchStorageClassList();
}, []);
useEffect(() => {
const commonValidation = commonFormValidation([validationElements[0]]);
setNameTenantValid(!("tenant-name" in commonValidation));
setValidationErrors(commonValidation);
}, [tenantName]);
useEffect(() => {
const commonValidation = commonFormValidation(
validationElements.slice(1, 3)
);
setConfigValid(
!("volumes_per_server" in commonValidation) &&
!("volume_size" in commonValidation)
);
setValidationErrors(commonValidation);
}, [volumesPerServer, volumeConfiguration]);
const validationElements: IValidation[] = [
{
fieldKey: "tenant-name",
required: true,
pattern: /^[a-z0-9-]{3,63}$/,
customPatternMessage:
"Name only can contain lowercase letters, numbers and '-'. Min. Length: 3",
value: tenantName,
},
{
fieldKey: "volumes_per_server",
required: true,
value: volumesPerServer.toString(10),
},
{
fieldKey: "volume_size",
required: true,
value: volumeConfiguration.size,
},
{
fieldKey: "image",
required: false,
value: imageName,
},
{
fieldKey: "service_name",
required: false,
value: serviceName,
},
{
fieldKey: "access_key",
required: false,
value: accessKey,
},
{
fieldKey: "secret_key",
required: false,
value: secretKey,
},
];
const clearValidationError = (fieldKey: string) => {
const newValidationElement = { ...validationErrors };
delete newValidationElement[fieldKey];
setValidationErrors(newValidationElement);
};
useEffect(() => {
if (addSending) {
let cleanZones: IZone[] = [];
for (let zone of zones) {
if (zone.name !== "") {
cleanZones.push(zone);
}
}
const commonValidation = commonFormValidation(validationElements);
setValidationErrors(commonValidation);
if (Object.keys(commonValidation).length === 0) {
api
.invoke("POST", `/api/v1/mkube/tenants`, {
name: tenantName,
service_name: tenantName,
image: imageName,
enable_ssl: enableSSL,
enable_mcs: enableMCS,
access_key: accessKey,
secret_key: secretKey,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: `${volumeConfiguration.size}${sizeFactor}`,
storage_class: volumeConfiguration.storage_class,
},
zones: cleanZones,
})
.then(() => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(true);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
} else {
setAddSending(false);
setAddError("Please fix the errors in the form and try again");
}
}
}, [addSending]);
const setVolumeConfig = (item: string, value: string) => {
const volumeCopy: IVolumeConfiguration = {
size: item !== "size" ? volumeConfiguration.size : value,
storage_class:
item !== "storage_class" ? volumeConfiguration.storage_class : value,
};
setVolumeConfiguration(volumeCopy);
};
const fetchStorageClassList = () => {
api
.invoke("GET", `/api/v1/mkube/storage-classes`)
.then((res: string[]) => {
let classes: string[] = [];
if (res !== null) {
classes = res;
}
setStorageClassesList(
classes.map((s: string) => ({
label: s,
value: s,
}))
);
const newStorage = { ...volumeConfiguration };
newStorage.storage_class = res[0];
setVolumeConfiguration(newStorage);
})
.catch((err: any) => {
console.log(err);
});
};
const cancelButton = {
label: "Cancel",
type: "other",
enabled: true,
action: () => {
closeModalAndRefresh(false);
},
};
const wizardSteps: IWizardElement[] = [
{
label: "Name Tenant",
componentRender: (
<Grid item xs={12}>
<div className={classes.headerElement}>
<h3>Name Tenant</h3>
<span>How would you like to name this new tenant?</span>
</div>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTenantName(e.target.value);
clearValidationError("tenant-name");
}}
label="Tenant Name"
value={tenantName}
required
error={validationErrors["tenant-name"] || ""}
/>
</Grid>
),
buttons: [
cancelButton,
{ label: "Next", type: "next", enabled: nameTenantValid },
],
},
{
label: "Configure",
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Configure</h3>
<span>Basic configurations for tenant management</span>
</div>
Please enter your access & secret keys
<Grid item xs={12}>
<InputBoxWrapper
id="access_key"
name="access_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
clearValidationError("access_key");
}}
label="Access Key"
value={accessKey}
error={validationErrors["access_key"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secret_key"
name="secret_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
clearValidationError("secret_key");
}}
label="Secret Key"
value={secretKey}
error={validationErrors["secret_key"] || ""}
/>
</Grid>
Please enter the MinIO image from dockerhub
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
clearValidationError("image");
}}
label="MinIO Image"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="Eg. minio/minio:RELEASE.2020-05-08T02-40-49Z"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="service_name"
name="service_name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setServiceName(e.target.value);
clearValidationError("service_name");
}}
label="Service Name"
value={serviceName}
error={validationErrors["service_name"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="namespace"
name="namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNamespace(e.target.value);
clearValidationError("namespace");
}}
label="Namespace"
value={namespace}
error={validationErrors["namespace"] || ""}
/>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: true },
],
},
{
label: "Storage Class",
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Choose your prefered Storage Class</h3>
<span>
Review the storage classes available in the tenant and decide
which one to allocate the tenant to
</span>
</div>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setVolumeConfig("storage_class", e.target.value as string);
}}
label="Storage Class"
value={volumeConfiguration.storage_class}
options={storageClasses}
/>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: true },
],
},
{
label: "Server Configuration",
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Server Configuration</h3>
<span>Define the server configuration</span>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="mount_path"
name="mount_path"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setMountPath(e.target.value);
}}
label="Mount Path"
value={mountPath}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_server"
name="volumes_per_server"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerServer(parseInt(e.target.value));
clearValidationError("volumes_per_server");
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
required
error={validationErrors["volumes_per_server"] || ""}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeConfig("size", e.target.value);
clearValidationError("volume_size");
}}
label="Size"
value={volumeConfiguration.size}
required
error={validationErrors["volume_size"] || ""}
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
id="size_factor"
name="size_factor"
value={sizeFactor}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSizeFactor(e.target.value as string);
}}
options={k8sfactorForDropdown()}
/>
</div>
</div>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: configValid },
],
},
{
label: "Zones Definition",
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Zones Definition</h3>
<span>Define the size of the tenant by defining the zone size</span>
</div>
<Grid item xs={12}>
<div>
<ZonesMultiSelector
label="Zones"
name="zones_selector"
onChange={(elements: IZone[]) => {
setZones(elements);
}}
elements={zones}
/>
</div>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: true },
],
},
{
label: "Extra Configurations",
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Extra Configurations</h3>
</div>
<Grid item xs={12}>
<CheckboxWrapper
value="enabled_mcs"
id="enabled_mcs"
name="enabled_mcs"
checked={enableMCS}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setEnableMCS(checked);
}}
label={"Enable mcs"}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="enable_ssl"
id="enable_ssl"
name="enable_ssl"
checked={enableSSL}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setEnableSSL(checked);
}}
label={"Enable SSL"}
/>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: true },
],
},
{
label: "Review",
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Review</h3>
<span>Review the details of the new tenant</span>
</div>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Table size="small">
<TableBody>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Tenant Name
</TableCell>
<TableCell>{tenantName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Access Key
</TableCell>
<TableCell>{accessKey}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Secret Key
</TableCell>
<TableCell>{secretKey}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
MinIO Image
</TableCell>
<TableCell>{imageName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Service Name
</TableCell>
<TableCell>{serviceName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Namespace
</TableCell>
<TableCell>{namespace}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Storage Class
</TableCell>
<TableCell>{volumeConfiguration.storage_class}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Mount Path
</TableCell>
<TableCell>{mountPath}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Volumes per Server
</TableCell>
<TableCell>{volumesPerServer}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Volume Size
</TableCell>
<TableCell>
{volumeConfiguration.size} {sizeFactor}
</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Total Zones
</TableCell>
<TableCell>{zones.length}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable SSL
</TableCell>
<TableCell>{enableSSL ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable MCS
</TableCell>
<TableCell>{enableMCS ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
</TableBody>
</Table>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Save",
type: "submit",
enabled: !addSending,
action: () => {
console.log("Save");
setAddSending(true);
},
},
],
},
];
return (
<ModalWrapper
title="Create Tenant"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<GenericWizard wizardSteps={wizardSteps} />
</ModalWrapper>
);
};
export default withStyles(styles)(AddTenant);

View File

@@ -0,0 +1,122 @@
// 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 {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
interface IDeleteTenant {
classes: any;
deleteOpen: boolean;
selectedTenant: string;
closeDeleteModalAndRefresh: (refreshList: boolean) => any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
});
const DeleteTenant = ({
classes,
deleteOpen,
selectedTenant,
closeDeleteModalAndRefresh,
}: IDeleteTenant) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/mkube/tenants/${selectedTenant}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading]);
const removeRecord = () => {
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Tenant</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete tenant <b>{selectedTenant}</b>?
{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={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteTenant);

View File

@@ -0,0 +1,282 @@
// 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, { useEffect, useState } from "react";
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 TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import { ITenant, ITenantsResponse } from "./types";
import { niceBytes } from "../../../../common/utils";
import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
interface ITenantsList {
classes: any;
}
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",
},
});
const ListTenants = ({ classes }: ITenantsList) => {
const [createTenantOpen, setCreateTenantOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedTenant, setSelectedTenant] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterTenants, setFilterTenants] = useState<string>("");
const [records, setRecords] = useState<any[]>([]);
const [offset, setOffset] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const closeAddModalAndRefresh = (reloadData: boolean) => {
setCreateTenantOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const confirmDeleteTenant = (tenant: string) => {
setSelectedTenant(tenant);
setDeleteOpen(true);
};
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 tableActions = [
{ type: "view", to: `/tenants`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteTenant, sendOnlyId: true },
];
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: any) => {
if (filterTenants === "") {
return true;
} else {
if (b.name.indexOf(filterTenants) >= 0) {
return true;
} else {
return false;
}
}
});
useEffect(() => {
if (isLoading) {
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke(
"GET",
`/api/v1/mkube/tenants?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ITenantsResponse) => {
if (res === null) {
setIsLoading(false);
return;
}
let resTenants: ITenant[] = [];
if (res.tenants !== null) {
resTenants = res.tenants;
}
for (let i = 0; i < resTenants.length; i++) {
const total =
resTenants[i].volume_count * resTenants[i].volume_size;
resTenants[i].capacity = niceBytes(total + "");
}
setRecords(resTenants);
setError("");
setIsLoading(false);
// if we get 0 results, and page > 0 , go down 1 page
if ((!res.tenants || res.tenants.length === 0) && page > 0) {
const newPage = page - 1;
setPage(newPage);
}
})
.catch((err) => {
setError(err);
setIsLoading(false);
});
};
fetchRecords();
}
}, [isLoading, page, rowsPerPage]);
useEffect(() => {
setIsLoading(true);
}, []);
return (
<React.Fragment>
{createTenantOpen && (
<AddTenant
open={createTenantOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
{deleteOpen && (
<DeleteTenant
deleteOpen={deleteOpen}
selectedTenant={selectedTenant}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Tenants</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setCreateTenantOpen(true);
}}
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Zones", elementKey: "zone_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
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)(ListTenants);

View File

@@ -0,0 +1,213 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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, createRef, ChangeEvent } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import { InputLabel, Tooltip } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import HelpIcon from "@material-ui/icons/Help";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import {
fieldBasic,
tooltipHelper,
} from "../../Common/FormComponents/common/styleLibrary";
import DeleteIcon from "../../../../icons/DeleteIcon";
import { IZone } from "./types";
interface IZonesMultiSelector {
elements: IZone[];
name: string;
label: string;
tooltip?: string;
classes: any;
onChange: (elements: IZone[]) => void;
}
const gridBasic = {
display: "grid",
gridTemplateColumns: "calc(50% - 20px) calc(50% - 20px) 30px",
gridGap: 8,
};
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
inputContainer: {
height: 150,
overflowY: "auto",
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
},
labelContainer: {
display: "flex",
},
inputGrid: {
...gridBasic,
},
inputTitles: {
...gridBasic,
marginBottom: 5,
},
deleteIconContainer: {
alignSelf: "center",
"& button": {
padding: 0,
marginBottom: 10,
},
},
});
const ZonesMultiSelector = ({
elements,
name,
label,
tooltip = "",
onChange,
classes,
}: IZonesMultiSelector) => {
const defaultZone: IZone = { name: "", servers: 0, capacity: "", volumes: 0 };
const [currentElements, setCurrentElements] = useState<IZone[]>([
{ ...defaultZone },
]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to send new values to onChange
useEffect(() => {
onChange(currentElements);
}, [currentElements]);
// If the last input is not empty, we add a new one
const addEmptyRow = (elementsUp: IZone[]) => {
const lastElement = elementsUp[elementsUp.length - 1];
if (lastElement.servers !== 0 && lastElement.name !== "") {
elementsUp.push({ ...defaultZone });
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
}
return elementsUp;
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeElement = (e: ChangeEvent<HTMLInputElement>, field: string) => {
e.persist();
let updatedElement = [...currentElements];
const index = get(e.target, "dataset.index", 0);
const rowPosition: IZone = updatedElement[index];
rowPosition.servers =
field === "servers" ? parseInt(e.target.value) : rowPosition.servers;
rowPosition.name = field === "name" ? e.target.value : rowPosition.name;
updatedElement[index] = rowPosition;
updatedElement = addEmptyRow(updatedElement);
setCurrentElements(updatedElement);
};
const deleteElement = (zoneToRemove: number) => {
const zonesClone = [...currentElements];
const newArray = zonesClone
.slice(0, zoneToRemove)
.concat(zonesClone.slice(zoneToRemove + 1, zonesClone.length));
setCurrentElements(newArray);
};
const inputs = currentElements.map((element, index) => {
return (
<React.Fragment key={`zone-${name}-${index.toString()}`}>
<div>
<InputBoxWrapper
id={`${name}-${index.toString()}-name`}
label={""}
name={`${name}-${index.toString()}-name`}
value={currentElements[index].name}
onChange={(e) => onChangeElement(e, "name")}
index={index}
key={`Zones-${name}-${index.toString()}-name`}
/>
</div>
<div>
<InputBoxWrapper
type="number"
id={`${name}-${index.toString()}-servers`}
label={""}
name={`${name}-${index.toString()}-servers`}
value={currentElements[index].servers.toString(10)}
onChange={(e) => onChangeElement(e, "servers")}
index={index}
key={`Zones-${name}-${index.toString()}-servers`}
/>
</div>
<div className={classes.deleteIconContainer}>
<IconButton
color="primary"
onClick={() => {
deleteElement(index);
}}
disabled={index === currentElements.length - 1}
>
<DeleteIcon />
</IconButton>
</div>
</React.Fragment>
);
});
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<Grid item xs={12}>
<div className={classes.inputTitles}>
<div>Name</div>
<div>Servers</div>
</div>
<div className={classes.inputContainer}>
<div className={classes.inputGrid}>{inputs}</div>
</div>
<div ref={bottomList} />
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ZonesMultiSelector);

View File

@@ -0,0 +1,46 @@
// 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 IZone {
name: string;
servers: number;
// computed
capacity: string;
volumes: number;
}
export interface IVolumeConfiguration {
size: string;
storage_class: string;
}
export interface ITenant {
name: string;
zone_count: number;
currentState: string;
instance_count: 4;
creation_date: Date;
volume_size: number;
volume_count: number;
volumes_per_server: number;
zones: IZone[];
// computed
capacity: string;
}
export interface ITenantsResponse {
tenants: ITenant[];
}

View File

@@ -0,0 +1,155 @@
import React, { useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import Grid from "@material-ui/core/Grid";
import {
factorForDropdown,
getTotalSize,
niceBytes,
} from "../../../../common/utils";
import { Button, LinearProgress } from "@material-ui/core";
interface IAddZoneProps {
classes: any;
open: boolean;
onCloseZoneAndReload: (shouldReload: boolean) => void;
volumesPerInstance: number;
volumeSize: number;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
"& div": {
flexGrow: 1,
width: "100%",
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
...modalBasic,
});
const AddZoneModal = ({
classes,
open,
onCloseZoneAndReload,
volumesPerInstance,
volumeSize,
}: IAddZoneProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [zoneName, setZoneName] = useState<string>("");
const [numberOfInstances, setNumberOfInstances] = useState<number>(0);
const instanceCapacity: number = volumeSize * volumesPerInstance;
const totalCapacity: number = instanceCapacity * numberOfInstances;
return (
<ModalWrapper
onClose={() => onCloseZoneAndReload(false)}
modalOpen={open}
title="Add Zone"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid item xs={12}>
<InputBoxWrapper
id="zone_name"
name="zone_name"
type="string"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setZoneName(e.target.value);
}}
label="Name"
value={zoneName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="number_instances"
name="number_instances"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfInstances(parseInt(e.target.value));
}}
label="Volumes per Server"
value={numberOfInstances.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>
<div>
<div className={classes.sizeNumber}>
{niceBytes(instanceCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Instance Capacity</div>
</div>
<div>
<div className={classes.sizeNumber}>
{niceBytes(totalCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Total Capacity</div>
</div>
</div>
<div className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</div>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddZoneModal);

View File

@@ -0,0 +1,218 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
interface IReplicationProps {
classes: any;
open: boolean;
closeModalAndRefresh: (refreshList: boolean) => void;
}
interface IDropDownElements {
label: string;
value: string;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
...modalBasic,
});
const ReplicationSetup = ({
classes,
open,
closeModalAndRefresh,
}: IReplicationProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [selectedTab, setSelectedTab] = useState<number>(0);
const [sourceBucket, setSourceBucket] = useState<string>("");
const [clusterSelected, setClusterSelected] = useState<string>("");
const [destinationBucket, setDestinationBucket] = useState<string>("");
const [address, setAddress] = useState<string>("");
const [bucket, setBucket] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const clustersList: IDropDownElements[] = [];
const sourceBuckets: IDropDownElements[] = [];
const destinationBuckets: IDropDownElements[] = [];
return (
<ModalWrapper
modalOpen={open}
title="Add Zone"
onClose={() => {
closeModalAndRefresh(false);
}}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid item xs={12}>
<SelectWrapper
label="Source Bucket"
options={sourceBuckets}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSourceBucket(e.target.value as string);
}}
value={sourceBucket}
name="source_bucket"
id="source_bucket"
/>
</Grid>
<Grid item xs={12}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="cluster-tabs"
>
<Tab label="Local Cluster" />
<Tab label="Remote Cluster" />
</Tabs>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{selectedTab === 0 && (
<React.Fragment>
<Grid item xs={12}>
<SelectWrapper
label="Cluster"
options={clustersList}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setClusterSelected(e.target.value as string);
}}
value={clusterSelected}
name="cluster"
id="cluster"
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
label="Destination Bucket"
options={destinationBuckets}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setDestinationBucket(e.target.value as string);
}}
value={destinationBucket}
name="destination_bucket"
id="destination_bucket"
/>
</Grid>
</React.Fragment>
)}
{selectedTab === 1 && (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="address"
name="address"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAddress(e.target.value);
}}
label="Address"
value={address}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="bucket"
name="bucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setBucket(e.target.value);
}}
label="Bucket"
value={bucket}
/>
</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>
</React.Fragment>
)}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(ReplicationSetup);

View File

@@ -0,0 +1,449 @@
// 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 { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button } from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import Paper from "@material-ui/core/Paper";
import { niceBytes } from "../../../../common/utils";
import AddZoneModal from "./AddZoneModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup";
import api from "../../../../common/api";
import { BucketInfo } from "../../Buckets/types";
import { ITenant, IZone } from "../ListTenants/types";
interface ITenantDetailsProps {
classes: any;
match: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
containerHeader: {
display: "flex",
justifyContent: "space-between",
},
paperContainer: {
padding: "15px 15px 15px 50px",
},
infoGrid: {
display: "grid",
gridTemplateColumns: "auto auto auto auto",
gridGap: 8,
"& div": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
paddingRight: 35,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
actionsTray: {
textAlign: "right",
},
...modalBasic,
});
const mainPagination = {
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
ActionsComponent: MinTablePaginationActions,
};
const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0);
const [externalIDP, setExternalIDP] = useState<boolean>(false);
const [externalKMS, setExternalKMS] = useState<boolean>(false);
const [zoneCount, setZoneCount] = useState<number>(0);
const [zones, setZones] = useState<IZone[]>([]);
const [instances, setInstances] = useState<number>(0);
const [volumes, setVolumes] = useState<number>(0);
const [addZoneOpen, setAddZone] = useState<boolean>(false);
const [addBucketOpen, setAddBucketOpen] = useState<boolean>(false);
const [addReplicationOpen, setAddReplicationOpen] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [tenant, setTenant] = useState<ITenant | null>(null);
const onCloseZoneAndRefresh = (reload: boolean) => {
setAddZone(false);
if (reload) {
console.log("reload");
}
};
const closeBucketsAndRefresh = () => {
setAddBucketOpen(false);
};
const closeReplicationAndRefresh = (reload: boolean) => {
setAddReplicationOpen(false);
if (reload) {
console.log("reload");
}
};
const loadInfo = () => {
const tenantName = match.params["tenantName"];
setLoading(true);
api
.invoke("GET", `/api/v1/mkube/tenants/${tenantName}`)
.then((res: ITenant) => {
const total = res.volume_count * res.volume_size;
setCapacity(total);
setZoneCount(res.zone_count);
setVolumes(res.volume_count);
setInstances(res.instance_count);
const resZones = !res.zones ? [] : res.zones;
for (let zone of resZones) {
zone.volumes = res.volumes_per_server;
const cap = res.volumes_per_server * res.volume_size * zone.servers;
zone.capacity = niceBytes(cap + "");
}
setZones(resZones);
setTenant(res);
setError("");
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
};
useEffect(() => {
loadInfo();
}, []);
return (
<React.Fragment>
{addZoneOpen && tenant !== null && (
<AddZoneModal
open={addZoneOpen}
onCloseZoneAndReload={onCloseZoneAndRefresh}
volumeSize={tenant.volume_size}
volumesPerInstance={tenant.volumes_per_server}
/>
)}
{addBucketOpen && (
<AddBucket
open={addBucketOpen}
closeModalAndRefresh={closeBucketsAndRefresh}
/>
)}
{addReplicationOpen && (
<ReplicationSetup
open={addReplicationOpen}
closeModalAndRefresh={closeReplicationAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Tenant > {match.params["tenantName"]}
</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.containerHeader}>
<Paper className={classes.paperContainer}>
<div className={classes.infoGrid}>
<div>Capacity:</div>
<div>{niceBytes(capacity.toString(10))}</div>
<div>Zones:</div>
<div>{zoneCount}</div>
<div>External IDP:</div>
<div>
{externalIDP ? "Yes" : "No"}&nbsp;&nbsp;
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {}}
>
Edit
</Button>
</div>
<div>Instances:</div>
<div>{instances}</div>
<div>External KMS:</div>
<div>{externalKMS ? "Yes" : "No"}&nbsp;&nbsp;</div>
<div>Volumes:</div>
<div>{volumes}</div>
</div>
</Paper>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {}}
>
Warp
</Button>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="tenant-tabs"
>
<Tab label="Zones" />
<Tab label="Buckets" />
<Tab label="Replication" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{selectedTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddZone(true);
}}
>
Add Zone
</Button>
)}
{selectedTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddBucketOpen(true);
}}
>
Create Bucket
</Button>
)}
{selectedTab === 2 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddReplicationOpen(true);
}}
>
Add Replication
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
{selectedTab === 0 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "servers" },
{ label: "# of Drives", elementKey: "volumes" },
]}
isLoading={false}
records={zones}
entityName="Zones"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 1 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "replicate",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "mirror",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Status",
elementKey: "status",
},
{ label: "Name", elementKey: "name" },
{ label: "AccessPolicy", elementKey: "access_policy" },
]}
isLoading={false}
records={[]}
entityName="Buckets"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 2 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Source",
elementKey: "source",
},
{ label: "Source Bucket", elementKey: "source_bucket" },
{ label: "Destination", elementKey: "destination" },
{
label: "Destination Bucket",
elementKey: "destination_bucket",
},
]}
isLoading={false}
records={[]}
entityName="Replication"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: () => {},
onChangeRowsPerPage: () => {},
ActionsComponent: MinTablePaginationActions,
}}
/>
)}
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(TenantDetails);

View File

@@ -34,6 +34,8 @@ import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DescriptionIcon from "@material-ui/icons/Description";
import SetPolicy from "../Policies/SetPolicy";
const styles = (theme: Theme) =>
createStyles({
@@ -102,6 +104,7 @@ interface IUsersState {
addGroupOpen: boolean;
filter: string;
checkedUsers: string[];
setPolicyOpen: boolean;
}
class Users extends React.Component<IUsersProps, IUsersState> {
@@ -119,6 +122,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
addGroupOpen: false,
filter: "",
checkedUsers: [],
setPolicyOpen: false,
};
fetchRecords() {
@@ -192,6 +196,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
filter,
checkedUsers,
addGroupOpen,
setPolicyOpen,
} = this.state;
const handleChangePage = (event: unknown, newPage: number) => {
@@ -243,6 +248,13 @@ class Users extends React.Component<IUsersProps, IUsersState> {
});
};
const setPolicyAction = (selectionElement: any): void => {
this.setState({
setPolicyOpen: true,
selectedUser: selectionElement,
});
};
const deleteAction = (selectionElement: any): void => {
this.setState({
deleteOpen: true,
@@ -252,6 +264,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
@@ -266,6 +279,16 @@ class Users extends React.Component<IUsersProps, IUsersState> {
}}
/>
)}
{setPolicyOpen && (
<SetPolicy
open={setPolicyOpen}
selectedUser={selectedUser}
selectedGroup={null}
closeModalAndRefresh={() => {
this.setState({ setPolicyOpen: false });
}}
/>
)}
{deleteOpen && (
<DeleteUser
deleteOpen={deleteOpen}

View File

@@ -18,6 +18,10 @@ interface userInterface {
accessKey: string;
}
interface policyInterface {
name: string;
}
export const usersSort = (a: userInterface, b: userInterface) => {
if (a.accessKey > b.accessKey) {
return 1;
@@ -29,6 +33,17 @@ export const usersSort = (a: userInterface, b: userInterface) => {
return 0;
};
export const policySort = (a: policyInterface, b: policyInterface) => {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
// a must be equal to b
return 0;
};
export const stringSort = (a: string, b: string) => {
if (a > b) {
return 1;

View File

@@ -0,0 +1,44 @@
// 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 IValidation {
fieldKey: string;
required: boolean;
pattern?: RegExp;
customPatternMessage?: string;
value: string;
}
export const commonFormValidation = (fieldsValidate: IValidation[]) => {
let returnErrors: any = {};
fieldsValidate.forEach((field) => {
if (field.required && field.value.trim() === "") {
returnErrors[field.fieldKey] = "Field cannot be empty";
return;
}
if (field.pattern && field.customPatternMessage) {
const rgx = new RegExp(field.pattern, "g");
if (!field.value.match(rgx)) {
returnErrors[field.fieldKey] = field.customPatternMessage;
}
}
});
return returnErrors;
};

View File

@@ -3013,6 +3013,29 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -3243,7 +3266,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0, color-convert@^1.9.1:
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -7632,7 +7655,7 @@ lodash.without@~4.4.0:
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@~4.17.4:
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.4:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -8028,6 +8051,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdir
dependencies:
minimist "^1.2.5"
moment@^2.10.2:
version "2.26.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -9959,7 +9987,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -10176,6 +10204,14 @@ react-app-polyfill@^1.0.6:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
react-chartjs-2@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.9.0.tgz#d054dbdd763fbe9a76296a4ae0752ea549b76d9e"
integrity sha512-IYwqUUnQRAJ9SNA978vxulHJTcUFTJk2LDVfbAyk0TnJFZZG7+6U/2flsE4MCw6WCbBjTTypy8T82Ch7XrPtRw==
dependencies:
lodash "^4.17.4"
prop-types "^15.5.8"
react-codemirror2@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-7.1.0.tgz#b874a275ad4f6f2ee5adb23b550c0f4b8b82776d"
@@ -12620,9 +12656,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
websocket@^1.0.31:
version "1.0.31"

378
restapi/admin_heal.go Normal file
View File

@@ -0,0 +1,378 @@
// 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 restapi
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
)
// An alias of string to represent the health color code of an object
type col string
const (
colGrey col = "Grey"
colRed col = "Red"
colYellow col = "Yellow"
colGreen col = "Green"
)
var (
hColOrder = []col{colRed, colYellow, colGreen}
hColTable = map[int][]int{
1: {0, -1, 1},
2: {0, 1, 2},
3: {1, 2, 3},
4: {1, 2, 4},
5: {1, 3, 5},
6: {2, 4, 6},
7: {2, 4, 7},
8: {2, 5, 8},
}
)
type healItemStatus struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
Type string `json:"type"`
Name string `json:"name"`
Before struct {
Color string `json:"color"`
Offline int `json:"offline"`
Online int `json:"online"`
Missing int `json:"missing"`
Corrupted int `json:"corrupted"`
Drives []madmin.HealDriveInfo `json:"drives"`
} `json:"before"`
After struct {
Color string `json:"color"`
Offline int `json:"offline"`
Online int `json:"online"`
Missing int `json:"missing"`
Corrupted int `json:"corrupted"`
Drives []madmin.HealDriveInfo `json:"drives"`
} `json:"after"`
Size int64 `json:"size"`
}
type healStatus struct {
// Total time since heal start in seconds
HealDuration float64 `json:"healDuration"`
// Accumulated statistics of heal result records
BytesScanned int64 `json:"bytesScanned"`
// Counter for objects, and another counter for all kinds of
// items
ObjectsScanned int64 `json:"objectsScanned"`
ItemsScanned int64 `json:"itemsScanned"`
// Counters for healed objects and all kinds of healed items
ObjectsHealed int64 `json:"objectsHealed"`
ItemsHealed int64 `json:"itemsHealed"`
ItemsHealthStatus []healItemStatus `json:"itemsHealthStatus"`
// Map of health color code to number of objects with that
// health color code.
HealthBeforeCols map[col]int64 `json:"healthBeforeCols"`
HealthAfterCols map[col]int64 `json:"healthAfterCols"`
}
type healOptions struct {
BucketName string
Prefix string
ForceStart bool
ForceStop bool
madmin.HealOpts
}
// startHeal starts healing of the servers based on heal options
func startHeal(ctx context.Context, conn WSConn, client MinioAdmin, hOpts *healOptions) error {
// Initialize heal
healStart, _, err := client.heal(ctx, hOpts.BucketName, hOpts.Prefix, hOpts.HealOpts, "", hOpts.ForceStart, hOpts.ForceStop)
if err != nil {
log.Println("error initializing healing:", err)
return err
}
if hOpts.ForceStop {
log.Println("heal stopped successfully")
return nil
}
clientToken := healStart.ClientToken
hs := healStatus{
HealthBeforeCols: make(map[col]int64),
HealthAfterCols: make(map[col]int64),
}
for {
select {
case <-ctx.Done():
return nil
default:
_, res, err := client.heal(ctx, hOpts.BucketName, hOpts.Prefix, hOpts.HealOpts, clientToken, hOpts.ForceStart, hOpts.ForceStop)
if err != nil {
log.Println("error on heal:", err)
return err
}
hs.writeStatus(&res, conn)
if res.Summary == "finished" {
log.Println("heal finished")
return nil
}
if res.Summary == "stopped" {
log.Println("heal stopped")
return fmt.Errorf("heal had an error - %s", res.FailureDetail)
}
time.Sleep(time.Second)
}
}
}
func (h *healStatus) writeStatus(s *madmin.HealTaskStatus, conn WSConn) error {
// Update state
h.updateDuration(s)
for _, item := range s.Items {
err := h.updateStats(item)
if err != nil {
fmt.Println("error on updateStats:", err)
return err
}
}
// Serialize message to be sent
infoBytes, err := json.Marshal(h)
if err != nil {
fmt.Println("error on json.Marshal:", err)
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, infoBytes)
if err != nil {
log.Println("error writeMessage:", err)
return err
}
return nil
}
func (h *healStatus) updateDuration(s *madmin.HealTaskStatus) {
h.HealDuration = time.Now().UTC().Sub(s.StartTime).Round(time.Second).Seconds()
}
func (h *healStatus) updateStats(i madmin.HealResultItem) error {
// update general status
if i.Type == madmin.HealItemObject {
// Objects whose size could not be found have -1 size
// returned.
if i.ObjectSize >= 0 {
h.BytesScanned += i.ObjectSize
}
h.ObjectsScanned++
}
h.ItemsScanned++
beforeUp, afterUp := i.GetOnlineCounts()
if afterUp > beforeUp {
if i.Type == madmin.HealItemObject {
h.ObjectsHealed++
}
h.ItemsHealed++
}
// update per item status
itemStatus := healItemStatus{}
// get color health status
var beforeColor, afterColor col
var err error
switch i.Type {
case madmin.HealItemMetadata, madmin.HealItemBucket:
beforeColor, afterColor, err = getReplicatedFileHCCChange(i)
default:
if i.Type == madmin.HealItemObject {
itemStatus.Size = i.ObjectSize
}
beforeColor, afterColor, err = getObjectHCCChange(i)
}
if err != nil {
return err
}
itemStatus.Status = "success"
itemStatus.Before.Color = strings.ToLower(string(beforeColor))
itemStatus.After.Color = strings.ToLower(string(afterColor))
itemStatus.Type, itemStatus.Name = getHRITypeAndName(i)
itemStatus.Before.Online, itemStatus.After.Online = beforeUp, afterUp
itemStatus.Before.Missing, itemStatus.After.Missing = i.GetMissingCounts()
itemStatus.Before.Corrupted, itemStatus.After.Corrupted = i.GetCorruptedCounts()
itemStatus.Before.Offline, itemStatus.After.Offline = i.GetOfflineCounts()
itemStatus.Before.Drives = i.Before.Drives
itemStatus.After.Drives = i.After.Drives
h.ItemsHealthStatus = append(h.ItemsHealthStatus, itemStatus)
h.HealthBeforeCols[beforeColor]++
h.HealthAfterCols[afterColor]++
return nil
}
// getObjectHCCChange - returns before and after color change for
// objects
func getObjectHCCChange(h madmin.HealResultItem) (b, a col, err error) {
parityShards := h.ParityBlocks
dataShards := h.DataBlocks
onlineBefore, onlineAfter := h.GetOnlineCounts()
surplusShardsBeforeHeal := onlineBefore - dataShards
surplusShardsAfterHeal := onlineAfter - dataShards
b, err = getHColCode(surplusShardsBeforeHeal, parityShards)
if err != nil {
return
}
a, err = getHColCode(surplusShardsAfterHeal, parityShards)
return
}
// getReplicatedFileHCCChange - fetches health color code for metadata
// files that are replicated.
func getReplicatedFileHCCChange(h madmin.HealResultItem) (b, a col, err error) {
getColCode := func(numAvail int) (c col, err error) {
// calculate color code for replicated object similar
// to erasure coded objects
quorum := h.DiskCount/h.SetCount/2 + 1
surplus := numAvail/h.SetCount - quorum
parity := h.DiskCount/h.SetCount - quorum
c, err = getHColCode(surplus, parity)
return
}
onlineBefore, onlineAfter := h.GetOnlineCounts()
b, err = getColCode(onlineBefore)
if err != nil {
return
}
a, err = getColCode(onlineAfter)
return
}
func getHColCode(surplusShards, parityShards int) (c col, err error) {
if parityShards < 1 || parityShards > 8 || surplusShards > parityShards {
return c, fmt.Errorf("invalid parity shard count/surplus shard count given")
}
if surplusShards < 0 {
return colGrey, err
}
colRow := hColTable[parityShards]
for index, val := range colRow {
if val != -1 && surplusShards <= val {
return hColOrder[index], err
}
}
return c, fmt.Errorf("cannot get a heal color code")
}
func getHRITypeAndName(i madmin.HealResultItem) (typ, name string) {
name = fmt.Sprintf("%s/%s", i.Bucket, i.Object)
switch i.Type {
case madmin.HealItemMetadata:
typ = "system"
name = i.Detail
case madmin.HealItemBucketMetadata:
typ = "system"
name = "bucket-metadata:" + name
case madmin.HealItemBucket:
typ = "bucket"
case madmin.HealItemObject:
typ = "object"
default:
typ = fmt.Sprintf("!! Unknown heal result record %#v !!", i)
name = typ
}
return
}
// getHealOptionsFromReq return options from request for healing process
// path come as : `/heal/bucket1` and query params come on request form
func getHealOptionsFromReq(req *http.Request) (*healOptions, error) {
hOptions := healOptions{}
re := regexp.MustCompile(`(/heal/)(.*?)(\?.*?$|$)`)
matches := re.FindAllSubmatch([]byte(req.URL.Path), -1)
// len matches is always 3
// matches comes as e.g.
// [["...", "/heal/" "bucket1"]]
// [["/heal/" "/heal/" ""]]
// bucket name is on the second group, third position
hOptions.BucketName = strings.TrimSpace(string(matches[0][2]))
hOptions.Prefix = req.FormValue("prefix")
hOptions.HealOpts.ScanMode = transformScanStr(req.FormValue("scan"))
if req.FormValue("force-start") != "" {
boolVal, err := strconv.ParseBool(req.FormValue("force-start"))
if err != nil {
return nil, err
}
hOptions.ForceStart = boolVal
}
if req.FormValue("force-stop") != "" {
boolVal, err := strconv.ParseBool(req.FormValue("force-stop"))
if err != nil {
return nil, err
}
hOptions.ForceStop = boolVal
}
// heal recursively
if req.FormValue("recursive") != "" {
boolVal, err := strconv.ParseBool(req.FormValue("recursive"))
if err != nil {
return nil, err
}
hOptions.HealOpts.Recursive = boolVal
}
// remove dangling objects in heal sequence
if req.FormValue("remove") != "" {
boolVal, err := strconv.ParseBool(req.FormValue("remove"))
if err != nil {
return nil, err
}
hOptions.HealOpts.Remove = boolVal
}
// only inspect data
if req.FormValue("dry-run") != "" {
boolVal, err := strconv.ParseBool(req.FormValue("dry-run"))
if err != nil {
return nil, err
}
hOptions.HealOpts.DryRun = boolVal
}
return &hOptions, nil
}
func transformScanStr(scanStr string) madmin.HealScanMode {
switch scanStr {
case "deep":
return madmin.HealDeepScan
}
return madmin.HealNormalScan
}

278
restapi/admin_heal_test.go Normal file
View File

@@ -0,0 +1,278 @@
// 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 restapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"testing"
"time"
"github.com/minio/minio/pkg/madmin"
"github.com/stretchr/testify/assert"
)
// assigning mock at runtime instead of compile time
var minioHealMock func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
func (ac adminClientMock) heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return minioHealMock(ctx, bucket, prefix, healOpts, clientToken, forceStart, forceStop)
}
func TestHeal(t *testing.T) {
assert := assert.New(t)
client := adminClientMock{}
mockWSConn := mockConn{}
ctx := context.Background()
function := "startHeal()"
mockResultItem1 := madmin.HealResultItem{
Type: madmin.HealItemObject,
SetCount: 1,
DiskCount: 4,
ParityBlocks: 2,
DataBlocks: 2,
Before: struct {
Drives []madmin.HealDriveInfo `json:"drives"`
}{
Drives: []madmin.HealDriveInfo{
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateMissing,
},
},
},
After: struct {
Drives []madmin.HealDriveInfo `json:"drives"`
}{
Drives: []madmin.HealDriveInfo{
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
},
},
}
mockResultItem2 := madmin.HealResultItem{
Type: madmin.HealItemBucket,
SetCount: 1,
DiskCount: 4,
ParityBlocks: 2,
DataBlocks: 2,
Before: struct {
Drives []madmin.HealDriveInfo `json:"drives"`
}{
Drives: []madmin.HealDriveInfo{
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateMissing,
},
},
},
After: struct {
Drives []madmin.HealDriveInfo `json:"drives"`
}{
Drives: []madmin.HealDriveInfo{
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
{
State: madmin.DriveStateOk,
},
},
},
}
mockHealTaskStatus := madmin.HealTaskStatus{
StartTime: time.Now().UTC().Truncate(time.Second * 2), // mock 2 sec duration
Items: []madmin.HealResultItem{
mockResultItem1,
mockResultItem2,
},
Summary: "finished",
}
testStreamSize := 1
testReceiver := make(chan healStatus, testStreamSize)
isClosed := false // testReceiver is closed?
testOptions := &healOptions{
BucketName: "testbucket",
Prefix: "",
ForceStart: false,
ForceStop: false,
}
// Test-1: startHeal send simple stream of data, no errors
minioHealMock = func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return healStart, mockHealTaskStatus, nil
}
writesCount := 1
// mock connection WriteMessage() no error
connWriteMessageMock = func(messageType int, data []byte) error {
// emulate that receiver gets the message written
var t healStatus
_ = json.Unmarshal(data, &t)
testReceiver <- t
if writesCount == testStreamSize {
// for testing we need to close the receiver channel
if !isClosed {
close(testReceiver)
isClosed = true
}
return nil
}
writesCount++
return nil
}
if err := startHeal(ctx, mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from Console.
for i := range testReceiver {
assert.Equal(int64(1), i.ObjectsScanned)
assert.Equal(int64(1), i.ObjectsHealed)
assert.Equal(int64(2), i.ItemsScanned)
assert.Equal(int64(2), i.ItemsHealed)
assert.Equal(int64(0), i.HealthBeforeCols[colGreen])
assert.Equal(int64(1), i.HealthBeforeCols[colYellow])
assert.Equal(int64(1), i.HealthBeforeCols[colRed])
assert.Equal(int64(0), i.HealthBeforeCols[colGrey])
assert.Equal(int64(2), i.HealthAfterCols[colGreen])
assert.Equal(int64(0), i.HealthAfterCols[colYellow])
assert.Equal(int64(0), i.HealthAfterCols[colRed])
assert.Equal(int64(0), i.HealthAfterCols[colGrey])
}
// Test-2: startHeal error on init
minioHealMock = func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return healStart, mockHealTaskStatus, errors.New("error")
}
if err := startHeal(ctx, mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error", err.Error())
}
// Test-3: getHealOptionsFromReq return heal options from request
u, _ := url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=true&remove=true&dry-run=true&scan=deep")
req := &http.Request{
URL: u,
}
opts, err := getHealOptionsFromReq(req)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "getHealOptionsFromReq", err.Error())
}
expectedOptions := healOptions{
BucketName: "bucket1",
ForceStart: true,
ForceStop: true,
Prefix: "file/",
HealOpts: madmin.HealOpts{
Recursive: true,
DryRun: true,
ScanMode: madmin.HealDeepScan,
},
}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Recursive, opts.Recursive)
assert.Equal(expectedOptions.ForceStart, opts.ForceStart)
assert.Equal(expectedOptions.DryRun, opts.DryRun)
assert.Equal(expectedOptions.ScanMode, opts.ScanMode)
// Test-3: getHealOptionsFromReq return error if boolean value not valid
u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=nonbool&force-start=true&force-stop=true&remove=true&dry-run=true&scan=deep")
req = &http.Request{
URL: u,
}
opts, err = getHealOptionsFromReq(req)
if assert.Error(err) {
assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
}
// Test-4: getHealOptionsFromReq return error if boolean value not valid
u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=true&remove=nonbool&dry-run=true&scan=deep")
req = &http.Request{
URL: u,
}
opts, err = getHealOptionsFromReq(req)
if assert.Error(err) {
assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
}
// Test-5: getHealOptionsFromReq return error if boolean value not valid
u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=nonbool&force-stop=true&remove=true&dry-run=true&scan=deep")
req = &http.Request{
URL: u,
}
opts, err = getHealOptionsFromReq(req)
if assert.Error(err) {
assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
}
// Test-6: getHealOptionsFromReq return error if boolean value not valid
u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=nonbool&remove=true&dry-run=true&scan=deep")
req = &http.Request{
URL: u,
}
opts, err = getHealOptionsFromReq(req)
if assert.Error(err) {
assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
}
// Test-7: getHealOptionsFromReq return error if boolean value not valid
u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=true&remove=true&dry-run=nonbool&scan=deep")
req = &http.Request{
URL: u,
}
opts, err = getHealOptionsFromReq(req)
if assert.Error(err) {
assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
}
}

View File

@@ -74,10 +74,10 @@ func registerUsersHandlers(api *operations.McsAPI) {
sessionID := string(*principal)
userInfoResponse, err := getUserInfoResponse(sessionID, params)
if err != nil {
return admin_api.NewGetUserDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
return admin_api.NewGetUserInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewGetUserOK().WithPayload(userInfoResponse)
return admin_api.NewGetUserInfoOK().WithPayload(userInfoResponse)
})
// Update User
api.AdminAPIUpdateUserInfoHandler = admin_api.UpdateUserInfoHandlerFunc(func(params admin_api.UpdateUserInfoParams, principal *models.Principal) middleware.Responder {

View File

@@ -85,6 +85,8 @@ type MinioAdmin interface {
serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
accountUsageInfo(ctx context.Context) (madmin.AccountUsageInfo, error)
heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
// Service Accounts
addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error)
listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error)
@@ -234,6 +236,11 @@ func (ac adminClient) accountUsageInfo(ctx context.Context) (madmin.AccountUsage
return ac.client.AccountUsageInfo(ctx)
}
func (ac adminClient) heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return ac.client.Heal(ctx, bucket, prefix, healOpts, clientToken, forceStart, forceStop)
}
func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
@@ -249,7 +256,9 @@ func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
tlsEnabled := getMinIOEndpointIsSecure()
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
endpoint := getMinIOEndpoint()
adminClient, err := madmin.NewWithOptions(endpoint, &madmin.Options{
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
Secure: tlsEnabled,
})

View File

@@ -28,10 +28,10 @@ import (
var Port = "9090"
// Hostname mcs hostname
var Hostname = "localhost"
var Hostname = "0.0.0.0"
// TLSHostname mcs tls hostname
var TLSHostname = "localhost"
var TLSHostname = "0.0.0.0"
// TLSPort mcs tls port
var TLSPort = "9443"
@@ -228,3 +228,8 @@ func getSecureFeaturePolicy() string {
func getSecureExpectCTHeader() string {
return env.Get(McsSecureExpectCTHeader, "")
}
// getM3Host returns the hostname of mkube
func getM3Host() string {
return env.Get(McsM3Host, "http://m3:8787")
}

View File

@@ -25,6 +25,7 @@ import (
"strings"
"github.com/minio/mcs/models"
"github.com/minio/mcs/pkg"
"github.com/minio/mcs/pkg/auth"
assetFS "github.com/elazarl/go-bindata-assetfs"
@@ -161,9 +162,13 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler {
// FileServerMiddleware serves files from the static folder
func FileServerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "mcs/"+pkg.Version) // add HTTP Server header
switch {
case strings.HasPrefix(r.URL.Path, "/ws"):
serveWS(w, r)
case strings.HasPrefix(r.URL.Path, "/api/v1/mkube"):
client := &http.Client{}
serverMkube(client, w, r)
case strings.HasPrefix(r.URL.Path, "/api"):
next.ServeHTTP(w, r)
default:

View File

@@ -49,4 +49,5 @@ const (
McsSecureReferrerPolicy = "MCS_SECURE_REFERRER_POLICY"
McsSecureFeaturePolicy = "MCS_SECURE_FEATURE_POLICY"
McsSecureExpectCTHeader = "MCS_SECURE_EXPECT_CT_HEADER"
McsM3Host = "MCS_M3_HOSTNAME"
)

68
restapi/mkube.go Normal file
View File

@@ -0,0 +1,68 @@
// 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 restapi
import (
"bufio"
"errors"
"fmt"
"log"
"net/http"
"strings"
apiErrors "github.com/go-openapi/errors"
)
// serverMkube handles calls for mkube
func serverMkube(client *http.Client, w http.ResponseWriter, req *http.Request) {
// destination of the request, the mkube server
req.URL.Path = strings.Replace(req.URL.Path, "/mkube", "", 1)
targetURL := fmt.Sprintf("%s%s", getM3Host(), req.URL.String())
// set the HTTP method, url, and m3Req body
m3Req, err := http.NewRequest(req.Method, targetURL, req.Body)
if err != nil {
apiErrors.ServeError(w, req, err)
log.Println("error creating m3 request:", err)
return
}
// set the m3Req headers
m3Req.Header = req.Header
resp, err := client.Do(m3Req)
if err != nil {
log.Println("error on m3 request:", err)
if strings.Contains(err.Error(), "connection refused") {
apiErrors.ServeError(w, req, errors.New("service M3 is not available"))
return
}
apiErrors.ServeError(w, req, err)
return
}
defer resp.Body.Close()
w.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
// Write the m3 response to the response writer
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
w.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil {
log.Println("error scanning m3 response:", err)
apiErrors.ServeError(w, req, err)
}
}

119
restapi/mkube_test.go Normal file
View File

@@ -0,0 +1,119 @@
// 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 restapi
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
// RoundTripFunc .
type RoundTripFunc func(req *http.Request) (*http.Response, error)
// RoundTrip .
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
//NewTestClient returns *http.Client with Transport replaced to avoid making real calls
func NewTestClient(fn RoundTripFunc) *http.Client {
return &http.Client{
Transport: fn,
}
}
func Test_serverMkube(t *testing.T) {
OKclient := NewTestClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`OK`)),
Header: make(http.Header),
}, nil
})
badClient := NewTestClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 500,
Body: ioutil.NopCloser(bytes.NewBufferString(`NOTOK`)),
Header: make(http.Header),
}, errors.New("something wrong")
})
refusedClient := NewTestClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 500,
Body: ioutil.NopCloser(bytes.NewBufferString(`NOTOK`)),
Header: make(http.Header),
}, errors.New("connection refused")
})
testURL, _ := url.Parse("/api/v1/clusters")
type args struct {
client *http.Client
recorder *httptest.ResponseRecorder
req *http.Request
}
tests := []struct {
name string
args args
wantCode int
}{
{
name: "Successful request",
args: args{
client: OKclient,
recorder: httptest.NewRecorder(),
req: &http.Request{URL: testURL},
},
wantCode: 200,
},
{
name: "Unsuccessful request",
args: args{
client: badClient,
recorder: httptest.NewRecorder(),
req: &http.Request{URL: testURL},
},
wantCode: 500,
},
{
name: "refused request",
args: args{
client: refusedClient,
recorder: httptest.NewRecorder(),
req: &http.Request{URL: testURL},
},
wantCode: 500,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverMkube(tt.args.client, tt.args.recorder, tt.args.req)
resp := tt.args.recorder.Result()
if resp.StatusCode != tt.wantCode {
t.Errorf("Invalid code returned")
return
}
})
}
}

View File

@@ -1,90 +0,0 @@
// 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 admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/mcs/models"
)
// GetUserHandlerFunc turns a function with the right signature into a get user handler
type GetUserHandlerFunc func(GetUserParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn GetUserHandlerFunc) Handle(params GetUserParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// GetUserHandler interface for that can handle valid get user params
type GetUserHandler interface {
Handle(GetUserParams, *models.Principal) middleware.Responder
}
// NewGetUser creates a new http.Handler for the get user operation
func NewGetUser(ctx *middleware.Context, handler GetUserHandler) *GetUser {
return &GetUser{Context: ctx, Handler: handler}
}
/*GetUser swagger:route GET /users/{name} AdminAPI getUser
Get User
*/
type GetUser struct {
Context *middleware.Context
Handler GetUserHandler
}
func (o *GetUser) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewGetUserParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,89 +0,0 @@
// 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 admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewGetUserParams creates a new GetUserParams object
// no default values defined in spec.
func NewGetUserParams() GetUserParams {
return GetUserParams{}
}
// GetUserParams contains all the bound params for the get user operation
// typically these are obtained from a http.Request
//
// swagger:parameters GetUser
type GetUserParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
Name string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewGetUserParams() beforehand.
func (o *GetUserParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rName, rhkName, _ := route.Params.GetOK("name")
if err := o.bindName(rName, rhkName, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindName binds and validates parameter Name from path.
func (o *GetUserParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Name = raw
return nil
}

View File

@@ -1,133 +0,0 @@
// 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 admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/mcs/models"
)
// GetUserOKCode is the HTTP code returned for type GetUserOK
const GetUserOKCode int = 200
/*GetUserOK A successful response.
swagger:response getUserOK
*/
type GetUserOK struct {
/*
In: Body
*/
Payload *models.User `json:"body,omitempty"`
}
// NewGetUserOK creates GetUserOK with default headers values
func NewGetUserOK() *GetUserOK {
return &GetUserOK{}
}
// WithPayload adds the payload to the get user o k response
func (o *GetUserOK) WithPayload(payload *models.User) *GetUserOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get user o k response
func (o *GetUserOK) SetPayload(payload *models.User) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetUserOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*GetUserDefault Generic error response.
swagger:response getUserDefault
*/
type GetUserDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewGetUserDefault creates GetUserDefault with default headers values
func NewGetUserDefault(code int) *GetUserDefault {
if code <= 0 {
code = 500
}
return &GetUserDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the get user default response
func (o *GetUserDefault) WithStatusCode(code int) *GetUserDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the get user default response
func (o *GetUserDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the get user default response
func (o *GetUserDefault) WithPayload(payload *models.Error) *GetUserDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get user default response
func (o *GetUserDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetUserDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -1,116 +0,0 @@
// 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 admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// GetUserURL generates an URL for the get user operation
type GetUserURL struct {
Name string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetUserURL) WithBasePath(bp string) *GetUserURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetUserURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *GetUserURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/users/{name}"
name := o.Name
if name != "" {
_path = strings.Replace(_path, "{name}", name, -1)
} else {
return nil, errors.New("name is required on GetUserURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *GetUserURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *GetUserURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *GetUserURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on GetUserURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on GetUserURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *GetUserURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,75 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
)
// LoginOauth2CallbackHandlerFunc turns a function with the right signature into a login oauth2 callback handler
type LoginOauth2CallbackHandlerFunc func(LoginOauth2CallbackParams) middleware.Responder
// Handle executing the request and returning a response
func (fn LoginOauth2CallbackHandlerFunc) Handle(params LoginOauth2CallbackParams) middleware.Responder {
return fn(params)
}
// LoginOauth2CallbackHandler interface for that can handle valid login oauth2 callback params
type LoginOauth2CallbackHandler interface {
Handle(LoginOauth2CallbackParams) middleware.Responder
}
// NewLoginOauth2Callback creates a new http.Handler for the login oauth2 callback operation
func NewLoginOauth2Callback(ctx *middleware.Context, handler LoginOauth2CallbackHandler) *LoginOauth2Callback {
return &LoginOauth2Callback{Context: ctx, Handler: handler}
}
/*LoginOauth2Callback swagger:route GET /login/oauth2/callback UserAPI loginOauth2Callback
Identity Provider oauth2 callback endpoint.
*/
type LoginOauth2Callback struct {
Context *middleware.Context
Handler LoginOauth2CallbackHandler
}
func (o *LoginOauth2Callback) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewLoginOauth2CallbackParams()
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,62 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
)
// NewLoginOauth2CallbackParams creates a new LoginOauth2CallbackParams object
// no default values defined in spec.
func NewLoginOauth2CallbackParams() LoginOauth2CallbackParams {
return LoginOauth2CallbackParams{}
}
// LoginOauth2CallbackParams contains all the bound params for the login oauth2 callback operation
// typically these are obtained from a http.Request
//
// swagger:parameters LoginOauth2Callback
type LoginOauth2CallbackParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewLoginOauth2CallbackParams() beforehand.
func (o *LoginOauth2CallbackParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,113 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/mcs/models"
)
// LoginOauth2CallbackOKCode is the HTTP code returned for type LoginOauth2CallbackOK
const LoginOauth2CallbackOKCode int = 200
/*LoginOauth2CallbackOK A successful response.
swagger:response loginOauth2CallbackOK
*/
type LoginOauth2CallbackOK struct {
}
// NewLoginOauth2CallbackOK creates LoginOauth2CallbackOK with default headers values
func NewLoginOauth2CallbackOK() *LoginOauth2CallbackOK {
return &LoginOauth2CallbackOK{}
}
// WriteResponse to the client
func (o *LoginOauth2CallbackOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(200)
}
/*LoginOauth2CallbackDefault Generic error response.
swagger:response loginOauth2CallbackDefault
*/
type LoginOauth2CallbackDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewLoginOauth2CallbackDefault creates LoginOauth2CallbackDefault with default headers values
func NewLoginOauth2CallbackDefault(code int) *LoginOauth2CallbackDefault {
if code <= 0 {
code = 500
}
return &LoginOauth2CallbackDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the login oauth2 callback default response
func (o *LoginOauth2CallbackDefault) WithStatusCode(code int) *LoginOauth2CallbackDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the login oauth2 callback default response
func (o *LoginOauth2CallbackDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the login oauth2 callback default response
func (o *LoginOauth2CallbackDefault) WithPayload(payload *models.Error) *LoginOauth2CallbackDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the login oauth2 callback default response
func (o *LoginOauth2CallbackDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *LoginOauth2CallbackDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -1,104 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// LoginOauth2CallbackURL generates an URL for the login oauth2 callback operation
type LoginOauth2CallbackURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *LoginOauth2CallbackURL) WithBasePath(bp string) *LoginOauth2CallbackURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *LoginOauth2CallbackURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *LoginOauth2CallbackURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/login/oauth2/callback"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *LoginOauth2CallbackURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *LoginOauth2CallbackURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *LoginOauth2CallbackURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on LoginOauth2CallbackURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on LoginOauth2CallbackURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *LoginOauth2CallbackURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,75 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
)
// Oauth2CallbackHandlerFunc turns a function with the right signature into a oauth2 callback handler
type Oauth2CallbackHandlerFunc func(Oauth2CallbackParams) middleware.Responder
// Handle executing the request and returning a response
func (fn Oauth2CallbackHandlerFunc) Handle(params Oauth2CallbackParams) middleware.Responder {
return fn(params)
}
// Oauth2CallbackHandler interface for that can handle valid oauth2 callback params
type Oauth2CallbackHandler interface {
Handle(Oauth2CallbackParams) middleware.Responder
}
// NewOauth2Callback creates a new http.Handler for the oauth2 callback operation
func NewOauth2Callback(ctx *middleware.Context, handler Oauth2CallbackHandler) *Oauth2Callback {
return &Oauth2Callback{Context: ctx, Handler: handler}
}
/*Oauth2Callback swagger:route GET /login/oauth2/callback UserAPI oauth2Callback
Identity Provider oauth2 callback endpoint.
*/
type Oauth2Callback struct {
Context *middleware.Context
Handler Oauth2CallbackHandler
}
func (o *Oauth2Callback) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewOauth2CallbackParams()
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,62 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
)
// NewOauth2CallbackParams creates a new Oauth2CallbackParams object
// no default values defined in spec.
func NewOauth2CallbackParams() Oauth2CallbackParams {
return Oauth2CallbackParams{}
}
// Oauth2CallbackParams contains all the bound params for the oauth2 callback operation
// typically these are obtained from a http.Request
//
// swagger:parameters Oauth2Callback
type Oauth2CallbackParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewOauth2CallbackParams() beforehand.
func (o *Oauth2CallbackParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,113 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/mcs/models"
)
// Oauth2CallbackOKCode is the HTTP code returned for type Oauth2CallbackOK
const Oauth2CallbackOKCode int = 200
/*Oauth2CallbackOK A successful response.
swagger:response oauth2CallbackOK
*/
type Oauth2CallbackOK struct {
}
// NewOauth2CallbackOK creates Oauth2CallbackOK with default headers values
func NewOauth2CallbackOK() *Oauth2CallbackOK {
return &Oauth2CallbackOK{}
}
// WriteResponse to the client
func (o *Oauth2CallbackOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(200)
}
/*Oauth2CallbackDefault Generic error response.
swagger:response oauth2CallbackDefault
*/
type Oauth2CallbackDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewOauth2CallbackDefault creates Oauth2CallbackDefault with default headers values
func NewOauth2CallbackDefault(code int) *Oauth2CallbackDefault {
if code <= 0 {
code = 500
}
return &Oauth2CallbackDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the oauth2 callback default response
func (o *Oauth2CallbackDefault) WithStatusCode(code int) *Oauth2CallbackDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the oauth2 callback default response
func (o *Oauth2CallbackDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the oauth2 callback default response
func (o *Oauth2CallbackDefault) WithPayload(payload *models.Error) *Oauth2CallbackDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the oauth2 callback default response
func (o *Oauth2CallbackDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *Oauth2CallbackDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -1,104 +0,0 @@
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// Oauth2CallbackURL generates an URL for the oauth2 callback operation
type Oauth2CallbackURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *Oauth2CallbackURL) WithBasePath(bp string) *Oauth2CallbackURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *Oauth2CallbackURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *Oauth2CallbackURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/login/oauth2/callback"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *Oauth2CallbackURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *Oauth2CallbackURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *Oauth2CallbackURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on Oauth2CallbackURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on Oauth2CallbackURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *Oauth2CallbackURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -53,6 +53,7 @@ func getSessionResponse(sessionID string) (*models.SessionResponse, error) {
log.Println("error getting claims from JWT", err)
return nil, errorGenericInvalidSession
}
sessionResp := &models.SessionResponse{
Pages: acl.GetAuthorizedEndpoints(claims.Actions),
Status: models.SessionResponseStatusOk,

View File

@@ -78,10 +78,10 @@ func startWatch(ctx context.Context, conn WSConn, wsc MCS3Client, options watchO
}
}
// getOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
// getWatchOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
// watch path if defined.
// path come as : `/watch/bucket1` and query params come on request form
func getOptionsFromReq(req *http.Request) watchOptions {
func getWatchOptionsFromReq(req *http.Request) watchOptions {
wOptions := watchOptions{}
// Default Events if not defined
wOptions.Events = []string{"put", "get", "delete"}

View File

@@ -44,7 +44,7 @@ func TestWatch(t *testing.T) {
mockWSConn := mockConn{}
ctx := context.Background()
function := "startWatch(ctx, )"
function := "startWatch()"
testStreamSize := 5
testReceiver := make(chan []mc.EventInfo, testStreamSize)
@@ -191,7 +191,7 @@ func TestWatch(t *testing.T) {
}
}
// Test-6: getOptionsFromReq return parameters from path
// Test-6: getWatchOptionsFromReq return parameters from path
u, err := url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=put,get")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
@@ -199,7 +199,7 @@ func TestWatch(t *testing.T) {
req := &http.Request{
URL: u,
}
opts := getOptionsFromReq(req)
opts := getWatchOptionsFromReq(req)
expectedOptions := watchOptions{
BucketName: "bucket1",
}
@@ -211,7 +211,7 @@ func TestWatch(t *testing.T) {
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
// Test-7: getOptionsFromReq return default events if not defined
// Test-7: getWatchOptionsFromReq return default events if not defined
u, err = url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
@@ -219,7 +219,7 @@ func TestWatch(t *testing.T) {
req = &http.Request{
URL: u,
}
opts = getOptionsFromReq(req)
opts = getWatchOptionsFromReq(req)
expectedOptions = watchOptions{
BucketName: "bucket1",
}
@@ -231,7 +231,7 @@ func TestWatch(t *testing.T) {
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
// Test-8: getOptionsFromReq return default events if not defined
// Test-8: getWatchOptionsFromReq return default events if not defined
u, err = url.Parse("http://localhost/api/v1/watch/bucket2?prefix=&suffix=")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
@@ -239,7 +239,7 @@ func TestWatch(t *testing.T) {
req = &http.Request{
URL: u,
}
opts = getOptionsFromReq(req)
opts = getWatchOptionsFromReq(req)
expectedOptions = watchOptions{
BucketName: "bucket2",
}

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